/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @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;
    };
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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 {
    /**
     * @param {Object} source The emitter.
     * @param {String} name The event name.
     */
    constructor(source, name) {
        this.source = source;
        this.name = name;
        this.path = [];
        // The following methods are defined in the constructor because they must be re-created per instance.
        this.stop = spy();
        this.off = spy();
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/uid
 */
// A hash table of hex numbers to avoid using toString() in uid() which is costly.
// [ '00', '01', '02', ..., 'fe', 'ff' ]
const HEX_NUMBERS = new Array(256).fill('')
    .map((_, index) => ('0' + (index).toString(16)).slice(-2));
/**
 * Returns a unique id. The id starts with an "e" character and a randomly generated string of
 * 32 alphanumeric characters.
 *
 * **Note**: The characters the unique id is built from correspond to the hex number notation
 * (from "0" to "9", from "a" to "f"). In other words, each id corresponds to an "e" followed
 * by 16 8-bit numbers next to each other.
 *
 * @returns {String} An unique id string.
 */
function uid() {
    // Let's create some positive random 32bit integers first.
    //
    // 1. Math.random() is a float between 0 and 1.
    // 2. 0x100000000 is 2^32 = 4294967296.
    // 3. >>> 0 enforces integer (in JS all numbers are floating point).
    //
    // For instance:
    //		Math.random() * 0x100000000 = 3366450031.853859
    // but
    //		Math.random() * 0x100000000 >>> 0 = 3366450031.
    const r1 = Math.random() * 0x100000000 >>> 0;
    const r2 = Math.random() * 0x100000000 >>> 0;
    const r3 = Math.random() * 0x100000000 >>> 0;
    const r4 = Math.random() * 0x100000000 >>> 0;
    // Make sure that id does not start with number.
    return 'e' +
        HEX_NUMBERS[r1 >> 0 & 0xFF] +
        HEX_NUMBERS[r1 >> 8 & 0xFF] +
        HEX_NUMBERS[r1 >> 16 & 0xFF] +
        HEX_NUMBERS[r1 >> 24 & 0xFF] +
        HEX_NUMBERS[r2 >> 0 & 0xFF] +
        HEX_NUMBERS[r2 >> 8 & 0xFF] +
        HEX_NUMBERS[r2 >> 16 & 0xFF] +
        HEX_NUMBERS[r2 >> 24 & 0xFF] +
        HEX_NUMBERS[r3 >> 0 & 0xFF] +
        HEX_NUMBERS[r3 >> 8 & 0xFF] +
        HEX_NUMBERS[r3 >> 16 & 0xFF] +
        HEX_NUMBERS[r3 >> 24 & 0xFF] +
        HEX_NUMBERS[r4 >> 0 & 0xFF] +
        HEX_NUMBERS[r4 >> 8 & 0xFF] +
        HEX_NUMBERS[r4 >> 16 & 0xFF] +
        HEX_NUMBERS[r4 >> 24 & 0xFF];
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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 = 'normal') {
        if (typeof priority != 'number') {
            return this[priority] || this.normal;
        }
        else {
            return priority;
        }
    },
    highest: 100000,
    high: 1000,
    normal: 0,
    low: -1000,
    lowest: -100000
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Inserts any object with priority at correct index by priority so registered objects are always sorted from highest to lowest priority.
 *
 * @param {Array.<module:utils/inserttopriorityarray~ObjectWithPriority>} objects Array of objects with priority to insert object to.
 * @param {module:utils/inserttopriorityarray~ObjectWithPriority} objectToInsert Object with `priority` property.
 */
function insertToPriorityArray(objects, objectToInsert) {
    const priority = priorities.get(objectToInsert.priority);
    for (let i = 0; i < objects.length; i++) {
        if (priorities.get(objects[i].priority) < priority) {
            objects.splice(i, 0, objectToInsert);
            return;
        }
    }
    objects.push(objectToInsert);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/ckeditorerror
 */
/* globals console */
/**
 * URL to the documentation with error codes.
 */
const DOCUMENTATION_URL = 'https://ckeditor.com/docs/ckeditor5/latest/support/error-codes.html';
/**
 * The CKEditor error class.
 *
 * You should throw `CKEditorError` when:
 *
 * * An unexpected situation occurred and the editor (most probably) will not work properly. Such exception will be handled
 * by the {@link module:watchdog/watchdog~Watchdog watchdog} (if it is integrated),
 * * If the editor is incorrectly integrated or the editor API is used in the wrong way. This way you will give
 * feedback to the developer as soon as possible. Keep in mind that for common integration issues which should not
 * stop editor initialization (like missing upload adapter, wrong name of a toolbar component) we use
 * {@link module:utils/ckeditorerror~logWarning `logWarning()`} and
 * {@link module:utils/ckeditorerror~logError `logError()`}
 * to improve developers experience and let them see the a working editor as soon as possible.
 *
 *		/**
 *		 * 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.
 *		 * /
 *		throw new CKEditorError( 'plugin-load', {
 *			pluginName: 'foo',
 *			moduleName: 'bar'
 *		} );
 *
 * @extends Error
 */
class CKEditorError extends Error {
    /**
     * Creates an instance of the CKEditorError class.
     *
     * @param {String} errorName The error id in an `error-name` format. A link to this error documentation page will be added
     * to the thrown error's `message`.
     * @param {Object|null} context A context of the error by which the {@link module:watchdog/watchdog~Watchdog watchdog}
     * is able to determine which editor crashed. It should be an editor instance or a property connected to it. It can be also
     * a `null` value if the editor should not be restarted in case of the error (e.g. during the editor initialization).
     * The error context should be checked using the `areConnectedThroughProperties( editor, context )` utility
     * to check if the object works as the context.
     * @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(errorName, context, data) {
        super(getErrorMessage(errorName, data));
        /**
         * @type {String}
         */
        this.name = 'CKEditorError';
        /**
         * A context of the error by which the Watchdog is able to determine which editor crashed.
         *
         * @type {Object|null}
         */
        this.context = context;
        /**
         * The additional error data passed to the constructor. Undefined if none was passed.
         *
         * @type {Object|undefined}
         */
        this.data = data;
    }
    /**
     * Checks if the error is of the `CKEditorError` type.
     * @returns {Boolean}
     */
    is(type) {
        return type === 'CKEditorError';
    }
    /**
     * A utility that ensures that the thrown error is a {@link module:utils/ckeditorerror~CKEditorError} one.
     * It is useful when combined with the {@link module:watchdog/watchdog~Watchdog} feature, which can restart the editor in case
     * of a {@link module:utils/ckeditorerror~CKEditorError} error.
     *
     * @static
     * @param {Error} err The error to rethrow.
     * @param {Object} context An object connected through properties with the editor instance. This context will be used
     * by the watchdog to verify which editor should be restarted.
     */
    static rethrowUnexpectedError(err, context) {
        if (err.is && err.is('CKEditorError')) {
            throw err;
        }
        /**
         * An unexpected error occurred inside the CKEditor 5 codebase. This error will look like the original one
         * to make the debugging easier.
         *
         * This error is only useful when the editor is initialized using the {@link module:watchdog/watchdog~Watchdog} feature.
         * In case of such error (or any {@link module:utils/ckeditorerror~CKEditorError} error) the watchdog should restart the editor.
         *
         * @error unexpected-error
         */
        const error = new CKEditorError(err.message, context);
        // Restore the original stack trace to make the error look like the original one.
        // See https://github.com/ckeditor/ckeditor5/issues/5595 for more details.
        error.stack = err.stack;
        throw error;
    }
}
/**
 * Logs a warning to the console with a properly formatted message and adds a link to the documentation.
 * Use whenever you want to log a warning to the console.
 *
 *		/**
 *		 * There was a problem processing the configuration of the toolbar. The item with the given
 *		 * name does not exist, so it was omitted when rendering the toolbar.
 *		 *
 *		 * @error toolbarview-item-unavailable
 *		 * @param {String} name The name of the component.
 *		 * /
 *		logWarning( 'toolbarview-item-unavailable', { name } );
 *
 * See also {@link module:utils/ckeditorerror~CKEditorError} for an explanation when to throw an error and when to log
 * a warning or an error to the console.
 *
 * @param {String} errorName The error name to be logged.
 * @param {Object} [data] Additional data to be logged.
 */
function logWarning(errorName, data) {
    console.warn(...formatConsoleArguments(errorName, data));
}
/**
 * Logs an error to the console with a properly formatted message and adds a link to the documentation.
 * Use whenever you want to log an error to the console.
 *
 *		/**
 *		 * There was a problem processing the configuration of the toolbar. The item with the given
 *		 * name does not exist, so it was omitted when rendering the toolbar.
 *		 *
 *		 * @error toolbarview-item-unavailable
 *		 * @param {String} name The name of the component.
 *		 * /
 *		 logError( 'toolbarview-item-unavailable', { name } );
 *
 * **Note**: In most cases logging a warning using {@link module:utils/ckeditorerror~logWarning} is enough.
 *
 * See also {@link module:utils/ckeditorerror~CKEditorError} for an explanation when to use each method.
 *
 * @param {String} errorName The error name to be logged.
 * @param {Object} [data] Additional data to be logged.
 */
function logError(errorName, data) {
    console.error(...formatConsoleArguments(errorName, data));
}
// Returns formatted link to documentation message.
//
// @private
// @param {String} errorName
// @returns {string}
function getLinkToDocumentationMessage(errorName) {
    return `\nRead more: ${DOCUMENTATION_URL}#error-${errorName}`;
}
// Returns formatted error message.
//
// @private
// @param {String} errorName
// @param {Object} [data]
// @returns {string}
function getErrorMessage(errorName, data) {
    const processedObjects = new WeakSet();
    const circularReferencesReplacer = (key, value) => {
        if (typeof value === 'object' && value !== null) {
            if (processedObjects.has(value)) {
                return `[object ${value.constructor.name}]`;
            }
            processedObjects.add(value);
        }
        return value;
    };
    const stringifiedData = data ? ` ${JSON.stringify(data, circularReferencesReplacer)}` : '';
    const documentationLink = getLinkToDocumentationMessage(errorName);
    return errorName + stringifiedData + documentationLink;
}
// Returns formatted console error arguments.
//
// @private
// @param {String} errorName
// @param {Object} [data]
// @returns {Array}
function formatConsoleArguments(errorName, data) {
    const documentationMessage = getLinkToDocumentationMessage(errorName);
    return data ? [errorName, data, documentationMessage] : [errorName, documentationMessage];
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const version = '35.1.0';
/* istanbul ignore next */
const windowOrGlobal = typeof window === 'object' ? window : global;
/* istanbul ignore next */
if (windowOrGlobal.CKEDITOR_VERSION) {
    /**
     * This error is thrown when due to a mistake in how CKEditor 5 was installed or initialized, some
     * of its modules were duplicated (evaluated and executed twice). Module duplication leads to inevitable runtime
     * errors.
     *
     * There are many situations in which some modules can be loaded twice. In the worst case scenario,
     * you may need to check your project for each of these issues and fix them all.
     *
     * # Trying to add a plugin to an existing build
     *
     * If you import an existing CKEditor 5 build and a plugin like this:
     *
     *		import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
     *		import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight';
     *
     * Then your project loads some CKEditor 5 packages twice. How does it happen?
     *
     * The build package contains a file which is already compiled with webpack. This means
     * that it contains all the necessary code from e.g. `@ckeditor/ckeditor5-engine` and `@ckeditor/ckeditor5-utils`.
     *
     * However, the `Highlight` plugin imports some of the modules from these packages, too. If you ask webpack to
     * build such a project, you will end up with the modules being included (and run) twice &mdash; first, because they are
     * included inside the build package, and second, because they are required by the `Highlight` plugin.
     *
     * Therefore, **you must never add plugins to an existing build** unless your plugin has no dependencies.
     *
     * Adding plugins to a build is done by taking the source version of this build (so, before it was built with webpack)
     * and adding plugins there. In this situation, webpack will know that it only needs to load each plugin once.
     *
     * Read more in the {@glink installation/getting-started/installing-plugins "Installing plugins"} guide.
     *
     * # Confused an editor build with an editor implementation
     *
     * This scenario is very similar to the previous one, but has a different origin.
     *
     * Let's assume that you wanted to use CKEditor 5 from source, as explained in the
     * {@glink installation/advanced/alternative-setups/integrating-from-source "Building from source"} section
     * or in the {@glink framework/guides/quick-start "Quick start"} guide of CKEditor 5 Framework.
     *
     * The correct way to do so is to import an editor and plugins and run them together like this:
     *
     *		import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
     *		import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
     *		import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
     *		import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
     *		import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
     *
     *		ClassicEditor
     *			.create( document.querySelector( '#editor' ), {
     *				plugins: [ Essentials, Paragraph, Bold, Italic ],
     *				toolbar: [ 'bold', 'italic' ]
     *			} )
     *			.then( editor => {
     *				console.log( 'Editor was initialized', editor );
     *			} )
     *			.catch( error => {
     *				console.error( error.stack );
     *			} );
     *
     * However, you might have mistakenly imported a build instead of the source `ClassicEditor`. In this case
     * your imports will look like this:
     *
     *		import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
     *		import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
     *		import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
     *		import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
     *		import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
     *
     * This creates the same situation as in the previous section because you use a build together with source plugins.
     *
     * Remember: `@ckeditor/ckeditor5-build-*` packages contain editor builds and `@ckeditor/ckeditor5-editor-*` contain source editors.
     *
     * # Loading two or more builds on one page
     *
     * If you use CKEditor 5 builds, you might have loaded two (or more) `ckeditor.js` files on one web page.
     * Check your web page for duplicated `<script>` elements or make sure your page builder/bundler includes CKEditor only once.
     *
     * If you want to use two different types of editors at once, see the
     * {@glink installation/advanced/using-two-editors "Using two different editors"}
     * section.
     *
     * # Using outdated packages
     *
     * Building CKEditor 5 from source requires using multiple npm packages. These packages have their dependencies
     * to other packages. If you use the latest version of, for example, `@ckeditor/ckeditor5-editor-classic` with
     * an outdated version of `@ckeditor/ckeditor5-image`, npm or yarn will need to install two different versions of
     * `@ckeditor/ckeditor5-core` because `@ckeditor/ckeditor5-editor-classic` and `@ckeditor/ckeditor5-image` may require
     * different versions of the core package.
     *
     * The solution to this issue is to update all packages to their latest version. We recommend
     * using tools like [`npm-check-updates`](https://www.npmjs.com/package/npm-check-updates) which simplify this process.
     *
     * # Conflicting version of dependencies
     *
     * This is a special case of the previous scenario. If you use CKEditor 5 with some third-party plugins,
     * it may happen that even if you use the latest versions of the official packages and the latest version of
     * these third-party packages, there will be a conflict between some of their dependencies.
     *
     * Such a problem can be resolved by either downgrading CKEditor 5 packages (which we do not recommend) or
     * asking the author of the third-party package to upgrade its depdendencies (or forking their project and doing this yourself).
     *
     * **Note:** All official CKEditor 5 packages (excluding integrations and `ckeditor5-dev-*` packages) are released in the
     * same major version. This is &mdash; in the `x.y.z`, the `x` is the same for all packages. This is the simplest way to check
     * whether you use packages coming from the same CKEditor 5 version. You can read more about versioning in the
     * {@glink support/versioning-policy Versioning policy} guide.
     *
     * # Packages were duplicated in `node_modules`
     *
     * In some situations, especially when calling `npm install` multiple times, it may happen
     * that npm will not correctly "deduplicate" packages.
     *
     * Normally, npm deduplicates all packages so, for example, `@ckeditor/ckeditor5-core` is installed only once in `node_modules/`.
     * However, it is known to fail to do so from time to time.
     *
     * We recommend checking if any of the steps listed below help:
     *
     * * `rm -rf node_modules && npm install` to make sure you have a clean `node_modules/` directory. This step
     * is known to help in most cases.
     * * If you use `yarn.lock` or `package-lock.json`, remove it before `npm install`.
     * * Check whether all CKEditor 5 packages are up to date and reinstall them
     * if you changed anything (`rm -rf node_modules && npm install`).
     *
     * If all packages are correct and compatible with each other, the steps above are known to help. If not, you may
     * try to check with `npm ls` how many times packages like `@ckeditor/ckeditor5-core`, `@ckeditor/ckeditor5-engine` and
     *`@ckeditor/ckeditor5-utils` are installed. If more than once, verify which package causes that.
     *
     * @error ckeditor-duplicated-modules
     */
    throw new CKEditorError('ckeditor-duplicated-modules', null);
}
else {
    windowOrGlobal.CKEDITOR_VERSION = version;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const _listeningTo = Symbol('listeningTo');
const _emitterId = Symbol('emitterId');
const _delegations = Symbol('delegations');
/**
 * Mixin that injects the {@link ~Emitter events API} into its host.
 *
 * Read more about the concept of emitters in the:
 * * {@glink framework/guides/architecture/core-editor-architecture#event-system-and-observables Event system and observables}
 * section of the {@glink framework/guides/architecture/core-editor-architecture Core editor architecture} guide.
 * * {@glink framework/guides/deep-dive/event-system Event system} deep dive guide.
 *
 * @mixin EmitterMixin
 * @implements module:utils/emittermixin~Emitter
 */
function EmitterMixin(base) {
    class Mixin extends base {
        on(event, callback, options) {
            this.listenTo(this, event, callback, options);
        }
        once(event, callback, options) {
            let wasFired = false;
            const onceCallback = (event, ...args) => {
                // Ensure the callback is called only once even if the callback itself leads to re-firing the event
                // (which would call the callback again).
                if (!wasFired) {
                    wasFired = true;
                    // 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.listenTo(this, event, onceCallback, options);
        }
        off(event, callback) {
            this.stopListening(this, event, callback);
        }
        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.
            addEventListener(this, emitter, event, callback, options);
        }
        stopListening(emitter, event, callback) {
            const emitters = this[_listeningTo];
            let emitterId = emitter && _getEmitterId(emitter);
            const emitterInfo = (emitters && emitterId) ? emitters[emitterId] : undefined;
            const eventCallbacks = (emitterInfo && event) ? emitterInfo.callbacks[event] : undefined;
            // Stop if nothing has been listened.
            if (!emitters || (emitter && !emitterInfo) || (event && !eventCallbacks)) {
                return;
            }
            // All params provided. off() that single callback.
            if (callback) {
                removeEventListener(this, emitter, event, callback);
                // We must remove callbacks as well in order to prevent memory leaks.
                // See https://github.com/ckeditor/ckeditor5/pull/8480
                const index = eventCallbacks.indexOf(callback);
                if (index !== -1) {
                    if (eventCallbacks.length === 1) {
                        delete emitterInfo.callbacks[event];
                    }
                    else {
                        removeEventListener(this, emitter, event, callback);
                    }
                }
            }
            // Only `emitter` and `event` provided. off() all callbacks for that event.
            else if (eventCallbacks) {
                while ((callback = eventCallbacks.pop())) {
                    removeEventListener(this, emitter, 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];
            }
        }
        fire(eventOrInfo, ...args) {
            try {
                const eventInfo = eventOrInfo instanceof EventInfo ? eventOrInfo : new 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._removeEventListener(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.
                const delegations = this[_delegations];
                if (delegations) {
                    const destinations = delegations.get(event);
                    const passAllDestinations = delegations.get('*');
                    if (destinations) {
                        fireDelegatedEvents(destinations, eventInfo, args);
                    }
                    if (passAllDestinations) {
                        fireDelegatedEvents(passAllDestinations, eventInfo, args);
                    }
                }
                return eventInfo.return;
            }
            catch (err) {
                // @if CK_DEBUG // throw err;
                /* istanbul ignore next */
                CKEditorError.rethrowUnexpectedError(err, this);
            }
        }
        delegate(...events) {
            return {
                to: (emitter, nameOrFunction) => {
                    if (!this[_delegations]) {
                        this[_delegations] = new Map();
                    }
                    // Originally there was a for..of loop which unfortunately caused an error in Babel that didn't allow
                    // build an application. See: https://github.com/ckeditor/ckeditor5-react/issues/40.
                    events.forEach(eventName => {
                        const destinations = this[_delegations].get(eventName);
                        if (!destinations) {
                            this[_delegations].set(eventName, new Map([[emitter, nameOrFunction]]));
                        }
                        else {
                            destinations.set(emitter, nameOrFunction);
                        }
                    });
                }
            };
        }
        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);
                }
            }
        }
        _addEventListener(event, callback, options) {
            createEventNamespace(this, event);
            const lists = getCallbacksListsForNamespace(this, event);
            const priority = priorities.get(options.priority);
            const callbackDefinition = {
                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.
                insertToPriorityArray(callbacks, callbackDefinition);
            }
        }
        _removeEventListener(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--;
                    }
                }
            }
        }
    }
    return Mixin;
}
const Emitter$1 = EmitterMixin(Object);
// Backward compatibility with `mix`
([
    'on', 'once', 'off', 'listenTo',
    'stopListening', 'fire', 'delegate', 'stopDelegating',
    '_addEventListener', '_removeEventListener'
]).forEach(key => {
    EmitterMixin[key] = Emitter$1.prototype[key];
});
/**
 * Checks if `listeningEmitter` listens to an emitter with given `listenedToEmitterId` and if so, returns that emitter.
 * If not, returns `null`.
 *
 * @internal
 * @protected
 * @param {module:utils/emittermixin~Emitter} listeningEmitter An emitter that listens.
 * @param {String} listenedToEmitterId Unique emitter id of emitter listened to.
 * @returns {module:utils/emittermixin~Emitter|null}
 */
function _getEmitterListenedTo(listeningEmitter, listenedToEmitterId) {
    const listeningTo = listeningEmitter[_listeningTo];
    if (listeningTo && listeningTo[listenedToEmitterId]) {
        return listeningTo[listenedToEmitterId].emitter;
    }
    return null;
}
/**
 * Sets emitter's unique id.
 *
 * **Note:** `_emitterId` can be set only once.
 *
 * @internal
 * @protected
 * @param {module:utils/emittermixin~Emitter} emitter An 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.
 *
 * @internal
 * @protected
 * @param {module:utils/emittermixin~Emitter} emitter An emitter which id will be returned.
 * @returns {String|undefined}
 */
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).
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 module:utils/emittermixin~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.source, name);
        delegatedInfo.path = [...eventInfo.path];
        emitter.fire(delegatedInfo, ...fireArgs);
    }
}
// Helper for registering event callback on the emitter.
function addEventListener(listener, emitter, event, callback, options) {
    if (emitter._addEventListener) {
        emitter._addEventListener(event, callback, options);
    }
    else {
        // Allow listening on objects that do not implement Emitter interface.
        // This is needed in some tests that are using mocks instead of the real objects with EmitterMixin mixed.
        (listener._addEventListener).call(emitter, event, callback, options);
    }
}
// Helper for removing event callback from the emitter.
function removeEventListener(listener, emitter, event, callback) {
    if (emitter._removeEventListener) {
        emitter._removeEventListener(event, callback);
    }
    else {
        // Allow listening on objects that do not implement Emitter interface.
        // This is needed in some tests that are using mocks instead of the real objects with EmitterMixin mixed.
        listener._removeEventListener.call(emitter, event, callback);
    }
}/** Detect free variable `global` from Node.js. */
var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;

var freeGlobal$1 = freeGlobal;/** Detect free variable `self`. */
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;

/** Used as a reference to the global object. */
var root = freeGlobal$1 || freeSelf || Function('return this')();

var root$1 = root;/** Built-in value references. */
var Symbol$1 = root$1.Symbol;

var Symbol$2 = Symbol$1;/** Used for built-in method references. */
var objectProto$f = Object.prototype;

/** Used to check objects for own properties. */
var hasOwnProperty$c = objectProto$f.hasOwnProperty;

/**
 * Used to resolve the
 * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
 * of values.
 */
var nativeObjectToString$1 = objectProto$f.toString;

/** Built-in value references. */
var symToStringTag$1 = Symbol$2 ? Symbol$2.toStringTag : undefined;

/**
 * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.
 *
 * @private
 * @param {*} value The value to query.
 * @returns {string} Returns the raw `toStringTag`.
 */
function getRawTag(value) {
  var isOwn = hasOwnProperty$c.call(value, symToStringTag$1),
      tag = value[symToStringTag$1];

  try {
    value[symToStringTag$1] = undefined;
    var unmasked = true;
  } catch (e) {}

  var result = nativeObjectToString$1.call(value);
  if (unmasked) {
    if (isOwn) {
      value[symToStringTag$1] = tag;
    } else {
      delete value[symToStringTag$1];
    }
  }
  return result;
}/** Used for built-in method references. */
var objectProto$e = Object.prototype;

/**
 * Used to resolve the
 * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
 * of values.
 */
var nativeObjectToString = objectProto$e.toString;

/**
 * Converts `value` to a string using `Object.prototype.toString`.
 *
 * @private
 * @param {*} value The value to convert.
 * @returns {string} Returns the converted string.
 */
function objectToString(value) {
  return nativeObjectToString.call(value);
}/** `Object#toString` result references. */
var nullTag = '[object Null]',
    undefinedTag = '[object Undefined]';

/** Built-in value references. */
var symToStringTag = Symbol$2 ? Symbol$2.toStringTag : undefined;

/**
 * The base implementation of `getTag` without fallbacks for buggy environments.
 *
 * @private
 * @param {*} value The value to query.
 * @returns {string} Returns the `toStringTag`.
 */
function baseGetTag(value) {
  if (value == null) {
    return value === undefined ? undefinedTag : nullTag;
  }
  return (symToStringTag && symToStringTag in Object(value))
    ? getRawTag(value)
    : objectToString(value);
}/**
 * 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 != null && typeof value == 'object';
}/** `Object#toString` result references. */
var symbolTag$3 = '[object Symbol]';

/**
 * 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 a symbol, else `false`.
 * @example
 *
 * _.isSymbol(Symbol.iterator);
 * // => true
 *
 * _.isSymbol('abc');
 * // => false
 */
function isSymbol(value) {
  return typeof value == 'symbol' ||
    (isObjectLike(value) && baseGetTag(value) == symbolTag$3);
}/**
 * 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 == null ? 0 : array.length,
      result = Array(length);

  while (++index < length) {
    result[index] = iteratee(array[index], index, array);
  }
  return result;
}/**
 * Checks if `value` is classified as an `Array` object.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is an array, else `false`.
 * @example
 *
 * _.isArray([1, 2, 3]);
 * // => true
 *
 * _.isArray(document.body.children);
 * // => false
 *
 * _.isArray('abc');
 * // => false
 *
 * _.isArray(_.noop);
 * // => false
 */
var isArray = Array.isArray;

var isArray$1 = isArray;/** Used as references for various `Number` constants. */
var INFINITY$1 = 1 / 0;

/** Used to convert symbols to primitives and strings. */
var symbolProto$2 = Symbol$2 ? Symbol$2.prototype : undefined,
    symbolToString = symbolProto$2 ? symbolProto$2.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 (isArray$1(value)) {
    // Recursively convert values (susceptible to call stack limits).
    return arrayMap(value, baseToString) + '';
  }
  if (isSymbol(value)) {
    return symbolToString ? symbolToString.call(value) : '';
  }
  var result = (value + '');
  return (result == '0' && (1 / value) == -INFINITY$1) ? '-0' : result;
}/** Used to match a single whitespace character. */
var reWhitespace = /\s/;

/**
 * Used by `_.trim` and `_.trimEnd` to get the index of the last non-whitespace
 * character of `string`.
 *
 * @private
 * @param {string} string The string to inspect.
 * @returns {number} Returns the index of the last non-whitespace character.
 */
function trimmedEndIndex(string) {
  var index = string.length;

  while (index-- && reWhitespace.test(string.charAt(index))) {}
  return index;
}/** Used to match leading whitespace. */
var reTrimStart = /^\s+/;

/**
 * The base implementation of `_.trim`.
 *
 * @private
 * @param {string} string The string to trim.
 * @returns {string} Returns the trimmed string.
 */
function baseTrim(string) {
  return string
    ? string.slice(0, trimmedEndIndex(string) + 1).replace(reTrimStart, '')
    : string;
}/**
 * Checks if `value` is the
 * [language type](http://www.ecma-international.org/ecma-262/7.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 != null && (type == 'object' || type == 'function');
}/** Used as references for various `Number` constants. */
var NAN = 0 / 0;

/** 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 (isSymbol(value)) {
    return NAN;
  }
  if (isObject(value)) {
    var other = typeof value.valueOf == 'function' ? value.valueOf() : value;
    value = isObject(other) ? (other + '') : other;
  }
  if (typeof value != 'string') {
    return value === 0 ? value : +value;
  }
  value = baseTrim(value);
  var isBinary = reIsBinary.test(value);
  return (isBinary || reIsOctal.test(value))
    ? freeParseInt(value.slice(2), isBinary ? 2 : 8)
    : (reIsBadHex.test(value) ? NAN : +value);
}/**
 * This method returns the first argument it receives.
 *
 * @static
 * @since 0.1.0
 * @memberOf _
 * @category Util
 * @param {*} value Any value.
 * @returns {*} Returns `value`.
 * @example
 *
 * var object = { 'a': 1 };
 *
 * console.log(_.identity(object) === object);
 * // => true
 */
function identity(value) {
  return value;
}/** `Object#toString` result references. */
var asyncTag = '[object AsyncFunction]',
    funcTag$2 = '[object Function]',
    genTag$1 = '[object GeneratorFunction]',
    proxyTag = '[object Proxy]';

/**
 * 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 a function, else `false`.
 * @example
 *
 * _.isFunction(_);
 * // => true
 *
 * _.isFunction(/abc/);
 * // => false
 */
function isFunction(value) {
  if (!isObject(value)) {
    return false;
  }
  // The use of `Object#toString` avoids issues with the `typeof` operator
  // in Safari 9 which returns 'object' for typed arrays and other constructors.
  var tag = baseGetTag(value);
  return tag == funcTag$2 || tag == genTag$1 || tag == asyncTag || tag == proxyTag;
}/** Used to detect overreaching core-js shims. */
var coreJsData = root$1['__core-js_shared__'];

var coreJsData$1 = coreJsData;/** Used to detect methods masquerading as native. */
var maskSrcKey = (function() {
  var uid = /[^.]+$/.exec(coreJsData$1 && coreJsData$1.keys && coreJsData$1.keys.IE_PROTO || '');
  return uid ? ('Symbol(src)_1.' + uid) : '';
}());

/**
 * Checks if `func` has its source masked.
 *
 * @private
 * @param {Function} func The function to check.
 * @returns {boolean} Returns `true` if `func` is masked, else `false`.
 */
function isMasked(func) {
  return !!maskSrcKey && (maskSrcKey in func);
}/** Used for built-in method references. */
var funcProto$2 = Function.prototype;

/** Used to resolve the decompiled source of functions. */
var funcToString$2 = funcProto$2.toString;

/**
 * Converts `func` to its source code.
 *
 * @private
 * @param {Function} func The function to convert.
 * @returns {string} Returns the source code.
 */
function toSource(func) {
  if (func != null) {
    try {
      return funcToString$2.call(func);
    } catch (e) {}
    try {
      return (func + '');
    } catch (e) {}
  }
  return '';
}/**
 * Used to match `RegExp`
 * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
 */
var reRegExpChar$1 = /[\\^$.*+?()[\]{}|]/g;

/** Used to detect host constructors (Safari). */
var reIsHostCtor = /^\[object .+?Constructor\]$/;

/** Used for built-in method references. */
var funcProto$1 = Function.prototype,
    objectProto$d = Object.prototype;

/** Used to resolve the decompiled source of functions. */
var funcToString$1 = funcProto$1.toString;

/** Used to check objects for own properties. */
var hasOwnProperty$b = objectProto$d.hasOwnProperty;

/** Used to detect if a method is native. */
var reIsNative = RegExp('^' +
  funcToString$1.call(hasOwnProperty$b).replace(reRegExpChar$1, '\\$&')
  .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
);

/**
 * The base implementation of `_.isNative` without bad shim checks.
 *
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a native function,
 *  else `false`.
 */
function baseIsNative(value) {
  if (!isObject(value) || isMasked(value)) {
    return false;
  }
  var pattern = isFunction(value) ? reIsNative : reIsHostCtor;
  return pattern.test(toSource(value));
}/**
 * Gets the value at `key` of `object`.
 *
 * @private
 * @param {Object} [object] The object to query.
 * @param {string} key The key of the property to get.
 * @returns {*} Returns the property value.
 */
function getValue(object, key) {
  return object == null ? undefined : object[key];
}/**
 * 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 = getValue(object, key);
  return baseIsNative(value) ? value : undefined;
}/* Built-in method references that are verified to be native. */
var WeakMap$1 = getNative(root$1, 'WeakMap');

var WeakMap$2 = WeakMap$1;/** 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} proto The object to inherit from.
 * @returns {Object} Returns the new object.
 */
var baseCreate = (function() {
  function object() {}
  return function(proto) {
    if (!isObject(proto)) {
      return {};
    }
    if (objectCreate) {
      return objectCreate(proto);
    }
    object.prototype = proto;
    var result = new object;
    object.prototype = undefined;
    return result;
  };
}());

var baseCreate$1 = baseCreate;/**
 * 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) {
  switch (args.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);
}/**
 * 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;
}/** Used to detect hot functions by number of calls within a span of milliseconds. */
var HOT_COUNT = 800,
    HOT_SPAN = 16;

/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeNow = Date.now;

/**
 * Creates a function that'll short out and invoke `identity` instead
 * of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN`
 * milliseconds.
 *
 * @private
 * @param {Function} func The function to restrict.
 * @returns {Function} Returns the new shortable function.
 */
function shortOut(func) {
  var count = 0,
      lastCalled = 0;

  return function() {
    var stamp = nativeNow(),
        remaining = HOT_SPAN - (stamp - lastCalled);

    lastCalled = stamp;
    if (remaining > 0) {
      if (++count >= HOT_COUNT) {
        return arguments[0];
      }
    } else {
      count = 0;
    }
    return func.apply(undefined, arguments);
  };
}/**
 * Creates a function that returns `value`.
 *
 * @static
 * @memberOf _
 * @since 2.4.0
 * @category Util
 * @param {*} value The value to return from the new function.
 * @returns {Function} Returns the new constant function.
 * @example
 *
 * var objects = _.times(2, _.constant({ 'a': 1 }));
 *
 * console.log(objects);
 * // => [{ 'a': 1 }, { 'a': 1 }]
 *
 * console.log(objects[0] === objects[1]);
 * // => true
 */
function constant(value) {
  return function() {
    return value;
  };
}var defineProperty = (function() {
  try {
    var func = getNative(Object, 'defineProperty');
    func({}, '', {});
    return func;
  } catch (e) {}
}());

var defineProperty$1 = defineProperty;/**
 * The base implementation of `setToString` without support for hot loop shorting.
 *
 * @private
 * @param {Function} func The function to modify.
 * @param {Function} string The `toString` result.
 * @returns {Function} Returns `func`.
 */
var baseSetToString = !defineProperty$1 ? identity : function(func, string) {
  return defineProperty$1(func, 'toString', {
    'configurable': true,
    'enumerable': false,
    'value': constant(string),
    'writable': true
  });
};

var baseSetToString$1 = baseSetToString;/**
 * Sets the `toString` method of `func` to return `string`.
 *
 * @private
 * @param {Function} func The function to modify.
 * @param {Function} string The `toString` result.
 * @returns {Function} Returns `func`.
 */
var setToString = shortOut(baseSetToString$1);

var setToString$1 = setToString;/**
 * 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 == null ? 0 : array.length;

  while (++index < length) {
    if (iteratee(array[index], index, array) === false) {
      break;
    }
  }
  return array;
}/**
 * The base implementation of `_.findIndex` and `_.findLastIndex` without
 * support for iteratee shorthands.
 *
 * @private
 * @param {Array} array The array to inspect.
 * @param {Function} predicate The function invoked per iteration.
 * @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 value, else `-1`.
 */
function baseFindIndex(array, predicate, fromIndex, fromRight) {
  var length = array.length,
      index = fromIndex + (fromRight ? 1 : -1);

  while ((fromRight ? index-- : ++index < length)) {
    if (predicate(array[index], index, array)) {
      return index;
    }
  }
  return -1;
}/**
 * The base implementation of `_.isNaN` without support for number objects.
 *
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`.
 */
function baseIsNaN(value) {
  return value !== value;
}/**
 * A specialized version of `_.indexOf` which performs strict equality
 * comparisons of values, i.e. `===`.
 *
 * @private
 * @param {Array} array The array to inspect.
 * @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 strictIndexOf(array, value, fromIndex) {
  var index = fromIndex - 1,
      length = array.length;

  while (++index < length) {
    if (array[index] === value) {
      return index;
    }
  }
  return -1;
}/**
 * The base implementation of `_.indexOf` without `fromIndex` bounds checks.
 *
 * @private
 * @param {Array} array The array to inspect.
 * @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) {
  return value === value
    ? strictIndexOf(array, value, fromIndex)
    : baseFindIndex(array, baseIsNaN, fromIndex);
}/** Used as references for various `Number` constants. */
var MAX_SAFE_INTEGER$1 = 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) {
  var type = typeof value;
  length = length == null ? MAX_SAFE_INTEGER$1 : length;

  return !!length &&
    (type == 'number' ||
      (type != 'symbol' && reIsUint.test(value))) &&
        (value > -1 && value % 1 == 0 && value < length);
}/**
 * The base implementation of `assignValue` and `assignMergeValue` without
 * value checks.
 *
 * @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 baseAssignValue(object, key, value) {
  if (key == '__proto__' && defineProperty$1) {
    defineProperty$1(object, key, {
      'configurable': true,
      'enumerable': true,
      'value': value,
      'writable': true
    });
  } else {
    object[key] = value;
  }
}/**
 * Performs a
 * [`SameValueZero`](http://ecma-international.org/ecma-262/7.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 = { 'a': 1 };
 * var other = { 'a': 1 };
 *
 * _.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);
}/** Used for built-in method references. */
var objectProto$c = Object.prototype;

/** Used to check objects for own properties. */
var hasOwnProperty$a = objectProto$c.hasOwnProperty;

/**
 * Assigns `value` to `key` of `object` if the existing value is not equivalent
 * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.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 (!(hasOwnProperty$a.call(object, key) && eq(objValue, value)) ||
      (value === undefined && !(key in object))) {
    baseAssignValue(object, key, value);
  }
}/**
 * 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) {
  var isNew = !object;
  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)
      : undefined;

    if (newValue === undefined) {
      newValue = source[key];
    }
    if (isNew) {
      baseAssignValue(object, key, newValue);
    } else {
      assignValue(object, key, newValue);
    }
  }
  return object;
}/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeMax$1 = Math.max;

/**
 * A specialized version of `baseRest` which transforms the rest array.
 *
 * @private
 * @param {Function} func The function to apply a rest parameter to.
 * @param {number} [start=func.length-1] The start position of the rest parameter.
 * @param {Function} transform The rest array transform.
 * @returns {Function} Returns the new function.
 */
function overRest(func, start, transform) {
  start = nativeMax$1(start === undefined ? (func.length - 1) : start, 0);
  return function() {
    var args = arguments,
        index = -1,
        length = nativeMax$1(args.length - start, 0),
        array = Array(length);

    while (++index < length) {
      array[index] = args[start + index];
    }
    index = -1;
    var otherArgs = Array(start + 1);
    while (++index < start) {
      otherArgs[index] = args[index];
    }
    otherArgs[start] = transform(array);
    return apply(func, this, otherArgs);
  };
}/**
 * The base implementation of `_.rest` which doesn't validate or coerce arguments.
 *
 * @private
 * @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.
 */
function baseRest(func, start) {
  return setToString$1(overRest(func, start, identity), func + '');
}/** Used as references for various `Number` constants. */
var MAX_SAFE_INTEGER = 9007199254740991;

/**
 * Checks if `value` is a valid array-like length.
 *
 * **Note:** This method is loosely based on
 * [`ToLength`](http://ecma-international.org/ecma-262/7.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$1(value) {
  return typeof value == 'number' &&
    value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
}/**
 * 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 && isLength$1(value.length) && !isFunction(value);
}/**
 * 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 (!isObject(object)) {
    return false;
  }
  var type = typeof index;
  if (type == 'number'
        ? (isArrayLike(object) && isIndex(index, object.length))
        : (type == 'string' && index in object)
      ) {
    return eq(object[index], value);
  }
  return false;
}/**
 * 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 baseRest(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;
  });
}/** Used for built-in method references. */
var objectProto$b = 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) || objectProto$b;

  return value === proto;
}/**
 * 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;
}/** `Object#toString` result references. */
var argsTag$3 = '[object Arguments]';

/**
 * The base implementation of `_.isArguments`.
 *
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is an `arguments` object,
 */
function baseIsArguments(value) {
  return isObjectLike(value) && baseGetTag(value) == argsTag$3;
}/** Used for built-in method references. */
var objectProto$a = Object.prototype;

/** Used to check objects for own properties. */
var hasOwnProperty$9 = objectProto$a.hasOwnProperty;

/** Built-in value references. */
var propertyIsEnumerable$1 = objectProto$a.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 an `arguments` object,
 *  else `false`.
 * @example
 *
 * _.isArguments(function() { return arguments; }());
 * // => true
 *
 * _.isArguments([1, 2, 3]);
 * // => false
 */
var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) {
  return isObjectLike(value) && hasOwnProperty$9.call(value, 'callee') &&
    !propertyIsEnumerable$1.call(value, 'callee');
};

var isArguments$1 = isArguments;/**
 * This method returns `false`.
 *
 * @static
 * @memberOf _
 * @since 4.13.0
 * @category Util
 * @returns {boolean} Returns `false`.
 * @example
 *
 * _.times(2, _.stubFalse);
 * // => [false, false]
 */
function stubFalse() {
  return false;
}/** Detect free variable `exports`. */
var freeExports$2 = typeof exports == 'object' && exports && !exports.nodeType && exports;

/** Detect free variable `module`. */
var freeModule$2 = freeExports$2 && typeof module == 'object' && module && !module.nodeType && module;

/** Detect the popular CommonJS extension `module.exports`. */
var moduleExports$2 = freeModule$2 && freeModule$2.exports === freeExports$2;

/** Built-in value references. */
var Buffer$1 = moduleExports$2 ? root$1.Buffer : undefined;

/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeIsBuffer = Buffer$1 ? Buffer$1.isBuffer : undefined;

/**
 * Checks if `value` is a buffer.
 *
 * @static
 * @memberOf _
 * @since 4.3.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a buffer, else `false`.
 * @example
 *
 * _.isBuffer(new Buffer(2));
 * // => true
 *
 * _.isBuffer(new Uint8Array(2));
 * // => false
 */
var isBuffer = nativeIsBuffer || stubFalse;

var isBuffer$1 = isBuffer;/** `Object#toString` result references. */
var argsTag$2 = '[object Arguments]',
    arrayTag$2 = '[object Array]',
    boolTag$3 = '[object Boolean]',
    dateTag$3 = '[object Date]',
    errorTag$2 = '[object Error]',
    funcTag$1 = '[object Function]',
    mapTag$5 = '[object Map]',
    numberTag$3 = '[object Number]',
    objectTag$4 = '[object Object]',
    regexpTag$3 = '[object RegExp]',
    setTag$5 = '[object Set]',
    stringTag$4 = '[object String]',
    weakMapTag$2 = '[object WeakMap]';

var arrayBufferTag$3 = '[object ArrayBuffer]',
    dataViewTag$4 = '[object DataView]',
    float32Tag$2 = '[object Float32Array]',
    float64Tag$2 = '[object Float64Array]',
    int8Tag$2 = '[object Int8Array]',
    int16Tag$2 = '[object Int16Array]',
    int32Tag$2 = '[object Int32Array]',
    uint8Tag$2 = '[object Uint8Array]',
    uint8ClampedTag$2 = '[object Uint8ClampedArray]',
    uint16Tag$2 = '[object Uint16Array]',
    uint32Tag$2 = '[object Uint32Array]';

/** Used to identify `toStringTag` values of typed arrays. */
var typedArrayTags = {};
typedArrayTags[float32Tag$2] = typedArrayTags[float64Tag$2] =
typedArrayTags[int8Tag$2] = typedArrayTags[int16Tag$2] =
typedArrayTags[int32Tag$2] = typedArrayTags[uint8Tag$2] =
typedArrayTags[uint8ClampedTag$2] = typedArrayTags[uint16Tag$2] =
typedArrayTags[uint32Tag$2] = true;
typedArrayTags[argsTag$2] = typedArrayTags[arrayTag$2] =
typedArrayTags[arrayBufferTag$3] = typedArrayTags[boolTag$3] =
typedArrayTags[dataViewTag$4] = typedArrayTags[dateTag$3] =
typedArrayTags[errorTag$2] = typedArrayTags[funcTag$1] =
typedArrayTags[mapTag$5] = typedArrayTags[numberTag$3] =
typedArrayTags[objectTag$4] = typedArrayTags[regexpTag$3] =
typedArrayTags[setTag$5] = typedArrayTags[stringTag$4] =
typedArrayTags[weakMapTag$2] = false;

/**
 * The base implementation of `_.isTypedArray` without Node.js optimizations.
 *
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
 */
function baseIsTypedArray(value) {
  return isObjectLike(value) &&
    isLength$1(value.length) && !!typedArrayTags[baseGetTag(value)];
}/**
 * The base implementation of `_.unary` without support for storing 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);
  };
}/** Detect free variable `exports`. */
var freeExports$1 = typeof exports == 'object' && exports && !exports.nodeType && exports;

/** Detect free variable `module`. */
var freeModule$1 = freeExports$1 && typeof module == 'object' && module && !module.nodeType && module;

/** Detect the popular CommonJS extension `module.exports`. */
var moduleExports$1 = freeModule$1 && freeModule$1.exports === freeExports$1;

/** Detect free variable `process` from Node.js. */
var freeProcess = moduleExports$1 && freeGlobal$1.process;

/** Used to access faster Node.js helpers. */
var nodeUtil = (function() {
  try {
    // Use `util.types` for Node.js 10+.
    var types = freeModule$1 && freeModule$1.require && freeModule$1.require('util').types;

    if (types) {
      return types;
    }

    // Legacy `process.binding('util')` for Node.js < 10.
    return freeProcess && freeProcess.binding && freeProcess.binding('util');
  } catch (e) {}
}());

var nodeUtil$1 = nodeUtil;/* Node.js helper references. */
var nodeIsTypedArray = nodeUtil$1 && nodeUtil$1.isTypedArray;

/**
 * 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 a typed array, else `false`.
 * @example
 *
 * _.isTypedArray(new Uint8Array);
 * // => true
 *
 * _.isTypedArray([]);
 * // => false
 */
var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray;

var isTypedArray$1 = isTypedArray;/** Used for built-in method references. */
var objectProto$9 = Object.prototype;

/** Used to check objects for own properties. */
var hasOwnProperty$8 = objectProto$9.hasOwnProperty;

/**
 * Creates an array of the enumerable property names of the array-like `value`.
 *
 * @private
 * @param {*} value The value to query.
 * @param {boolean} inherited Specify returning inherited property names.
 * @returns {Array} Returns the array of property names.
 */
function arrayLikeKeys(value, inherited) {
  var isArr = isArray$1(value),
      isArg = !isArr && isArguments$1(value),
      isBuff = !isArr && !isArg && isBuffer$1(value),
      isType = !isArr && !isArg && !isBuff && isTypedArray$1(value),
      skipIndexes = isArr || isArg || isBuff || isType,
      result = skipIndexes ? baseTimes(value.length, String) : [],
      length = result.length;

  for (var key in value) {
    if ((inherited || hasOwnProperty$8.call(value, key)) &&
        !(skipIndexes && (
           // Safari 9 has enumerable `arguments.length` in strict mode.
           key == 'length' ||
           // Node.js 0.10 has enumerable non-index properties on buffers.
           (isBuff && (key == 'offset' || key == 'parent')) ||
           // PhantomJS 2 has enumerable non-index properties on typed arrays.
           (isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) ||
           // Skip index properties.
           isIndex(key, length)
        ))) {
      result.push(key);
    }
  }
  return result;
}/**
 * Creates a unary function that invokes `func` with its argument transformed.
 *
 * @private
 * @param {Function} func The function to wrap.
 * @param {Function} transform The argument transform.
 * @returns {Function} Returns the new function.
 */
function overArg(func, transform) {
  return function(arg) {
    return func(transform(arg));
  };
}/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeKeys = overArg(Object.keys, Object);

var nativeKeys$1 = nativeKeys;/** Used for built-in method references. */
var objectProto$8 = Object.prototype;

/** Used to check objects for own properties. */
var hasOwnProperty$7 = objectProto$8.hasOwnProperty;

/**
 * The base implementation of `_.keys` which doesn't treat sparse arrays as dense.
 *
 * @private
 * @param {Object} object The object to query.
 * @returns {Array} Returns the array of property names.
 */
function baseKeys(object) {
  if (!isPrototype(object)) {
    return nativeKeys$1(object);
  }
  var result = [];
  for (var key in Object(object)) {
    if (hasOwnProperty$7.call(object, key) && key != 'constructor') {
      result.push(key);
    }
  }
  return result;
}/**
 * 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/7.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(object) {
  return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
}/**
 * This function is like
 * [`Object.keys`](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
 * except that it includes inherited enumerable properties.
 *
 * @private
 * @param {Object} object The object to query.
 * @returns {Array} Returns the array of property names.
 */
function nativeKeysIn(object) {
  var result = [];
  if (object != null) {
    for (var key in Object(object)) {
      result.push(key);
    }
  }
  return result;
}/** Used for built-in method references. */
var objectProto$7 = Object.prototype;

/** Used to check objects for own properties. */
var hasOwnProperty$6 = objectProto$7.hasOwnProperty;

/**
 * The base implementation of `_.keysIn` which doesn't treat sparse arrays as dense.
 *
 * @private
 * @param {Object} object The object to query.
 * @returns {Array} Returns the array of property names.
 */
function baseKeysIn(object) {
  if (!isObject(object)) {
    return nativeKeysIn(object);
  }
  var isProto = isPrototype(object),
      result = [];

  for (var key in object) {
    if (!(key == 'constructor' && (isProto || !hasOwnProperty$6.call(object, key)))) {
      result.push(key);
    }
  }
  return result;
}/**
 * 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) {
  return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object);
}/**
 * 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.a = 1;
 * }
 *
 * function Bar() {
 *   this.c = 3;
 * }
 *
 * Foo.prototype.b = 2;
 * Bar.prototype.d = 4;
 *
 * _.assignIn({ 'a': 0 }, new Foo, new Bar);
 * // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4 }
 */
var assignIn = createAssigner(function(object, source) {
  copyObject(source, keysIn(source), object);
});

var extend = assignIn;/** 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 (isArray$1(value)) {
    return false;
  }
  var type = typeof value;
  if (type == 'number' || type == 'symbol' || type == 'boolean' ||
      value == null || isSymbol(value)) {
    return true;
  }
  return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
    (object != null && value in Object(object));
}/* Built-in method references that are verified to be native. */
var nativeCreate = getNative(Object, 'create');

var nativeCreate$1 = nativeCreate;/**
 * Removes all key-value entries from the hash.
 *
 * @private
 * @name clear
 * @memberOf Hash
 */
function hashClear() {
  this.__data__ = nativeCreate$1 ? nativeCreate$1(null) : {};
  this.size = 0;
}/**
 * 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) {
  var result = this.has(key) && delete this.__data__[key];
  this.size -= result ? 1 : 0;
  return result;
}/** Used to stand-in for `undefined` hash values. */
var HASH_UNDEFINED$2 = '__lodash_hash_undefined__';

/** Used for built-in method references. */
var objectProto$6 = Object.prototype;

/** Used to check objects for own properties. */
var hasOwnProperty$5 = objectProto$6.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$1) {
    var result = data[key];
    return result === HASH_UNDEFINED$2 ? undefined : result;
  }
  return hasOwnProperty$5.call(data, key) ? data[key] : undefined;
}/** Used for built-in method references. */
var objectProto$5 = Object.prototype;

/** Used to check objects for own properties. */
var hasOwnProperty$4 = objectProto$5.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$1 ? (data[key] !== undefined) : hasOwnProperty$4.call(data, key);
}/** Used to stand-in for `undefined` hash values. */
var HASH_UNDEFINED$1 = '__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__;
  this.size += this.has(key) ? 0 : 1;
  data[key] = (nativeCreate$1 && value === undefined) ? HASH_UNDEFINED$1 : value;
  return this;
}/**
 * Creates a hash object.
 *
 * @private
 * @constructor
 * @param {Array} [entries] The key-value pairs to cache.
 */
function Hash(entries) {
  var index = -1,
      length = entries == null ? 0 : entries.length;

  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;/**
 * Removes all key-value entries from the list cache.
 *
 * @private
 * @name clear
 * @memberOf ListCache
 */
function listCacheClear() {
  this.__data__ = [];
  this.size = 0;
}/**
 * Gets the index at which the `key` is found in `array` of key-value pairs.
 *
 * @private
 * @param {Array} array The array to inspect.
 * @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 (eq(array[length][0], key)) {
      return length;
    }
  }
  return -1;
}/** Used for built-in method references. */
var arrayProto$1 = Array.prototype;

/** Built-in value references. */
var splice$1 = arrayProto$1.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$1.call(data, index, 1);
  }
  --this.size;
  return true;
}/**
 * 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];
}/**
 * 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;
}/**
 * 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) {
    ++this.size;
    data.push([key, value]);
  } else {
    data[index][1] = value;
  }
  return this;
}/**
 * Creates an list cache object.
 *
 * @private
 * @constructor
 * @param {Array} [entries] The key-value pairs to cache.
 */
function ListCache(entries) {
  var index = -1,
      length = entries == null ? 0 : entries.length;

  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;/* Built-in method references that are verified to be native. */
var Map$1 = getNative(root$1, 'Map');

var Map$2 = Map$1;/**
 * Removes all key-value entries from the map.
 *
 * @private
 * @name clear
 * @memberOf MapCache
 */
function mapCacheClear() {
  this.size = 0;
  this.__data__ = {
    'hash': new Hash,
    'map': new (Map$2 || ListCache),
    'string': new Hash
  };
}/**
 * 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);
}/**
 * 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;
}/**
 * 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) {
  var result = getMapData(this, key)['delete'](key);
  this.size -= result ? 1 : 0;
  return result;
}/**
 * 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);
}/**
 * 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);
}/**
 * 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) {
  var data = getMapData(this, key),
      size = data.size;

  data.set(key, value);
  this.size += data.size == size ? 0 : 1;
  return this;
}/**
 * 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 == null ? 0 : entries.length;

  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;/** Error message constants. */
var FUNC_ERROR_TEXT$2 = '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/7.0/#sec-properties-of-the-map-prototype-object)
 * method interface of `clear`, `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 != null && typeof resolver != 'function')) {
    throw new TypeError(FUNC_ERROR_TEXT$2);
  }
  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) || cache;
    return result;
  };
  memoized.cache = new (memoize.Cache || MapCache);
  return memoized;
}

// Expose `MapCache`.
memoize.Cache = MapCache;/** Used as the maximum memoize cache size. */
var MAX_MEMOIZE_SIZE = 500;

/**
 * A specialized version of `_.memoize` which clears the memoized function's
 * cache when it exceeds `MAX_MEMOIZE_SIZE`.
 *
 * @private
 * @param {Function} func The function to have its output memoized.
 * @returns {Function} Returns the new memoized function.
 */
function memoizeCapped(func) {
  var result = memoize(func, function(key) {
    if (cache.size === MAX_MEMOIZE_SIZE) {
      cache.clear();
    }
    return key;
  });

  var cache = result.cache;
  return result;
}/** 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 = memoizeCapped(function(string) {
  var result = [];
  if (string.charCodeAt(0) === 46 /* . */) {
    result.push('');
  }
  string.replace(rePropName, function(match, number, quote, subString) {
    result.push(quote ? subString.replace(reEscapeChar, '$1') : (number || match));
  });
  return result;
});

var stringToPath$1 = stringToPath;/**
 * 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 convert.
 * @returns {string} Returns the converted string.
 * @example
 *
 * _.toString(null);
 * // => ''
 *
 * _.toString(-0);
 * // => '-0'
 *
 * _.toString([1, 2, 3]);
 * // => '1,2,3'
 */
function toString(value) {
  return value == null ? '' : baseToString(value);
}/**
 * Casts `value` to a path array if it's not one.
 *
 * @private
 * @param {*} value The value to inspect.
 * @param {Object} [object] The object to query keys on.
 * @returns {Array} Returns the cast property path array.
 */
function castPath(value, object) {
  if (isArray$1(value)) {
    return value;
  }
  return isKey(value, object) ? [value] : stringToPath$1(toString(value));
}/** Used as references for various `Number` constants. */
var 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' || isSymbol(value)) {
    return value;
  }
  var result = (value + '');
  return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
}/**
 * 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 = castPath(path, object);

  var index = 0,
      length = path.length;

  while (object != null && index < length) {
    object = object[toKey(path[index++])];
  }
  return (index && index == length) ? object : undefined;
}/**
 * Gets the value at `path` of `object`. If the resolved value is
 * `undefined`, the `defaultValue` is returned 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;
}/**
 * 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;
}/** Built-in value references. */
var getPrototype = overArg(Object.getPrototypeOf, Object);

var getPrototype$1 = getPrototype;/** `Object#toString` result references. */
var objectTag$3 = '[object Object]';

/** Used for built-in method references. */
var funcProto = Function.prototype,
    objectProto$4 = Object.prototype;

/** Used to resolve the decompiled source of functions. */
var funcToString = funcProto.toString;

/** Used to check objects for own properties. */
var hasOwnProperty$3 = objectProto$4.hasOwnProperty;

/** Used to infer the `Object` constructor. */
var objectCtorString = funcToString.call(Object);

/**
 * 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 (!isObjectLike(value) || baseGetTag(value) != objectTag$3) {
    return false;
  }
  var proto = getPrototype$1(value);
  if (proto === null) {
    return true;
  }
  var Ctor = hasOwnProperty$3.call(proto, 'constructor') && proto.constructor;
  return typeof Ctor == 'function' && Ctor instanceof Ctor &&
    funcToString.call(Ctor) == objectCtorString;
}/**
 * 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;
}/**
 * Casts `array` to a slice if it's needed.
 *
 * @private
 * @param {Array} array The array to inspect.
 * @param {number} start The start position.
 * @param {number} [end=array.length] The end position.
 * @returns {Array} Returns the cast slice.
 */
function castSlice(array, start, end) {
  var length = array.length;
  end = end === undefined ? length : end;
  return (!start && end >= length) ? array : baseSlice(array, start, end);
}/** Used to compose unicode character classes. */
var rsAstralRange$1 = '\\ud800-\\udfff',
    rsComboMarksRange$1 = '\\u0300-\\u036f',
    reComboHalfMarksRange$1 = '\\ufe20-\\ufe2f',
    rsComboSymbolsRange$1 = '\\u20d0-\\u20ff',
    rsComboRange$1 = rsComboMarksRange$1 + reComboHalfMarksRange$1 + rsComboSymbolsRange$1,
    rsVarRange$1 = '\\ufe0e\\ufe0f';

/** Used to compose unicode capture groups. */
var rsZWJ$1 = '\\u200d';

/** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */
var reHasUnicode = RegExp('[' + rsZWJ$1 + rsAstralRange$1  + rsComboRange$1 + rsVarRange$1 + ']');

/**
 * Checks if `string` contains Unicode symbols.
 *
 * @private
 * @param {string} string The string to inspect.
 * @returns {boolean} Returns `true` if a symbol is found, else `false`.
 */
function hasUnicode(string) {
  return reHasUnicode.test(string);
}/**
 * Converts an ASCII `string` to an array.
 *
 * @private
 * @param {string} string The string to convert.
 * @returns {Array} Returns the converted array.
 */
function asciiToArray(string) {
  return string.split('');
}/** Used to compose unicode character classes. */
var rsAstralRange = '\\ud800-\\udfff',
    rsComboMarksRange = '\\u0300-\\u036f',
    reComboHalfMarksRange = '\\ufe20-\\ufe2f',
    rsComboSymbolsRange = '\\u20d0-\\u20ff',
    rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange,
    rsVarRange = '\\ufe0e\\ufe0f';

/** Used to compose unicode capture groups. */
var rsAstral = '[' + rsAstralRange + ']',
    rsCombo = '[' + rsComboRange + ']',
    rsFitz = '\\ud83c[\\udffb-\\udfff]',
    rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')',
    rsNonAstral = '[^' + rsAstralRange + ']',
    rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}',
    rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]',
    rsZWJ = '\\u200d';

/** Used to compose unicode regexes. */
var reOptMod = rsModifier + '?',
    rsOptVar = '[' + rsVarRange + ']?',
    rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*',
    rsSeq = rsOptVar + reOptMod + rsOptJoin,
    rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')';

/** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */
var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g');

/**
 * Converts a Unicode `string` to an array.
 *
 * @private
 * @param {string} string The string to convert.
 * @returns {Array} Returns the converted array.
 */
function unicodeToArray(string) {
  return string.match(reUnicode) || [];
}/**
 * Converts `string` to an array.
 *
 * @private
 * @param {string} string The string to convert.
 * @returns {Array} Returns the converted array.
 */
function stringToArray(string) {
  return hasUnicode(string)
    ? unicodeToArray(string)
    : asciiToArray(string);
}/**
 * Creates a function like `_.lowerFirst`.
 *
 * @private
 * @param {string} methodName The name of the `String` case method to use.
 * @returns {Function} Returns the new case function.
 */
function createCaseFirst(methodName) {
  return function(string) {
    string = toString(string);

    var strSymbols = hasUnicode(string)
      ? stringToArray(string)
      : undefined;

    var chr = strSymbols
      ? strSymbols[0]
      : string.charAt(0);

    var trailing = strSymbols
      ? castSlice(strSymbols, 1).join('')
      : string.slice(1);

    return chr[methodName]() + trailing;
  };
}/**
 * Converts the first character of `string` to upper case.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category String
 * @param {string} [string=''] The string to convert.
 * @returns {string} Returns the converted string.
 * @example
 *
 * _.upperFirst('fred');
 * // => 'Fred'
 *
 * _.upperFirst('FRED');
 * // => 'FRED'
 */
var upperFirst = createCaseFirst('toUpperCase');

var upperFirst$1 = upperFirst;/**
 * Removes all key-value entries from the stack.
 *
 * @private
 * @name clear
 * @memberOf Stack
 */
function stackClear() {
  this.__data__ = new ListCache;
  this.size = 0;
}/**
 * 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) {
  var data = this.__data__,
      result = data['delete'](key);

  this.size = data.size;
  return result;
}/**
 * 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);
}/**
 * 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);
}/** 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 data = this.__data__;
  if (data instanceof ListCache) {
    var pairs = data.__data__;
    if (!Map$2 || (pairs.length < LARGE_ARRAY_SIZE - 1)) {
      pairs.push([key, value]);
      this.size = ++data.size;
      return this;
    }
    data = this.__data__ = new MapCache(pairs);
  }
  data.set(key, value);
  this.size = data.size;
  return this;
}/**
 * Creates a stack cache object to store key-value pairs.
 *
 * @private
 * @constructor
 * @param {Array} [entries] The key-value pairs to cache.
 */
function Stack(entries) {
  var data = this.__data__ = new ListCache(entries);
  this.size = data.size;
}

// Add methods to `Stack`.
Stack.prototype.clear = stackClear;
Stack.prototype['delete'] = stackDelete;
Stack.prototype.get = stackGet;
Stack.prototype.has = stackHas;
Stack.prototype.set = stackSet;/**
 * 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, keys(source), object);
}/**
 * The base implementation of `_.assignIn` 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 baseAssignIn(object, source) {
  return object && copyObject(source, keysIn(source), object);
}/** Detect free variable `exports`. */
var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;

/** Detect free variable `module`. */
var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;

/** Detect the popular CommonJS extension `module.exports`. */
var moduleExports = freeModule && freeModule.exports === freeExports;

/** Built-in value references. */
var Buffer = moduleExports ? root$1.Buffer : undefined,
    allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined;

/**
 * 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 length = buffer.length,
      result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length);

  buffer.copy(result);
  return result;
}/**
 * A specialized version of `_.filter` 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 {Array} Returns the new filtered array.
 */
function arrayFilter(array, predicate) {
  var index = -1,
      length = array == null ? 0 : array.length,
      resIndex = 0,
      result = [];

  while (++index < length) {
    var value = array[index];
    if (predicate(value, index, array)) {
      result[resIndex++] = value;
    }
  }
  return result;
}/**
 * This method returns a new empty array.
 *
 * @static
 * @memberOf _
 * @since 4.13.0
 * @category Util
 * @returns {Array} Returns the new empty array.
 * @example
 *
 * var arrays = _.times(2, _.stubArray);
 *
 * console.log(arrays);
 * // => [[], []]
 *
 * console.log(arrays[0] === arrays[1]);
 * // => false
 */
function stubArray() {
  return [];
}/** Used for built-in method references. */
var objectProto$3 = Object.prototype;

/** Built-in value references. */
var propertyIsEnumerable = objectProto$3.propertyIsEnumerable;

/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeGetSymbols$1 = Object.getOwnPropertySymbols;

/**
 * Creates an array of the own enumerable symbols of `object`.
 *
 * @private
 * @param {Object} object The object to query.
 * @returns {Array} Returns the array of symbols.
 */
var getSymbols = !nativeGetSymbols$1 ? stubArray : function(object) {
  if (object == null) {
    return [];
  }
  object = Object(object);
  return arrayFilter(nativeGetSymbols$1(object), function(symbol) {
    return propertyIsEnumerable.call(object, symbol);
  });
};

var getSymbols$1 = getSymbols;/**
 * Copies own symbols 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$1(source), object);
}/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeGetSymbols = Object.getOwnPropertySymbols;

/**
 * Creates an array of the own and inherited enumerable symbols of `object`.
 *
 * @private
 * @param {Object} object The object to query.
 * @returns {Array} Returns the array of symbols.
 */
var getSymbolsIn = !nativeGetSymbols ? stubArray : function(object) {
  var result = [];
  while (object) {
    arrayPush(result, getSymbols$1(object));
    object = getPrototype$1(object);
  }
  return result;
};

var getSymbolsIn$1 = getSymbolsIn;/**
 * Copies own and inherited symbols 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 copySymbolsIn(source, object) {
  return copyObject(source, getSymbolsIn$1(source), object);
}/**
 * 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 isArray$1(object) ? result : arrayPush(result, symbolsFunc(object));
}/**
 * 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, keys, getSymbols$1);
}/**
 * Creates an array of own and inherited 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 getAllKeysIn(object) {
  return baseGetAllKeys(object, keysIn, getSymbolsIn$1);
}/* Built-in method references that are verified to be native. */
var DataView = getNative(root$1, 'DataView');

var DataView$1 = DataView;/* Built-in method references that are verified to be native. */
var Promise$1 = getNative(root$1, 'Promise');

var Promise$2 = Promise$1;/* Built-in method references that are verified to be native. */
var Set$1 = getNative(root$1, 'Set');

var Set$2 = Set$1;/** `Object#toString` result references. */
var mapTag$4 = '[object Map]',
    objectTag$2 = '[object Object]',
    promiseTag = '[object Promise]',
    setTag$4 = '[object Set]',
    weakMapTag$1 = '[object WeakMap]';

var dataViewTag$3 = '[object DataView]';

/** Used to detect maps, sets, and weakmaps. */
var dataViewCtorString = toSource(DataView$1),
    mapCtorString = toSource(Map$2),
    promiseCtorString = toSource(Promise$2),
    setCtorString = toSource(Set$2),
    weakMapCtorString = toSource(WeakMap$2);

/**
 * Gets the `toStringTag` of `value`.
 *
 * @private
 * @param {*} value The value to query.
 * @returns {string} Returns the `toStringTag`.
 */
var getTag = baseGetTag;

// Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6.
if ((DataView$1 && getTag(new DataView$1(new ArrayBuffer(1))) != dataViewTag$3) ||
    (Map$2 && getTag(new Map$2) != mapTag$4) ||
    (Promise$2 && getTag(Promise$2.resolve()) != promiseTag) ||
    (Set$2 && getTag(new Set$2) != setTag$4) ||
    (WeakMap$2 && getTag(new WeakMap$2) != weakMapTag$1)) {
  getTag = function(value) {
    var result = baseGetTag(value),
        Ctor = result == objectTag$2 ? value.constructor : undefined,
        ctorString = Ctor ? toSource(Ctor) : '';

    if (ctorString) {
      switch (ctorString) {
        case dataViewCtorString: return dataViewTag$3;
        case mapCtorString: return mapTag$4;
        case promiseCtorString: return promiseTag;
        case setCtorString: return setTag$4;
        case weakMapCtorString: return weakMapTag$1;
      }
    }
    return result;
  };
}

var getTag$1 = getTag;/** Used for built-in method references. */
var objectProto$2 = Object.prototype;

/** Used to check objects for own properties. */
var hasOwnProperty$2 = objectProto$2.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 = new array.constructor(length);

  // Add properties assigned by `RegExp#exec`.
  if (length && typeof array[0] == 'string' && hasOwnProperty$2.call(array, 'index')) {
    result.index = array.index;
    result.input = array.input;
  }
  return result;
}/** Built-in value references. */
var Uint8Array = root$1.Uint8Array;

var Uint8Array$1 = Uint8Array;/**
 * 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$1(result).set(new Uint8Array$1(arrayBuffer));
  return result;
}/**
 * 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);
}/** 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;
}/** Used to convert symbols to primitives and strings. */
var symbolProto$1 = Symbol$2 ? Symbol$2.prototype : undefined,
    symbolValueOf$1 = symbolProto$1 ? symbolProto$1.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$1 ? Object(symbolValueOf$1.call(symbol)) : {};
}/**
 * 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);
}/** `Object#toString` result references. */
var boolTag$2 = '[object Boolean]',
    dateTag$2 = '[object Date]',
    mapTag$3 = '[object Map]',
    numberTag$2 = '[object Number]',
    regexpTag$2 = '[object RegExp]',
    setTag$3 = '[object Set]',
    stringTag$3 = '[object String]',
    symbolTag$2 = '[object Symbol]';

var arrayBufferTag$2 = '[object ArrayBuffer]',
    dataViewTag$2 = '[object DataView]',
    float32Tag$1 = '[object Float32Array]',
    float64Tag$1 = '[object Float64Array]',
    int8Tag$1 = '[object Int8Array]',
    int16Tag$1 = '[object Int16Array]',
    int32Tag$1 = '[object Int32Array]',
    uint8Tag$1 = '[object Uint8Array]',
    uint8ClampedTag$1 = '[object Uint8ClampedArray]',
    uint16Tag$1 = '[object Uint16Array]',
    uint32Tag$1 = '[object Uint32Array]';

/**
 * Initializes an object clone based on its `toStringTag`.
 *
 * **Note:** This function only supports cloning values with tags of
 * `Boolean`, `Date`, `Error`, `Map`, `Number`, `RegExp`, `Set`, or `String`.
 *
 * @private
 * @param {Object} object The object to clone.
 * @param {string} tag The `toStringTag` of the object to clone.
 * @param {boolean} [isDeep] Specify a deep clone.
 * @returns {Object} Returns the initialized clone.
 */
function initCloneByTag(object, tag, isDeep) {
  var Ctor = object.constructor;
  switch (tag) {
    case arrayBufferTag$2:
      return cloneArrayBuffer(object);

    case boolTag$2:
    case dateTag$2:
      return new Ctor(+object);

    case dataViewTag$2:
      return cloneDataView(object, isDeep);

    case float32Tag$1: case float64Tag$1:
    case int8Tag$1: case int16Tag$1: case int32Tag$1:
    case uint8Tag$1: case uint8ClampedTag$1: case uint16Tag$1: case uint32Tag$1:
      return cloneTypedArray(object, isDeep);

    case mapTag$3:
      return new Ctor;

    case numberTag$2:
    case stringTag$3:
      return new Ctor(object);

    case regexpTag$2:
      return cloneRegExp(object);

    case setTag$3:
      return new Ctor;

    case symbolTag$2:
      return cloneSymbol(object);
  }
}/**
 * 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$1(getPrototype$1(object))
    : {};
}/** `Object#toString` result references. */
var mapTag$2 = '[object Map]';

/**
 * The base implementation of `_.isMap` without Node.js optimizations.
 *
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a map, else `false`.
 */
function baseIsMap(value) {
  return isObjectLike(value) && getTag$1(value) == mapTag$2;
}/* Node.js helper references. */
var nodeIsMap = nodeUtil$1 && nodeUtil$1.isMap;

/**
 * Checks if `value` is classified as a `Map` object.
 *
 * @static
 * @memberOf _
 * @since 4.3.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a map, else `false`.
 * @example
 *
 * _.isMap(new Map);
 * // => true
 *
 * _.isMap(new WeakMap);
 * // => false
 */
var isMap = nodeIsMap ? baseUnary(nodeIsMap) : baseIsMap;

var isMap$1 = isMap;/** `Object#toString` result references. */
var setTag$2 = '[object Set]';

/**
 * The base implementation of `_.isSet` without Node.js optimizations.
 *
 * @private
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a set, else `false`.
 */
function baseIsSet(value) {
  return isObjectLike(value) && getTag$1(value) == setTag$2;
}/* Node.js helper references. */
var nodeIsSet = nodeUtil$1 && nodeUtil$1.isSet;

/**
 * Checks if `value` is classified as a `Set` object.
 *
 * @static
 * @memberOf _
 * @since 4.3.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a set, else `false`.
 * @example
 *
 * _.isSet(new Set);
 * // => true
 *
 * _.isSet(new WeakSet);
 * // => false
 */
var isSet = nodeIsSet ? baseUnary(nodeIsSet) : baseIsSet;

var isSet$1 = isSet;/** Used to compose bitmasks for cloning. */
var CLONE_DEEP_FLAG$2 = 1,
    CLONE_FLAT_FLAG = 2,
    CLONE_SYMBOLS_FLAG$3 = 4;

/** `Object#toString` result references. */
var argsTag$1 = '[object Arguments]',
    arrayTag$1 = '[object Array]',
    boolTag$1 = '[object Boolean]',
    dateTag$1 = '[object Date]',
    errorTag$1 = '[object Error]',
    funcTag = '[object Function]',
    genTag = '[object GeneratorFunction]',
    mapTag$1 = '[object Map]',
    numberTag$1 = '[object Number]',
    objectTag$1 = '[object Object]',
    regexpTag$1 = '[object RegExp]',
    setTag$1 = '[object Set]',
    stringTag$2 = '[object String]',
    symbolTag$1 = '[object Symbol]',
    weakMapTag = '[object WeakMap]';

var arrayBufferTag$1 = '[object ArrayBuffer]',
    dataViewTag$1 = '[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]';

/** Used to identify `toStringTag` values supported by `_.clone`. */
var cloneableTags = {};
cloneableTags[argsTag$1] = cloneableTags[arrayTag$1] =
cloneableTags[arrayBufferTag$1] = cloneableTags[dataViewTag$1] =
cloneableTags[boolTag$1] = cloneableTags[dateTag$1] =
cloneableTags[float32Tag] = cloneableTags[float64Tag] =
cloneableTags[int8Tag] = cloneableTags[int16Tag] =
cloneableTags[int32Tag] = cloneableTags[mapTag$1] =
cloneableTags[numberTag$1] = cloneableTags[objectTag$1] =
cloneableTags[regexpTag$1] = cloneableTags[setTag$1] =
cloneableTags[stringTag$2] = cloneableTags[symbolTag$1] =
cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] =
cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true;
cloneableTags[errorTag$1] = cloneableTags[funcTag] =
cloneableTags[weakMapTag] = false;

/**
 * The base implementation of `_.clone` and `_.cloneDeep` which tracks
 * traversed objects.
 *
 * @private
 * @param {*} value The value to clone.
 * @param {boolean} bitmask The bitmask flags.
 *  1 - Deep clone
 *  2 - Flatten inherited properties
 *  4 - Clone 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, bitmask, customizer, key, object, stack) {
  var result,
      isDeep = bitmask & CLONE_DEEP_FLAG$2,
      isFlat = bitmask & CLONE_FLAT_FLAG,
      isFull = bitmask & CLONE_SYMBOLS_FLAG$3;

  if (customizer) {
    result = object ? customizer(value, key, object, stack) : customizer(value);
  }
  if (result !== undefined) {
    return result;
  }
  if (!isObject(value)) {
    return value;
  }
  var isArr = isArray$1(value);
  if (isArr) {
    result = initCloneArray(value);
    if (!isDeep) {
      return copyArray(value, result);
    }
  } else {
    var tag = getTag$1(value),
        isFunc = tag == funcTag || tag == genTag;

    if (isBuffer$1(value)) {
      return cloneBuffer(value, isDeep);
    }
    if (tag == objectTag$1 || tag == argsTag$1 || (isFunc && !object)) {
      result = (isFlat || isFunc) ? {} : initCloneObject(value);
      if (!isDeep) {
        return isFlat
          ? copySymbolsIn(value, baseAssignIn(result, value))
          : copySymbols(value, baseAssign(result, value));
      }
    } else {
      if (!cloneableTags[tag]) {
        return object ? value : {};
      }
      result = initCloneByTag(value, tag, 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 (isSet$1(value)) {
    value.forEach(function(subValue) {
      result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack));
    });
  } else if (isMap$1(value)) {
    value.forEach(function(subValue, key) {
      result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack));
    });
  }

  var keysFunc = isFull
    ? (isFlat ? getAllKeysIn : getAllKeys)
    : (isFlat ? keysIn : keys);

  var props = isArr ? undefined : keysFunc(value);
  arrayEach(props || value, function(subValue, key) {
    if (props) {
      key = subValue;
      subValue = value[key];
    }
    // Recursively populate clone (susceptible to call stack limits).
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));
  });
  return result;
}/** Used to compose bitmasks for cloning. */
var CLONE_SYMBOLS_FLAG$2 = 4;

/**
 * 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$1(value) {
  return baseClone(value, CLONE_SYMBOLS_FLAG$2);
}/** Used to compose bitmasks for cloning. */
var CLONE_DEEP_FLAG$1 = 1,
    CLONE_SYMBOLS_FLAG$1 = 4;

/**
 * This method is like `_.clone` except that it recursively clones `value`.
 *
 * @static
 * @memberOf _
 * @since 1.0.0
 * @category Lang
 * @param {*} value The value to recursively clone.
 * @returns {*} Returns the deep cloned value.
 * @see _.clone
 * @example
 *
 * var objects = [{ 'a': 1 }, { 'b': 2 }];
 *
 * var deep = _.cloneDeep(objects);
 * console.log(deep[0] === objects[0]);
 * // => false
 */
function cloneDeep(value) {
  return baseClone(value, CLONE_DEEP_FLAG$1 | CLONE_SYMBOLS_FLAG$1);
}/** Used to compose bitmasks for cloning. */
var CLONE_DEEP_FLAG = 1,
    CLONE_SYMBOLS_FLAG = 4;

/**
 * This method is like `_.cloneWith` except that it recursively clones `value`.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to recursively clone.
 * @param {Function} [customizer] The function to customize cloning.
 * @returns {*} Returns the deep cloned value.
 * @see _.cloneWith
 * @example
 *
 * function customizer(value) {
 *   if (_.isElement(value)) {
 *     return value.cloneNode(true);
 *   }
 * }
 *
 * var el = _.cloneDeepWith(document.body, customizer);
 *
 * console.log(el === document.body);
 * // => false
 * console.log(el.nodeName);
 * // => 'BODY'
 * console.log(el.childNodes.length);
 * // => 20
 */
function cloneDeepWith(value, customizer) {
  customizer = typeof customizer == 'function' ? customizer : undefined;
  return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG, customizer);
}/** Used to stand-in for `undefined` hash values. */
var 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, HASH_UNDEFINED);
  return this;
}/**
 * 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);
}/**
 *
 * 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 == null ? 0 : values.length;

  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;/**
 * 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 == null ? 0 : array.length;

  while (++index < length) {
    if (predicate(array[index], index, array)) {
      return true;
    }
  }
  return false;
}/**
 * 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);
}/** Used to compose bitmasks for value comparisons. */
var COMPARE_PARTIAL_FLAG$3 = 1,
    COMPARE_UNORDERED_FLAG$1 = 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 {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
 * @param {Function} customizer The function to customize comparisons.
 * @param {Function} equalFunc The function to determine equivalents of values.
 * @param {Object} stack Tracks traversed `array` and `other` objects.
 * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`.
 */
function equalArrays(array, other, bitmask, customizer, equalFunc, stack) {
  var isPartial = bitmask & COMPARE_PARTIAL_FLAG$3,
      arrLength = array.length,
      othLength = other.length;

  if (arrLength != othLength && !(isPartial && othLength > arrLength)) {
    return false;
  }
  // Check that cyclic values are equal.
  var arrStacked = stack.get(array);
  var othStacked = stack.get(other);
  if (arrStacked && othStacked) {
    return arrStacked == other && othStacked == array;
  }
  var index = -1,
      result = true,
      seen = (bitmask & COMPARE_UNORDERED_FLAG$1) ? new SetCache : undefined;

  stack.set(array, other);
  stack.set(other, array);

  // 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 (!cacheHas(seen, othIndex) &&
                (arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) {
              return seen.push(othIndex);
            }
          })) {
        result = false;
        break;
      }
    } else if (!(
          arrValue === othValue ||
            equalFunc(arrValue, othValue, bitmask, customizer, stack)
        )) {
      result = false;
      break;
    }
  }
  stack['delete'](array);
  stack['delete'](other);
  return result;
}/**
 * 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;
}/**
 * 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;
}/** Used to compose bitmasks for value comparisons. */
var COMPARE_PARTIAL_FLAG$2 = 1,
    COMPARE_UNORDERED_FLAG = 2;

/** `Object#toString` result references. */
var boolTag = '[object Boolean]',
    dateTag = '[object Date]',
    errorTag = '[object Error]',
    mapTag = '[object Map]',
    numberTag = '[object Number]',
    regexpTag = '[object RegExp]',
    setTag = '[object Set]',
    stringTag$1 = '[object String]',
    symbolTag = '[object Symbol]';

var arrayBufferTag = '[object ArrayBuffer]',
    dataViewTag = '[object DataView]';

/** Used to convert symbols to primitives and strings. */
var symbolProto = Symbol$2 ? Symbol$2.prototype : undefined,
    symbolValueOf = symbolProto ? 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 {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
 * @param {Function} customizer The function to customize comparisons.
 * @param {Function} equalFunc The function to determine equivalents of values.
 * @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, bitmask, customizer, equalFunc, stack) {
  switch (tag) {
    case dataViewTag:
      if ((object.byteLength != other.byteLength) ||
          (object.byteOffset != other.byteOffset)) {
        return false;
      }
      object = object.buffer;
      other = other.buffer;

    case arrayBufferTag:
      if ((object.byteLength != other.byteLength) ||
          !equalFunc(new Uint8Array$1(object), new Uint8Array$1(other))) {
        return false;
      }
      return true;

    case boolTag:
    case dateTag:
    case numberTag:
      // Coerce booleans to `1` or `0` and dates to milliseconds.
      // Invalid dates are coerced to `NaN`.
      return eq(+object, +other);

    case errorTag:
      return object.name == other.name && object.message == other.message;

    case regexpTag:
    case stringTag$1:
      // Coerce regexes to strings and treat strings, primitives and objects,
      // as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring
      // for more details.
      return object == (other + '');

    case mapTag:
      var convert = mapToArray;

    case setTag:
      var isPartial = bitmask & COMPARE_PARTIAL_FLAG$2;
      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 |= COMPARE_UNORDERED_FLAG;

      // Recursively compare objects (susceptible to call stack limits).
      stack.set(object, other);
      var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack);
      stack['delete'](object);
      return result;

    case symbolTag:
      if (symbolValueOf) {
        return symbolValueOf.call(object) == symbolValueOf.call(other);
      }
  }
  return false;
}/** Used to compose bitmasks for value comparisons. */
var COMPARE_PARTIAL_FLAG$1 = 1;

/** Used for built-in method references. */
var objectProto$1 = Object.prototype;

/** Used to check objects for own properties. */
var hasOwnProperty$1 = objectProto$1.hasOwnProperty;

/**
 * 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 {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
 * @param {Function} customizer The function to customize comparisons.
 * @param {Function} equalFunc The function to determine equivalents of values.
 * @param {Object} stack Tracks traversed `object` and `other` objects.
 * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
 */
function equalObjects(object, other, bitmask, customizer, equalFunc, stack) {
  var isPartial = bitmask & COMPARE_PARTIAL_FLAG$1,
      objProps = getAllKeys(object),
      objLength = objProps.length,
      othProps = getAllKeys(other),
      othLength = othProps.length;

  if (objLength != othLength && !isPartial) {
    return false;
  }
  var index = objLength;
  while (index--) {
    var key = objProps[index];
    if (!(isPartial ? key in other : hasOwnProperty$1.call(other, key))) {
      return false;
    }
  }
  // Check that cyclic values are equal.
  var objStacked = stack.get(object);
  var othStacked = stack.get(other);
  if (objStacked && othStacked) {
    return objStacked == other && othStacked == object;
  }
  var result = true;
  stack.set(object, other);
  stack.set(other, object);

  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, bitmask, customizer, 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);
  stack['delete'](other);
  return result;
}/** Used to compose bitmasks for value comparisons. */
var COMPARE_PARTIAL_FLAG = 1;

/** `Object#toString` result references. */
var argsTag = '[object Arguments]',
    arrayTag = '[object Array]',
    objectTag = '[object Object]';

/** Used for built-in method references. */
var objectProto = Object.prototype;

/** Used to check objects for own properties. */
var hasOwnProperty = 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 {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
 * @param {Function} customizer The function to customize comparisons.
 * @param {Function} equalFunc The function to determine equivalents of values.
 * @param {Object} [stack] Tracks traversed `object` and `other` objects.
 * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
 */
function baseIsEqualDeep(object, other, bitmask, customizer, equalFunc, stack) {
  var objIsArr = isArray$1(object),
      othIsArr = isArray$1(other),
      objTag = objIsArr ? arrayTag : getTag$1(object),
      othTag = othIsArr ? arrayTag : getTag$1(other);

  objTag = objTag == argsTag ? objectTag : objTag;
  othTag = othTag == argsTag ? objectTag : othTag;

  var objIsObj = objTag == objectTag,
      othIsObj = othTag == objectTag,
      isSameTag = objTag == othTag;

  if (isSameTag && isBuffer$1(object)) {
    if (!isBuffer$1(other)) {
      return false;
    }
    objIsArr = true;
    objIsObj = false;
  }
  if (isSameTag && !objIsObj) {
    stack || (stack = new Stack);
    return (objIsArr || isTypedArray$1(object))
      ? equalArrays(object, other, bitmask, customizer, equalFunc, stack)
      : equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack);
  }
  if (!(bitmask & COMPARE_PARTIAL_FLAG)) {
    var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'),
        othIsWrapped = othIsObj && 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, bitmask, customizer, stack);
    }
  }
  if (!isSameTag) {
    return false;
  }
  stack || (stack = new Stack);
  return equalObjects(object, other, bitmask, customizer, equalFunc, stack);
}/**
 * 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 {boolean} bitmask The bitmask flags.
 *  1 - Unordered comparison
 *  2 - Partial comparison
 * @param {Function} [customizer] The function to customize comparisons.
 * @param {Object} [stack] Tracks traversed `value` and `other` objects.
 * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
 */
function baseIsEqual(value, other, bitmask, customizer, stack) {
  if (value === other) {
    return true;
  }
  if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) {
    return value !== value && other !== other;
  }
  return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack);
}/**
 * Creates a base function for methods like `_.forIn` and `_.forOwn`.
 *
 * @private
 * @param {boolean} [fromRight] Specify iterating from right to left.
 * @returns {Function} Returns the new base function.
 */
function createBaseFor(fromRight) {
  return function(object, iteratee, keysFunc) {
    var index = -1,
        iterable = Object(object),
        props = keysFunc(object),
        length = props.length;

    while (length--) {
      var key = props[fromRight ? length : ++index];
      if (iteratee(iterable[key], key, iterable) === false) {
        break;
      }
    }
    return object;
  };
}/**
 * The base implementation of `baseForOwn` which iterates over `object`
 * properties returned by `keysFunc` and invokes `iteratee` for each property.
 * Iteratee functions may exit iteration early by explicitly returning `false`.
 *
 * @private
 * @param {Object} object The object to iterate over.
 * @param {Function} iteratee The function invoked per iteration.
 * @param {Function} keysFunc The function to get the keys of `object`.
 * @returns {Object} Returns `object`.
 */
var baseFor = createBaseFor();

var baseFor$1 = baseFor;/**
 * Gets the timestamp of the number of milliseconds that have elapsed since
 * the Unix epoch (1 January 1970 00:00:00 UTC).
 *
 * @static
 * @memberOf _
 * @since 2.4.0
 * @category Date
 * @returns {number} Returns the timestamp.
 * @example
 *
 * _.defer(function(stamp) {
 *   console.log(_.now() - stamp);
 * }, _.now());
 * // => Logs the number of milliseconds it took for the deferred invocation.
 */
var now = function() {
  return root$1.Date.now();
};

var now$1 = now;/** Error message constants. */
var FUNC_ERROR_TEXT$1 = 'Expected a function';

/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeMax = Math.max,
    nativeMin = Math.min;

/**
 * Creates a debounced function that delays invoking `func` until after `wait`
 * milliseconds have elapsed since the last time the debounced function was
 * invoked. The debounced function comes with a `cancel` method to cancel
 * delayed `func` invocations and a `flush` method to immediately invoke them.
 * Provide `options` to indicate whether `func` should be invoked on the
 * leading and/or trailing edge of the `wait` timeout. The `func` is invoked
 * with the last arguments provided to the debounced function. Subsequent
 * calls to the debounced function return the result of the last `func`
 * invocation.
 *
 * **Note:** If `leading` and `trailing` options are `true`, `func` is
 * invoked on the trailing edge of the timeout only if the debounced function
 * is invoked more than once during the `wait` timeout.
 *
 * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
 * until to the next tick, similar to `setTimeout` with a timeout of `0`.
 *
 * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
 * for details over the differences between `_.debounce` and `_.throttle`.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Function
 * @param {Function} func The function to debounce.
 * @param {number} [wait=0] The number of milliseconds to delay.
 * @param {Object} [options={}] The options object.
 * @param {boolean} [options.leading=false]
 *  Specify invoking on the leading edge of the timeout.
 * @param {number} [options.maxWait]
 *  The maximum time `func` is allowed to be delayed before it's invoked.
 * @param {boolean} [options.trailing=true]
 *  Specify invoking on the trailing edge of the timeout.
 * @returns {Function} Returns the new debounced function.
 * @example
 *
 * // Avoid costly calculations while the window size is in flux.
 * jQuery(window).on('resize', _.debounce(calculateLayout, 150));
 *
 * // Invoke `sendMail` when clicked, debouncing subsequent calls.
 * jQuery(element).on('click', _.debounce(sendMail, 300, {
 *   'leading': true,
 *   'trailing': false
 * }));
 *
 * // Ensure `batchLog` is invoked once after 1 second of debounced calls.
 * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
 * var source = new EventSource('/stream');
 * jQuery(source).on('message', debounced);
 *
 * // Cancel the trailing debounced invocation.
 * jQuery(window).on('popstate', debounced.cancel);
 */
function debounce(func, wait, options) {
  var lastArgs,
      lastThis,
      maxWait,
      result,
      timerId,
      lastCallTime,
      lastInvokeTime = 0,
      leading = false,
      maxing = false,
      trailing = true;

  if (typeof func != 'function') {
    throw new TypeError(FUNC_ERROR_TEXT$1);
  }
  wait = toNumber(wait) || 0;
  if (isObject(options)) {
    leading = !!options.leading;
    maxing = 'maxWait' in options;
    maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }

  function invokeFunc(time) {
    var args = lastArgs,
        thisArg = lastThis;

    lastArgs = lastThis = undefined;
    lastInvokeTime = time;
    result = func.apply(thisArg, args);
    return result;
  }

  function leadingEdge(time) {
    // Reset any `maxWait` timer.
    lastInvokeTime = time;
    // Start the timer for the trailing edge.
    timerId = setTimeout(timerExpired, wait);
    // Invoke the leading edge.
    return leading ? invokeFunc(time) : result;
  }

  function remainingWait(time) {
    var timeSinceLastCall = time - lastCallTime,
        timeSinceLastInvoke = time - lastInvokeTime,
        timeWaiting = wait - timeSinceLastCall;

    return maxing
      ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting;
  }

  function shouldInvoke(time) {
    var timeSinceLastCall = time - lastCallTime,
        timeSinceLastInvoke = time - lastInvokeTime;

    // Either this is the first call, activity has stopped and we're at the
    // trailing edge, the system time has gone backwards and we're treating
    // it as the trailing edge, or we've hit the `maxWait` limit.
    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
  }

  function timerExpired() {
    var time = now$1();
    if (shouldInvoke(time)) {
      return trailingEdge(time);
    }
    // Restart the timer.
    timerId = setTimeout(timerExpired, remainingWait(time));
  }

  function trailingEdge(time) {
    timerId = undefined;

    // Only invoke if we have `lastArgs` which means `func` has been
    // debounced at least once.
    if (trailing && lastArgs) {
      return invokeFunc(time);
    }
    lastArgs = lastThis = undefined;
    return result;
  }

  function cancel() {
    if (timerId !== undefined) {
      clearTimeout(timerId);
    }
    lastInvokeTime = 0;
    lastArgs = lastCallTime = lastThis = timerId = undefined;
  }

  function flush() {
    return timerId === undefined ? result : trailingEdge(now$1());
  }

  function debounced() {
    var time = now$1(),
        isInvoking = shouldInvoke(time);

    lastArgs = arguments;
    lastThis = this;
    lastCallTime = time;

    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime);
      }
      if (maxing) {
        // Handle invocations in a tight loop.
        clearTimeout(timerId);
        timerId = setTimeout(timerExpired, wait);
        return invokeFunc(lastCallTime);
      }
    }
    if (timerId === undefined) {
      timerId = setTimeout(timerExpired, wait);
    }
    return result;
  }
  debounced.cancel = cancel;
  debounced.flush = flush;
  return debounced;
}/**
 * This function is like `assignValue` except that it doesn't assign
 * `undefined` values.
 *
 * @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 assignMergeValue(object, key, value) {
  if ((value !== undefined && !eq(object[key], value)) ||
      (value === undefined && !(key in object))) {
    baseAssignValue(object, key, value);
  }
}/**
 * 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 isObjectLike(value) && isArrayLike(value);
}/**
 * Gets the value at `key`, unless `key` is "__proto__" or "constructor".
 *
 * @private
 * @param {Object} object The object to query.
 * @param {string} key The key of the property to get.
 * @returns {*} Returns the property value.
 */
function safeGet(object, key) {
  if (key === 'constructor' && typeof object[key] === 'function') {
    return;
  }

  if (key == '__proto__') {
    return;
  }

  return object[key];
}/**
 * Converts `value` to a plain object flattening inherited enumerable string
 * keyed properties of `value` to own properties of the plain object.
 *
 * @static
 * @memberOf _
 * @since 3.0.0
 * @category Lang
 * @param {*} value The value to convert.
 * @returns {Object} Returns the converted plain object.
 * @example
 *
 * function Foo() {
 *   this.b = 2;
 * }
 *
 * Foo.prototype.c = 3;
 *
 * _.assign({ 'a': 1 }, new Foo);
 * // => { 'a': 1, 'b': 2 }
 *
 * _.assign({ 'a': 1 }, _.toPlainObject(new Foo));
 * // => { 'a': 1, 'b': 2, 'c': 3 }
 */
function toPlainObject(value) {
  return copyObject(value, keysIn(value));
}/**
 * A specialized version of `baseMerge` for arrays and objects which performs
 * deep merges and tracks traversed objects enabling objects with circular
 * references to be merged.
 *
 * @private
 * @param {Object} object The destination object.
 * @param {Object} source The source object.
 * @param {string} key The key of the value to merge.
 * @param {number} srcIndex The index of `source`.
 * @param {Function} mergeFunc The function to merge values.
 * @param {Function} [customizer] The function to customize assigned values.
 * @param {Object} [stack] Tracks traversed source values and their merged
 *  counterparts.
 */
function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) {
  var objValue = safeGet(object, key),
      srcValue = safeGet(source, key),
      stacked = stack.get(srcValue);

  if (stacked) {
    assignMergeValue(object, key, stacked);
    return;
  }
  var newValue = customizer
    ? customizer(objValue, srcValue, (key + ''), object, source, stack)
    : undefined;

  var isCommon = newValue === undefined;

  if (isCommon) {
    var isArr = isArray$1(srcValue),
        isBuff = !isArr && isBuffer$1(srcValue),
        isTyped = !isArr && !isBuff && isTypedArray$1(srcValue);

    newValue = srcValue;
    if (isArr || isBuff || isTyped) {
      if (isArray$1(objValue)) {
        newValue = objValue;
      }
      else if (isArrayLikeObject(objValue)) {
        newValue = copyArray(objValue);
      }
      else if (isBuff) {
        isCommon = false;
        newValue = cloneBuffer(srcValue, true);
      }
      else if (isTyped) {
        isCommon = false;
        newValue = cloneTypedArray(srcValue, true);
      }
      else {
        newValue = [];
      }
    }
    else if (isPlainObject(srcValue) || isArguments$1(srcValue)) {
      newValue = objValue;
      if (isArguments$1(objValue)) {
        newValue = toPlainObject(objValue);
      }
      else if (!isObject(objValue) || isFunction(objValue)) {
        newValue = initCloneObject(srcValue);
      }
    }
    else {
      isCommon = false;
    }
  }
  if (isCommon) {
    // Recursively merge objects and arrays (susceptible to call stack limits).
    stack.set(srcValue, newValue);
    mergeFunc(newValue, srcValue, srcIndex, customizer, stack);
    stack['delete'](srcValue);
  }
  assignMergeValue(object, key, newValue);
}/**
 * The base implementation of `_.merge` without support for multiple sources.
 *
 * @private
 * @param {Object} object The destination object.
 * @param {Object} source The source object.
 * @param {number} srcIndex The index of `source`.
 * @param {Function} [customizer] The function to customize merged values.
 * @param {Object} [stack] Tracks traversed source values and their merged
 *  counterparts.
 */
function baseMerge(object, source, srcIndex, customizer, stack) {
  if (object === source) {
    return;
  }
  baseFor$1(source, function(srcValue, key) {
    stack || (stack = new Stack);
    if (isObject(srcValue)) {
      baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack);
    }
    else {
      var newValue = customizer
        ? customizer(safeGet(object, key), srcValue, (key + ''), object, source, stack)
        : undefined;

      if (newValue === undefined) {
        newValue = srcValue;
      }
      assignMergeValue(object, key, newValue);
    }
  }, keysIn);
}/**
 * This method is like `_.merge` except that it accepts `customizer` which
 * is invoked to produce the merged values of the destination and source
 * properties. If `customizer` returns `undefined`, merging is handled by the
 * method instead. The `customizer` is invoked with six arguments:
 * (objValue, srcValue, key, object, source, stack).
 *
 * **Note:** This method mutates `object`.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Object
 * @param {Object} object The destination object.
 * @param {...Object} sources The source objects.
 * @param {Function} customizer The function to customize assigned values.
 * @returns {Object} Returns `object`.
 * @example
 *
 * function customizer(objValue, srcValue) {
 *   if (_.isArray(objValue)) {
 *     return objValue.concat(srcValue);
 *   }
 * }
 *
 * var object = { 'a': [1], 'b': [2] };
 * var other = { 'a': [3], 'b': [4] };
 *
 * _.mergeWith(object, other, customizer);
 * // => { 'a': [1, 3], 'b': [2, 4] }
 */
var mergeWith = createAssigner(function(object, source, srcIndex, customizer) {
  baseMerge(object, source, srcIndex, customizer);
});

var mergeWith$1 = mergeWith;/**
 * 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(array) {
  var length = array == null ? 0 : array.length;
  return length ? array[length - 1] : undefined;
}/**
 * Used to match `RegExp`
 * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
 */
var reRegExpChar = /[\\^$.*+?()[\]{}|]/g,
    reHasRegExpChar = RegExp(reRegExpChar.source);

/**
 * Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+",
 * "?", "(", ")", "[", "]", "{", "}", and "|" in `string`.
 *
 * @static
 * @memberOf _
 * @since 3.0.0
 * @category String
 * @param {string} [string=''] The string to escape.
 * @returns {string} Returns the escaped string.
 * @example
 *
 * _.escapeRegExp('[lodash](https://lodash.com/)');
 * // => '\[lodash\]\(https://lodash\.com/\)'
 */
function escapeRegExp(string) {
  string = toString(string);
  return (string && reHasRegExpChar.test(string))
    ? string.replace(reRegExpChar, '\\$&')
    : string;
}/** `Object#toString` result references. */
var stringTag = '[object String]';

/**
 * 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 a string, else `false`.
 * @example
 *
 * _.isString('abc');
 * // => true
 *
 * _.isString(1);
 * // => false
 */
function isString(value) {
  return typeof value == 'string' ||
    (!isArray$1(value) && isObjectLike(value) && baseGetTag(value) == stringTag);
}/**
 * 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(object, path) {
  return path.length < 2 ? object : baseGet(object, baseSlice(path, 0, -1));
}/**
 * Checks if `value` is likely a DOM element.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a DOM element, else `false`.
 * @example
 *
 * _.isElement(document.body);
 * // => true
 *
 * _.isElement('<body>');
 * // => false
 */
function isElement(value) {
  return isObjectLike(value) && value.nodeType === 1 && !isPlainObject(value);
}/**
 * 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 compared by strict equality, i.e. `===`.
 *
 * @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 = { 'a': 1 };
 * var other = { 'a': 1 };
 *
 * _.isEqual(object, other);
 * // => true
 *
 * object === other;
 * // => false
 */
function isEqual(value, other) {
  return baseIsEqual(value, other);
}/**
 * This method is like `_.isEqual` except that it accepts `customizer` which
 * is invoked to compare values. If `customizer` returns `undefined`, comparisons
 * are handled by the method instead. The `customizer` is invoked with up to
 * six arguments: (objValue, othValue [, index|key, object, other, stack]).
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to compare.
 * @param {*} other The other value to compare.
 * @param {Function} [customizer] The function to customize comparisons.
 * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
 * @example
 *
 * function isGreeting(value) {
 *   return /^h(?:i|ello)$/.test(value);
 * }
 *
 * function customizer(objValue, othValue) {
 *   if (isGreeting(objValue) && isGreeting(othValue)) {
 *     return true;
 *   }
 * }
 *
 * var array = ['hello', 'goodbye'];
 * var other = ['hi', 'goodbye'];
 *
 * _.isEqualWith(array, other, customizer);
 * // => true
 */
function isEqualWith(value, other, customizer) {
  customizer = typeof customizer == 'function' ? customizer : undefined;
  var result = customizer ? customizer(value, other) : undefined;
  return result === undefined ? baseIsEqual(value, other, undefined, customizer) : !!result;
}/**
 * This method is like `_.assign` except that it recursively merges own and
 * inherited enumerable string keyed properties of source objects into the
 * destination object. Source properties that resolve to `undefined` are
 * skipped if a destination value exists. Array and plain object properties
 * are merged recursively. Other objects and value types are overridden by
 * assignment. Source objects are applied from left to right. Subsequent
 * sources overwrite property assignments of previous sources.
 *
 * **Note:** This method mutates `object`.
 *
 * @static
 * @memberOf _
 * @since 0.5.0
 * @category Object
 * @param {Object} object The destination object.
 * @param {...Object} [sources] The source objects.
 * @returns {Object} Returns `object`.
 * @example
 *
 * var object = {
 *   'a': [{ 'b': 2 }, { 'd': 4 }]
 * };
 *
 * var other = {
 *   'a': [{ 'c': 3 }, { 'e': 5 }]
 * };
 *
 * _.merge(object, other);
 * // => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }
 */
var merge = createAssigner(function(object, source, srcIndex) {
  baseMerge(object, source, srcIndex);
});

var merge$1 = merge;/**
 * The base implementation of `_.unset`.
 *
 * @private
 * @param {Object} object The object to modify.
 * @param {Array|string} path The property path to unset.
 * @returns {boolean} Returns `true` if the property is deleted, else `false`.
 */
function baseUnset(object, path) {
  path = castPath(path, object);
  object = parent(object, path);
  return object == null || delete object[toKey(last(path))];
}/**
 * The base implementation of `_.set`.
 *
 * @private
 * @param {Object} object The object to modify.
 * @param {Array|string} path The path of the property to set.
 * @param {*} value The value to set.
 * @param {Function} [customizer] The function to customize path creation.
 * @returns {Object} Returns `object`.
 */
function baseSet(object, path, value, customizer) {
  if (!isObject(object)) {
    return object;
  }
  path = castPath(path, object);

  var index = -1,
      length = path.length,
      lastIndex = length - 1,
      nested = object;

  while (nested != null && ++index < length) {
    var key = toKey(path[index]),
        newValue = value;

    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      return object;
    }

    if (index != lastIndex) {
      var objValue = nested[key];
      newValue = customizer ? customizer(objValue, key, nested) : undefined;
      if (newValue === undefined) {
        newValue = isObject(objValue)
          ? objValue
          : (isIndex(path[index + 1]) ? [] : {});
      }
    }
    assignValue(nested, key, newValue);
    nested = nested[key];
  }
  return object;
}/**
 * This function is like `baseIndexOf` except that it accepts a comparator.
 *
 * @private
 * @param {Array} array The array to inspect.
 * @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;
}/** Used for built-in method references. */
var arrayProto = Array.prototype;

/** Built-in value references. */
var splice = 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 (array === values) {
    values = copyArray(values);
  }
  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) {
        splice.call(seen, fromIndex, 1);
      }
      splice.call(array, fromIndex, 1);
    }
  }
  return array;
}/**
 * 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 = ['a', 'b', 'c', 'a', 'b', 'c'];
 *
 * _.pullAll(array, ['a', 'c']);
 * console.log(array);
 * // => ['b', 'b']
 */
function pullAll(array, values) {
  return (array && array.length && values && values.length)
    ? basePullAll(array, values)
    : array;
}/**
 * Removes all given values from `array` using
 * [`SameValueZero`](http://ecma-international.org/ecma-262/7.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 = ['a', 'b', 'c', 'a', 'b', 'c'];
 *
 * _.pull(array, 'a', 'c');
 * console.log(array);
 * // => ['b', 'b']
 */
var pull = baseRest(pullAll);

var removeItemFromArray = pull;/**
 * Sets the value at `path` of `object`. If a portion of `path` doesn't exist,
 * it's created. Arrays are created for missing index properties while objects
 * are created for all other missing properties. Use `_.setWith` to customize
 * `path` creation.
 *
 * **Note:** This method mutates `object`.
 *
 * @static
 * @memberOf _
 * @since 3.7.0
 * @category Object
 * @param {Object} object The object to modify.
 * @param {Array|string} path The path of the property to set.
 * @param {*} value The value to set.
 * @returns {Object} Returns `object`.
 * @example
 *
 * var object = { 'a': [{ 'b': { 'c': 3 } }] };
 *
 * _.set(object, 'a[0].b.c', 4);
 * console.log(object.a[0].b.c);
 * // => 4
 *
 * _.set(object, ['x', '0', 'y', 'z'], 5);
 * console.log(object.x[0].y.z);
 * // => 5
 */
function set(object, path, value) {
  return object == null ? object : baseSet(object, path, value);
}/** Error message constants. */
var FUNC_ERROR_TEXT = 'Expected a function';

/**
 * Creates a throttled function that only invokes `func` at most once per
 * every `wait` milliseconds. The throttled function comes with a `cancel`
 * method to cancel delayed `func` invocations and a `flush` method to
 * immediately invoke them. Provide `options` to indicate whether `func`
 * should be invoked on the leading and/or trailing edge of the `wait`
 * timeout. The `func` is invoked with the last arguments provided to the
 * throttled function. Subsequent calls to the throttled function return the
 * result of the last `func` invocation.
 *
 * **Note:** If `leading` and `trailing` options are `true`, `func` is
 * invoked on the trailing edge of the timeout only if the throttled function
 * is invoked more than once during the `wait` timeout.
 *
 * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
 * until to the next tick, similar to `setTimeout` with a timeout of `0`.
 *
 * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
 * for details over the differences between `_.throttle` and `_.debounce`.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Function
 * @param {Function} func The function to throttle.
 * @param {number} [wait=0] The number of milliseconds to throttle invocations to.
 * @param {Object} [options={}] The options object.
 * @param {boolean} [options.leading=true]
 *  Specify invoking on the leading edge of the timeout.
 * @param {boolean} [options.trailing=true]
 *  Specify invoking on the trailing edge of the timeout.
 * @returns {Function} Returns the new throttled function.
 * @example
 *
 * // Avoid excessively updating the position while scrolling.
 * jQuery(window).on('scroll', _.throttle(updatePosition, 100));
 *
 * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
 * var throttled = _.throttle(renewToken, 300000, { 'trailing': false });
 * jQuery(element).on('click', throttled);
 *
 * // Cancel the trailing throttled invocation.
 * jQuery(window).on('popstate', throttled.cancel);
 */
function throttle(func, wait, options) {
  var leading = true,
      trailing = true;

  if (typeof func != 'function') {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  if (isObject(options)) {
    leading = 'leading' in options ? !!options.leading : leading;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }
  return debounce(func, wait, {
    'leading': leading,
    'maxWait': wait,
    'trailing': trailing
  });
}/**
 * Removes the property at `path` of `object`.
 *
 * **Note:** This method mutates `object`.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Object
 * @param {Object} object The object to modify.
 * @param {Array|string} path The path of the property to unset.
 * @returns {boolean} Returns `true` if the property is deleted, else `false`.
 * @example
 *
 * var object = { 'a': [{ 'b': { 'c': 7 } }] };
 * _.unset(object, 'a[0].b.c');
 * // => true
 *
 * console.log(object);
 * // => { 'a': [{ 'b': {} }] };
 *
 * _.unset(object, ['a', '0', 'b', 'c']);
 * // => true
 *
 * console.log(object);
 * // => { 'a': [{ 'b': {} }] };
 */
function unset(object, path) {
  return object == null ? true : baseUnset(object, path);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const observablePropertiesSymbol = Symbol('observableProperties');
const boundObservablesSymbol = Symbol('boundObservables');
const boundPropertiesSymbol = Symbol('boundProperties');
const decoratedMethods = Symbol('decoratedMethods');
const decoratedOriginal = Symbol('decoratedOriginal');
/**
 * A mixin that injects the "observable properties" and data binding functionality described in the
 * {@link ~Observable} interface.
 *
 * Read more about the concept of observables in the:
 * * {@glink framework/guides/architecture/core-editor-architecture#event-system-and-observables Event system and observables}
 * section of the {@glink framework/guides/architecture/core-editor-architecture Core editor architecture} guide,
 * * {@glink framework/guides/deep-dive/observables Observables deep dive} guide.
 *
 * @mixin ObservableMixin
 * @mixes module:utils/emittermixin~EmitterMixin
 * @implements module:utils/observablemixin~Observable
 */
function ObservableMixin(base) {
    class Mixin extends base {
        set(name, value) {
            // If the first parameter is an Object, iterate over its properties.
            if (isObject(name)) {
                Object.keys(name).forEach(property => {
                    this.set(property, name[property]);
                }, this);
                return;
            }
            initObservable(this);
            const properties = this[observablePropertiesSymbol];
            if ((name in this) && !properties.has(name)) {
                /**
                 * Cannot override an existing property.
                 *
                 * This error is thrown when trying to {@link ~Observable#set set} a property with
                 * a name of an already existing property. For example:
                 *
                 *		let observable = new Model();
                 *		observable.property = 1;
                 *		observable.set( 'property', 2 );			// throws
                 *
                 *		observable.set( 'property', 1 );
                 *		observable.set( 'property', 2 );			// ok, because this is an existing property.
                 *
                 * @error observable-set-cannot-override
                 */
                throw new CKEditorError('observable-set-cannot-override', this);
            }
            Object.defineProperty(this, name, {
                enumerable: true,
                configurable: true,
                get() {
                    return properties.get(name);
                },
                set(value) {
                    const oldValue = properties.get(name);
                    // Fire `set` event before the new value will be set to make it possible
                    // to override observable property without affecting `change` event.
                    // See https://github.com/ckeditor/ckeditor5-utils/issues/171.
                    let newValue = this.fire(`set:${name}`, name, value, oldValue);
                    if (newValue === undefined) {
                        newValue = value;
                    }
                    // Allow undefined as an initial value like A.define( 'x', undefined ) (#132).
                    // Note: When properties map has no such own property, then its value is undefined.
                    if (oldValue !== newValue || !properties.has(name)) {
                        properties.set(name, newValue);
                        this.fire(`change:${name}`, name, newValue, oldValue);
                    }
                }
            });
            this[name] = value;
        }
        bind(...bindProperties) {
            if (!bindProperties.length || !isStringArray$1(bindProperties)) {
                /**
                 * All properties must be strings.
                 *
                 * @error observable-bind-wrong-properties
                 */
                throw new CKEditorError('observable-bind-wrong-properties', this);
            }
            if ((new Set(bindProperties)).size !== bindProperties.length) {
                /**
                 * Properties must be unique.
                 *
                 * @error observable-bind-duplicate-properties
                 */
                throw new CKEditorError('observable-bind-duplicate-properties', this);
            }
            initObservable(this);
            const boundProperties = this[boundPropertiesSymbol];
            bindProperties.forEach(propertyName => {
                if (boundProperties.has(propertyName)) {
                    /**
                     * Cannot bind the same property more than once.
                     *
                     * @error observable-bind-rebind
                     */
                    throw new CKEditorError('observable-bind-rebind', this);
                }
            });
            const bindings = new Map();
            // @typedef {Object} Binding
            // @property {Array} property Property which is bound.
            // @property {Array} to Array of observable–property components of the binding (`{ observable: ..., property: .. }`).
            // @property {Array} callback A function which processes `to` components.
            bindProperties.forEach(a => {
                const binding = { property: a, to: [] };
                boundProperties.set(a, binding);
                bindings.set(a, binding);
            });
            // @typedef {Object} BindChain
            // @property {Function} to See {@link ~ObservableMixin#_bindTo}.
            // @property {Function} toMany See {@link ~ObservableMixin#_bindToMany}.
            // @property {module:utils/observablemixin~Observable} _observable The observable which initializes the binding.
            // @property {Array} _bindProperties Array of `_observable` properties to be bound.
            // @property {Array} _to Array of `to()` observable–properties (`{ observable: toObservable, properties: ...toProperties }`).
            // @property {Map} _bindings Stores bindings to be kept in
            // {@link ~ObservableMixin#_boundProperties}/{@link ~ObservableMixin#_boundObservables}
            // initiated in this binding chain.
            return {
                to: bindTo,
                toMany: bindToMany,
                _observable: this,
                _bindProperties: bindProperties,
                _to: [],
                _bindings: bindings
            };
        }
        unbind(...unbindProperties) {
            // Nothing to do here if not inited yet.
            if (!(this[observablePropertiesSymbol])) {
                return;
            }
            const boundProperties = this[boundPropertiesSymbol];
            const boundObservables = this[boundObservablesSymbol];
            if (unbindProperties.length) {
                if (!isStringArray$1(unbindProperties)) {
                    /**
                     * Properties must be strings.
                     *
                     * @error observable-unbind-wrong-properties
                     */
                    throw new CKEditorError('observable-unbind-wrong-properties', this);
                }
                unbindProperties.forEach(propertyName => {
                    const binding = boundProperties.get(propertyName);
                    // Nothing to do if the binding is not defined
                    if (!binding) {
                        return;
                    }
                    binding.to.forEach(([toObservable, toProperty]) => {
                        const toProperties = boundObservables.get(toObservable);
                        const toPropertyBindings = toProperties[toProperty];
                        toPropertyBindings.delete(binding);
                        if (!toPropertyBindings.size) {
                            delete toProperties[toProperty];
                        }
                        if (!Object.keys(toProperties).length) {
                            boundObservables.delete(toObservable);
                            this.stopListening(toObservable, 'change');
                        }
                    });
                    boundProperties.delete(propertyName);
                });
            }
            else {
                boundObservables.forEach((bindings, boundObservable) => {
                    this.stopListening(boundObservable, 'change');
                });
                boundObservables.clear();
                boundProperties.clear();
            }
        }
        decorate(methodName) {
            initObservable(this);
            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', this, { object: this, methodName });
            }
            this.on(methodName, (evt, args) => {
                evt.return = originalMethod.apply(this, args);
            });
            this[methodName] = function (...args) {
                return this.fire(methodName, args);
            };
            this[methodName][decoratedOriginal] = originalMethod;
            if (!this[decoratedMethods]) {
                this[decoratedMethods] = [];
            }
            this[decoratedMethods].push(methodName);
        }
        // Override the EmitterMixin stopListening method to be able to clean (and restore) decorated methods.
        // This is needed in case of:
        //  1. Have x.foo() decorated.
        //  2. Call x.stopListening()
        //  3. Call x.foo(). Problem: nothing happens (the original foo() method is not executed)
        stopListening(emitter, event, callback) {
            // Removing all listeners so let's clean the decorated methods to the original state.
            if (!emitter && this[decoratedMethods]) {
                for (const methodName of this[decoratedMethods]) {
                    this[methodName] = this[methodName][decoratedOriginal];
                }
                delete this[decoratedMethods];
            }
            Emitter$1.prototype.stopListening.call(this, emitter, event, callback);
        }
    }
    return Mixin;
}
const Observable = ObservableMixin(Emitter$1);
// Backward compatibility with `mix`
([
    'set', 'bind', 'unbind', 'decorate',
    'on', 'once', 'off', 'listenTo',
    'stopListening', 'fire', 'delegate', 'stopDelegating',
    '_addEventListener', '_removeEventListener'
]).forEach(key => {
    ObservableMixin[key] = Observable.prototype[key];
});
// Init symbol properties needed for the observable mechanism to work.
//
// @private
// @param {module:utils/observablemixin~ObservableMixin} observable
function initObservable(observable) {
    // Do nothing if already inited.
    if (observable[observablePropertiesSymbol]) {
        return;
    }
    // The internal hash containing the observable's state.
    //
    // @private
    // @type {Map}
    Object.defineProperty(observable, observablePropertiesSymbol, {
        value: new Map()
    });
    // Map containing bindings to external observables. It shares the binding objects
    // (`{ observable: A, property: 'a', to: ... }`) with {@link module:utils/observablemixin~ObservableMixin#_boundProperties} and
    // it is used to observe external observables to update own properties 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, property: 'a', to: [ [ B, 'x' ] ] },
    //						{ observable: A, property: 'c', to: [ [ B, 'x' ] ] }
    //					] ),
    //					y: Set( [
    //						{ observable: A, property: '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, property: 'a', to: [ [ B, 'x' ] ] },
    //						{ observable: A, property: 'c', to: [ [ B, 'x' ] ] }
    //					] ),
    //					y: Set( [
    //						{ observable: A, property: 'b', to: [ [ B, 'y' ] ] },
    //					] ),
    //					z: Set( [
    //						{ observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
    //					] )
    //				},
    //				C: {
    //					w: Set( [
    //						{ observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
    //					] )
    //				}
    //			} )
    //
    // @private
    // @type {Map}
    Object.defineProperty(observable, boundObservablesSymbol, {
        value: new Map()
    });
    // Object that stores which properties of this observable are bound and how. It shares
    // the binding objects (`{ observable: A, property: 'a', to: ... }`) with
    // {@link module:utils/observablemixin~ObservableMixin#_boundObservables}. This data structure is
    // a reverse of {@link module:utils/observablemixin~ObservableMixin#_boundObservables} and it is helpful for
    // {@link module:utils/observablemixin~ObservableMixin#unbind}.
    //
    // See {@link module:utils/observablemixin~ObservableMixin#bind}.
    //
    //		A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
    //		console.log( A._boundProperties );
    //
    //			Map( {
    //				a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
    //				b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
    //				c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
    //			} )
    //
    //		A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
    //		console.log( A._boundProperties );
    //
    //			Map( {
    //				a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
    //				b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
    //				c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] },
    //				d: { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
    //			} )
    //
    // @private
    // @type {Map}
    Object.defineProperty(observable, boundPropertiesSymbol, {
        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-to-no-callback
         */
        throw new CKEditorError('observable-bind-to-no-callback', this);
    }
    // Eliminate A.bind( 'x', 'y' ).to( B, callback )
    if (numberOfBindings > 1 && parsedArgs.callback) {
        /**
         * Cannot bind multiple properties and use a callback in one binding.
         *
         * @error observable-bind-to-extra-callback
         */
        throw new CKEditorError('observable-bind-to-extra-callback', this);
    }
    parsedArgs.to.forEach(to => {
        // Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
        if (to.properties.length && to.properties.length !== numberOfBindings) {
            /**
             * The number of properties must match.
             *
             * @error observable-bind-to-properties-length
             */
            throw new CKEditorError('observable-bind-to-properties-length', this);
        }
        // When no to.properties specified, observing source properties instead i.e.
        // A.bind( 'x', 'y' ).to( B ) -> Observe B.x and B.y
        if (!to.properties.length) {
            to.properties = this._bindProperties;
        }
    });
    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._boundProperties and observable._boundObservables.
    updateBindToBound(this);
    // Set initial values of bound properties.
    this._bindProperties.forEach(propertyName => {
        updateBoundObservableProperty(this._observable, propertyName);
    });
}
// Binds to an attribute in a set of iterable observables.
//
// @private
// @param {Array.<Observable>} observables
// @param {String} attribute
// @param {Function} callback
function bindToMany(observables, attribute, callback) {
    if (this._bindings.size > 1) {
        /**
         * Binding one attribute to many observables only possible with one attribute.
         *
         * @error observable-bind-to-many-not-one-binding
         */
        throw new CKEditorError('observable-bind-to-many-not-one-binding', this);
    }
    this.to(
    // Bind to #attribute of each observable...
    ...getBindingTargets(observables, attribute), 
    // ...using given callback to parse attribute values.
    callback);
}
// Returns an array of binding components for
// {@link Observable#bind} from a set of iterable observables.
//
// @param {Array.<Observable>} observables
// @param {String} attribute
// @returns {Array.<String|Observable>}
function getBindingTargets(observables, attribute) {
    const observableAndAttributePairs = observables.map(observable => [observable, attribute]);
    // Merge pairs to one-dimension array of observables and attributes.
    return Array.prototype.concat.apply([], observableAndAttributePairs);
}
// Check if all entries of the array are of `String` type.
//
// @private
// @param {Array} arr An array to be checked.
// @returns {Boolean}
function isStringArray$1(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, properties: [ 'a' ] },
//				{ observable: C, properties: [ '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', null);
    }
    const parsed = { to: [] };
    let lastObservable;
    if (typeof args[args.length - 1] == 'function') {
        parsed.callback = args.pop();
    }
    args.forEach(a => {
        if (typeof a == 'string') {
            lastObservable.properties.push(a);
        }
        else if (typeof a == 'object') {
            lastObservable = { observable: a, properties: [] };
            parsed.to.push(lastObservable);
        }
        else {
            throw new CKEditorError('observable-bind-to-parse-error', null);
        }
    });
    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} toPropertyName A name of `toObservable`'s property, a new component of the `binding`.
function updateBoundObservables(observable, binding, toObservable, toPropertyName) {
    const boundObservables = observable[boundObservablesSymbol];
    const bindingsToObservable = boundObservables.get(toObservable);
    const bindings = bindingsToObservable || {};
    if (!bindings[toPropertyName]) {
        bindings[toPropertyName] = new Set();
    }
    // Pass the binding to a corresponding Set in `observable._boundObservables`.
    bindings[toPropertyName].add(binding);
    if (!bindingsToObservable) {
        boundObservables.set(toObservable, bindings);
    }
}
// Synchronizes {@link Observable#_boundProperties} 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, property: 'a', to: [] },
// 			b: { observable: A, property: '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, property: 'a', to: [ B, 'x' ] },
// 			b: { observable: A, property: 'b', to: [ B, 'y' ] },
// 		}
//
// Then update the structure of {@link Observable#_boundObservables} with updated
// binding, so it becomes:
//
// 		Map( {
// 			B: {
// 				x: Set( [
// 					{ observable: A, property: 'a', to: [ [ B, 'x' ] ] }
// 				] ),
// 				y: Set( [
// 					{ observable: A, property: 'b', to: [ [ B, 'y' ] ] },
// 				] )
//			}
// 		} )
//
// @private
// @param {BindChain} chain The binding initialized by {@link Observable#bind}.
function updateBindToBound(chain) {
    let toProperty;
    chain._bindings.forEach((binding, propertyName) => {
        // 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 => {
            toProperty = to.properties[binding.callback ? 0 : chain._bindProperties.indexOf(propertyName)];
            binding.to.push([to.observable, toProperty]);
            updateBoundObservables(chain._observable, binding, to.observable, toProperty);
        });
    });
}
// Updates an property of a {@link Observable} with a value
// determined by an entry in {@link Observable#_boundProperties}.
//
// @private
// @param {Observable} observable A observable which property is to be updated.
// @param {String} propertyName An property to be updated.
function updateBoundObservableProperty(observable, propertyName) {
    const boundProperties = observable[boundPropertiesSymbol];
    const binding = boundProperties.get(propertyName);
    let propertyValue;
    // 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) {
        propertyValue = binding.callback.apply(observable, binding.to.map(to => to[0][to[1]]));
    }
    else {
        propertyValue = binding.to[0];
        propertyValue = propertyValue[0][propertyValue[1]];
    }
    if (Object.prototype.hasOwnProperty.call(observable, propertyName)) {
        observable[propertyName] = propertyValue;
    }
    else {
        observable.set(propertyName, propertyValue);
    }
}
// Starts listening to changes in {@link BindChain._to} observables to update
// {@link BindChain._observable} {@link BindChain._bindProperties}. Also sets the
// initial state of {@link BindChain._observable}.
//
// @private
// @param {Observable} observable
// @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, propertyName) => {
                bindings = boundObservables.get(to.observable)[propertyName];
                // Note: to.observable will fire for any property change, react
                // to changes of properties which are bound only.
                if (bindings) {
                    bindings.forEach(binding => {
                        updateBoundObservableProperty(observable, binding.property);
                    });
                }
            });
        }
    });
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @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.
 *
 * @depreciated Use mixin pattern, see: https://www.typescriptlang.org/docs/handbook/mixins.html.
 * @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 => {
        const propertyNames = Object.getOwnPropertyNames(mixin);
        const propertySymbols = Object.getOwnPropertySymbols(mixin);
        propertyNames.concat(propertySymbols).forEach(key => {
            if (key in baseClass.prototype) {
                return;
            }
            if (typeof mixin == 'function' && (key == 'length' || key == 'name' || key == 'prototype')) {
                return;
            }
            const sourceDescriptor = Object.getOwnPropertyDescriptor(mixin, key);
            sourceDescriptor.enumerable = false;
            Object.defineProperty(baseClass.prototype, key, sourceDescriptor);
        });
    });
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The base class for CKEditor plugin classes.
 *
 * @implements module:core/plugin~PluginInterface
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class Plugin {
	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		/**
		 * The editor instance.
		 *
		 * Note that most editors implement the {@link module:core/editor/editorwithui~EditorWithUI} interface in addition
		 * to the base {@link module:core/editor/editor~Editor} interface. However, editors with an external UI
		 * (i.e. Bootstrap-based) or a headless editor may not implement the {@link module:core/editor/editorwithui~EditorWithUI}
		 * interface.
		 *
		 * Because of above, to make plugins more universal, it is recommended to split features into:
		 *  - The "editing" part that only uses the {@link module:core/editor/editor~Editor} interface.
		 *  - The "UI" part that uses both the {@link module:core/editor/editor~Editor} interface and
		 *  the {@link module:core/editor/editorwithui~EditorWithUI} interface.
		 *
		 * @readonly
		 * @member {module:core/editor/editor~Editor} #editor
		 */
		this.editor = editor;

		/**
		 * Flag indicating whether a plugin is enabled or disabled.
		 * A disabled plugin will not transform text.
		 *
		 * Plugin can be simply disabled like that:
		 *
		 *		// Disable the plugin so that no toolbars are visible.
		 *		editor.plugins.get( 'TextTransformation' ).isEnabled = false;
		 *
		 * You can also use {@link #forceDisabled} method.
		 *
		 * @observable
		 * @readonly
		 * @member {Boolean} #isEnabled
		 */
		this.set( 'isEnabled', true );

		/**
		 * Holds identifiers for {@link #forceDisabled} mechanism.
		 *
		 * @type {Set.<String>}
		 * @private
		 */
		this._disableStack = new Set();
	}

	/**
	 * Disables the plugin.
	 *
	 * Plugin may be disabled by multiple features or algorithms (at once). When disabling a plugin, unique id should be passed
	 * (e.g. feature name). The same identifier should be used when {@link #clearForceDisabled enabling back} the plugin.
	 * The plugin becomes enabled only after all features {@link #clearForceDisabled enabled it back}.
	 *
	 * Disabling and enabling a plugin:
	 *
	 *		plugin.isEnabled; // -> true
	 *		plugin.forceDisabled( 'MyFeature' );
	 *		plugin.isEnabled; // -> false
	 *		plugin.clearForceDisabled( 'MyFeature' );
	 *		plugin.isEnabled; // -> true
	 *
	 * Plugin disabled by multiple features:
	 *
	 *		plugin.forceDisabled( 'MyFeature' );
	 *		plugin.forceDisabled( 'OtherFeature' );
	 *		plugin.clearForceDisabled( 'MyFeature' );
	 *		plugin.isEnabled; // -> false
	 *		plugin.clearForceDisabled( 'OtherFeature' );
	 *		plugin.isEnabled; // -> true
	 *
	 * Multiple disabling with the same identifier is redundant:
	 *
	 *		plugin.forceDisabled( 'MyFeature' );
	 *		plugin.forceDisabled( 'MyFeature' );
	 *		plugin.clearForceDisabled( 'MyFeature' );
	 *		plugin.isEnabled; // -> true
	 *
	 * **Note:** some plugins or algorithms may have more complex logic when it comes to enabling or disabling certain plugins,
	 * so the plugin might be still disabled after {@link #clearForceDisabled} was used.
	 *
	 * @param {String} id Unique identifier for disabling. Use the same id when {@link #clearForceDisabled enabling back} the plugin.
	 */
	forceDisabled( id ) {
		this._disableStack.add( id );

		if ( this._disableStack.size == 1 ) {
			this.on( 'set:isEnabled', forceDisable$1, { priority: 'highest' } );
			this.isEnabled = false;
		}
	}

	/**
	 * Clears forced disable previously set through {@link #forceDisabled}. See {@link #forceDisabled}.
	 *
	 * @param {String} id Unique identifier, equal to the one passed in {@link #forceDisabled} call.
	 */
	clearForceDisabled( id ) {
		this._disableStack.delete( id );

		if ( this._disableStack.size == 0 ) {
			this.off( 'set:isEnabled', forceDisable$1 );
			this.isEnabled = true;
		}
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		this.stopListening();
	}

	/**
	 * @inheritDoc
	 */
	static get isContextPlugin() {
		return false;
	}
}

mix( Plugin, ObservableMixin );

/**
 * The base interface for CKEditor plugins.
 *
 * In its minimal form a plugin can be a simple function that accepts {@link module:core/editor/editor~Editor the editor}
 * as a parameter:
 *
 *		// A simple plugin that enables a data processor.
 *		function MyPlugin( editor ) {
 *			editor.data.processor = new MyDataProcessor();
 *		}
 *
 * In most cases however, you will want to inherit from the {@link module:core/plugin~Plugin} class which implements the
 * {@link module:utils/observablemixin~ObservableMixin} and is, therefore, more convenient:
 *
 *		class MyPlugin extends Plugin {
 *			init() {
 *				// `listenTo()` and `editor` are available thanks to `Plugin`.
 *				// By using `listenTo()` you will ensure that the listener is removed when
 *				// the plugin is destroyed.
 *				this.listenTo( this.editor.data, 'ready', () => {
 *					// Do something when the data is ready.
 *				} );
 *			}
 *		}
 *
 * The plugin can also implement methods (e.g. {@link module:core/plugin~PluginInterface#init `init()`} or
 * {@link module:core/plugin~PluginInterface#destroy `destroy()`}) which, when present, will be used to properly
 * initialize and destroy the plugin.
 *
 * **Note:** When defined as a plain function, the plugin acts as a constructor and will be
 * called in parallel with other plugins' {@link module:core/plugin~PluginInterface#constructor constructors}.
 * This means the code of that plugin will be executed **before** {@link module:core/plugin~PluginInterface#init `init()`} and
 * {@link module:core/plugin~PluginInterface#afterInit `afterInit()`} methods of other plugins and, for instance,
 * you cannot use it to extend other plugins' {@glink framework/guides/architecture/editing-engine#schema schema}
 * rules as they are defined later on during the `init()` stage.
 *
 * @interface PluginInterface
 */

/**
 * Creates a new plugin instance. This is the first step of the plugin initialization.
 * See also {@link #init} and {@link #afterInit}.
 *
 * A plugin is always instantiated after its {@link module:core/plugin~PluginInterface.requires dependencies} and the
 * {@link #init} and {@link #afterInit} methods are called in the same order.
 *
 * Usually, you will want to put your plugin's initialization code in the {@link #init} method.
 * The constructor can be understood as "before init" and used in special cases, just like
 * {@link #afterInit} serves the special "after init" scenarios (e.g.the code which depends on other
 * plugins, but which does not {@link module:core/plugin~PluginInterface.requires explicitly require} them).
 *
 * @method #constructor
 * @param {module:core/editor/editor~Editor} editor
 */

/**
 * An array of plugins required by this plugin.
 *
 * To keep the plugin class definition tight it is recommended to define this property as a static getter:
 *
 *		import Image from './image.js';
 *
 *		export default class ImageCaption {
 *			static get requires() {
 *				return [ Image ];
 *			}
 *		}
 *
 * @static
 * @readonly
 * @member {Array.<Function>|undefined} module:core/plugin~PluginInterface.requires
 */

/**
 * An optional name of the plugin. If set, the plugin will be available in
 * {@link module:core/plugincollection~PluginCollection#get} by its
 * name and its constructor. If not, then only by its constructor.
 *
 * The name should reflect the constructor name.
 *
 * To keep the plugin class definition tight, it is recommended to define this property as a static getter:
 *
 *		export default class ImageCaption {
 *			static get pluginName() {
 *				return 'ImageCaption';
 *			}
 *		}
 *
 * Note: The native `Function.name` property could not be used to keep the plugin name because
 * it will be mangled during code minification.
 *
 * Naming a plugin is necessary to enable removing it through the
 * {@link module:core/editor/editorconfig~EditorConfig#removePlugins `config.removePlugins`} option.
 *
 * @static
 * @readonly
 * @member {String|undefined} module:core/plugin~PluginInterface.pluginName
 */

/**
 * The second stage (after plugin {@link #constructor}) of the plugin initialization.
 * Unlike the plugin constructor this method can be asynchronous.
 *
 * A plugin's `init()` method is called after its {@link module:core/plugin~PluginInterface.requires dependencies} are initialized,
 * so in the same order as the constructors of these plugins.
 *
 * **Note:** This method is optional. A plugin instance does not need to have it defined.
 *
 * @method #init
 * @returns {null|Promise}
 */

/**
 * The third (and last) stage of the plugin initialization. See also {@link #constructor} and {@link #init}.
 *
 * **Note:** This method is optional. A plugin instance does not need to have it defined.
 *
 * @method #afterInit
 * @returns {null|Promise}
 */

/**
 * Destroys the plugin.
 *
 * **Note:** This method is optional. A plugin instance does not need to have it defined.
 *
 * @method #destroy
 * @returns {null|Promise}
 */

/**
 * A flag which defines if a plugin is allowed or not allowed to be used directly by a {@link module:core/context~Context}.
 *
 * @static
 * @readonly
 * @member {Boolean} module:core/plugin~PluginInterface.isContextPlugin
 */

/**
 * An array of loaded plugins.
 *
 * @typedef {Array.<module:core/plugin~PluginInterface>} module:core/plugin~LoadedPlugins
 */

// Helper function that forces plugin to be disabled.
function forceDisable$1( evt ) {
	evt.return = false;
	evt.stop();
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The base class for CKEditor commands.
 *
 * Commands are the main way to manipulate editor contents and state. They are mostly used by UI elements (or by other
 * commands) to make changes in the model. Commands are available in every part of code that has access to
 * the {@link module:core/editor/editor~Editor editor} instance.
 *
 * Instances of registered commands can be retrieved from {@link module:core/editor/editor~Editor#commands `editor.commands`}.
 * The easiest way to execute a command is through {@link module:core/editor/editor~Editor#execute `editor.execute()`}.
 *
 * By default, commands are disabled when the editor is in {@link module:core/editor/editor~Editor#isReadOnly read-only} mode
 * but commands with the {@link module:core/command~Command#affectsData `affectsData`} flag set to `false` will not be disabled.
 *
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class Command {
	/**
	 * Creates a new `Command` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
	 */
	constructor( editor ) {
		/**
		 * The editor on which this command will be used.
		 *
		 * @readonly
		 * @member {module:core/editor/editor~Editor}
		 */
		this.editor = editor;

		/**
		 * The value of the command. A concrete command class should define what it represents for it.
		 *
		 * For example, the `'bold'` command's value indicates whether the selection starts in a bolded text.
		 * And the value of the `'link'` command may be an object with links details.
		 *
		 * It is possible for a command to have no value (e.g. for stateless actions such as `'uploadImage'`).
		 *
		 * A concrete command class should control this value by overriding the {@link #refresh `refresh()`} method.
		 *
		 * @observable
		 * @readonly
		 * @member #value
		 */
		this.set( 'value', undefined );

		/**
		 * Flag indicating whether a command is enabled or disabled.
		 * A disabled command will do nothing when executed.
		 *
		 * A concrete command class should control this value by overriding the {@link #refresh `refresh()`} method.
		 *
		 * It is possible to disable a command from "outside". For instance, in your integration you may want to disable
		 * a certain set of commands for the time being. To do that, you can use the fact that `isEnabled` is observable
		 * and it fires the `set:isEnabled` event every time anyone tries to modify its value:
		 *
		 *		function disableCommand( cmd ) {
		 *			cmd.on( 'set:isEnabled', forceDisable, { priority: 'highest' } );
		 *
		 *			cmd.isEnabled = false;
		 *
		 *			// Make it possible to enable the command again.
		 *			return () => {
		 *				cmd.off( 'set:isEnabled', forceDisable );
		 *				cmd.refresh();
		 *			};
		 *
		 *			function forceDisable( evt ) {
		 *				evt.return = false;
		 *				evt.stop();
		 *			}
		 *		}
		 *
		 *		// Usage:
		 *
		 *		// Disabling the command.
		 *		const enableBold = disableCommand( editor.commands.get( 'bold' ) );
		 *
		 *		// Enabling the command again.
		 *		enableBold();
		 *
		 * @observable
		 * @readonly
		 * @member {Boolean} #isEnabled
		 */
		this.set( 'isEnabled', false );

		/**
		 * A flag indicating whether a command execution changes the editor data or not.
		 *
		 * Commands with `affectsData` set to `false` will not be automatically disabled in
		 * the {@link module:core/editor/editor~Editor#isReadOnly read-only mode} and
		 * {@glink features/read-only#related-features other editor modes} with restricted user write permissions.
		 *
		 * **Note:** You do not have to set it for your every command. It is `true` by default.
		 *
		 * @readonly
		 * @default true
		 * @member {Boolean} #affectsData
		 */
		this.affectsData = true;

		/**
		 * Holds identifiers for {@link #forceDisabled} mechanism.
		 *
		 * @type {Set.<String>}
		 * @private
		 */
		this._disableStack = new Set();

		this.decorate( 'execute' );

		// By default every command is refreshed when changes are applied to the model.
		this.listenTo( this.editor.model.document, 'change', () => {
			this.refresh();
		} );

		this.on( 'execute', evt => {
			if ( !this.isEnabled ) {
				evt.stop();
			}
		}, { priority: 'high' } );

		// By default commands are disabled when the editor is in read-only mode.
		this.listenTo( editor, 'change:isReadOnly', ( evt, name, value ) => {
			if ( value && this.affectsData ) {
				this.forceDisabled( 'readOnlyMode' );
			} else {
				this.clearForceDisabled( 'readOnlyMode' );
			}
		} );
	}

	/**
	 * Refreshes the command. The command should update its {@link #isEnabled} and {@link #value} properties
	 * in this method.
	 *
	 * This method is automatically called when
	 * {@link module:engine/model/document~Document#event:change any changes are applied to the document}.
	 */
	refresh() {
		this.isEnabled = true;
	}

	/**
	 * Disables the command.
	 *
	 * Command may be disabled by multiple features or algorithms (at once). When disabling a command, unique id should be passed
	 * (e.g. feature name). The same identifier should be used when {@link #clearForceDisabled enabling back} the command.
	 * The command becomes enabled only after all features {@link #clearForceDisabled enabled it back}.
	 *
	 * Disabling and enabling a command:
	 *
	 *		command.isEnabled; // -> true
	 *		command.forceDisabled( 'MyFeature' );
	 *		command.isEnabled; // -> false
	 *		command.clearForceDisabled( 'MyFeature' );
	 *		command.isEnabled; // -> true
	 *
	 * Command disabled by multiple features:
	 *
	 *		command.forceDisabled( 'MyFeature' );
	 *		command.forceDisabled( 'OtherFeature' );
	 *		command.clearForceDisabled( 'MyFeature' );
	 *		command.isEnabled; // -> false
	 *		command.clearForceDisabled( 'OtherFeature' );
	 *		command.isEnabled; // -> true
	 *
	 * Multiple disabling with the same identifier is redundant:
	 *
	 *		command.forceDisabled( 'MyFeature' );
	 *		command.forceDisabled( 'MyFeature' );
	 *		command.clearForceDisabled( 'MyFeature' );
	 *		command.isEnabled; // -> true
	 *
	 * **Note:** some commands or algorithms may have more complex logic when it comes to enabling or disabling certain commands,
	 * so the command might be still disabled after {@link #clearForceDisabled} was used.
	 *
	 * @param {String} id Unique identifier for disabling. Use the same id when {@link #clearForceDisabled enabling back} the command.
	 */
	forceDisabled( id ) {
		this._disableStack.add( id );

		if ( this._disableStack.size == 1 ) {
			this.on( 'set:isEnabled', forceDisable, { priority: 'highest' } );
			this.isEnabled = false;
		}
	}

	/**
	 * Clears forced disable previously set through {@link #forceDisabled}. See {@link #forceDisabled}.
	 *
	 * @param {String} id Unique identifier, equal to the one passed in {@link #forceDisabled} call.
	 */
	clearForceDisabled( id ) {
		this._disableStack.delete( id );

		if ( this._disableStack.size == 0 ) {
			this.off( 'set:isEnabled', forceDisable );
			this.refresh();
		}
	}

	/**
	 * Executes the command.
	 *
	 * A command may accept parameters. They will be passed from {@link module:core/editor/editor~Editor#execute `editor.execute()`}
	 * to the command.
	 *
	 * The `execute()` method will automatically abort when the command is disabled ({@link #isEnabled} is `false`).
	 * This behavior is implemented by a high priority listener to the {@link #event:execute} event.
	 *
	 * In order to see how to disable a command from "outside" see the {@link #isEnabled} documentation.
	 *
	 * This method may return a value, which would be forwarded all the way down to the
	 * {@link module:core/editor/editor~Editor#execute `editor.execute()`}.
	 *
	 * @fires execute
	 */
	execute() {}

	/**
	 * Destroys the command.
	 */
	destroy() {
		this.stopListening();
	}

	/**
	 * Event fired by the {@link #execute} method. The command action is a listener to this event so it's
	 * possible to change/cancel the behavior of the command by listening to this event.
	 *
	 * See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples.
	 *
	 * **Note:** This event is fired even if command is disabled. However, it is automatically blocked
	 * by a high priority listener in order to prevent command execution.
	 *
	 * @event execute
	 */
}

mix( Command, ObservableMixin );

// Helper function that forces command to be disabled.
function forceDisable( evt ) {
	evt.return = false;
	evt.stop();
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module core/multicommand
 */

/**
 * A CKEditor command that aggregates other commands.
 *
 * This command is used to proxy multiple commands. The multi-command is enabled when
 * at least one of its registered child commands is enabled.
 * When executing a multi-command the first enabled command with highest priority will be executed.
 *
 *		const multiCommand = new MultiCommand( editor );
 *
 *		const commandFoo = new Command( editor );
 *		const commandBar = new Command( editor );
 *
 *		// Register a child command.
 *		multiCommand.registerChildCommand( commandFoo );
 *		// Register a child command with a low priority.
 *		multiCommand.registerChildCommand( commandBar, { priority: 'low' } );
 *
 *		// Enable one of the commands.
 *		commandBar.isEnabled = true;
 *
 *		multiCommand.execute(); // Will execute commandBar.
 *
 * @extends module:core/command~Command
 */
class MultiCommand extends Command {
	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		/**
		 * Registered child commands definitions.
		 *
		 * @type {Array.<Object>}
		 * @private
		 */
		this._childCommandsDefinitions = [];
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		// Override base command refresh(): the command's state is changed when one of child commands changes states.
	}

	/**
	 * Executes the first enabled command which has the highest priority of all registered child commands.
	 *
	 * @returns {*} The value returned by the {@link module:core/command~Command#execute `command.execute()`}.
	 */
	execute( ...args ) {
		const command = this._getFirstEnabledCommand();

		return !!command && command.execute( args );
	}

	/**
	 * Registers a child command.
	 *
	 * @param {module:core/command~Command} command
	 * @param {Object} options An object with configuration options.
	 * @param {module:utils/priorities~PriorityString} [options.priority='normal'] Priority of a command to register.
	 */
	registerChildCommand( command, options = { priority: 'normal' } ) {
		insertToPriorityArray( this._childCommandsDefinitions, { command, priority: options.priority } );

		// Change multi command enabled state when one of registered commands changes state.
		command.on( 'change:isEnabled', () => this._checkEnabled() );

		this._checkEnabled();
	}

	/**
	 * Checks if any of child commands is enabled.
	 *
	 * @private
	 */
	_checkEnabled() {
		this.isEnabled = !!this._getFirstEnabledCommand();
	}

	/**
	 * Returns a first enabled command with the highest priority or `undefined` if none of them is enabled.
	 *
	 * @returns {module:core/command~Command|undefined}
	 * @private
	 */
	_getFirstEnabledCommand() {
		const commandDefinition = this._childCommandsDefinitions.find( ( { command } ) => command.isEnabled );

		return commandDefinition && commandDefinition.command;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Handles a configuration dictionary.
 */
class 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) {
            // Clone the configuration to make sure that the properties will not be shared
            // between editors and make the watchdog feature work correctly.
            this.define(cloneConfig(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);
    }
    /**
     * Iterates over all top level configuration names.
     *
     * @returns {Iterable.<String>}
     */
    *names() {
        for (const name of Object.keys(this._config)) {
            yield 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 (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 (!isPlainObject(target[part])) {
                target[part] = {};
            }
            // Nested object becomes a target.
            target = target[part];
        }
        // In case of value is an object.
        if (isPlainObject(value)) {
            // We take care of proper config structure.
            if (!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 (!isPlainObject(source[part])) {
                source = null;
                break;
            }
            // Nested object becomes a source.
            source = source[part];
        }
        // Always returns undefined for non existing configuration.
        return source ? cloneConfig(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);
        });
    }
}
// Clones configuration object or value.
// @param {*} source Source configuration
// @returns {*} Cloned configuration value.
function cloneConfig(source) {
    return cloneDeepWith(source, leaveDOMReferences);
}
// A customized function for cloneDeepWith.
// It will leave references to DOM Elements instead of cloning them.
//
// @param {*} value
// @returns {Element|undefined}
function leaveDOMReferences(value) {
    return isElement(value) ? value : undefined;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @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]);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Collections are ordered sets of objects. Items in the collection can be retrieved by their indexes
 * in the collection (like in an array) or by their ids.
 *
 * If an object without an `id` property is being added to the collection, the `id` property will be generated
 * automatically. Note that the automatically generated id is unique only within this single collection instance.
 *
 * By default an item in the collection is identified by its `id` property. The name of the identifier can be
 * configured through the constructor of the collection.
 *
 * @mixes module:utils/emittermixin~EmitterMixin
 */
class Collection extends Emitter$1 {
    /**
     * Creates a new Collection instance.
     *
     * You can provide an iterable of initial items the collection will be created with:
     *
     *		const collection = new Collection( [ { id: 'John' }, { id: 'Mike' } ] );
     *
     *		console.log( collection.get( 0 ) ); // -> { id: 'John' }
     *		console.log( collection.get( 1 ) ); // -> { id: 'Mike' }
     *		console.log( collection.get( 'Mike' ) ); // -> { id: 'Mike' }
     *
     * Or you can first create a collection and then add new items using the {@link #add} method:
     *
     *		const collection = new Collection();
     *
     *		collection.add( { id: 'John' } );
     *		console.log( collection.get( 0 ) ); // -> { id: 'John' }
     *
     * Whatever option you choose, you can always pass a configuration object as the last argument
     * of the constructor:
     *
     *		const emptyCollection = new Collection( { idProperty: 'name' } );
     *		emptyCollection.add( { name: 'John' } );
     *		console.log( collection.get( 'John' ) ); // -> { name: 'John' }
     *
     *		const nonEmptyCollection = new Collection( [ { name: 'John' } ], { idProperty: 'name' } );
     *		nonEmptyCollection.add( { name: 'George' } );
     *		console.log( collection.get( 'George' ) ); // -> { name: 'George' }
     *		console.log( collection.get( 'John' ) ); // -> { name: 'John' }
     *
     * @param {Iterable.<Object>|Object} [initialItemsOrOptions] The initial items of the collection or
     * the options object.
     * @param {Object} [options={}] The options object, when the first argument is an array of initial items.
     * @param {String} [options.idProperty='id'] The name of the property which is used to identify an item.
     * Items that do not have such a property will be assigned one when added to the collection.
     */
    constructor(initialItemsOrOptions = {}, options = {}) {
        super();
        const hasInitialItems = isIterable(initialItemsOrOptions);
        if (!hasInitialItems) {
            options = initialItemsOrOptions;
        }
        this._items = [];
        this._itemMap = new Map();
        this._idProperty = options.idProperty || 'id';
        this._bindToExternalToInternalMap = new WeakMap();
        this._bindToInternalToExternalMap = new WeakMap();
        this._skippedIndexesFromExternal = [];
        // Set the initial content of the collection (if provided in the constructor).
        if (hasInitialItems) {
            for (const item of initialItemsOrOptions) {
                this._items.push(item);
                this._itemMap.set(this._getItemIdBeforeAdding(item), item);
            }
        }
    }
    /**
     * The number of items available in the collection.
     *
     * @member {Number} #length
     */
    get length() {
        return this._items.length;
    }
    /**
     * Returns the first item from the collection or null when collection is empty.
     *
     * @returns {Object|null} The first item or `null` if collection is empty.
     */
    get first() {
        return this._items[0] || null;
    }
    /**
     * Returns the last item from the collection or null when collection is empty.
     *
     * @returns {Object|null} The last item or `null` if collection is empty.
     */
    get last() {
        return this._items[this.length - 1] || null;
    }
    /**
     * Adds an item into the collection.
     *
     * If the item does not have an id, then it will be automatically generated and set on the item.
     *
     * @chainable
     * @param {Object} item
     * @param {Number} [index] The position of the item in the collection. The item
     * is pushed to the collection when `index` not specified.
     * @fires add
     * @fires change
     */
    add(item, index) {
        return this.addMany([item], index);
    }
    /**
     * Adds multiple items into the collection.
     *
     * Any item not containing an id will get an automatically generated one.
     *
     * @chainable
     * @param {Iterable.<Object>} items
     * @param {Number} [index] The position of the insertion. Items will be appended if no `index` is specified.
     * @fires add
     * @fires change
     */
    addMany(items, index) {
        if (index === undefined) {
            index = this._items.length;
        }
        else if (index > this._items.length || index < 0) {
            /**
             * The `index` passed to {@link module:utils/collection~Collection#addMany `Collection#addMany()`}
             * is invalid. It must be a number between 0 and the collection's length.
             *
             * @error collection-add-item-invalid-index
             */
            throw new CKEditorError('collection-add-item-invalid-index', this);
        }
        let offset = 0;
        for (const item of items) {
            const itemId = this._getItemIdBeforeAdding(item);
            const currentItemIndex = index + offset;
            this._items.splice(currentItemIndex, 0, item);
            this._itemMap.set(itemId, item);
            this.fire('add', item, currentItemIndex);
            offset++;
        }
        this.fire('change', {
            added: items,
            removed: [],
            index
        });
        return this;
    }
    /**
     * Gets an item by its ID or index.
     *
     * @param {String|Number} idOrIndex The item ID or index in the collection.
     * @returns {Object|null} The requested item or `null` if such item does not exist.
     */
    get(idOrIndex) {
        let item;
        if (typeof idOrIndex == 'string') {
            item = this._itemMap.get(idOrIndex);
        }
        else if (typeof idOrIndex == 'number') {
            item = this._items[idOrIndex];
        }
        else {
            /**
             * An index or ID must be given.
             *
             * @error collection-get-invalid-arg
             */
            throw new CKEditorError('collection-get-invalid-arg', this);
        }
        return item || null;
    }
    /**
     * Returns a Boolean indicating whether the collection contains an item.
     *
     * @param {Object|String} itemOrId The item or its ID in the collection.
     * @returns {Boolean} `true` if the collection contains the item, `false` otherwise.
     */
    has(itemOrId) {
        if (typeof itemOrId == 'string') {
            return this._itemMap.has(itemOrId);
        }
        else { // Object
            const idProperty = this._idProperty;
            const id = itemOrId[idProperty];
            return id && this._itemMap.has(id);
        }
    }
    /**
     * Gets an index of an item in the collection.
     * When an item is not defined in the collection, the index will equal -1.
     *
     * @param {Object|String} itemOrId The item or its ID in the collection.
     * @returns {Number} The index of a given item.
     */
    getIndex(itemOrId) {
        let item;
        if (typeof itemOrId == 'string') {
            item = this._itemMap.get(itemOrId);
        }
        else {
            item = itemOrId;
        }
        return item ? this._items.indexOf(item) : -1;
    }
    /**
     * Removes an item from the collection.
     *
     * @param {Object|Number|String} subject The item to remove, its ID or index in the collection.
     * @returns {Object} The removed item.
     * @fires remove
     * @fires change
     */
    remove(subject) {
        const [item, index] = this._remove(subject);
        this.fire('change', {
            added: [],
            removed: [item],
            index
        });
        return item;
    }
    /**
     * Executes the callback for each item in the collection and composes an array or values returned by this callback.
     *
     * @param {Function} callback
     * @param {Object} callback.item
     * @param {Number} callback.index
     * @param {Object} [ctx] Context in which the `callback` will be called.
     * @returns {Array} The result of mapping.
     */
    map(callback, ctx) {
        return this._items.map(callback, ctx);
    }
    /**
     * Finds the first item in the collection for which the `callback` returns a true value.
     *
     * @param {Function} callback
     * @param {Object} callback.item
     * @param {Number} callback.index
     * @param {Object} [ctx] Context in which the `callback` will be called.
     * @returns {Object|undefined} The item for which `callback` returned a true value.
     */
    find(callback, ctx) {
        return this._items.find(callback, ctx);
    }
    /**
     * Returns an array with items for which the `callback` returned a true value.
     *
     * @param {Function} callback
     * @param {Object} callback.item
     * @param {Number} callback.index
     * @param {Object} [ctx] Context in which the `callback` will be called.
     * @returns {Array} The array with matching items.
     */
    filter(callback, ctx) {
        return this._items.filter(callback, ctx);
    }
    /**
     * Removes all items from the collection and destroys the binding created using
     * {@link #bindTo}.
     *
     * @fires remove
     * @fires change
     */
    clear() {
        if (this._bindToCollection) {
            this.stopListening(this._bindToCollection);
            this._bindToCollection = null;
        }
        const removedItems = Array.from(this._items);
        while (this.length) {
            this._remove(0);
        }
        this.fire('change', {
            added: [],
            removed: removedItems,
            index: 0
        });
    }
    /**
     * Binds and synchronizes the collection with another one.
     *
     * The binding can be a simple factory:
     *
     *		class FactoryClass {
     *			constructor( data ) {
     *				this.label = data.label;
     *			}
     *		}
     *
     *		const source = new Collection( { idProperty: 'label' } );
     *		const target = new Collection();
     *
     *		target.bindTo( source ).as( FactoryClass );
     *
     *		source.add( { label: 'foo' } );
     *		source.add( { label: 'bar' } );
     *
     *		console.log( target.length ); // 2
     *		console.log( target.get( 1 ).label ); // 'bar'
     *
     *		source.remove( 0 );
     *		console.log( target.length ); // 1
     *		console.log( target.get( 0 ).label ); // 'bar'
     *
     * or the factory driven by a custom callback:
     *
     *		class FooClass {
     *			constructor( data ) {
     *				this.label = data.label;
     *			}
     *		}
     *
     *		class BarClass {
     *			constructor( data ) {
     *				this.label = data.label;
     *			}
     *		}
     *
     *		const source = new Collection( { idProperty: 'label' } );
     *		const target = new Collection();
     *
     *		target.bindTo( source ).using( ( item ) => {
     *			if ( item.label == 'foo' ) {
     *				return new FooClass( item );
     *			} else {
     *				return new BarClass( item );
     *			}
     *		} );
     *
     *		source.add( { label: 'foo' } );
     *		source.add( { label: 'bar' } );
     *
     *		console.log( target.length ); // 2
     *		console.log( target.get( 0 ) instanceof FooClass ); // true
     *		console.log( target.get( 1 ) instanceof BarClass ); // true
     *
     * or the factory out of property name:
     *
     *		const source = new Collection( { idProperty: 'label' } );
     *		const target = new Collection();
     *
     *		target.bindTo( source ).using( 'label' );
     *
     *		source.add( { label: { value: 'foo' } } );
     *		source.add( { label: { value: 'bar' } } );
     *
     *		console.log( target.length ); // 2
     *		console.log( target.get( 0 ).value ); // 'foo'
     *		console.log( target.get( 1 ).value ); // 'bar'
     *
     * It's possible to skip specified items by returning falsy value:
     *
     *		const source = new Collection();
     *		const target = new Collection();
     *
     *		target.bindTo( source ).using( item => {
     *			if ( item.hidden ) {
     *				return null;
     *			}
     *
     *			return item;
     *		} );
     *
     *		source.add( { hidden: true } );
     *		source.add( { hidden: false } );
     *
     *		console.log( source.length ); // 2
     *		console.log( target.length ); // 1
     *
     * **Note**: {@link #clear} can be used to break the binding.
     *
     * @param {module:utils/collection~Collection} externalCollection A collection to be bound.
     * @returns {module:utils/collection~CollectionBindToChain} The binding chain object.
     */
    bindTo(externalCollection) {
        if (this._bindToCollection) {
            /**
             * The collection cannot be bound more than once.
             *
             * @error collection-bind-to-rebind
             */
            throw new CKEditorError('collection-bind-to-rebind', this);
        }
        this._bindToCollection = externalCollection;
        return {
            as: Class => {
                this._setUpBindToBinding(item => new Class(item));
            },
            using: callbackOrProperty => {
                if (typeof callbackOrProperty == 'function') {
                    this._setUpBindToBinding(callbackOrProperty);
                }
                else {
                    this._setUpBindToBinding(item => item[callbackOrProperty]);
                }
            }
        };
    }
    /**
     * Finalizes and activates a binding initiated by {#bindTo}.
     *
     * @private
     * @param {Function} factory A function which produces collection items.
     */
    _setUpBindToBinding(factory) {
        const externalCollection = this._bindToCollection;
        // Adds the item to the collection once a change has been done to the external collection.
        //
        // @private
        const addItem = (evt, externalItem, index) => {
            const isExternalBoundToThis = externalCollection._bindToCollection == this;
            const externalItemBound = externalCollection._bindToInternalToExternalMap.get(externalItem);
            // If an external collection is bound to this collection, which makes it a 2–way binding,
            // and the particular external collection item is already bound, don't add it here.
            // The external item has been created **out of this collection's item** and (re)adding it will
            // cause a loop.
            if (isExternalBoundToThis && externalItemBound) {
                this._bindToExternalToInternalMap.set(externalItem, externalItemBound);
                this._bindToInternalToExternalMap.set(externalItemBound, externalItem);
            }
            else {
                const item = factory(externalItem);
                // When there is no item we need to remember skipped index first and then we can skip this item.
                if (!item) {
                    this._skippedIndexesFromExternal.push(index);
                    return;
                }
                // Lets try to put item at the same index as index in external collection
                // but when there are a skipped items in one or both collections we need to recalculate this index.
                let finalIndex = index;
                // When we try to insert item after some skipped items from external collection we need
                // to include this skipped items and decrease index.
                //
                // For the following example:
                // external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal' ]
                // internal -> [ A ]
                //
                // Another item is been added at the end of external collection:
                // external.add( 'D' )
                // external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal', 'D' ]
                //
                // We can't just add 'D' to internal at the same index as index in external because
                // this will produce empty indexes what is invalid:
                // internal -> [ 'A', empty, empty, 'D' ]
                //
                // So we need to include skipped items and decrease index
                // internal -> [ 'A', 'D' ]
                for (const skipped of this._skippedIndexesFromExternal) {
                    if (index > skipped) {
                        finalIndex--;
                    }
                }
                // We need to take into consideration that external collection could skip some items from
                // internal collection.
                //
                // For the following example:
                // internal -> [ 'A', 'B - skipped for external', 'C - skipped for external' ]
                // external -> [ A ]
                //
                // Another item is been added at the end of external collection:
                // external.add( 'D' )
                // external -> [ 'A', 'D' ]
                //
                // We need to include skipped items and place new item after them:
                // internal -> [ 'A', 'B - skipped for external', 'C - skipped for external', 'D' ]
                for (const skipped of externalCollection._skippedIndexesFromExternal) {
                    if (finalIndex >= skipped) {
                        finalIndex++;
                    }
                }
                this._bindToExternalToInternalMap.set(externalItem, item);
                this._bindToInternalToExternalMap.set(item, externalItem);
                this.add(item, finalIndex);
                // After adding new element to internal collection we need update indexes
                // of skipped items in external collection.
                for (let i = 0; i < externalCollection._skippedIndexesFromExternal.length; i++) {
                    if (finalIndex <= externalCollection._skippedIndexesFromExternal[i]) {
                        externalCollection._skippedIndexesFromExternal[i]++;
                    }
                }
            }
        };
        // Load the initial content of the collection.
        for (const externalItem of externalCollection) {
            addItem(null, externalItem, externalCollection.getIndex(externalItem));
        }
        // Synchronize the with collection as new items are added.
        this.listenTo(externalCollection, 'add', addItem);
        // Synchronize the with collection as new items are removed.
        this.listenTo(externalCollection, 'remove', (evt, externalItem, index) => {
            const item = this._bindToExternalToInternalMap.get(externalItem);
            if (item) {
                this.remove(item);
            }
            // After removing element from external collection we need update/remove indexes
            // of skipped items in internal collection.
            this._skippedIndexesFromExternal = this._skippedIndexesFromExternal.reduce((result, skipped) => {
                if (index < skipped) {
                    result.push(skipped - 1);
                }
                if (index > skipped) {
                    result.push(skipped);
                }
                return result;
            }, []);
        });
    }
    /**
     * Returns an unique id property for a given `item`.
     *
     * The method will generate new id and assign it to the `item` if it doesn't have any.
     *
     * @private
     * @param {Object} item Item to be added.
     * @returns {String}
     */
    _getItemIdBeforeAdding(item) {
        const idProperty = this._idProperty;
        let itemId;
        if ((idProperty in item)) {
            itemId = item[idProperty];
            if (typeof itemId != 'string') {
                /**
                 * This item's ID should be a string.
                 *
                 * @error collection-add-invalid-id
                 */
                throw new CKEditorError('collection-add-invalid-id', this);
            }
            if (this.get(itemId)) {
                /**
                 * This item already exists in the collection.
                 *
                 * @error collection-add-item-already-exists
                 */
                throw new CKEditorError('collection-add-item-already-exists', this);
            }
        }
        else {
            item[idProperty] = itemId = uid();
        }
        return itemId;
    }
    /**
     * Core {@link #remove} method implementation shared in other functions.
     *
     * In contrast this method **does not** fire the {@link #event:change} event.
     *
     * @private
     * @param {Object|Number|String} subject The item to remove, its id or index in the collection.
     * @returns {Array} Returns an array with the removed item and its index.
     * @fires remove
     */
    _remove(subject) {
        let index, id, item;
        let itemDoesNotExist = false;
        const idProperty = this._idProperty;
        if (typeof subject == 'string') {
            id = subject;
            item = this._itemMap.get(id);
            itemDoesNotExist = !item;
            if (item) {
                index = this._items.indexOf(item);
            }
        }
        else if (typeof subject == 'number') {
            index = subject;
            item = this._items[index];
            itemDoesNotExist = !item;
            if (item) {
                id = item[idProperty];
            }
        }
        else {
            item = subject;
            id = item[idProperty];
            index = this._items.indexOf(item);
            itemDoesNotExist = (index == -1 || !this._itemMap.get(id));
        }
        if (itemDoesNotExist) {
            /**
             * Item not found.
             *
             * @error collection-remove-404
             */
            throw new CKEditorError('collection-remove-404', this);
        }
        this._items.splice(index, 1);
        this._itemMap.delete(id);
        const externalItem = this._bindToInternalToExternalMap.get(item);
        this._bindToInternalToExternalMap.delete(item);
        this._bindToExternalToInternalMap.delete(externalItem);
        this.fire('remove', item, index);
        return [item, index];
    }
    /**
     * Iterable interface.
     *
     * @returns {Iterator.<*>}
     */
    [Symbol.iterator]() {
        return this._items[Symbol.iterator]();
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Manages a list of CKEditor plugins, including loading, resolving dependencies and initialization.
 *
 * @mixes module:utils/emittermixin~EmitterMixin
 */
class PluginCollection {
	/**
	 * Creates an instance of the plugin collection class.
	 * Allows loading and initializing plugins and their dependencies.
	 * Allows providing a list of already loaded plugins. These plugins will not be destroyed along with this collection.
	 *
	 * @param {module:core/editor/editor~Editor|module:core/context~Context} context
	 * @param {Array.<Function>} [availablePlugins] Plugins (constructors) which the collection will be able to use
	 * when {@link module:core/plugincollection~PluginCollection#init} is used with the 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.
	 * @param {Iterable.<Array>} contextPlugins A list of already initialized plugins represented by a
	 * `[ PluginConstructor, pluginInstance ]` pair.
	 */
	constructor( context, availablePlugins = [], contextPlugins = [] ) {
		/**
		 * @protected
		 * @type {module:core/editor/editor~Editor|module:core/context~Context}
		 */
		this._context = context;

		/**
		 * @protected
		 * @type {Map}
		 */
		this._plugins = new Map();

		/**
		 * A map of plugin constructors that can be retrieved by their names.
		 *
		 * @protected
		 * @type {Map.<String|Function,Function>}
		 */
		this._availablePlugins = new Map();

		for ( const PluginConstructor of availablePlugins ) {
			if ( PluginConstructor.pluginName ) {
				this._availablePlugins.set( PluginConstructor.pluginName, PluginConstructor );
			}
		}

		/**
		 * Map of {@link module:core/contextplugin~ContextPlugin context plugins} which can be retrieved by their constructors or instances.
		 *
		 * @protected
		 * @type {Map<Function,Function>}
		 */
		this._contextPlugins = new Map();

		for ( const [ PluginConstructor, pluginInstance ] of contextPlugins ) {
			this._contextPlugins.set( PluginConstructor, pluginInstance );
			this._contextPlugins.set( pluginInstance, PluginConstructor );

			// To make it possible to require a plugin by its name.
			if ( PluginConstructor.pluginName ) {
				this._availablePlugins.set( PluginConstructor.pluginName, PluginConstructor );
			}
		}
	}

	/**
	 * Iterable interface.
	 *
	 * Returns `[ PluginConstructor, pluginInstance ]` pairs.
	 *
	 * @returns {Iterable.<Array>}
	 */
	* [ Symbol.iterator ]() {
		for ( const entry of this._plugins ) {
			if ( typeof entry[ 0 ] == 'function' ) {
				yield entry;
			}
		}
	}

	/**
	 * Gets the plugin instance by its constructor or name.
	 *
	 *		// Check if 'Clipboard' plugin was loaded.
	 *		if ( editor.plugins.has( 'ClipboardPipeline' ) ) {
	 *			// Get clipboard plugin instance
	 *			const clipboard = editor.plugins.get( 'ClipboardPipeline' );
	 *
	 *			this.listenTo( clipboard, 'inputTransformation', ( evt, data ) => {
	 *				// Do something on clipboard input.
	 *			} );
	 *		}
	 *
	 * **Note**: This method will throw an error if a plugin is not loaded. Use `{@link #has editor.plugins.has()}`
	 * to check if a plugin is available.
	 *
	 * @param {Function|String} key The plugin constructor or {@link module:core/plugin~PluginInterface.pluginName name}.
	 * @returns {module:core/plugin~PluginInterface}
	 */
	get( key ) {
		const plugin = this._plugins.get( key );

		if ( !plugin ) {
			let pluginName = key;

			if ( typeof key == 'function' ) {
				pluginName = key.pluginName || key.name;
			}

			/**
			 * The plugin is not loaded and could not be obtained.
			 *
			 * Plugin classes (constructors) need to be provided to the editor and must be loaded before they can be obtained from
			 * the plugin collection.
			 * This is usually done in CKEditor 5 builds by setting the {@link module:core/editor/editor~Editor.builtinPlugins}
			 * property.
			 *
			 * **Note**: You can use `{@link module:core/plugincollection~PluginCollection#has editor.plugins.has()}`
			 * to check if a plugin was loaded.
			 *
			 * @error plugincollection-plugin-not-loaded
			 * @param {String} plugin The name of the plugin which is not loaded.
			 */
			throw new CKEditorError( 'plugincollection-plugin-not-loaded', this._context, { plugin: pluginName } );
		}

		return plugin;
	}

	/**
	 * Checks if a plugin is loaded.
	 *
	 *		// Check if the 'Clipboard' plugin was loaded.
	 *		if ( editor.plugins.has( 'ClipboardPipeline' ) ) {
	 *			// Now use the clipboard plugin instance:
	 *			const clipboard = editor.plugins.get( 'ClipboardPipeline' );
	 *
	 *			// ...
	 *		}
	 *
	 * @param {Function|String} key The plugin constructor or {@link module:core/plugin~PluginInterface.pluginName name}.
	 * @returns {Boolean}
	 */
	has( key ) {
		return this._plugins.has( key );
	}

	/**
	 * Initializes 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}.
	 * @param {Array.<String|Function>} [pluginsToRemove] Names of the plugins or plugin constructors
	 * that should not be loaded (despite being specified in the `plugins` array).
	 * @param {Array.<Function>} [pluginsSubstitutions] An array of {@link module:core/plugin~PluginInterface plugin constructors}
	 * that will be used to replace plugins of the same names that were passed in `plugins` or that are in their dependency tree.
	 * A useful option for replacing built-in plugins while creating tests (for mocking their APIs). Plugins that will be replaced
	 * must follow these rules:
	 *   * The new plugin must be a class.
	 *   * The new plugin must be named.
	 *   * Both plugins must not depend on other plugins.
	 * @returns {Promise.<module:core/plugin~LoadedPlugins>} A promise which gets resolved once all plugins are loaded
	 * and available in the collection.
	 */
	init( plugins, pluginsToRemove = [], pluginsSubstitutions = [] ) {
		// Plugin initialization procedure consists of 2 main steps:
		// 1) collecting all available plugin constructors,
		// 2) verification whether all required plugins can be instantiated.
		//
		// In the first step, all plugin constructors, available in the provided `plugins` array and inside
		// plugin's dependencies (from the `Plugin.requires` array), are recursively collected and added to the existing
		// `this._availablePlugins` map, but without any verification at the given moment. Performing the verification
		// at this point (during the plugin constructor searching) would cause false errors to occur, that some plugin
		// is missing but in fact it may be defined further in the array as the dependency of other plugin. After
		// traversing the entire dependency tree, it will be checked if all required "top level" plugins are available.
		//
		// In the second step, the list of plugins that have not been explicitly removed is traversed to get all the
		// plugin constructors to be instantiated in the correct order and to validate against some rules. Finally, if
		// no plugin is missing and no other error has been found, they all will be instantiated.
		const that = this;
		const context = this._context;

		findAvailablePluginConstructors( plugins );

		validatePlugins( plugins );

		const pluginsToLoad = plugins.filter( plugin => !isPluginRemoved( plugin, pluginsToRemove ) );

		const pluginConstructors = [ ...getPluginConstructors( pluginsToLoad ) ];

		substitutePlugins( pluginConstructors, pluginsSubstitutions );

		const pluginInstances = loadPlugins( pluginConstructors );

		return initPlugins( pluginInstances, 'init' )
			.then( () => initPlugins( pluginInstances, 'afterInit' ) )
			.then( () => pluginInstances );

		function isPluginConstructor( plugin ) {
			return typeof plugin === 'function';
		}

		function isContextPlugin( plugin ) {
			return isPluginConstructor( plugin ) && plugin.isContextPlugin;
		}

		function isPluginRemoved( plugin, pluginsToRemove ) {
			return pluginsToRemove.some( removedPlugin => {
				if ( removedPlugin === plugin ) {
					return true;
				}

				if ( getPluginName( plugin ) === removedPlugin ) {
					return true;
				}

				if ( getPluginName( removedPlugin ) === plugin ) {
					return true;
				}

				return false;
			} );
		}

		function getPluginName( plugin ) {
			return isPluginConstructor( plugin ) ?
				plugin.pluginName || plugin.name :
				plugin;
		}

		function findAvailablePluginConstructors( plugins, processed = new Set() ) {
			plugins.forEach( plugin => {
				if ( !isPluginConstructor( plugin ) ) {
					return;
				}

				if ( processed.has( plugin ) ) {
					return;
				}

				processed.add( plugin );

				if ( plugin.pluginName && !that._availablePlugins.has( plugin.pluginName ) ) {
					that._availablePlugins.set( plugin.pluginName, plugin );
				}

				if ( plugin.requires ) {
					findAvailablePluginConstructors( plugin.requires, processed );
				}
			} );
		}

		function getPluginConstructors( plugins, processed = new Set() ) {
			return plugins
				.map( plugin => {
					return isPluginConstructor( plugin ) ?
						plugin :
						that._availablePlugins.get( plugin );
				} )
				.reduce( ( result, plugin ) => {
					if ( processed.has( plugin ) ) {
						return result;
					}

					processed.add( plugin );

					if ( plugin.requires ) {
						validatePlugins( plugin.requires, plugin );

						getPluginConstructors( plugin.requires, processed ).forEach( plugin => result.add( plugin ) );
					}

					return result.add( plugin );
				}, new Set() );
		}

		function validatePlugins( plugins, parentPluginConstructor = null ) {
			plugins
				.map( plugin => {
					return isPluginConstructor( plugin ) ?
						plugin :
						that._availablePlugins.get( plugin ) || plugin;
				} )
				.forEach( plugin => {
					checkMissingPlugin( plugin, parentPluginConstructor );
					checkContextPlugin( plugin, parentPluginConstructor );
					checkRemovedPlugin( plugin, parentPluginConstructor );
				} );
		}

		function checkMissingPlugin( plugin, parentPluginConstructor ) {
			if ( isPluginConstructor( plugin ) ) {
				return;
			}

			if ( parentPluginConstructor ) {
				/**
				 * A required "soft" dependency was not found on the plugin list.
				 *
				 * When configuring the editor, either prior to building (via
				 * {@link module:core/editor/editor~Editor.builtinPlugins `Editor.builtinPlugins`}) or when
				 * creating a new instance of the editor (e.g. via
				 * {@link module:core/editor/editorconfig~EditorConfig#plugins `config.plugins`}), you need to provide
				 * some of the dependencies for other plugins that you used.
				 *
				 * This error is thrown when one of these dependencies was not provided. The name of the missing plugin
				 * can be found in `missingPlugin` and the plugin that required it in `requiredBy`.
				 *
				 * In order to resolve it, you need to import the missing plugin and add it to the
				 * current list of plugins (`Editor.builtinPlugins` or `config.plugins`/`config.extraPlugins`).
				 *
				 * Soft requirements were introduced in version 26.0.0. If you happen to stumble upon this error
				 * when upgrading to version 26.0.0, read also the
				 * {@glink updating/migration-to-26 Migration to 26.0.0} guide.
				 *
				 * @error plugincollection-soft-required
				 * @param {String} missingPlugin The name of the required plugin.
				 * @param {String} requiredBy The name of the plugin that requires the other plugin.
				 */
				throw new CKEditorError(
					'plugincollection-soft-required',
					context,
					{ missingPlugin: plugin, requiredBy: getPluginName( parentPluginConstructor ) }
				);
			}

			/**
			 * A plugin is 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 in CKEditor 5 builds by setting the {@link module:core/editor/editor~Editor.builtinPlugins}
			 * property.
			 *
			 * **If you see this warning when using one of the {@glink installation/getting-started/predefined-builds
			 * CKEditor 5 Builds}**,
			 * it means that you try to enable a plugin which was not included in that build. This may be due to a typo
			 * in the plugin name or simply because that plugin is not a part of this build. In the latter scenario,
			 * read more about {@glink installation/getting-started/quick-start 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 a reference (as a constructor function). Check out the examples in
			 * {@glink installation/advanced/alternative-setups/integrating-from-source "Building from source"}.
			 *
			 * @error plugincollection-plugin-not-found
			 * @param {String} plugin The name of the plugin which could not be loaded.
			 */
			throw new CKEditorError(
				'plugincollection-plugin-not-found',
				context,
				{ plugin }
			);
		}

		function checkContextPlugin( plugin, parentPluginConstructor ) {
			if ( !isContextPlugin( parentPluginConstructor ) ) {
				return;
			}

			if ( isContextPlugin( plugin ) ) {
				return;
			}

			/**
			 * If a plugin is a context plugin, all plugins it requires should also be context plugins
			 * instead of plugins. In other words, if one plugin can be used in the context,
			 * all its requirements should also be ready to be used in the context. Note that the context
			 * provides only a part of the API provided by the editor. If one plugin needs a full
			 * editor API, all plugins which require it are considered as plugins that need a full
			 * editor API.
			 *
			 * @error plugincollection-context-required
			 * @param {String} plugin The name of the required plugin.
			 * @param {String} requiredBy The name of the parent plugin.
			 */
			throw new CKEditorError(
				'plugincollection-context-required',
				context,
				{ plugin: getPluginName( plugin ), requiredBy: getPluginName( parentPluginConstructor ) }
			);
		}

		function checkRemovedPlugin( plugin, parentPluginConstructor ) {
			if ( !parentPluginConstructor ) {
				return;
			}

			if ( !isPluginRemoved( plugin, pluginsToRemove ) ) {
				return;
			}

			/**
			 * Cannot load a plugin because one of its dependencies is listed in the `removePlugins` option.
			 *
			 * @error plugincollection-required
			 * @param {String} plugin The name of the required plugin.
			 * @param {String} requiredBy The name of the parent plugin.
			 */
			throw new CKEditorError(
				'plugincollection-required',
				context,
				{ plugin: getPluginName( plugin ), requiredBy: getPluginName( parentPluginConstructor ) }
			);
		}

		function loadPlugins( pluginConstructors ) {
			return pluginConstructors.map( PluginConstructor => {
				const pluginInstance = that._contextPlugins.get( PluginConstructor ) || new PluginConstructor( context );

				that._add( PluginConstructor, pluginInstance );

				return pluginInstance;
			} );
		}

		function initPlugins( pluginInstances, method ) {
			return pluginInstances.reduce( ( promise, plugin ) => {
				if ( !plugin[ method ] ) {
					return promise;
				}

				if ( that._contextPlugins.has( plugin ) ) {
					return promise;
				}

				return promise.then( plugin[ method ].bind( plugin ) );
			}, Promise.resolve() );
		}

		// Replaces plugin constructors with the specified set of plugins.
		//
		// @param {Array.<Function>} pluginConstructors
		// @param {Array.<Function>} pluginsSubstitutions
		function substitutePlugins( pluginConstructors, pluginsSubstitutions ) {
			for ( const pluginItem of pluginsSubstitutions ) {
				if ( typeof pluginItem != 'function' ) {
					/**
					 * The plugin replacing an existing plugin must be a function.
					 *
					 * @error plugincollection-replace-plugin-invalid-type
					 */
					throw new CKEditorError( 'plugincollection-replace-plugin-invalid-type', null, { pluginItem } );
				}
				const pluginName = pluginItem.pluginName;

				if ( !pluginName ) {
					/**
					 * The plugin replacing an existing plugin must have a name.
					 *
					 * @error plugincollection-replace-plugin-missing-name
					 */
					throw new CKEditorError( 'plugincollection-replace-plugin-missing-name', null, { pluginItem } );
				}

				if ( pluginItem.requires && pluginItem.requires.length ) {
					/**
					 * The plugin replacing an existing plugin cannot depend on other plugins.
					 *
					 * @error plugincollection-plugin-for-replacing-cannot-have-dependencies
					 */
					throw new CKEditorError( 'plugincollection-plugin-for-replacing-cannot-have-dependencies', null, { pluginName } );
				}

				const pluginToReplace = that._availablePlugins.get( pluginName );

				if ( !pluginToReplace ) {
					/**
					 * The replaced plugin does not exist in the
					 * {@link module:core/plugincollection~PluginCollection available plugins} collection.
					 *
					 * @error plugincollection-plugin-for-replacing-not-exist
					 */
					throw new CKEditorError( 'plugincollection-plugin-for-replacing-not-exist', null, { pluginName } );
				}

				const indexInPluginConstructors = pluginConstructors.indexOf( pluginToReplace );

				if ( indexInPluginConstructors === -1 ) {
					// The Context feature can substitute plugins as well.
					// It may happen that the editor will be created with the given context, where the plugin for substitute
					// was already replaced. In such a case, we don't want to do it again.
					if ( that._contextPlugins.has( pluginToReplace ) ) {
						return;
					}

					/**
					 * The replaced plugin will not be loaded so it cannot be replaced.
					 *
					 * @error plugincollection-plugin-for-replacing-not-loaded
					 */
					throw new CKEditorError( 'plugincollection-plugin-for-replacing-not-loaded', null, { pluginName } );
				}

				if ( pluginToReplace.requires && pluginToReplace.requires.length ) {
					/**
					 * The replaced plugin cannot depend on other plugins.
					 *
					 * @error plugincollection-replaced-plugin-cannot-have-dependencies
					 */
					throw new CKEditorError( 'plugincollection-replaced-plugin-cannot-have-dependencies', null, { pluginName } );
				}

				pluginConstructors.splice( indexInPluginConstructors, 1, pluginItem );
				that._availablePlugins.set( pluginName, pluginItem );
			}
		}
	}

	/**
	 * Destroys all loaded plugins.
	 *
	 * @returns {Promise}
	 */
	destroy() {
		const promises = [];

		for ( const [ , pluginInstance ] of this ) {
			if ( typeof pluginInstance.destroy == 'function' && !this._contextPlugins.has( pluginInstance ) ) {
				promises.push( 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 will lead to runtime conflicts between these plugins.
			 *
			 * In practice, this warning usually means that new plugins were added to an existing CKEditor 5 build.
			 * Plugins should always be added to a source version of the editor (`@ckeditor/ckeditor5-editor-*`),
			 * not to an editor imported from one of the `@ckeditor/ckeditor5-build-*` packages.
			 *
			 * Check your import paths and the list of plugins passed to
			 * {@link module:core/editor/editor~Editor.create `Editor.create()`}
			 * or specified in {@link module:core/editor/editor~Editor.builtinPlugins `Editor.builtinPlugins`}.
			 *
			 * The second option is that your `node_modules/` directory contains duplicated versions of the same
			 * CKEditor 5 packages. Normally, on clean installations, npm deduplicates packages in `node_modules/`, so
			 * it may be enough to call `rm -rf node_modules && npm i`. However, if you installed conflicting versions
			 * of some packages, their dependencies may need to be installed in more than one version which may lead to this
			 * warning.
			 *
			 * Technically speaking, this error occurs because after adding a plugin to an existing editor build
			 * the dependencies of this plugin are being duplicated.
			 * They are already built into that editor build and now get added for the second time as dependencies
			 * of the plugin you are installing.
			 *
			 * Read more about {@glink installation/getting-started/installing-plugins installing plugins}.
			 *
			 * @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.
			 */
			throw new CKEditorError(
				'plugincollection-plugin-name-conflict',
				null,
				{ pluginName, plugin1: this._plugins.get( pluginName ).constructor, plugin2: PluginConstructor }
			);
		}

		this._plugins.set( pluginName, plugin );
	}
}

mix( PluginCollection, EmitterMixin );/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
function toArray(data) {
    return Array.isArray(data) ? data : [data];
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/* globals window, document */
/**
 * @module utils/dom/global
 */
/**
 * A helper (module) giving an access to the global DOM objects such as `window` and
 * `document`. Accessing these objects using this helper allows easy and bulletproof
 * testing, i.e. stubbing native properties:
 *
 *		import global from 'ckeditor5/utils/dom/global.js';
 *
 *		// This stub will work for any code using global module.
 *		testUtils.sinon.stub( global, 'window', {
 *			innerWidth: 10000
 *		} );
 *
 *		console.log( global.window.innerWidth );
 */
let global$1;
// In some environments window and document API might not be available.
try {
    global$1 = { window, document };
}
catch (e) {
    // It's not possible to mock a window object to simulate lack of a window object without writing extremely convoluted code.
    /* istanbul ignore next */
    // Let's cast it to not change module's API.
    // We only handle this so loading editor in environments without window and document doesn't fail.
    // For better DX we shouldn't introduce mixed types and require developers to check the type manually.
    // This module should not be used on purpose in any environment outside browser.
    global$1 = { window: {}, document: {} };
}
var global$2 = global$1;/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/* istanbul ignore else */
if (!global$2.window.CKEDITOR_TRANSLATIONS) {
    global$2.window.CKEDITOR_TRANSLATIONS = {};
}
/**
 * **Note:** This method is internal, use {@link module:utils/locale~Locale#t the `t()` function} instead to translate
 * the editor UI parts.
 *
 * This function is responsible for translating messages to the specified language. It uses translations added perviously
 * by {@link module:utils/translation-service~add} (a translations dictionary and the `getPluralForm()` function
 * to provide accurate translations of plural forms).
 *
 * When no translation is defined in the dictionary or the dictionary does not exist, this function returns
 * the original message string or the message plural depending on the number of elements.
 *
 *		translate( 'pl', { string: 'Cancel' } ); // 'Cancel'
 *
 * The third optional argument is the number of elements, based on which the single form or one of the plural forms
 * should be picked when the message is supposed to support various plural forms.
 *
 * 		translate( 'en', { string: 'Add a space', plural: 'Add %0 spaces' }, 1 ); // 'Add a space'
 * 		translate( 'en', { string: 'Add a space', plural: 'Add %0 spaces' }, 3 ); // 'Add %0 spaces'
 *
 * The message should provide an ID using the `id` property when the message strings are not unique and their
 * translations should be different.
 *
 *		translate( 'en', { string: 'image', id: 'ADD_IMAGE' } );
 *		translate( 'en', { string: 'image', id: 'AN_IMAGE' } );
 *
 * @protected
 * @param {String} language Target language.
 * @param {module:utils/translation-service~Message} message A message that will be translated.
 * @param {Number} [quantity] The number of elements for which a plural form should be picked from the target language dictionary.
 * @returns {String} Translated sentence.
 */
function _translate(language, message, quantity = 1) {
    if (typeof quantity !== 'number') {
        /**
         * An incorrect value was passed to the translation function. This was probably caused
         * by an incorrect message interpolation of a plural form. Note that for messages supporting plural forms
         * the second argument of the `t()` function should always be a number or an array with a number as the first element.
         *
         * @error translation-service-quantity-not-a-number
         */
        throw new CKEditorError('translation-service-quantity-not-a-number', null, { quantity });
    }
    const numberOfLanguages = getNumberOfLanguages();
    if (numberOfLanguages === 1) {
        // Override the language to the only supported one.
        // This can't be done in the `Locale` class, because the translations comes after the `Locale` class initialization.
        language = Object.keys(global$2.window.CKEDITOR_TRANSLATIONS)[0];
    }
    const messageId = message.id || message.string;
    if (numberOfLanguages === 0 || !hasTranslation(language, messageId)) {
        if (quantity !== 1) {
            // Return the default plural form that was passed in the `message.plural` parameter.
            return message.plural;
        }
        return message.string;
    }
    const dictionary = global$2.window.CKEDITOR_TRANSLATIONS[language].dictionary;
    const getPluralForm = global$2.window.CKEDITOR_TRANSLATIONS[language].getPluralForm || (n => n === 1 ? 0 : 1);
    const translation = dictionary[messageId];
    if (typeof translation === 'string') {
        return translation;
    }
    const pluralFormIndex = Number(getPluralForm(quantity));
    // Note: The `translate` function is not responsible for replacing `%0, %1, ...` with values.
    return translation[pluralFormIndex];
}
// Checks whether the dictionary exists and translation in that dictionary exists.
function hasTranslation(language, messageId) {
    return (!!global$2.window.CKEDITOR_TRANSLATIONS[language] &&
        !!global$2.window.CKEDITOR_TRANSLATIONS[language].dictionary[messageId]);
}
function getNumberOfLanguages() {
    return Object.keys(global$2.window.CKEDITOR_TRANSLATIONS).length;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const RTL_LANGUAGE_CODES = [
    'ar', 'ara',
    'fa', 'per', 'fas',
    'he', 'heb',
    'ku', 'kur',
    'ug', 'uig' // Uighur, Uyghur
];
/**
 * Helps determine whether a language text direction is LTR or RTL.
 *
 * @param {String} languageCode The ISO 639-1 or ISO 639-2 language code.
 * @returns {module:utils/language~LanguageDirection}
 */
function getLanguageDirection(languageCode) {
    return RTL_LANGUAGE_CODES.includes(languageCode) ? 'rtl' : 'ltr';
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Represents the localization services.
 */
class Locale {
    /**
     * Creates a new instance of the locale class. Learn more about
     * {@glink features/ui-language configuring the language of the editor}.
     *
     * @param {Object} [options] Locale configuration.
     * @param {String} [options.uiLanguage='en'] The editor UI language code in the
     * [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format. See {@link #uiLanguage}.
     * @param {String} [options.contentLanguage] The editor content language code in the
     * [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format. If not specified, the same as `options.language`.
     * See {@link #contentLanguage}.
     */
    constructor(options = {}) {
        this.uiLanguage = options.uiLanguage || 'en';
        this.contentLanguage = options.contentLanguage || this.uiLanguage;
        this.uiLanguageDirection = getLanguageDirection(this.uiLanguage);
        this.contentLanguageDirection = getLanguageDirection(this.contentLanguage);
        this.t = (message, values) => this._t(message, values);
    }
    /**
     * The editor UI language code in the [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) format.
     *
     * **Note**: This property was deprecated. Please use {@link #uiLanguage} and {@link #contentLanguage}
     * properties instead.
     *
     * @deprecated
     * @member {String}
     */
    get language() {
        /**
         * The {@link module:utils/locale~Locale#language `Locale#language`} property was deprecated and will
         * be removed in the near future. Please use the {@link #uiLanguage} and {@link #contentLanguage} properties instead.
         *
         * @error locale-deprecated-language-property
         */
        console.warn('locale-deprecated-language-property: ' +
            'The Locale#language property has been deprecated and will be removed in the near future. ' +
            'Please use #uiLanguage and #contentLanguage properties instead.');
        return this.uiLanguage;
    }
    /**
     * An unbound version of the {@link #t} method.
     *
     * @private
     * @param {String|module:utils/translation-service~Message} message
     * @param {Number|String|Array.<Number|String>} [values]
     * @returns {String}
     */
    _t(message, values = []) {
        values = toArray(values);
        if (typeof message === 'string') {
            message = { string: message };
        }
        const hasPluralForm = !!message.plural;
        const quantity = hasPluralForm ? values[0] : 1;
        const translatedString = _translate(this.uiLanguage, message, quantity);
        return interpolateString(translatedString, values);
    }
}
// Fills the `%0, %1, ...` string placeholders with values.
function interpolateString(string, values) {
    return string.replace(/%(\d+)/g, (match, index) => {
        return (index < values.length) ? values[index] : match;
    });
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Provides a common, higher-level environment for solutions that use multiple {@link module:core/editor/editor~Editor editors}
 * or plugins that work outside the editor. Use it instead of {@link module:core/editor/editor~Editor.create `Editor.create()`}
 * in advanced application integrations.
 *
 * All configuration options passed to a context will be used as default options for editor instances initialized in that context.
 *
 * {@link module:core/contextplugin~ContextPlugin Context plugins} passed to a context instance will be shared among all
 * editor instances initialized in this context. These will be the same plugin instances for all the editors.
 *
 * **Note:** The context can only be initialized with {@link module:core/contextplugin~ContextPlugin context plugins}
 * (e.g. [comments](https://ckeditor.com/collaboration/comments/)). Regular {@link module:core/plugin~Plugin plugins} require an
 * editor instance to work and cannot be added to a context.
 *
 * **Note:** You can add a context plugin to an editor instance, though.
 *
 * If you are using multiple editor instances on one page and use any context plugins, create a context to share the configuration and
 * plugins among these editors. Some plugins will use the information about all existing editors to better integrate between them.
 *
 * If you are using plugins that do not require an editor to work (e.g. [comments](https://ckeditor.com/collaboration/comments/)),
 * enable and configure them using the context.
 *
 * If you are using only a single editor on each page, use {@link module:core/editor/editor~Editor.create `Editor.create()`} instead.
 * In such case, a context instance will be created by the editor instance in a transparent way.
 *
 * See {@link module:core/context~Context.create `Context.create()`} for usage examples.
 */
class Context {
	/**
	 * Creates a context instance with a given configuration.
	 *
	 * Usually not to be used directly. See the static {@link module:core/context~Context.create `create()`} method.
	 *
	 * @param {Object} [config={}] The context configuration.
	 */
	constructor( config ) {
		/**
		 * Stores all the configurations specific to this context instance.
		 *
		 * @readonly
		 * @type {module:utils/config~Config}
		 */
		this.config = new Config( config, this.constructor.defaultConfig );

		const availablePlugins = this.constructor.builtinPlugins;

		this.config.define( 'plugins', availablePlugins );

		/**
		 * The plugins loaded and in use by this context instance.
		 *
		 * @readonly
		 * @type {module:core/plugincollection~PluginCollection}
		 */
		this.plugins = new PluginCollection( this, availablePlugins );

		const languageConfig = this.config.get( 'language' ) || {};

		/**
		 * @readonly
		 * @type {module:utils/locale~Locale}
		 */
		this.locale = new Locale( {
			uiLanguage: typeof languageConfig === 'string' ? languageConfig : languageConfig.ui,
			contentLanguage: this.config.get( 'language.content' )
		} );

		/**
		 * Shorthand for {@link module:utils/locale~Locale#t}.
		 *
		 * @see module:utils/locale~Locale#t
		 * @method #t
		 */
		this.t = this.locale.t;

		/**
		 * A list of editors that this context instance is injected to.
		 *
		 * @readonly
		 * @type {module:utils/collection~Collection}
		 */
		this.editors = new Collection();

		/**
		 * Reference to the editor which created the context.
		 * Null when the context was created outside of the editor.
		 *
		 * It is used to destroy the context when removing the editor that has created the context.
		 *
		 * @private
		 * @type {module:core/editor/editor~Editor|null}
		 */
		this._contextOwner = null;
	}

	/**
	 * Loads and initializes plugins specified in the configuration.
	 *
	 * @returns {Promise.<module:core/plugin~LoadedPlugins>} A promise which resolves
	 * once the initialization is completed, providing an array of loaded plugins.
	 */
	initPlugins() {
		const plugins = this.config.get( 'plugins' ) || [];
		const substitutePlugins = this.config.get( 'substitutePlugins' ) || [];

		// Plugins for substitution should be checked as well.
		for ( const Plugin of plugins.concat( substitutePlugins ) ) {
			if ( typeof Plugin != 'function' ) {
				/**
				 * Only a constructor function is allowed as a {@link module:core/contextplugin~ContextPlugin context plugin}.
				 *
				 * @error context-initplugins-constructor-only
				 */
				throw new CKEditorError(
					'context-initplugins-constructor-only',
					null,
					{ Plugin }
				);
			}

			if ( Plugin.isContextPlugin !== true ) {
				/**
				 * Only a plugin marked as a {@link module:core/contextplugin~ContextPlugin.isContextPlugin context plugin}
				 * is allowed to be used with a context.
				 *
				 * @error context-initplugins-invalid-plugin
				 */
				throw new CKEditorError(
					'context-initplugins-invalid-plugin',
					null,
					{ Plugin }
				);
			}
		}

		return this.plugins.init( plugins, [], substitutePlugins );
	}

	/**
	 * Destroys the context instance and all editors used with the context,
	 * releasing all resources used by the context.
	 *
	 * @returns {Promise} A promise that resolves once the context instance is fully destroyed.
	 */
	destroy() {
		return Promise.all( Array.from( this.editors, editor => editor.destroy() ) )
			.then( () => this.plugins.destroy() );
	}

	/**
	 * Adds a reference to the editor which is used with this context.
	 *
	 * When the given editor has created the context, the reference to this editor will be stored
	 * as a {@link ~Context#_contextOwner}.
	 *
	 * This method should only be used by the editor.
	 *
	 * @protected
	 * @param {module:core/editor/editor~Editor} editor
	 * @param {Boolean} isContextOwner Stores the given editor as a context owner.
	 */
	_addEditor( editor, isContextOwner ) {
		if ( this._contextOwner ) {
			/**
			 * Cannot add multiple editors to the context which is created by the editor.
			 *
			 * @error context-addeditor-private-context
			 */
			throw new CKEditorError( 'context-addeditor-private-context' );
		}

		this.editors.add( editor );

		if ( isContextOwner ) {
			this._contextOwner = editor;
		}
	}

	/**
	 * Removes a reference to the editor which was used with this context.
	 * When the context was created by the given editor, the context will be destroyed.
	 *
	 * This method should only be used by the editor.
	 *
	 * @protected
	 * @param {module:core/editor/editor~Editor} editor
	 * @return {Promise} A promise that resolves once the editor is removed from the context or when the context was destroyed.
	 */
	_removeEditor( editor ) {
		if ( this.editors.has( editor ) ) {
			this.editors.remove( editor );
		}

		if ( this._contextOwner === editor ) {
			return this.destroy();
		}

		return Promise.resolve();
	}

	/**
	 * Returns the context configuration which will be copied to the editors created using this context.
	 *
	 * The configuration returned by this method has the plugins configuration removed &mdash; plugins are shared with all editors
	 * through another mechanism.
	 *
	 * This method should only be used by the editor.
	 *
	 * @protected
	 * @returns {Object} Configuration as a plain object.
	 */
	_getEditorConfig() {
		const result = {};

		for ( const name of this.config.names() ) {
			if ( ![ 'plugins', 'removePlugins', 'extraPlugins' ].includes( name ) ) {
				result[ name ] = this.config.get( name );
			}
		}

		return result;
	}

	/**
	 * Creates and initializes a new context instance.
	 *
	 *		const commonConfig = { ... }; // Configuration for all the plugins and editors.
	 *		const editorPlugins = [ ... ]; // Regular plugins here.
	 *
	 *		Context
	 *			.create( {
	 *				// Only context plugins here.
	 *				plugins: [ ... ],
	 *
	 *				// Configure the language for all the editors (it cannot be overwritten).
	 *				language: { ... },
	 *
	 *				// Configuration for context plugins.
	 *				comments: { ... },
	 *				...
	 *
	 *				// Default configuration for editor plugins.
	 *				toolbar: { ... },
	 *				image: { ... },
	 *				...
	 *			} )
	 *			.then( context => {
	 *				const promises = [];
	 *
	 *				promises.push( ClassicEditor.create(
	 *					document.getElementById( 'editor1' ),
	 *					{
	 *						editorPlugins,
	 *						context
	 *					}
	 *				) );
	 *
	 *				promises.push( ClassicEditor.create(
	 *					document.getElementById( 'editor2' ),
	 *					{
	 *						editorPlugins,
	 *						context,
	 *						toolbar: { ... } // You can overwrite the configuration of the context.
	 *					}
	 *				) );
	 *
	 *				return Promise.all( promises );
	 *			} );
	 *
	 * @param {Object} [config] The context configuration.
	 * @returns {Promise} A promise resolved once the context is ready. The promise resolves with the created context instance.
	 */
	static create( config ) {
		return new Promise( resolve => {
			const context = new this( config );

			resolve( context.initPlugins().then( () => context ) );
		} );
	}
}

/**
 * An array of plugins built into the `Context` class.
 *
 * It is used in CKEditor 5 builds featuring `Context` to provide a list of context plugins which are later automatically initialized
 * during the context initialization.
 *
 * They will be automatically initialized by `Context` unless `config.plugins` is passed.
 *
 *		// Build some context plugins into the Context class first.
 *		Context.builtinPlugins = [ FooPlugin, BarPlugin ];
 *
 *		// Normally, you need to define config.plugins, but since Context.builtinPlugins was
 *		// defined, now you can call create() without any configuration.
 *		Context
 *			.create()
 *			.then( context => {
 *				context.plugins.get( FooPlugin ); // -> An instance of the Foo plugin.
 *				context.plugins.get( BarPlugin ); // -> An instance of the Bar plugin.
 *			} );
 *
 * See also {@link module:core/context~Context.defaultConfig `Context.defaultConfig`}
 * and {@link module:core/editor/editor~Editor.builtinPlugins `Editor.builtinPlugins`}.
 *
 * @static
 * @member {Array.<Function>} module:core/context~Context.builtinPlugins
 */

/**
 * The default configuration which is built into the `Context` class.
 *
 * It is used in CKEditor 5 builds featuring `Context` to provide the default configuration options which are later used during the
 * context initialization.
 *
 *		Context.defaultConfig = {
 *			foo: 1,
 *			bar: 2
 *		};
 *
 *		Context
 *			.create()
 *			.then( context => {
 *				context.config.get( 'foo' ); // -> 1
 *				context.config.get( 'bar' ); // -> 2
 *			} );
 *
 *		// The default options can be overridden by the configuration passed to create().
 *		Context
 *			.create( { bar: 3 } )
 *			.then( context => {
 *				context.config.get( 'foo' ); // -> 1
 *				context.config.get( 'bar' ); // -> 3
 *			} );
 *
 * See also {@link module:core/context~Context.builtinPlugins `Context.builtinPlugins`}
 * and {@link module:core/editor/editor~Editor.defaultConfig `Editor.defaultConfig`}.
 *
 * @static
 * @member {Object} module:core/context~Context.defaultConfig
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The base class for {@link module:core/context~Context} plugin classes.
 *
 * A context plugin can either be initialized for an {@link module:core/editor/editor~Editor editor} or for
 * a {@link module:core/context~Context context}. In other words, it can either
 * work within one editor instance or with one or more editor instances that use a single context.
 * It is the context plugin's role to implement handling for both modes.
 *
 * There are a few rules for interaction between the editor plugins and context plugins:
 *
 * * A context plugin can require another context plugin.
 * * An {@link module:core/plugin~Plugin editor plugin} can require a context plugin.
 * * A context plugin MUST NOT require an {@link module:core/plugin~Plugin editor plugin}.
 *
 * @implements module:core/plugin~PluginInterface
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class ContextPlugin {
	/**
	 * Creates a new plugin instance.
	 *
	 * @param {module:core/context~Context|module:core/editor/editor~Editor} context
	 */
	constructor( context ) {
		/**
		 * The context instance.
		 *
		 * @readonly
		 * @type {module:core/context~Context|module:core/editor/editor~Editor}
		 */
		this.context = context;
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		this.stopListening();
	}

	/**
	 * @inheritDoc
	 */
	static get isContextPlugin() {
		return true;
	}
}

mix( ContextPlugin, ObservableMixin );/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module engine/view/typecheckable
 */
class TypeCheckable$1 {
    /* istanbul ignore next */
    is() {
        // There are a lot of overloads above.
        // Overriding method in derived classes remove them and only `is( type: string ): boolean` is visible which we don't want.
        // One option would be to copy them all to all classes, but that's ugly.
        // It's best when TypeScript compiler doesn't see those overloads, except the one in the top base class.
        // To overload a method, but not let the compiler see it, do after class definition:
        // `MyClass.prototype.is = function( type: string ) {...}`
        throw new Error('is() method is abstract');
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @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|Number} 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';
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Abstract view node class.
 *
 * This is an abstract class. Its constructor should not be used directly.
 * Use the {@link module:engine/view/downcastwriter~DowncastWriter} or {@link module:engine/view/upcastwriter~UpcastWriter}
 * to create new instances of view nodes.
 *
 * @abstract
 */
class Node$2 extends EmitterMixin(TypeCheckable$1) {
    /**
     * Creates a tree view node.
     *
     * @protected
     * @param {module:engine/view/document~Document} document The document instance to which this node belongs.
     */
    constructor(document) {
        super();
        /**
         * The document instance to which this node belongs.
         *
         * @readonly
         * @member {module:engine/view/document~Document}
         */
        this.document = document;
        /**
         * Parent element. Null by default. Set by {@link module:engine/view/element~Element#_insertChild}.
         *
         * @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', this);
        }
        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() {
        // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this
        let root = this;
        while (root.parent) {
            root = root.parent;
        }
        return root;
    }
    /**
     * Returns true if the node is in a tree rooted in the document (is a descendant of one of its roots).
     *
     * @returns {Boolean}
     */
    isAttached() {
        return this.root.is('rootElement');
    }
    /**
     * Gets a path to the node. The path is an array containing indices of consecutive ancestors of this node,
     * beginning from {@link module:engine/view/node~Node#root root}, down to this node's index.
     *
     *		const abc = downcastWriter.createText( 'abc' );
     *		const foo = downcastWriter.createText( 'foo' );
     *		const h1 = downcastWriter.createElement( 'h1', null, downcastWriter.createText( 'header' ) );
     *		const p = downcastWriter.createElement( 'p', null, [ abc, foo ] );
     *		const div = downcastWriter.createElement( '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 = [];
        // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this
        let node = this;
        while (node.parent) {
            path.unshift(node.index);
            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 = {}) {
        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];
    }
    /**
     * Returns whether this node is before given node. `false` is returned if nodes are in different trees (for example,
     * in different {@link module:engine/view/documentfragment~DocumentFragment}s).
     *
     * @param {module:engine/view/node~Node} node Node to compare with.
     * @returns {Boolean}
     */
    isBefore(node) {
        // Given node is not before this node if they are same.
        if (this == node) {
            return false;
        }
        // Return `false` if it is impossible to compare nodes.
        if (this.root !== node.root) {
            return false;
        }
        const thisPath = this.getPath();
        const nodePath = node.getPath();
        const result = compareArrays(thisPath, nodePath);
        switch (result) {
            case 'prefix':
                return true;
            case 'extension':
                return false;
            default:
                return thisPath[result] < nodePath[result];
        }
    }
    /**
     * Returns whether this node is after given node. `false` is returned if nodes are in different trees (for example,
     * in different {@link module:engine/view/documentfragment~DocumentFragment}s).
     *
     * @param {module:engine/view/node~Node} node Node to compare with.
     * @returns {Boolean}
     */
    isAfter(node) {
        // Given node is not before this node if they are same.
        if (this == node) {
            return false;
        }
        // Return `false` if it is impossible to compare nodes.
        if (this.root !== node.root) {
            return false;
        }
        // In other cases, just check if the `node` is before, and return the opposite.
        return !this.isBefore(node);
    }
    /**
     * Removes node from parent.
     *
     * @internal
     * @protected
     */
    _remove() {
        this.parent._removeChildren(this.index);
    }
    /**
     * @internal
     * @protected
     * @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 = clone$1(this);
        // Due to circular references we need to remove parent reference.
        delete json.parent;
        return json;
    }
}
/**
 * Checks whether this object is of the given type.
 *
 * This method is useful when processing view objects that are of unknown type. For example, a function
 * may return a {@link module:engine/view/documentfragment~DocumentFragment} or a {@link module:engine/view/node~Node}
 * that can be either a text node or an element. This method can be used to check what kind of object is returned.
 *
 *		someObject.is( 'element' ); // -> true if this is an element
 *		someObject.is( 'node' ); // -> true if this is a node (a text node or an element)
 *		someObject.is( 'documentFragment' ); // -> true if this is a document fragment
 *
 * Since this method is also available on a range of model objects, you can prefix the type of the object with
 * `model:` or `view:` to check, for example, if this is the model's or view's element:
 *
 *		viewElement.is( 'view:element' ); // -> true
 *		viewElement.is( 'model:element' ); // -> false
 *
 * By using this method it is also possible to check a name of an element:
 *
 *		imgElement.is( 'element', 'img' ); // -> true
 *		imgElement.is( 'view:element', 'img' ); // -> same as above, but more precise
 *
 * The list of view objects which implement the `is()` method:
 *
 * * {@link module:engine/view/attributeelement~AttributeElement#is `AttributeElement#is()`}
 * * {@link module:engine/view/containerelement~ContainerElement#is `ContainerElement#is()`}
 * * {@link module:engine/view/documentfragment~DocumentFragment#is `DocumentFragment#is()`}
 * * {@link module:engine/view/documentselection~DocumentSelection#is `DocumentSelection#is()`}
 * * {@link module:engine/view/editableelement~EditableElement#is `EditableElement#is()`}
 * * {@link module:engine/view/element~Element#is `Element#is()`}
 * * {@link module:engine/view/emptyelement~EmptyElement#is `EmptyElement#is()`}
 * * {@link module:engine/view/node~Node#is `Node#is()`}
 * * {@link module:engine/view/position~Position#is `Position#is()`}
 * * {@link module:engine/view/range~Range#is `Range#is()`}
 * * {@link module:engine/view/rooteditableelement~RootEditableElement#is `RootEditableElement#is()`}
 * * {@link module:engine/view/selection~Selection#is `Selection#is()`}
 * * {@link module:engine/view/text~Text#is `Text#is()`}
 * * {@link module:engine/view/textproxy~TextProxy#is `TextProxy#is()`}
 * * {@link module:engine/view/uielement~UIElement#is `UIElement#is()`}
 *
 * @method #is
 * @param {String} type Type to check.
 * @returns {Boolean}
 */
Node$2.prototype.is = function (type) {
    return type === 'node' || type === 'view:node';
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Tree view text node.
 *
 * The constructor of this class should not be used directly. To create a new text node instance
 * use the {@link module:engine/view/downcastwriter~DowncastWriter#createText `DowncastWriter#createText()`}
 * method when working on data downcasted from the model or the
 * {@link module:engine/view/upcastwriter~UpcastWriter#createText `UpcastWriter#createText()`}
 * method when working on non-semantic views.
 *
 * @extends module:engine/view/node~Node
 */
class Text$2 extends Node$2 {
    /**
     * Creates a tree view text node.
     *
     * @protected
     * @param {module:engine/view/document~Document} document The document instance to which this text node belongs.
     * @param {String} data The text's data.
     */
    constructor(document, data) {
        super(document);
        /**
         * The text content.
         *
         * Setting the data fires the {@link module:engine/view/node~Node#event:change:text change event}.
         *
         * @protected
         * @member {String} module:engine/view/text~Text#_textData
         */
        this._textData = data;
    }
    /**
     * The text content.
     *
     * @readonly
     * @type {String}
     */
    get data() {
        return this._textData;
    }
    /**
     * The `_data` property is controlled by a getter and a setter.
     *
     * The getter is required when using the addition assignment operator on protected property:
     *
     *		const foo = downcastWriter.createText( 'foo' );
     *		const bar = downcastWriter.createText( 'bar' );
     *
     *		foo._data += bar.data;   // executes: `foo._data = foo._data + bar.data`
     *		console.log( foo.data ); // prints: 'foobar'
     *
     * If the protected getter didn't exist, `foo._data` will return `undefined` and result of the merge will be invalid.
     *
     * The setter sets data and fires the {@link module:engine/view/node~Node#event:change:text change event}.
     *
     * @protected
     * @type {String}
     */
    get _data() {
        return this.data;
    }
    set _data(data) {
        this._fireChange('text', this);
        this._textData = 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/node~Node} otherNode Node to check if it is same as this node.
     * @returns {Boolean}
     */
    isSimilar(otherNode) {
        if (!(otherNode instanceof Text$2)) {
            return false;
        }
        return this === otherNode || this.data === otherNode.data;
    }
    /**
     * Clones this node.
     *
     * @protected
     * @returns {module:engine/view/text~Text} Text node that is a clone of this node.
     */
    _clone() {
        return new Text$2(this.document, this.data);
    }
}
/**
 * Checks whether this object is of the given type.
 *
 *		text.is( '$text' ); // -> true
 *		text.is( 'node' ); // -> true
 *		text.is( 'view:$text' ); // -> true
 *		text.is( 'view:node' ); // -> true
 *
 *		text.is( 'model:$text' ); // -> false
 *		text.is( 'element' ); // -> false
 *		text.is( 'range' ); // -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * **Note:** Until version 20.0.0 this method wasn't accepting `'$text'` type. The legacy `'text'` type is still
 * accepted for backward compatibility.
 *
 * @param {String} type Type to check.
 * @returns {Boolean}
 */
Text$2.prototype.is = function (type) {
    return type === '$text' || type === 'view:$text' ||
        // This are legacy values kept for backward compatibility.
        type === 'text' || type === 'view:text' ||
        // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
        type === 'node' || type === 'view:node';
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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 TextProxy$1 extends TypeCheckable$1 {
    /**
     * 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) {
        super();
        /**
         * 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', this);
        }
        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', this);
        }
        /**
         * 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;
    }
    /**
     * Offset size of this node.
     *
     * @readonly
     * @type {Number}
     */
    get offsetSize() {
        return this.data.length;
    }
    /**
     * 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;
    }
    /**
     * 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 = {}) {
        const ancestors = [];
        let parent = options.includeSelf ? this.textNode : this.parent;
        while (parent !== null) {
            ancestors[options.parentFirst ? 'push' : 'unshift'](parent);
            parent = parent.parent;
        }
        return ancestors;
    }
}
/**
 * Checks whether this object is of the given type.
 *
 *		textProxy.is( '$textProxy' ); // -> true
 *		textProxy.is( 'view:$textProxy' ); // -> true
 *
 *		textProxy.is( 'model:$textProxy' ); // -> false
 *		textProxy.is( 'element' ); // -> false
 *		textProxy.is( 'range' ); // -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * **Note:** Until version 20.0.0 this method wasn't accepting `'$textProxy'` type. The legacy `'textProxy'` type is still
 * accepted for backward compatibility.
 *
 * @param {String} type Type to check.
 * @returns {Boolean}
 */
TextProxy$1.prototype.is = function (type) {
    return type === '$textProxy' || type === 'view:$textProxy' ||
        // This are legacy values kept for backward compatibility.
        type === 'textProxy' || type === 'view:textProxy';
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/objecttomap
 */
/**
 * Transforms object to map.
 *
 *		const map = objectToMap( { 'foo': 1, 'bar': 2 } );
 *		map.get( 'foo' ); // 1
 *
 * **Note**: For mixed data (`Object` or `Iterable`) there's a dedicated {@link module:utils/tomap~toMap} function.
 *
 * @param {Object|null} 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;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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|null} data Object or iterable to transform.
 * @returns {Map} Map created from data.
 */
function toMap(data) {
    if (isIterable(data)) {
        return new Map(data);
    }
    else {
        return objectToMap(data);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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|Function} [pattern] Match patterns. See {@link module:engine/view/matcher~Matcher#add add method} for
     * more information.
     */
    constructor(...pattern) {
        /**
         * @private
         * @type {Array<Object|Function>}
         */
        this._patterns = [];
        this.add(...pattern);
    }
    /**
     * Adds pattern or patterns to matcher instance.
     *
     *		// String.
     *		matcher.add( 'div' );
     *
     *		// Regular expression.
     *		matcher.add( /^\w/ );
     *
     *		// Single class.
     *		matcher.add( {
     *			classes: 'foobar'
     *		} );
     *
     * See {@link module:engine/view/matcher~MatcherPattern} for more examples.
     *
     * Multiple patterns can be added in one call:
     *
     * 		matcher.add( 'div', { classes: '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.attributes] Object with key-value pairs representing attributes to match. Each object key
     * represents attribute name. Value under that key can be either:
     * * `true` - then attribute is just required (can be empty),
     * * a string - then attribute has to be equal, or
     * * a regular expression - then attribute has to match the expression.
     * @param {String|RegExp|Array} [pattern.classes] 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.styles] 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 };
            }
            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,
     *				attributes: [ 'title', 'href' ],
     *				classes: [ 'foo' ],
     *				styles: [ '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.attributes] Array with matched attribute names.
     * @returns {Array} [result.match.classes] Array with matched class names.
     * @returns {Array} [result.match.styles] 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.attributes) {
        match.attributes = matchAttributes(pattern.attributes, element);
        if (!match.attributes) {
            return null;
        }
    }
    // Check element's classes.
    if (pattern.classes) {
        match.classes = matchClasses(pattern.classes, element);
        if (!match.classes) {
            return null;
        }
    }
    // Check element's styles.
    if (pattern.styles) {
        match.styles = matchStyles(pattern.styles, element);
        if (!match.styles) {
            return null;
        }
    }
    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 !!name.match(pattern);
    }
    return pattern === name;
}
// Checks if an array of key/value pairs can be matched against provided patterns.
//
// Patterns can be provided in a following ways:
// 	- a boolean value matches any attribute with any value (or no value):
//
//			pattern: true
//
//	- a RegExp expression or object matches any attribute name:
//
//			pattern: /h[1-6]/
//
//	- an object matches any attribute that has the same name as the object item's key, where object item's value is:
//		- equal to `true`, which matches any attribute value:
//
//			pattern: {
//				required: true
//			}
//
//		- a string that is equal to attribute value:
//
//			pattern: {
//				rel: 'nofollow'
//			}
//
//		- a regular expression that matches attribute value,
//
//			pattern: {
//				src: /https.*/
//			}
//
//	- an array with items, where the item is:
//		- a string that is equal to attribute value:
//
//			pattern: [ 'data-property-1', 'data-property-2' ],
//
//		- an object with `key` and `value` property, where `key` is a regular expression matching attribute name and
//		  `value` is either regular expression matching attribute value or a string equal to attribute value:
//
//			pattern: [
//				{ key: /data-property-.*/, value: true },
//				// or:
//				{ key: /data-property-.*/, value: 'foobar' },
//				// or:
//				{ key: /data-property-.*/, value: /foo.*/ }
//			]
//
// @param {Object} patterns Object with information about attributes to match.
// @param {Iterable.<String>} keys Attribute, style or class keys.
// @param {Function} valueGetter A function providing value for a given item key.
// @returns {Array|null} Returns array with matched attribute names or `null` if no attributes were matched.
function matchPatterns(patterns, keys, valueGetter) {
    const normalizedPatterns = normalizePatterns(patterns);
    const normalizedItems = Array.from(keys);
    const match = [];
    normalizedPatterns.forEach(([patternKey, patternValue]) => {
        normalizedItems.forEach(itemKey => {
            if (isKeyMatched(patternKey, itemKey) &&
                isValueMatched(patternValue, itemKey, valueGetter)) {
                match.push(itemKey);
            }
        });
    });
    // Return matches only if there are at least as many of them as there are patterns.
    // The RegExp pattern can match more than one item.
    if (!normalizedPatterns.length || match.length < normalizedPatterns.length) {
        return undefined;
    }
    return match;
}
// Bring all the possible pattern forms to an array of arrays where first item is a key and second is a value.
//
// Examples:
//
// Boolean pattern value:
//
//		true
//
// to
//
//		[ [ true, true ] ]
//
// Textual pattern value:
//
//		'attribute-name-or-class-or-style'
//
// to
//
//		[ [ 'attribute-name-or-class-or-style', true ] ]
//
// Regular expression:
//
//		/^data-.*$/
//
// to
//
//		[ [ /^data-.*$/, true ] ]
//
// Objects (plain or with `key` and `value` specified explicitly):
//
//		{
//			src: /^https:.*$/
//		}
//
// or
//
//		[ {
//			key: 'src',
//			value: /^https:.*$/
//		} ]
//
// to:
//
//		[ [ 'src', /^https:.*$/ ] ]
//
// @param {Object|Array} patterns
// @returns {Array|null} Returns an array of objects or null if provided patterns were not in an expected form.
function normalizePatterns(patterns) {
    if (Array.isArray(patterns)) {
        return patterns.map((pattern) => {
            if (isPlainObject(pattern)) {
                if (pattern.key === undefined || pattern.value === undefined) {
                    // Documented at the end of matcher.js.
                    logWarning('matcher-pattern-missing-key-or-value', pattern);
                }
                return [pattern.key, pattern.value];
            }
            // Assume the pattern is either String or RegExp.
            return [pattern, true];
        });
    }
    if (isPlainObject(patterns)) {
        return Object.entries(patterns);
    }
    // Other cases (true, string or regexp).
    return [[patterns, true]];
}
// @param {String|RegExp} patternKey A pattern representing a key we want to match.
// @param {String} itemKey An actual item key (e.g. `'src'`, `'background-color'`, `'ck-widget'`) we're testing against pattern.
// @returns {Boolean}
function isKeyMatched(patternKey, itemKey) {
    return patternKey === true ||
        patternKey === itemKey ||
        patternKey instanceof RegExp && itemKey.match(patternKey);
}
// @param {String|RegExp} patternValue A pattern representing a value we want to match.
// @param {String} itemKey An item key, e.g. `background`, `href`, 'rel', etc.
// @param {Function} valueGetter A function used to provide a value for a given `itemKey`.
// @returns {Boolean}
function isValueMatched(patternValue, itemKey, valueGetter) {
    if (patternValue === true) {
        return true;
    }
    const itemValue = valueGetter(itemKey);
    // For now, the reducers are not returning the full tree of properties.
    // Casting to string preserves the old behavior until the root cause is fixed.
    // More can be found in https://github.com/ckeditor/ckeditor5/issues/10399.
    return patternValue === itemValue ||
        patternValue instanceof RegExp && !!String(itemValue).match(patternValue);
}
// 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 attributeKeys = new Set(element.getAttributeKeys());
    // `style` and `class` attribute keys are deprecated. Only allow them in object pattern
    // for backward compatibility.
    if (isPlainObject(patterns)) {
        if (patterns.style !== undefined) {
            // Documented at the end of matcher.js.
            logWarning('matcher-pattern-deprecated-attributes-style-key', patterns);
        }
        if (patterns.class !== undefined) {
            // Documented at the end of matcher.js.
            logWarning('matcher-pattern-deprecated-attributes-class-key', patterns);
        }
    }
    else {
        attributeKeys.delete('style');
        attributeKeys.delete('class');
    }
    return matchPatterns(patterns, attributeKeys, key => element.getAttribute(key));
}
// 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) {
    // We don't need `getter` here because patterns for classes are always normalized to `[ className, true ]`.
    return matchPatterns(patterns, element.getClassNames(), /* istanbul ignore next */ () => { });
}
// 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) {
    return matchPatterns(patterns, element.getStyleNames(true), key => element.getStyle(key));
}
/**
 * The key-value matcher pattern is missing key or value. Both must be present.
 * Refer the documentation: {@link module:engine/view/matcher~MatcherPattern}.
 *
 * @param {Object} pattern Pattern with missing properties.
 * @error matcher-pattern-missing-key-or-value
 */
/**
 * The key-value matcher pattern for `attributes` option is using deprecated `style` key.
 *
 * Use `styles` matcher pattern option instead:
 *
 * 		// Instead of:
 * 		const pattern = {
 * 			attributes: {
 * 				key1: 'value1',
 * 				key2: 'value2',
 * 				style: /^border.*$/
 * 			}
 * 		}
 *
 * 		// Use:
 * 		const pattern = {
 * 			attributes: {
 * 				key1: 'value1',
 * 				key2: 'value2'
 * 			},
 * 			styles: /^border.*$/
 * 		}
 *
 * Refer to the {@glink updating/migration-to-29##migration-to-ckeditor-5-v2910 Migration to v29.1.0} guide
 * and {@link module:engine/view/matcher~MatcherPattern} documentation.
 *
 * @param {Object} pattern Pattern with missing properties.
 * @error matcher-pattern-deprecated-attributes-style-key
 */
/**
 * The key-value matcher pattern for `attributes` option is using deprecated `class` key.
 *
 * Use `classes` matcher pattern option instead:
 *
 * 		// Instead of:
 * 		const pattern = {
 * 			attributes: {
 * 				key1: 'value1',
 * 				key2: 'value2',
 * 				class: 'foobar'
 * 			}
 * 		}
 *
 * 		// Use:
 * 		const pattern = {
 * 			attributes: {
 * 				key1: 'value1',
 * 				key2: 'value2'
 * 			},
 * 			classes: 'foobar'
 * 		}
 *
 * Refer to the {@glink updating/migration-to-29##migration-to-ckeditor-5-v2910 Migration to v29.1.0} guide
 * and the {@link module:engine/view/matcher~MatcherPattern} documentation.
 *
 * @param {Object} pattern Pattern with missing properties.
 * @error matcher-pattern-deprecated-attributes-class-key
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Styles map. Allows handling (adding, removing, retrieving) a set of style rules (usually, of an element).
 *
 * The styles map is capable of normalizing style names so e.g. the following operations are possible:
 */
class StylesMap {
    /**
     * Creates Styles instance.
     *
     * @param {module:engine/view/stylesmap~StylesProcessor} styleProcessor
     */
    constructor(styleProcessor) {
        /**
         * Keeps an internal representation of styles map. Normalized styles are kept as object tree to allow unified modification and
         * value access model using lodash's get, set, unset, etc methods.
         *
         * When no style processor rules are defined it acts as simple key-value storage.
         *
         * @private
         * @type {Object}
         */
        this._styles = {};
        /**
         * An instance of the {@link module:engine/view/stylesmap~StylesProcessor}.
         *
         * @private
         * @member {module:engine/view/stylesmap~StylesProcessor}
         */
        this._styleProcessor = styleProcessor;
    }
    /**
     * Returns true if style map has no styles set.
     *
     * @type {Boolean}
     */
    get isEmpty() {
        const entries = Object.entries(this._styles);
        const from = Array.from(entries);
        return !from.length;
    }
    /**
     * Number of styles defined.
     *
     * @type {Number}
     */
    get size() {
        if (this.isEmpty) {
            return 0;
        }
        return this.getStyleNames().length;
    }
    /**
     * Set styles map to a new value.
     *
     *		styles.setTo( 'border:1px solid blue;margin-top:1px;' );
     *
     * @param {String} inlineStyle
     */
    setTo(inlineStyle) {
        this.clear();
        const parsedStyles = Array.from(parseInlineStyles(inlineStyle).entries());
        for (const [key, value] of parsedStyles) {
            this._styleProcessor.toNormalizedForm(key, value, this._styles);
        }
    }
    /**
     * Checks if a given style is set.
     *
     *		styles.setTo( 'margin-left:1px;' );
     *
     *		styles.has( 'margin-left' );    // -> true
     *		styles.has( 'padding' );        // -> false
     *
     * **Note**: This check supports normalized style names.
     *
     *		// Enable 'margin' shorthand processing:
     *		editor.data.addStyleProcessorRules( addMarginRules );
     *
     *		styles.setTo( 'margin:2px;' );
     *
     *		styles.has( 'margin' );         // -> true
     *		styles.has( 'margin-top' );     // -> true
     *		styles.has( 'margin-left' );    // -> true
     *
     *		styles.remove( 'margin-top' );
     *
     *		styles.has( 'margin' );         // -> false
     *		styles.has( 'margin-top' );     // -> false
     *		styles.has( 'margin-left' );    // -> true
     *
     * @param {String} name Style name.
     * @returns {Boolean}
     */
    has(name) {
        if (this.isEmpty) {
            return false;
        }
        const styles = this._styleProcessor.getReducedForm(name, this._styles);
        const propertyDescriptor = styles.find(([property]) => property === name);
        // Only return a value if it is set;
        return Array.isArray(propertyDescriptor);
    }
    set(nameOrObject, valueOrObject) {
        if (isObject(nameOrObject)) {
            for (const [key, value] of Object.entries(nameOrObject)) {
                this._styleProcessor.toNormalizedForm(key, value, this._styles);
            }
        }
        else {
            this._styleProcessor.toNormalizedForm(nameOrObject, valueOrObject, this._styles);
        }
    }
    /**
     * Removes given style.
     *
     *		styles.setTo( 'background:#f00;margin-right:2px;' );
     *
     *		styles.remove( 'background' );
     *
     *		styles.toString();   // -> 'margin-right:2px;'
     *
     * ***Note**:* This method uses {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules
     * enabled style processor rules} to normalize passed values.
     *
     *		// Enable 'margin' shorthand processing:
     *		editor.data.addStyleProcessorRules( addMarginRules );
     *
     *		styles.setTo( 'margin:1px' );
     *
     *		styles.remove( 'margin-top' );
     *		styles.remove( 'margin-right' );
     *
     *		styles.toString(); // -> 'margin-bottom:1px;margin-left:1px;'
     *
     * @param {String} name Style name.
     */
    remove(name) {
        const path = toPath(name);
        unset(this._styles, path);
        delete this._styles[name];
        this._cleanEmptyObjectsOnPath(path);
    }
    /**
     * Returns a normalized style object or a single value.
     *
     *		// Enable 'margin' shorthand processing:
     *		editor.data.addStyleProcessorRules( addMarginRules );
     *
     *		const styles = new Styles();
     *		styles.setTo( 'margin:1px 2px 3em;' );
     *
     *		styles.getNormalized( 'margin' );
     *		// will log:
     *		// {
     *		//     top: '1px',
     *		//     right: '2px',
     *		//     bottom: '3em',
     *		//     left: '2px'     // normalized value from margin shorthand
     *		// }
     *
     *		styles.getNormalized( 'margin-left' ); // -> '2px'
     *
     * **Note**: This method will only return normalized styles if a style processor was defined.
     *
     * @param {String} name Style name.
     * @returns {Object|String|undefined}
     */
    getNormalized(name) {
        return this._styleProcessor.getNormalized(name, this._styles);
    }
    /**
     * Returns a normalized style string. Styles are sorted by name.
     *
     *		styles.set( 'margin' , '1px' );
     *		styles.set( 'background', '#f00' );
     *
     *		styles.toString(); // -> 'background:#f00;margin:1px;'
     *
     * **Note**: This method supports normalized styles if defined.
     *
     *		// Enable 'margin' shorthand processing:
     *		editor.data.addStyleProcessorRules( addMarginRules );
     *
     *		styles.set( 'margin' , '1px' );
     *		styles.set( 'background', '#f00' );
     *		styles.remove( 'margin-top' );
     *		styles.remove( 'margin-right' );
     *
     *		styles.toString(); // -> 'background:#f00;margin-bottom:1px;margin-left:1px;'
     *
     * @returns {String}
     */
    toString() {
        if (this.isEmpty) {
            return '';
        }
        return this._getStylesEntries()
            .map(arr => arr.join(':'))
            .sort()
            .join(';') + ';';
    }
    /**
     * Returns property as a value string or undefined if property is not set.
     *
     *		// Enable 'margin' shorthand processing:
     *		editor.data.addStyleProcessorRules( addMarginRules );
     *
     *		const styles = new Styles();
     *		styles.setTo( 'margin:1px;' );
     *		styles.set( 'margin-bottom', '3em' );
     *
     *		styles.getAsString( 'margin' ); // -> 'margin: 1px 1px 3em;'
     *
     * Note, however, that all sub-values must be set for the longhand property name to return a value:
     *
     *		const styles = new Styles();
     *		styles.setTo( 'margin:1px;' );
     *		styles.remove( 'margin-bottom' );
     *
     *		styles.getAsString( 'margin' ); // -> undefined
     *
     * In the above scenario, it is not possible to return a `margin` value, so `undefined` is returned.
     * Instead, you should use:
     *
     *		const styles = new Styles();
     *		styles.setTo( 'margin:1px;' );
     *		styles.remove( 'margin-bottom' );
     *
     *		for ( const styleName of styles.getStyleNames() ) {
     *			console.log( styleName, styles.getAsString( styleName ) );
     *		}
     *		// 'margin-top', '1px'
     *		// 'margin-right', '1px'
     *		// 'margin-left', '1px'
     *
     * In general, it is recommend to iterate over style names like in the example above. This way, you will always get all
     * the currently set style values. So, if all the 4 margin values would be set
     * the for-of loop above would yield only `'margin'`, `'1px'`:
     *
     *		const styles = new Styles();
     *		styles.setTo( 'margin:1px;' );
     *
     *		for ( const styleName of styles.getStyleNames() ) {
     *			console.log( styleName, styles.getAsString( styleName ) );
     *		}
     *		// 'margin', '1px'
     *
     * **Note**: To get a normalized version of a longhand property use the {@link #getNormalized `#getNormalized()`} method.
     *
     * @param {String} propertyName
     * @returns {String|undefined}
     */
    getAsString(propertyName) {
        if (this.isEmpty) {
            return;
        }
        if (this._styles[propertyName] && !isObject(this._styles[propertyName])) {
            // Try return styles set directly - values that are not parsed.
            return this._styles[propertyName];
        }
        const styles = this._styleProcessor.getReducedForm(propertyName, this._styles);
        const propertyDescriptor = styles.find(([property]) => property === propertyName);
        // Only return a value if it is set;
        if (Array.isArray(propertyDescriptor)) {
            return propertyDescriptor[1];
        }
    }
    /**
     * Returns all style properties names as they would appear when using {@link #toString `#toString()`}.
     *
     * When `expand` is set to true and there's a shorthand style property set, it will also return all equivalent styles:
     *
     * 		stylesMap.setTo( 'margin: 1em' )
     *
     * will be expanded to:
     *
     * 		[ 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left' ]
     *
     * @param {Boolean} [expand=false] Expand shorthand style properties and all return equivalent style representations.
     * @returns {Array.<String>}
     */
    getStyleNames(expand = false) {
        if (this.isEmpty) {
            return [];
        }
        if (expand) {
            return this._styleProcessor.getStyleNames(this._styles);
        }
        const entries = this._getStylesEntries();
        return entries.map(([key]) => key);
    }
    /**
     * Removes all styles.
     */
    clear() {
        this._styles = {};
    }
    /**
     * Returns normalized styles entries for further processing.
     *
     * @private
     * @returns {Array.<module:engine/view/stylesmap~PropertyDescriptor>}
     */
    _getStylesEntries() {
        const parsed = [];
        const keys = Object.keys(this._styles);
        for (const key of keys) {
            parsed.push(...this._styleProcessor.getReducedForm(key, this._styles));
        }
        return parsed;
    }
    /**
     * Removes empty objects upon removing an entry from internal object.
     *
     * @param {String} path
     * @private
     */
    _cleanEmptyObjectsOnPath(path) {
        const pathParts = path.split('.');
        const isChildPath = pathParts.length > 1;
        if (!isChildPath) {
            return;
        }
        const parentPath = pathParts.splice(0, pathParts.length - 1).join('.');
        const parentObject = get(this._styles, parentPath);
        if (!parentObject) {
            return;
        }
        const isParentEmpty = !Array.from(Object.keys(parentObject)).length;
        if (isParentEmpty) {
            this.remove(parentPath);
        }
    }
}
/**
 * Style processor is responsible for writing and reading a normalized styles object.
 */
class StylesProcessor {
    /**
     * Creates StylesProcessor instance.
     *
     * @private
     */
    constructor() {
        this._normalizers = new Map();
        this._extractors = new Map();
        this._reducers = new Map();
        this._consumables = new Map();
    }
    /**
     * Parse style string value to a normalized object and appends it to styles object.
     *
     *		const styles = {};
     *
     *		stylesProcessor.toNormalizedForm( 'margin', '1px', styles );
     *
     *		// styles will consist: { margin: { top: '1px', right: '1px', bottom: '1px', left: '1px; } }
     *
     * **Note**: To define normalizer callbacks use {@link #setNormalizer}.
     *
     * @param {String} name Name of style property.
     * @param {String} propertyValue Value of style property.
     * @param {Object} styles Object holding normalized styles.
     */
    toNormalizedForm(name, propertyValue, styles) {
        if (isObject(propertyValue)) {
            appendStyleValue(styles, toPath(name), propertyValue);
            return;
        }
        if (this._normalizers.has(name)) {
            const normalizer = this._normalizers.get(name);
            const { path, value } = normalizer(propertyValue);
            appendStyleValue(styles, path, value);
        }
        else {
            appendStyleValue(styles, name, propertyValue);
        }
    }
    /**
     * Returns a normalized version of a style property.
     *		const styles = {
     *			margin: { top: '1px', right: '1px', bottom: '1px', left: '1px; },
     *			background: { color: '#f00' }
     *		};
     *
     *		stylesProcessor.getNormalized( 'background' );
     *		// will return: { color: '#f00' }
     *
     *		stylesProcessor.getNormalized( 'margin-top' );
     *		// will return: '1px'
     *
     * **Note**: In some cases extracting single value requires defining an extractor callback {@link #setExtractor}.
     *
     * @param {String} name Name of style property.
     * @param {Object} styles Object holding normalized styles.
     * @returns {*}
     */
    getNormalized(name, styles) {
        if (!name) {
            return merge$1({}, styles);
        }
        // Might be empty string.
        if (styles[name] !== undefined) {
            return styles[name];
        }
        if (this._extractors.has(name)) {
            const extractor = this._extractors.get(name);
            if (typeof extractor === 'string') {
                return get(styles, extractor);
            }
            const value = extractor(name, styles);
            if (value) {
                return value;
            }
        }
        return get(styles, toPath(name));
    }
    /**
     * Returns a reduced form of style property form normalized object.
     *
     * For default margin reducer, the below code:
     *
     *		stylesProcessor.getReducedForm( 'margin', {
     *			margin: { top: '1px', right: '1px', bottom: '2px', left: '1px; }
     *		} );
     *
     * will return:
     *
     *		[
     *			[ 'margin', '1px 1px 2px' ]
     *		]
     *
     * because it might be represented as a shorthand 'margin' value. However if one of margin long hand values is missing it should return:
     *
     *		[
     *			[ 'margin-top', '1px' ],
     *			[ 'margin-right', '1px' ],
     *			[ 'margin-bottom', '2px' ]
     *			// the 'left' value is missing - cannot use 'margin' shorthand.
     *		]
     *
     * **Note**: To define reducer callbacks use {@link #setReducer}.
     *
     * @param {String} name Name of style property.
     * @param {Object} styles Object holding normalized styles.
     * @returns {Array.<module:engine/view/stylesmap~PropertyDescriptor>}
     */
    getReducedForm(name, styles) {
        const normalizedValue = this.getNormalized(name, styles);
        // Might be empty string.
        if (normalizedValue === undefined) {
            return [];
        }
        if (this._reducers.has(name)) {
            const reducer = this._reducers.get(name);
            return reducer(normalizedValue);
        }
        return [[name, normalizedValue]];
    }
    /**
     * Return all style properties. Also expand shorthand properties (e.g. `margin`, `background`) if respective extractor is available.
     *
     * @param {Object} styles Object holding normalized styles.
     * @returns {Array.<String>}
     */
    getStyleNames(styles) {
        // Find all extractable styles that have a value.
        const expandedStyleNames = Array.from(this._consumables.keys()).filter(name => {
            const style = this.getNormalized(name, styles);
            if (style && typeof style == 'object') {
                return Object.keys(style).length;
            }
            return style;
        });
        // For simple styles (for example `color`) we don't have a map of those styles
        // but they are 1 to 1 with normalized object keys.
        const styleNamesKeysSet = new Set([
            ...expandedStyleNames,
            ...Object.keys(styles)
        ]);
        return Array.from(styleNamesKeysSet.values());
    }
    /**
     * Returns related style names.
     *
     *		stylesProcessor.getRelatedStyles( 'margin' );
     *		// will return: [ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left' ];
     *
     *		stylesProcessor.getRelatedStyles( 'margin-top' );
     *		// will return: [ 'margin' ];
     *
     * **Note**: To define new style relations load an existing style processor or use
     * {@link module:engine/view/stylesmap~StylesProcessor#setStyleRelation `StylesProcessor.setStyleRelation()`}.
     *
     * @param {String} name
     * @returns {Array.<String>}
     */
    getRelatedStyles(name) {
        return this._consumables.get(name) || [];
    }
    /**
     * Adds a normalizer method for a style property.
     *
     * A normalizer returns describing how the value should be normalized.
     *
     * For instance 'margin' style is a shorthand for four margin values:
     *
     * - 'margin-top'
     * - 'margin-right'
     * - 'margin-bottom'
     * - 'margin-left'
     *
     * and can be written in various ways if some values are equal to others. For instance `'margin: 1px 2em;'` is a shorthand for
     * `'margin-top: 1px;margin-right: 2em;margin-bottom: 1px;margin-left: 2em'`.
     *
     * A normalizer should parse various margin notations as a single object:
     *
     *		const styles = {
     *			margin: {
     *				top: '1px',
     *				right: '2em',
     *				bottom: '1px',
     *				left: '2em'
     *			}
     *		};
     *
     * Thus a normalizer for 'margin' style should return an object defining style path and value to store:
     *
     *		const returnValue = {
     *			path: 'margin',
     *			value: {
     *				top: '1px',
     *				right: '2em',
     *				bottom: '1px',
     *				left: '2em'
     *			}
     *		};
     *
     * Additionally to fully support all margin notations there should be also defined 4 normalizers for longhand margin notations. Below
     * is an example for 'margin-top' style property normalizer:
     *
     *		stylesProcessor.setNormalizer( 'margin-top', valueString => {
     *			return {
     *				path: 'margin.top',
     *				value: valueString
     *			}
     *		} );
     *
     * @param {String} name
     * @param {Function} callback
     */
    setNormalizer(name, callback) {
        this._normalizers.set(name, callback);
    }
    /**
     * Adds a extractor callback for a style property.
     *
     * Most normalized style values are stored as one level objects. It is assumed that `'margin-top'` style will be stored as:
     *
     *		const styles = {
     *			margin: {
     *				top: 'value'
     *			}
     *		}
     *
     * However, some styles can have conflicting notations and thus it might be harder to extract a style value from shorthand. For instance
     * the 'border-top-style' can be defined using `'border-top:solid'`, `'border-style:solid none none none'` or by `'border:solid'`
     * shorthands. The default border styles processors stores styles as:
     *
     *		const styles = {
     *			border: {
     *				style: {
     *					top: 'solid'
     *				}
     *			}
     *		}
     *
     * as it is better to modify border style independently from other values. On the other part the output of the border might be
     * desired as `border-top`, `border-left`, etc notation.
     *
     * In the above example a reducer should return a side border value that combines style, color and width:
     *
     *		styleProcessor.setExtractor( 'border-top', styles => {
     *			return {
     *				color: styles.border.color.top,
     *				style: styles.border.style.top,
     *				width: styles.border.width.top
     *			}
     *		} );
     *
     * @param {String} name
     * @param {Function|String} callbackOrPath Callback that return a requested value or path string for single values.
     */
    setExtractor(name, callbackOrPath) {
        this._extractors.set(name, callbackOrPath);
    }
    /**
     * Adds a reducer callback for a style property.
     *
     * Reducer returns a minimal notation for given style name. For longhand properties it is not required to write a reducer as
     * by default the direct value from style path is taken.
     *
     * For shorthand styles a reducer should return minimal style notation either by returning single name-value tuple or multiple tuples
     * if a shorthand cannot be used. For instance for a margin shorthand a reducer might return:
     *
     *		const marginShortHandTuple = [
     *			[ 'margin', '1px 1px 2px' ]
     *		];
     *
     * or a longhand tuples for defined values:
     *
     *		// Considering margin.bottom and margin.left are undefined.
     *		const marginLonghandsTuples = [
     *			[ 'margin-top', '1px' ],
     *			[ 'margin-right', '1px' ]
     *		];
     *
     * A reducer obtains a normalized style value:
     *
     *		// Simplified reducer that always outputs 4 values which are always present:
     *		stylesProcessor.setReducer( 'margin', margin => {
     *			return [
     *				[ 'margin', `${ margin.top } ${ margin.right } ${ margin.bottom } ${ margin.left }` ]
     *			]
     *		} );
     *
     * @param {String} name
     * @param {Function} callback
     */
    setReducer(name, callback) {
        this._reducers.set(name, callback);
    }
    /**
     * Defines a style shorthand relation to other style notations.
     *
     *		stylesProcessor.setStyleRelation( 'margin', [
     *			'margin-top',
     *			'margin-right',
     *			'margin-bottom',
     *			'margin-left'
     *		] );
     *
     * This enables expanding of style names for shorthands. For instance, if defined,
     * {@link module:engine/conversion/viewconsumable~ViewConsumable view consumable} items are automatically created
     * for long-hand margin style notation alongside the `'margin'` item.
     *
     * This means that when an element being converted has a style `margin`, a converter for `margin-left` will work just
     * fine since the view consumable will contain a consumable `margin-left` item (thanks to the relation) and
     * `element.getStyle( 'margin-left' )` will work as well assuming that the style processor was correctly configured.
     * However, once `margin-left` is consumed, `margin` will not be consumable anymore.
     *
     * @param {String} shorthandName
     * @param {Array.<String>} styleNames
     */
    setStyleRelation(shorthandName, styleNames) {
        this._mapStyleNames(shorthandName, styleNames);
        for (const alsoName of styleNames) {
            this._mapStyleNames(alsoName, [shorthandName]);
        }
    }
    /**
     * Set two-way binding of style names.
     *
     * @param {String} name
     * @param {Array.<String>} styleNames
     * @private
     */
    _mapStyleNames(name, styleNames) {
        if (!this._consumables.has(name)) {
            this._consumables.set(name, []);
        }
        this._consumables.get(name).push(...styleNames);
    }
}
// Parses inline styles and puts property - value pairs into styles map.
//
// @param {String} stylesString Styles to parse.
// @returns {Map.<String, String>} stylesMap Map of parsed properties and values.
function parseInlineStyles(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;
    const stylesMap = new Map();
    // Do not set anything if input string is empty.
    if (stylesString === '') {
        return stylesMap;
    }
    // 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;
                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;
        }
    }
    return stylesMap;
}
// Return lodash compatible path from style name.
function toPath(name) {
    return name.replace('-', '.');
}
// Appends style definition to the styles object.
//
// @param {String} nameOrPath
// @param {String|Object} valueOrObject
// @private
function appendStyleValue(stylesObject, nameOrPath, valueOrObject) {
    let valueToSet = valueOrObject;
    if (isObject(valueOrObject)) {
        valueToSet = merge$1({}, get(stylesObject, nameOrPath), valueOrObject);
    }
    set(stylesObject, nameOrPath, valueToSet);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// @if CK_DEBUG_ENGINE // const { convertMapToTags } = require( '../dev-utils/utils' );
/**
 * View element.
 *
 * The editing engine does not define a fixed semantics of its elements (it is "DTD-free").
 * This is why the type of the {@link module:engine/view/element~Element} need to
 * be defined by the feature developer. When creating an element you should use one of the following methods:
 *
 * * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement `downcastWriter#createContainerElement()`}
 * in order to create a {@link module:engine/view/containerelement~ContainerElement},
 * * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement `downcastWriter#createAttributeElement()`}
 * in order to create a {@link module:engine/view/attributeelement~AttributeElement},
 * * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement `downcastWriter#createEmptyElement()`}
 * in order to create a {@link module:engine/view/emptyelement~EmptyElement}.
 * * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement `downcastWriter#createUIElement()`}
 * in order to create a {@link module:engine/view/uielement~UIElement}.
 * * {@link module:engine/view/downcastwriter~DowncastWriter#createEditableElement `downcastWriter#createEditableElement()`}
 * in order to create a {@link module:engine/view/editableelement~EditableElement}.
 *
 * Note that for view elements which are not created from the 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.
 * In such cases the {@link module:engine/view/upcastwriter~UpcastWriter#createElement `UpcastWriter#createElement()`} method
 * should be used to create generic view elements.
 *
 * @extends module:engine/view/node~Node
 */
class Element$1 extends Node$2 {
    /**
     * Creates a view element.
     *
     * Attributes can be passed in various formats:
     *
     *		new Element( viewDocument, 'div', { class: 'editor', contentEditable: 'true' } ); // object
     *		new Element( viewDocument, 'div', [ [ 'class', 'editor' ], [ 'contentEditable', 'true' ] ] ); // map-like iterator
     *		new Element( viewDocument, 'div', mapOfAttributes ); // map
     *
     * @protected
     * @param {module:engine/view/document~Document} document The document instance to which this element belongs.
     * @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]
     * A list of nodes to be inserted into created element.
     */
    constructor(document, name, attrs, children) {
        super(document);
        /**
         * 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
         */
        this._attrs = parseAttributes(attrs);
        /**
         * Array of child nodes.
         *
         * @protected
         * @member {Array.<module:engine/view/node~Node>}
         */
        this._children = [];
        if (children) {
            this._insertChild(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');
        }
        /**
         * Normalized styles.
         *
         * @protected
         * @member {module:engine/view/stylesmap~StylesMap} module:engine/view/element~Element#_styles
         */
        this._styles = new StylesMap(this.document.stylesProcessor);
        if (this._attrs.has('style')) {
            // Remove style attribute and handle it by styles map.
            this._styles.setTo(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
         * @member {Map}
         */
        this._customProperties = new Map();
        /**
         * A list of attribute names that should be rendered in the editing pipeline even though filtering mechanisms
         * implemented in the {@link module:engine/view/domconverter~DomConverter} (for instance,
         * {@link module:engine/view/domconverter~DomConverter#shouldRenderAttribute}) would filter them out.
         *
         * These attributes can be specified as an option when the element is created by
         * the {@link module:engine/view/downcastwriter~DowncastWriter}. To check whether an unsafe an attribute should
         * be permitted, use the {@link #shouldRenderUnsafeAttribute} method.
         *
         * @private
         * @readonly
         * @member {Array.<String>}
         */
        this._unsafeAttributesToRender = [];
    }
    /**
     * 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;
    }
    /**
     * 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 {Iterable.<String>} Keys for attributes.
     */
    *getAttributeKeys() {
        if (this._classes.size > 0) {
            yield 'class';
        }
        if (!this._styles.isEmpty) {
            yield 'style';
        }
        yield* this._attrs.keys();
    }
    /**
     * 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.isEmpty) {
            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') {
            const inlineStyle = this._styles.toString();
            return inlineStyle == '' ? undefined : inlineStyle;
        }
        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.isEmpty;
        }
        return this._attrs.has(key);
    }
    /**
     * 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 Element$1)) {
            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 of this._styles.getStyleNames()) {
            if (!otherElement._styles.has(property) ||
                otherElement._styles.getAsString(property) !== this._styles.getAsString(property)) {
                return false;
            }
        }
        return true;
    }
    /**
     * 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 {Iterable.<String>}
     */
    getClassNames() {
        return this._classes.keys();
    }
    /**
     * Returns style value for the given property mae.
     * If the style does not exist `undefined` is returned.
     *
     * **Note**: This method can work with normalized style names if
     * {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
     * See {@link module:engine/view/stylesmap~StylesMap#getAsString `StylesMap#getAsString()`} for details.
     *
     * For an element with style set to `'margin:1px'`:
     *
     *		// Enable 'margin' shorthand processing:
     *		editor.data.addStyleProcessorRules( addMarginRules );
     *
     *		const element = view.change( writer => {
     *			const element = writer.createElement();
     *			writer.setStyle( 'margin', '1px' );
     *			writer.setStyle( 'margin-bottom', '3em' );
     *
     *			return element;
     *		} );
     *
     *		element.getStyle( 'margin' ); // -> 'margin: 1px 1px 3em;'
     *
     * @param {String} property
     * @returns {String|undefined}
     */
    getStyle(property) {
        return this._styles.getAsString(property);
    }
    /**
     * Returns a normalized style object or single style value.
     *
     * For an element with style set to: margin:1px 2px 3em;
     *
     *		element.getNormalizedStyle( 'margin' ) );
     *
     * will return:
     *
     *		{
     *			top: '1px',
     *			right: '2px',
     *			bottom: '3em',
     *			left: '2px'    // a normalized value from margin shorthand
     *		}
     *
     * and reading for single style value:
     *
     *		styles.getNormalizedStyle( 'margin-left' );
     *
     * Will return a `2px` string.
     *
     * **Note**: This method will return normalized values only if
     * {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
     * See {@link module:engine/view/stylesmap~StylesMap#getNormalized `StylesMap#getNormalized()`} for details.
     *
     *
     * @param {String} property Name of CSS property
     * @returns {Object|String|undefined}
     */
    getNormalizedStyle(property) {
        return this._styles.getNormalized(property);
    }
    /**
     * Returns iterator that contains all style names.
     *
     * @param {Boolean} [expand=false] Expand shorthand style properties and return all equivalent style representations.
     * @returns {Iterable.<String>}
     */
    getStyleNames(expand) {
        return this._styles.getStyleNames(expand);
    }
    /**
     * 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;
    }
    /**
     * 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 && !parent.is('documentFragment')) {
            if (matcher.match(parent)) {
                return parent;
            }
            parent = parent.parent;
        }
        return null;
    }
    /**
     * Returns the custom property value for the given key.
     *
     * @param {String|Symbol} key
     * @returns {*}
     */
    getCustomProperty(key) {
        return this._customProperties.get(key);
    }
    /**
     * Returns an iterator which iterates over this element's custom properties.
     * Iterator provides `[ key, value ]` pairs 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 = writer.createContainerElement( 'foo', {
     *			banana: '10',
     *			apple: '20',
     *			style: 'color: red; border-color: white;',
     *			class: '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 = this._styles.toString();
        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}`);
    }
    /**
     * Decides whether an unsafe attribute is whitelisted and should be rendered in the editing pipeline even though filtering mechanisms
     * like {@link module:engine/view/domconverter~DomConverter#shouldRenderAttribute} say it should not.
     *
     * Unsafe attribute names can be specified when creating an element via {@link module:engine/view/downcastwriter~DowncastWriter}.
     *
     * @param {String} attributeName The name of the attribute to be checked.
     * @returns {Boolean}
     */
    shouldRenderUnsafeAttribute(attributeName) {
        return this._unsafeAttributesToRender.includes(attributeName);
    }
    /**
     * Clones provided element.
     *
     * @protected
     * @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.document, 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.set(this._styles.getNormalized());
        // 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;
        // Clone unsafe attributes list.
        cloned._unsafeAttributesToRender = this._unsafeAttributesToRender;
        return cloned;
    }
    /**
     * {@link module:engine/view/element~Element#_insertChild 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.
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#insert
     * @protected
     * @param {module:engine/view/item~Item|Iterable.<module:engine/view/item~Item>} items Items to be inserted.
     * @fires module:engine/view/node~Node#change
     * @returns {Number} Number of appended nodes.
     */
    _appendChild(items) {
        return this._insertChild(this.childCount, items);
    }
    /**
     * Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to
     * this element.
     *
     * @internal
     * @see module:engine/view/downcastwriter~DowncastWriter#insert
     * @protected
     * @param {Number} index Position where nodes should be inserted.
     * @param {module:engine/view/item~Item|Iterable.<module:engine/view/item~Item>} items Items to be inserted.
     * @fires module:engine/view/node~Node#change
     * @returns {Number} Number of inserted nodes.
     */
    _insertChild(index, items) {
        this._fireChange('children', this);
        let count = 0;
        const nodes = normalize$4(this.document, items);
        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;
            node.document = this.document;
            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`.
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#remove
     * @protected
     * @param {Number} index Number of the first node to remove.
     * @param {Number} [howMany=1] Number of nodes to remove.
     * @fires module:engine/view/node~Node#change
     * @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);
    }
    /**
     * Adds or overwrite attribute with a specified key and value.
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#setAttribute
     * @protected
     * @param {String} key Attribute key.
     * @param {String} value Attribute value.
     * @fires module:engine/view/node~Node#change
     */
    _setAttribute(key, value) {
        value = String(value);
        this._fireChange('attributes', this);
        if (key == 'class') {
            parseClasses(this._classes, value);
        }
        else if (key == 'style') {
            this._styles.setTo(value);
        }
        else {
            this._attrs.set(key, value);
        }
    }
    /**
     * Removes attribute from the element.
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#removeAttribute
     * @protected
     * @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.isEmpty) {
                this._styles.clear();
                return true;
            }
            return false;
        }
        // Remove other attributes.
        return this._attrs.delete(key);
    }
    /**
     * Adds specified class.
     *
     *		element._addClass( 'foo' ); // Adds 'foo' class.
     *		element._addClass( [ 'foo', 'bar' ] ); // Adds 'foo' and 'bar' classes.
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#addClass
     * @protected
     * @param {Array.<String>|String} className
     * @fires module:engine/view/node~Node#change
     */
    _addClass(className) {
        this._fireChange('attributes', this);
        for (const name of toArray(className)) {
            this._classes.add(name);
        }
    }
    /**
     * Removes specified class.
     *
     *		element._removeClass( 'foo' );  // Removes 'foo' class.
     *		element._removeClass( [ 'foo', 'bar' ] ); // Removes both 'foo' and 'bar' classes.
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#removeClass
     * @protected
     * @param {Array.<String>|String} className
     * @fires module:engine/view/node~Node#change
     */
    _removeClass(className) {
        this._fireChange('attributes', this);
        for (const name of toArray(className)) {
            this._classes.delete(name);
        }
    }
    _setStyle(property, value) {
        this._fireChange('attributes', this);
        if (isPlainObject(property)) {
            this._styles.set(property);
        }
        else {
            this._styles.set(property, value);
        }
    }
    /**
     * Removes specified style.
     *
     *		element._removeStyle( 'color' );  // Removes 'color' style.
     *		element._removeStyle( [ 'color', 'border-top' ] ); // Removes both 'color' and 'border-top' styles.
     *
     * **Note**: This method can work with normalized style names if
     * {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
     * See {@link module:engine/view/stylesmap~StylesMap#remove `StylesMap#remove()`} for details.
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#removeStyle
     * @protected
     * @param {Array.<String>|String} property
     * @fires module:engine/view/node~Node#change
     */
    _removeStyle(property) {
        this._fireChange('attributes', this);
        for (const name of toArray(property)) {
            this._styles.remove(name);
        }
    }
    /**
     * 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.
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#setCustomProperty
     * @protected
     * @param {String|Symbol} key
     * @param {*} value
     */
    _setCustomProperty(key, value) {
        this._customProperties.set(key, value);
    }
    /**
     * Removes the custom property stored under the given key.
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#removeCustomProperty
     * @protected
     * @param {String|Symbol} key
     * @returns {Boolean} Returns true if property was removed.
     */
    _removeCustomProperty(key) {
        return this._customProperties.delete(key);
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		element.is( 'element' ); // -> true
 *		element.is( 'node' ); // -> true
 *		element.is( 'view:element' ); // -> true
 *		element.is( 'view:node' ); // -> true
 *
 *		element.is( 'model:element' ); // -> false
 *		element.is( 'documentSelection' ); // -> false
 *
 * Assuming that the object being checked is an element, you can also check its
 * {@link module:engine/view/element~Element#name name}:
 *
 *		element.is( 'element', 'img' ); // -> true if this is an <img> element
 *		text.is( 'element', 'img' ); -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * @param {String} type Type to check.
 * @param {String} [name] Element name.
 * @returns {Boolean}
 */
Element$1.prototype.is = function (type, name) {
    if (!name) {
        return type === 'element' || type === 'view:element' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === 'node' || type === 'view:node';
    }
    else {
        return name === this.name && (type === 'element' || type === 'view:element');
    }
};
// Parses attributes provided to the element constructor before they are applied to an element. If attributes are passed
// as an object (instead of `Iterable`), the object is transformed to the map. Attributes with `null` value are removed.
// Attributes with non-`String` value are converted to `String`.
//
// @param {Object|Iterable} attrs Attributes to parse.
// @returns {Map} Parsed attributes.
function parseAttributes(attrs) {
    const attrsMap = toMap(attrs);
    for (const [key, value] of attrsMap) {
        if (value === null) {
            attrsMap.delete(key);
        }
        else if (typeof value != 'string') {
            attrsMap.set(key, String(value));
        }
    }
    return attrsMap;
}
// 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/item~Item|Iterable.<String|module:engine/view/item~Item>}
// @returns {Iterable.<module:engine/view/node~Node>}
function normalize$4(document, nodes) {
    // Separate condition because string is iterable.
    if (typeof nodes == 'string') {
        return [new Text$2(document, nodes)];
    }
    if (!isIterable(nodes)) {
        nodes = [nodes];
    }
    // Array.from to enable .map() on non-arrays.
    return Array.from(nodes)
        .map(node => {
        if (typeof node == 'string') {
            return new Text$2(document, node);
        }
        if (node instanceof TextProxy$1) {
            return new Text$2(document, node.data);
        }
        return node;
    });
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Containers are elements which define document structure. They define boundaries for
 * {@link module:engine/view/attributeelement~AttributeElement attributes}. They are mostly used for block elements like `<p>` or `<div>`.
 *
 * Editing engine does not define a fixed HTML DTD. This is why a feature developer needs to choose between various
 * types (container element, {@link module:engine/view/attributeelement~AttributeElement attribute element},
 * {@link module:engine/view/emptyelement~EmptyElement empty element}, etc) when developing a feature.
 *
 * The container element should be your default choice when writing a converter, unless:
 *
 * * this element represents a model text attribute (then use {@link module:engine/view/attributeelement~AttributeElement}),
 * * this is an empty element like `<img>` (then use {@link module:engine/view/emptyelement~EmptyElement}),
 * * this is a root element,
 * * this is a nested editable element (then use  {@link module:engine/view/editableelement~EditableElement}).
 *
 * To create a new container element instance use the
 * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement `DowncastWriter#createContainerElement()`}
 * method.
 *
 * @extends module:engine/view/element~Element
 */
class ContainerElement extends Element$1 {
    /**
     * Creates a container element.
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#createContainerElement
     * @see module:engine/view/element~Element
     * @protected
     * @param {module:engine/view/document~Document} document The document instance to which this element belongs.
     * @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]
     * A list of nodes to be inserted into created element.
     */
    constructor(...args) {
        super(...args);
        /**
         * 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$5;
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		containerElement.is( 'containerElement' ); // -> true
 *		containerElement.is( 'element' ); // -> true
 *		containerElement.is( 'node' ); // -> true
 *		containerElement.is( 'view:containerElement' ); // -> true
 *		containerElement.is( 'view:element' ); // -> true
 *		containerElement.is( 'view:node' ); // -> true
 *
 *		containerElement.is( 'model:element' ); // -> false
 *		containerElement.is( 'documentFragment' ); // -> false
 *
 * Assuming that the object being checked is a container element, you can also check its
 * {@link module:engine/view/containerelement~ContainerElement#name name}:
 *
 *		containerElement.is( 'element', 'div' ); // -> true if this is a div container element
 *		containerElement.is( 'contaienrElement', 'div' ); // -> same as above
 *		text.is( 'element', 'div' ); -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * @param {String} type Type to check.
 * @param {String} [name] Element name.
 * @returns {Boolean}
 */
ContainerElement.prototype.is = function (type, name) {
    if (!name) {
        return type === 'containerElement' || type === 'view:containerElement' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === 'element' || type === 'view:element' ||
            type === 'node' || type === 'view:node';
    }
    else {
        return name === this.name && (type === 'containerElement' || type === 'view:containerElement' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === 'element' || type === 'view:element');
    }
};
/**
 * 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$5() {
    const children = [...this.getChildren()];
    const lastChild = children[this.childCount - 1];
    // Block filler is required after a `<br>` if it's the last element in its container. See #1422.
    if (lastChild && lastChild.is('element', 'br')) {
        return this.childCount;
    }
    for (const child of children) {
        // 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;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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 {@link module:engine/view/document~Document Document} is read-only.
 *
 * The constructor of this class shouldn't be used directly. To create new `EditableElement` use the
 * {@link module:engine/view/downcastwriter~DowncastWriter#createEditableElement `downcastWriter#createEditableElement()`} method.
 *
 * @extends module:engine/view/containerelement~ContainerElement
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class EditableElement extends ObservableMixin(ContainerElement) {
    /**
     * Creates an editable element.
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#createEditableElement
     * @protected
     */
    constructor(...args) {
        super(...args);
        const document = args[0];
        /**
         * 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} or view
         * selection is changed.
         *
         * @readonly
         * @observable
         * @member {Boolean} module:engine/view/editableelement~EditableElement#isFocused
         */
        this.set('isFocused', false);
        this.bind('isReadOnly').to(document);
        this.bind('isFocused').to(document, 'isFocused', isFocused => isFocused && document.selection.editableElement == this);
        // Update focus state based on selection changes.
        this.listenTo(document.selection, 'change', () => {
            this.isFocused = document.isFocused && document.selection.editableElement == this;
        });
    }
    destroy() {
        this.stopListening();
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		editableElement.is( 'editableElement' ); // -> true
 *		editableElement.is( 'element' ); // -> true
 *		editableElement.is( 'node' ); // -> true
 *		editableElement.is( 'view:editableElement' ); // -> true
 *		editableElement.is( 'view:element' ); // -> true
 *		editableElement.is( 'view:node' ); // -> true
 *
 *		editableElement.is( 'model:element' ); // -> false
 *		editableElement.is( 'documentFragment' ); // -> false
 *
 * Assuming that the object being checked is an editbale element, you can also check its
 * {@link module:engine/view/editableelement~EditableElement#name name}:
 *
 *		editableElement.is( 'element', 'div' ); // -> true if this is a div element
 *		editableElement.is( 'editableElement', 'div' ); // -> same as above
 *		text.is( 'element', 'div' ); -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * @param {String} type Type to check.
 * @param {String} [name] Element name.
 * @returns {Boolean}
 */
EditableElement.prototype.is = function (type, name) {
    if (!name) {
        return type === 'editableElement' || type === 'view:editableElement' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === 'containerElement' || type === 'view:containerElement' ||
            type === 'element' || type === 'view:element' ||
            type === 'node' || type === 'view:node';
    }
    else {
        return name === this.name && (type === 'editableElement' || type === 'view:editableElement' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === 'containerElement' || type === 'view:containerElement' ||
            type === 'element' || type === 'view:element');
    }
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const rootNameSymbol = Symbol('rootName');
/**
 * Class representing a single root in the data view. A root can be either {@link ~RootEditableElement#isReadOnly editable or read-only},
 * but in both cases it is called "an editable". Roots can contain other {@link module:engine/view/editableelement~EditableElement
 * editable elements} making them "nested editables".
 *
 * @extends module:engine/view/editableelement~EditableElement
 */
class RootEditableElement extends EditableElement {
    /**
     * Creates root editable element.
     *
     * @param {module:engine/view/document~Document} document The document instance to which this element belongs.
     * @param {String} name Node name.
     */
    constructor(document, name) {
        super(document, name);
        /**
         * Name of this root inside {@link module:engine/view/document~Document} that is an owner of this root. If no
         * other name is set, `main` name is used.
         *
         * @readonly
         * @member {String}
         */
        this.rootName = 'main';
    }
    get rootName() {
        return this.getCustomProperty(rootNameSymbol);
    }
    set rootName(rootName) {
        this._setCustomProperty(rootNameSymbol, rootName);
    }
    /**
     * Overrides old element name and sets new one.
     * This is needed because view roots are created before they are attached to the DOM.
     * The name of the root element is temporary at this stage. It has to be changed when the
     * view root element is attached to the DOM element.
     *
     * @protected
     * @param {String} name The new name of element.
     */
    set _name(name) {
        this.name = name;
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		rootEditableElement.is( 'rootElement' ); // -> true
 *		rootEditableElement.is( 'editableElement' ); // -> true
 *		rootEditableElement.is( 'element' ); // -> true
 *		rootEditableElement.is( 'node' ); // -> true
 *		rootEditableElement.is( 'view:editableElement' ); // -> true
 *		rootEditableElement.is( 'view:element' ); // -> true
 *		rootEditableElement.is( 'view:node' ); // -> true
 *
 *		rootEditableElement.is( 'model:element' ); // -> false
 *		rootEditableElement.is( 'documentFragment' ); // -> false
 *
 * Assuming that the object being checked is a root editable element, you can also check its
 * {@link module:engine/view/rooteditableelement~RootEditableElement#name name}:
 *
 *		rootEditableElement.is( 'element', 'div' ); // -> true if this is a div root editable element
 *		rootEditableElement.is( 'rootElement', 'div' ); // -> same as above
 *		text.is( 'element', 'div' ); -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * @param {String} type Type to check.
 * @param {String} [name] Element name.
 * @returns {Boolean}
 */
RootEditableElement.prototype.is = function (type, name) {
    if (!name) {
        return type === 'rootElement' || type === 'view:rootElement' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === 'editableElement' || type === 'view:editableElement' ||
            type === 'containerElement' || type === 'view:containerElement' ||
            type === 'element' || type === 'view:element' ||
            type === 'node' || type === 'view:node';
    }
    else {
        return name === this.name && (type === 'rootElement' || type === 'view:rootElement' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === 'editableElement' || type === 'view:editableElement' ||
            type === 'containerElement' || type === 'view:containerElement' ||
            type === 'element' || type === 'view:element');
    }
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Position iterator class. It allows to iterate forward and backward over the document.
 */
class TreeWalker$1 {
    /**
     * Creates a range iterator. All parameters are optional, but you have to specify either `boundaries` or `startPosition`.
     *
     * @constructor
     * @param {TODO ~TreeWalkerOptions} options Object with configuration.
     */
    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', null);
        }
        if (options.direction && options.direction != 'forward' && options.direction != 'backward') {
            /**
             * Only `backward` and `forward` direction allowed.
             *
             * @error view-tree-walker-unknown-direction
             */
            throw new CKEditorError('view-tree-walker-unknown-direction', options.startPosition, { 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 = Position$1._createAt(options.startPosition);
        }
        else {
            this.position = Position$1._createAt(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;
    }
    /**
     * Iterable interface.
     *
     * @returns {Iterable.<module:engine/view/treewalker~TreeWalkerValue>}
     */
    [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;
        }
    }
    /**
     * Gets the next tree walker's value.
     *
     * @returns {module:engine/view/treewalker~TreeWalkerValue} 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 = this.position.clone();
        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, value: undefined };
        }
        // We reached the walker boundary.
        if (parent === this._boundaryEndParent && position.offset == this.boundaries.end.offset) {
            return { done: true, value: undefined };
        }
        // Get node just after current position.
        let node;
        // Text is a specific parent because it contains string instead of child nodes.
        if (parent instanceof Text$2) {
            if (position.isAtEnd) {
                // Prevent returning "elementEnd" for Text node. Skip that value and return the next walker step.
                this.position = Position$1._createAfter(parent);
                return this._next();
            }
            node = parent.data[position.offset];
        }
        else {
            node = parent.getChild(position.offset);
        }
        if (node instanceof Element$1) {
            if (!this.shallow) {
                position = new Position$1(node, 0);
            }
            else {
                position.offset++;
            }
            this.position = position;
            return this._formatReturnValue('elementStart', node, previousPosition, position, 1);
        }
        else if (node instanceof Text$2) {
            if (this.singleCharacters) {
                position = new Position$1(node, 0);
                this.position = position;
                return this._next();
            }
            else {
                let charactersCount = node.data.length;
                let item;
                // If text stick out of walker range, we need to cut it and wrap in TextProxy.
                if (node == this._boundaryEndParent) {
                    charactersCount = this.boundaries.end.offset;
                    item = new TextProxy$1(node, 0, charactersCount);
                    position = Position$1._createAfter(item);
                }
                else {
                    item = new TextProxy$1(node, 0, node.data.length);
                    // 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 TextProxy$1(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 = Position$1._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 = this.position.clone();
        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, value: undefined };
        }
        // We reached the walker boundary.
        if (parent == this._boundaryStartParent && position.offset == this.boundaries.start.offset) {
            return { done: true, value: undefined };
        }
        // 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 Text$2) {
            if (position.isAtStart) {
                // Prevent returning "elementStart" for Text node. Skip that value and return the next walker step.
                this.position = Position$1._createBefore(parent);
                return this._previous();
            }
            node = parent.data[position.offset - 1];
        }
        else {
            node = parent.getChild(position.offset - 1);
        }
        if (node instanceof Element$1) {
            if (!this.shallow) {
                position = new Position$1(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 Text$2) {
            if (this.singleCharacters) {
                position = new Position$1(node, node.data.length);
                this.position = position;
                return this._previous();
            }
            else {
                let charactersCount = node.data.length;
                let item;
                // If text stick out of walker range, we need to cut it and wrap in TextProxy.
                if (node == this._boundaryStartParent) {
                    const offset = this.boundaries.start.offset;
                    item = new TextProxy$1(node, offset, node.data.length - offset);
                    charactersCount = item.data.length;
                    position = Position$1._createBefore(item);
                }
                else {
                    item = new TextProxy$1(node, 0, node.data.length);
                    // 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 TextProxy$1(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 = Position$1._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 TextProxy$1) {
            // 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 = Position$1._createAfter(item.textNode);
                    // When we change nextPosition of returned value we need also update walker current position.
                    this.position = nextPosition;
                }
                else {
                    previousPosition = Position$1._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 = Position$1._createBefore(item.textNode);
                    // When we change nextPosition of returned value we need also update walker current position.
                    this.position = nextPosition;
                }
                else {
                    previousPosition = Position$1._createBefore(item.textNode);
                }
            }
        }
        return {
            done: false,
            value: {
                type,
                item,
                previousPosition,
                nextPosition,
                length
            }
        };
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Position in the view tree. Position is represented by its parent node and an offset in this parent.
 *
 * In order to create a new position instance use the `createPosition*()` factory methods available in:
 *
 * * {@link module:engine/view/view~View}
 * * {@link module:engine/view/downcastwriter~DowncastWriter}
 * * {@link module:engine/view/upcastwriter~UpcastWriter}
 */
class Position$1 extends TypeCheckable$1 {
    /**
     * 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) {
        super();
        /**
         * Position parent.
         *
         * @readonly
         * @member {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment}
         * module:engine/view/position~Position#parent
         */
        this.parent = parent;
        /**
         * Position offset.
         *
         * @readonly
         * @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)) {
            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 = Position$1._createAt(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~type 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 TreeWalker$1(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.root !== otherPosition.root) {
            return 'different';
        }
        if (this.isEqual(otherPosition)) {
            return 'same';
        }
        // Get path from root to position's parent element.
        const thisPath = this.parent.is('node') ? this.parent.getPath() : [];
        const otherPath = otherPosition.parent.is('node') ? otherPosition.parent.getPath() : [];
        // Add the positions' offsets to the parents offsets.
        thisPath.push(this.offset);
        otherPath.push(otherPosition.offset);
        // Compare both path arrays to find common ancestor.
        const result = compareArrays(thisPath, otherPath);
        switch (result) {
            case 'prefix':
                return 'before';
            case 'extension':
                return 'after';
            default:
                return thisPath[result] < otherPath[result] ? 'before' : 'after';
        }
    }
    /**
     * Creates a {@link module:engine/view/treewalker~TreeWalker TreeWalker} instance with this positions as a start position.
     *
     * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}
     * @param {module:engine/view/range~Range} [options.boundaries=null] Range to define boundaries of the iterator.
     * @param {Boolean} [options.singleCharacters=false]
     * @param {Boolean} [options.shallow=false]
     * @param {Boolean} [options.ignoreElementEnd=false]
     */
    getWalker(options = {}) {
        options.startPosition = this;
        return new TreeWalker$1(options);
    }
    clone() {
        return new Position$1(this.parent, this.offset);
    }
    /**
     * 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}.
     *
     * @protected
     * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] 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 Position$1) {
            return new this(itemOrPosition.parent, itemOrPosition.offset);
        }
        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 !== 0 && !offset) {
                /**
                 * {@link module:engine/view/view~View#createPositionAt `View#createPositionAt()`}
                 * requires the offset to be specified when the first parameter is a view item.
                 *
                 * @error view-createpositionat-offset-required
                 */
                throw new CKEditorError('view-createpositionat-offset-required', node);
            }
            return new Position$1(node, offset);
        }
    }
    /**
     * Creates a new position after given view item.
     *
     * @protected
     * @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 Position$1(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', item, { root: item });
        }
        return new Position$1(item.parent, item.index + 1);
    }
    /**
     * Creates a new position before given view item.
     *
     * @protected
     * @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 Position$1(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', item, { root: item });
        }
        return new Position$1(item.parent, item.index);
    }
}
/**
 * Checks whether this object is of the given type.
 *
 *		position.is( 'position' ); // -> true
 *		position.is( 'view:position' ); // -> true
 *
 *		position.is( 'model:position' ); // -> false
 *		position.is( 'element' ); // -> false
 *		position.is( 'range' ); // -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * @param {String} type
 * @returns {Boolean}
 */
Position$1.prototype.is = function (type) {
    return type === 'position' || type === 'view:position';
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Range in the view tree. A range is represented by its start and end {@link module:engine/view/position~Position positions}.
 *
 * In order to create a new position instance use the `createPosition*()` factory methods available in:
 *
 * * {@link module:engine/view/view~View}
 * * {@link module:engine/view/downcastwriter~DowncastWriter}
 * * {@link module:engine/view/upcastwriter~UpcastWriter}
 */
class Range$1 extends TypeCheckable$1 {
    /**
     * 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 the `start` position.
     */
    constructor(start, end = null) {
        super();
        /**
         * Start position.
         *
         * @readonly
         * @member {module:engine/view/position~Position}
         */
        this.start = start.clone();
        /**
         * End position.
         *
         * @readonly
         * @member {module:engine/view/position~Position}
         */
        this.end = end ? end.clone() : start.clone();
    }
    /**
     * Iterable interface.
     *
     * 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 TreeWalker$1({ 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 = Position$1._createBefore(start.parent);
        }
        if (end.parent.is('$text') && end.isAtEnd) {
            end = Position$1._createAfter(end.parent);
        }
        return new Range$1(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 Range$1(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 Position$1(nodeAfterStart, 0);
        }
        if (nodeBeforeEnd && nodeBeforeEnd.is('$text')) {
            end = new Position$1(nodeBeforeEnd, nodeBeforeEnd.data.length);
        }
        return new Range$1(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 = downcastWriter.createText( 'foo' );
     *		let img = downcastWriter.createContainerElement( 'img' );
     *		let bar = downcastWriter.createText( 'bar' );
     *		let p = downcastWriter.createContainerElement( 'p', null, [ foo, img, bar ] );
     *
     *		let range = view.createRange( view.createPositionAt( foo, 2 ), view.createPositionAt( bar, 1 ); // "o", img, "b" are in range.
     *		let otherRange = view.createRange( // "oo", img, "ba" are in range.
     *			view.createPositionAt( foo, 1 ),
     *			view.createPositionAt( bar, 2 )
     *		);
     *		let transformed = range.getDifference( otherRange );
     *		// transformed array has no ranges because `otherRange` contains `range`
     *
     *		otherRange = view.createRange( view.createPositionAt( foo, 1 ), view.createPositionAt( p, 2 ); // "oo", img are in range.
     *		transformed = range.getDifference( otherRange );
     *		// transformed array has one range: from ( p, 2 ) to ( bar, 1 )
     *
     *		otherRange = view.createRange( view.createPositionAt( p, 1 ), view.createPositionAt( 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 Range$1(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$1(otherRange.end, this.end));
            }
        }
        else {
            // Ranges do not intersect, return the original range.
            ranges.push(this.clone());
        }
        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 = downcastWriter.createText( 'foo' );
     *		let img = downcastWriter.createContainerElement( 'img' );
     *		let bar = downcastWriter.createText( 'bar' );
     *		let p = downcastWriter.createContainerElement( 'p', null, [ foo, img, bar ] );
     *
     *		let range = view.createRange( view.createPositionAt( foo, 2 ), view.createPositionAt( bar, 1 ); // "o", img, "b" are in range.
     *		let otherRange = view.createRange( view.createPositionAt( foo, 1 ), view.createPositionAt( p, 2 ); // "oo", img are in range.
     *		let transformed = range.getIntersection( otherRange ); // range from ( foo, 1 ) to ( p, 2 ).
     *
     *		otherRange = view.createRange( view.createPositionAt( bar, 1 ), view.createPositionAt( 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 Range$1(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]
     * @returns {module:engine/view/treewalker~TreeWalker}
     */
    getWalker(options = {}) {
        options.boundaries = this;
        return new TreeWalker$1(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 {@link module:engine/view/element~Element Element} contained by the range.
     * The element will be returned when it is the **only** node within the range and **fully–contained**
     * at the same time.
     *
     * @returns {module:engine/view/element~Element|null}
     */
    getContainedElement() {
        if (this.isCollapsed) {
            return null;
        }
        let nodeAfterStart = this.start.nodeAfter;
        let nodeBeforeEnd = this.end.nodeBefore;
        // Handle the situation when the range position is at the beginning / at the end of a text node.
        // In such situation `.nodeAfter` and `.nodeBefore` are `null` but the range still might be spanning
        // over one element.
        //
        // <p>Foo{<span class="widget"></span>}bar</p> vs <p>Foo[<span class="widget"></span>]bar</p>
        //
        // These are basically the same range, only the difference is if the range position is at
        // at the end/at the beginning of a text node or just before/just after the text node.
        //
        if (this.start.parent.is('$text') && this.start.isAtEnd && this.start.parent.nextSibling) {
            nodeAfterStart = this.start.parent.nextSibling;
        }
        if (this.end.parent.is('$text') && this.end.isAtStart && this.end.parent.previousSibling) {
            nodeBeforeEnd = this.end.parent.previousSibling;
        }
        if (nodeAfterStart && nodeAfterStart.is('element') && nodeAfterStart === nodeBeforeEnd) {
            return nodeAfterStart;
        }
        return null;
    }
    /**
     * Clones this range.
     *
     * @returns {module:engine/view/range~Range}
     */
    clone() {
        return new Range$1(this.start, 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 TreeWalker$1(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 TreeWalker$1(options);
        yield treeWalker.position;
        for (const value of treeWalker) {
            yield value.nextPosition;
        }
    }
    /**
     * Checks and returns whether this range intersects with the 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 the given parents and offsets.
     *
     * @protected
     * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} startElement Start position
     * parent element.
     * @param {Number} startOffset Start position offset.
     * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} 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 Position$1(startElement, startOffset), new Position$1(endElement, endOffset));
    }
    /**
     * 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.
     *
     * @protected
     * @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.
     *
     * @protected
     * @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.
     *
     * @protected
     * @param {module:engine/view/item~Item} item
     * @returns {module:engine/view/range~Range}
     */
    static _createOn(item) {
        const size = item.is('$textProxy') ? item.offsetSize : 1;
        return this._createFromPositionAndShift(Position$1._createBefore(item), size);
    }
}
/**
 * Checks whether this object is of the given type.
 *
 *		range.is( 'range' ); // -> true
 *		range.is( 'view:range' ); // -> true
 *
 *		range.is( 'model:range' ); // -> false
 *		range.is( 'element' ); // -> false
 *		range.is( 'selection' ); // -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * @param {String} type
 * @returns {Boolean}
 */
Range$1.prototype.is = function (type) {
    return type === 'range' || type === 'view:range';
};
// Function used by getEnlarged and getTrimmed methods.
function enlargeTrimSkip(value) {
    if (value.item.is('attributeElement') || value.item.is('uiElement')) {
        return true;
    }
    return false;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/count
 */
/**
 * Returns the number of items return by the iterator.
 *
 *		count( [ 1, 2, 3, 4, 5 ] ); // 5;
 *
 * @param {Iterable.<*>} iterable Any iterable.
 * @returns {Number} Number of items returned by that iterable.
 */
function count(iterable) {
    let count = 0;
    for (const _ of iterable) { // eslint-disable-line no-unused-vars
        count++;
    }
    return count;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Class representing an arbirtary selection in the view.
 * See also {@link module:engine/view/documentselection~DocumentSelection}.
 *
 * New selection instances can be created via the constructor or one these methods:
 *
 * * {@link module:engine/view/view~View#createSelection `View#createSelection()`},
 * * {@link module:engine/view/upcastwriter~UpcastWriter#createSelection `UpcastWriter#createSelection()`}.
 *
 * A selection can consist of {@link module:engine/view/range~Range ranges} that can be set by using
 * the {@link module:engine/view/selection~Selection#setTo `Selection#setTo()`} method.
 */
class Selection$1 extends EmitterMixin(TypeCheckable$1) {
    /**
     * Creates new selection instance.
     *
     * **Note**: The selection constructor is available as a factory method:
     *
     * * {@link module:engine/view/view~View#createSelection `View#createSelection()`},
     * * {@link module:engine/view/upcastwriter~UpcastWriter#createSelection `UpcastWriter#createSelection()`}.
     *
     * 		// Creates empty selection without ranges.
     *		const selection = writer.createSelection();
     *
     *		// Creates selection at the given range.
     *		const range = writer.createRange( start, end );
     *		const selection = writer.createSelection( range );
     *
     *		// Creates selection at the given ranges
     * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
     *		const selection = writer.createSelection( ranges );
     *
     *		// Creates selection from the other selection.
     *		const otherSelection = writer.createSelection();
     *		const selection = writer.createSelection( otherSelection );
     *
     *		// Creates selection from the document selection.
     *		const selection = writer.createSelection( editor.editing.view.document.selection );
     *
     * 		// Creates selection at the given position.
     *		const position = writer.createPositionFromPath( root, path );
     *		const selection = writer.createSelection( position );
     *
     *		// Creates collapsed selection at the position of given item and offset.
     *		const paragraph = writer.createContainerElement( 'paragraph' );
     *		const selection = writer.createSelection( paragraph, offset );
     *
     *		// 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.
     *		const selection = writer.createSelection( paragraph, 'in' );
     *
     *		// Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
     *		// just after the item.
     *		const selection = writer.createSelection( paragraph, 'on' );
     *
     * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
     *
     *		// Creates backward selection.
     *		const selection = writer.createSelection( range, { backward: true } );
     *
     * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
     * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
     * represented in other way, for example by applying proper CSS class.
     *
     * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
     * (and be  properly handled by screen readers).
     *
     *		// Creates fake selection with label.
     *		const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
     *
     * @param {module:engine/view/selection~Selectable} [selectable=null]
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
     * @param {String} [options.label] Label for the fake selection.
     */
    constructor(...args) {
        super();
        /**
         * Stores all ranges that are selected.
         *
         * @protected
         * @member {Array.<module:engine/view/range~Range>}
         */
        this._ranges = [];
        /**
         * Specifies whether the last added range was added as a backward or forward range.
         *
         * @protected
         * @member {Boolean}
         */
        this._lastRangeBackward = false;
        /**
         * Specifies whether selection instance is fake.
         *
         * @private
         * @member {Boolean}
         */
        this._isFake = false;
        /**
         * Fake selection's label.
         *
         * @private
         * @member {String}
         */
        this._fakeSelectionLabel = '';
        if (args.length) {
            this.setTo(...args);
        }
    }
    /**
     * Returns true if selection instance is marked as `fake`.
     *
     * @see #setTo
     * @type {Boolean}
     */
    get isFake() {
        return this._isFake;
    }
    /**
     * Returns fake selection label.
     *
     * @see #setTo
     * @type {String}
     */
    get fakeSelectionLabel() {
        return this._fakeSelectionLabel;
    }
    /**
     * Selection anchor. Anchor may be described as a position where the selection starts. Together with
     * {@link #focus focus} they define the direction of selection, which is important
     * when expanding/shrinking selection. Anchor is always the start or end of the most recent added range.
     * It may be a bit unintuitive when there are multiple ranges in selection.
     *
     * @see #focus
     * @type {module:engine/view/position~Position}
     */
    get anchor() {
        if (!this._ranges.length) {
            return null;
        }
        const range = this._ranges[this._ranges.length - 1];
        const anchor = this._lastRangeBackward ? range.end : range.start;
        return anchor.clone();
    }
    /**
     * Selection focus. Focus is a position where the selection ends.
     *
     * @see #anchor
     * @type {module:engine/view/position~Position}
     */
    get focus() {
        if (!this._ranges.length) {
            return null;
        }
        const range = this._ranges[this._ranges.length - 1];
        const focus = this._lastRangeBackward ? range.start : range.end;
        return focus.clone();
    }
    /**
     * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
     * collapsed.
     *
     * @type {Boolean}
     */
    get isCollapsed() {
        return this.rangeCount === 1 && this._ranges[0].isCollapsed;
    }
    /**
     * Returns number of ranges in selection.
     *
     * @type {Number}
     */
    get rangeCount() {
        return this._ranges.length;
    }
    /**
     * Specifies whether the {@link #focus} precedes {@link #anchor}.
     *
     * @type {Boolean}
     */
    get isBackward() {
        return !this.isCollapsed && this._lastRangeBackward;
    }
    /**
     * {@link module:engine/view/editableelement~EditableElement EditableElement} instance that contains this selection, or `null`
     * if the selection is not inside an editable element.
     *
     * @type {module:engine/view/editableelement~EditableElement|null}
     */
    get editableElement() {
        if (this.anchor) {
            return this.anchor.editableElement;
        }
        return null;
    }
    /**
     * Returns an iterable that contains copies of all ranges added to the selection.
     *
     * @returns {Iterable.<module:engine/view/range~Range>}
     */
    *getRanges() {
        for (const range of this._ranges) {
            yield range.clone();
        }
    }
    /**
     * Returns copy of the first range in the selection. First range is the one which
     * {@link module:engine/view/range~Range#start start} position {@link module:engine/view/position~Position#isBefore is before} start
     * position of all other ranges (not to confuse with the first range added to the selection).
     * Returns `null` if no ranges are added to selection.
     *
     * @returns {module:engine/view/range~Range|null}
     */
    getFirstRange() {
        let first = null;
        for (const range of this._ranges) {
            if (!first || range.start.isBefore(first.start)) {
                first = range;
            }
        }
        return first ? first.clone() : null;
    }
    /**
     * Returns copy of the last range in the selection. Last range is the one which {@link module:engine/view/range~Range#end end}
     * position {@link module:engine/view/position~Position#isAfter is after} end position of all other ranges (not to confuse
     * with the last range added to the selection). Returns `null` if no ranges are added to selection.
     *
     * @returns {module:engine/view/range~Range|null}
     */
    getLastRange() {
        let last = null;
        for (const range of this._ranges) {
            if (!last || range.end.isAfter(last.end)) {
                last = range;
            }
        }
        return last ? last.clone() : null;
    }
    /**
     * Returns copy of the first position in the selection. First position is the position that
     * {@link module:engine/view/position~Position#isBefore is before} any other position in the selection ranges.
     * Returns `null` if no ranges are added to selection.
     *
     * @returns {module:engine/view/position~Position|null}
     */
    getFirstPosition() {
        const firstRange = this.getFirstRange();
        return firstRange ? firstRange.start.clone() : null;
    }
    /**
     * Returns copy of the last position in the selection. Last position is the position that
     * {@link module:engine/view/position~Position#isAfter is after} any other position in the selection ranges.
     * Returns `null` if no ranges are added to selection.
     *
     * @returns {module:engine/view/position~Position|null}
     */
    getLastPosition() {
        const lastRange = this.getLastRange();
        return lastRange ? lastRange.end.clone() : null;
    }
    /**
     * Checks whether, this selection is equal to given selection. Selections are equal if they have same directions,
     * same number of ranges and all ranges from one selection equal to a range from other selection.
     *
     * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection
     * Selection to compare with.
     * @returns {Boolean} `true` if selections are equal, `false` otherwise.
     */
    isEqual(otherSelection) {
        if (this.isFake != otherSelection.isFake) {
            return false;
        }
        if (this.isFake && this.fakeSelectionLabel != otherSelection.fakeSelectionLabel) {
            return false;
        }
        if (this.rangeCount != otherSelection.rangeCount) {
            return false;
        }
        else if (this.rangeCount === 0) {
            return true;
        }
        if (!this.anchor.isEqual(otherSelection.anchor) || !this.focus.isEqual(otherSelection.focus)) {
            return false;
        }
        for (const thisRange of this._ranges) {
            let found = false;
            for (const otherRange of otherSelection._ranges) {
                if (thisRange.isEqual(otherRange)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                return false;
            }
        }
        return true;
    }
    /**
     * Checks whether this selection is similar to given selection. Selections are similar if they have same directions, same
     * number of ranges, and all {@link module:engine/view/range~Range#getTrimmed trimmed} ranges from one selection are
     * equal to any trimmed range from other selection.
     *
     * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection
     * Selection to compare with.
     * @returns {Boolean} `true` if selections are similar, `false` otherwise.
     */
    isSimilar(otherSelection) {
        if (this.isBackward != otherSelection.isBackward) {
            return false;
        }
        const numOfRangesA = count(this.getRanges());
        const numOfRangesB = count(otherSelection.getRanges());
        // If selections have different number of ranges, they cannot be similar.
        if (numOfRangesA != numOfRangesB) {
            return false;
        }
        // If both selections have no ranges, they are similar.
        if (numOfRangesA == 0) {
            return true;
        }
        // Check if each range in one selection has a similar range in other selection.
        for (let rangeA of this.getRanges()) {
            rangeA = rangeA.getTrimmed();
            let found = false;
            for (let rangeB of otherSelection.getRanges()) {
                rangeB = rangeB.getTrimmed();
                if (rangeA.start.isEqual(rangeB.start) && rangeA.end.isEqual(rangeB.end)) {
                    found = true;
                    break;
                }
            }
            // For `rangeA`, neither range in `otherSelection` was similar. So selections are not similar.
            if (!found) {
                return false;
            }
        }
        // There were no ranges that weren't matched. Selections are similar.
        return true;
    }
    /**
     * Returns the selected element. {@link module:engine/view/element~Element Element} is considered as selected if there is only
     * one range in the selection, and that range contains exactly one element.
     * Returns `null` if there is no selected element.
     *
     * @returns {module:engine/view/element~Element|null}
     */
    getSelectedElement() {
        if (this.rangeCount !== 1) {
            return null;
        }
        return this.getFirstRange().getContainedElement();
    }
    /**
     * Sets this selection's ranges and direction to the specified location based on the given
     * {@link module:engine/view/selection~Selectable selectable}.
     *
     *		// Sets selection to the given range.
     *		const range = writer.createRange( start, end );
     *		selection.setTo( range );
     *
     *		// Sets selection to given ranges.
     * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
     *		selection.setTo( range );
     *
     *		// Sets selection to the other selection.
     *		const otherSelection = writer.createSelection();
     *		selection.setTo( otherSelection );
     *
     *	 	// Sets selection to contents of DocumentSelection.
     *		selection.setTo( editor.editing.view.document.selection );
     *
     * 		// Sets collapsed selection at the given position.
     *		const position = writer.createPositionAt( root, path );
     *		selection.setTo( position );
     *
     * 		// Sets collapsed selection at the position of given item and offset.
     *		selection.setTo( paragraph, offset );
     *
     * 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.
     *
     *		selection.setTo( paragraph, 'in' );
     *
     * Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends just after the item.
     *
     *		selection.setTo( paragraph, 'on' );
     *
     * 		// Clears selection. Removes all ranges.
     *		selection.setTo( null );
     *
     * `Selection#setTo()` method allow passing additional options (`backward`, `fake` and `label`) as the last argument.
     *
     *		// Sets selection as backward.
     *		selection.setTo( range, { backward: true } );
     *
     * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
     * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
     * represented in other way, for example by applying proper CSS class.
     *
     * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
     * (and be  properly handled by screen readers).
     *
     *		// Creates fake selection with label.
     *		selection.setTo( range, { fake: true, label: 'foo' } );
     *
     * @fires change
     * @param {module:engine/view/selection~Selectable} selectable
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
     * @param {String} [options.label] Label for the fake selection.
     */
    setTo(...args) {
        let [selectable, placeOrOffset, options] = args;
        if (typeof placeOrOffset == 'object') {
            options = placeOrOffset;
            placeOrOffset = undefined;
        }
        if (selectable === null) {
            this._setRanges([]);
            this._setFakeOptions(options);
        }
        else if (selectable instanceof Selection$1 || selectable instanceof DocumentSelection$1) {
            this._setRanges(selectable.getRanges(), selectable.isBackward);
            this._setFakeOptions({ fake: selectable.isFake, label: selectable.fakeSelectionLabel });
        }
        else if (selectable instanceof Range$1) {
            this._setRanges([selectable], options && options.backward);
            this._setFakeOptions(options);
        }
        else if (selectable instanceof Position$1) {
            this._setRanges([new Range$1(selectable)]);
            this._setFakeOptions(options);
        }
        else if (selectable instanceof Node$2) {
            const backward = !!options && !!options.backward;
            let range;
            if (placeOrOffset === undefined) {
                /**
                 * selection.setTo requires the second parameter when the first parameter is a node.
                 *
                 * @error view-selection-setto-required-second-parameter
                 */
                throw new CKEditorError('view-selection-setto-required-second-parameter', this);
            }
            else if (placeOrOffset == 'in') {
                range = Range$1._createIn(selectable);
            }
            else if (placeOrOffset == 'on') {
                range = Range$1._createOn(selectable);
            }
            else {
                range = new Range$1(Position$1._createAt(selectable, placeOrOffset));
            }
            this._setRanges([range], backward);
            this._setFakeOptions(options);
        }
        else if (isIterable(selectable)) {
            // We assume that the selectable is an iterable of ranges.
            // Array.from() is used to prevent setting ranges to the old iterable
            this._setRanges(selectable, options && options.backward);
            this._setFakeOptions(options);
        }
        else {
            /**
             * Cannot set selection to given place.
             *
             * @error view-selection-setto-not-selectable
             */
            throw new CKEditorError('view-selection-setto-not-selectable', this);
        }
        this.fire('change');
    }
    /**
     * Moves {@link #focus} to the specified location.
     *
     * The location can be specified in the same form as {@link module:engine/view/view~View#createPositionAt view.createPositionAt()}
     * parameters.
     *
     * @fires change
     * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * first parameter is a {@link module:engine/view/item~Item view item}.
     */
    setFocus(itemOrPosition, offset) {
        if (this.anchor === null) {
            /**
             * Cannot set selection focus if there are no ranges in selection.
             *
             * @error view-selection-setfocus-no-ranges
             */
            throw new CKEditorError('view-selection-setfocus-no-ranges', this);
        }
        const newFocus = Position$1._createAt(itemOrPosition, offset);
        if (newFocus.compareWith(this.focus) == 'same') {
            return;
        }
        const anchor = this.anchor;
        this._ranges.pop();
        if (newFocus.compareWith(anchor) == 'before') {
            this._addRange(new Range$1(newFocus, anchor), true);
        }
        else {
            this._addRange(new Range$1(anchor, newFocus));
        }
        this.fire('change');
    }
    /**
     * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
     * is treated like the last added range and is used to set {@link #anchor anchor} and {@link #focus focus}.
     * Accepts a flag describing in which way the selection is made.
     *
     * @private
     * @param {Iterable.<module:engine/view/range~Range>} newRanges Iterable object of ranges to set.
     * @param {Boolean} [isLastBackward=false] Flag describing if last added range was selected forward - from start to end
     * (`false`) or backward - from end to start (`true`). Defaults to `false`.
     */
    _setRanges(newRanges, isLastBackward = false) {
        // New ranges should be copied to prevent removing them by setting them to `[]` first.
        // Only applies to situations when selection is set to the same selection or same selection's ranges.
        newRanges = Array.from(newRanges);
        this._ranges = [];
        for (const range of newRanges) {
            this._addRange(range);
        }
        this._lastRangeBackward = !!isLastBackward;
    }
    /**
     * Sets this selection instance to be marked as `fake`. A fake selection does not render as browser native selection
     * over selected elements and is hidden to the user. This way, no native selection UI artifacts are displayed to
     * the user and selection over elements can be represented in other way, for example by applying proper CSS class.
     *
     * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM (and be
     * properly handled by screen readers).
     *
     * @private
     * @param {Object} [options] Options.
     * @param {Boolean} [options.fake] If set to true selection will be marked as `fake`.
     * @param {String} [options.label=''] Fake selection label.
     */
    _setFakeOptions(options = {}) {
        this._isFake = !!options.fake;
        this._fakeSelectionLabel = options.fake ? options.label || '' : '';
    }
    /**
     * Adds a range to the selection. Added range is copied. This means that passed range is not saved in the
     * selection instance and you can safely operate on it.
     *
     * Accepts a flag describing in which way the selection is made - passed range might be selected from
     * {@link module:engine/view/range~Range#start start} to {@link module:engine/view/range~Range#end end}
     * or from {@link module:engine/view/range~Range#end end} to {@link module:engine/view/range~Range#start start}.
     * The flag is used to set {@link #anchor anchor} and {@link #focus focus} properties.
     *
     * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-selection-range-intersects` if added range intersects
     * with ranges already stored in Selection instance.
     *
     * @private
     * @fires change
     * @param {module:engine/view/range~Range} range
     * @param {Boolean} [isBackward]
     */
    _addRange(range, isBackward = false) {
        if (!(range instanceof Range$1)) {
            /**
             * Selection range set to an object that is not an instance of {@link module:engine/view/range~Range}.
             *
             * @error view-selection-add-range-not-range
             */
            throw new CKEditorError('view-selection-add-range-not-range', this);
        }
        this._pushRange(range);
        this._lastRangeBackward = !!isBackward;
    }
    /**
     * Adds range to selection - creates copy of given range so it can be safely used and modified.
     *
     * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-selection-range-intersects` if added range intersects
     * with ranges already stored in selection instance.
     *
     * @private
     * @param {module:engine/view/range~Range} range
     */
    _pushRange(range) {
        for (const storedRange of this._ranges) {
            if (range.isIntersecting(storedRange)) {
                /**
                 * Trying to add a range that intersects with another range from selection.
                 *
                 * @error view-selection-range-intersects
                 * @param {module:engine/view/range~Range} addedRange Range that was added to the selection.
                 * @param {module:engine/view/range~Range} intersectingRange Range from selection that intersects with `addedRange`.
                 */
                throw new CKEditorError('view-selection-range-intersects', this, { addedRange: range, intersectingRange: storedRange });
            }
        }
        this._ranges.push(new Range$1(range.start, range.end));
    }
}
/**
 * Checks whether this object is of the given type.
 *
 *		selection.is( 'selection' ); // -> true
 *		selection.is( 'view:selection' ); // -> true
 *
 *		selection.is( 'model:selection' ); // -> false
 *		selection.is( 'element' ); // -> false
 *		selection.is( 'range' ); // -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * @param {String} type
 * @returns {Boolean}
 */
Selection$1.prototype.is = function (type) {
    return type === 'selection' || type === 'view:selection';
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Class representing the document selection in the view.
 *
 * Its instance is available in {@link module:engine/view/document~Document#selection `Document#selection`}.
 *
 * It is similar to {@link module:engine/view/selection~Selection} but
 * it has a read-only API and can be modified only by the writer available in
 * the {@link module:engine/view/view~View#change `View#change()`} block
 * (so via {@link module:engine/view/downcastwriter~DowncastWriter#setSelection `DowncastWriter#setSelection()`}).
 */
class DocumentSelection$1 extends EmitterMixin(TypeCheckable$1) {
    /**
     * Creates new DocumentSelection instance.
     *
     * 		// Creates empty selection without ranges.
     *		const selection = new DocumentSelection();
     *
     *		// Creates selection at the given range.
     *		const range = writer.createRange( start, end );
     *		const selection = new DocumentSelection( range );
     *
     *		// Creates selection at the given ranges
     * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( start2, end2 ) ];
     *		const selection = new DocumentSelection( ranges );
     *
     *		// Creates selection from the other selection.
     *		const otherSelection = writer.createSelection();
     *		const selection = new DocumentSelection( otherSelection );
     *
     * 		// Creates selection at the given position.
     *		const position = writer.createPositionAt( root, offset );
     *		const selection = new DocumentSelection( position );
     *
     *		// Creates collapsed selection at the position of given item and offset.
     *		const paragraph = writer.createContainerElement( 'paragraph' );
     *		const selection = new DocumentSelection( paragraph, offset );
     *
     *		// 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.
     *		const selection = new DocumentSelection( paragraph, 'in' );
     *
     *		// Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
     *		// just after the item.
     *		const selection = new DocumentSelection( paragraph, 'on' );
     *
     * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
     *
     *		// Creates backward selection.
     *		const selection = new DocumentSelection( range, { backward: true } );
     *
     * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
     * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
     * represented in other way, for example by applying proper CSS class.
     *
     * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
     * (and be  properly handled by screen readers).
     *
     *		// Creates fake selection with label.
     *		const selection = new DocumentSelection( range, { fake: true, label: 'foo' } );
     *
     * @param {module:engine/view/selection~Selectable} [selectable=null]
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
     * @param {String} [options.label] Label for the fake selection.
     */
    constructor(...args) {
        super();
        /**
         * Selection is used internally (`DocumentSelection` is a proxy to that selection).
         *
         * @private
         * @member {module:engine/view/selection~Selection}
         */
        this._selection = new Selection$1();
        // Delegate change event to be fired on DocumentSelection instance.
        this._selection.delegate('change').to(this);
        // Set selection data.
        if (args.length) {
            this._selection.setTo(...args);
        }
    }
    /**
     * Returns true if selection instance is marked as `fake`.
     *
     * @see #_setTo
     * @type {Boolean}
     */
    get isFake() {
        return this._selection.isFake;
    }
    /**
     * Returns fake selection label.
     *
     * @see #_setTo
     * @type {String}
     */
    get fakeSelectionLabel() {
        return this._selection.fakeSelectionLabel;
    }
    /**
     * Selection anchor. Anchor may be described as a position where the selection starts. Together with
     * {@link #focus focus} they define the direction of selection, which is important
     * when expanding/shrinking selection. Anchor is always the start or end of the most recent added range.
     * It may be a bit unintuitive when there are multiple ranges in selection.
     *
     * @see #focus
     * @type {module:engine/view/position~Position}
     */
    get anchor() {
        return this._selection.anchor;
    }
    /**
     * Selection focus. Focus is a position where the selection ends.
     *
     * @see #anchor
     * @type {module:engine/view/position~Position}
     */
    get focus() {
        return this._selection.focus;
    }
    /**
     * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
     * collapsed.
     *
     * @type {Boolean}
     */
    get isCollapsed() {
        return this._selection.isCollapsed;
    }
    /**
     * Returns number of ranges in selection.
     *
     * @type {Number}
     */
    get rangeCount() {
        return this._selection.rangeCount;
    }
    /**
     * Specifies whether the {@link #focus} precedes {@link #anchor}.
     *
     * @type {Boolean}
     */
    get isBackward() {
        return this._selection.isBackward;
    }
    /**
     * {@link module:engine/view/editableelement~EditableElement EditableElement} instance that contains this selection, or `null`
     * if the selection is not inside an editable element.
     *
     * @type {module:engine/view/editableelement~EditableElement|null}
     */
    get editableElement() {
        return this._selection.editableElement;
    }
    /**
     * Used for the compatibility with the {@link module:engine/view/selection~Selection#isEqual} method.
     *
     * @protected
     */
    get _ranges() {
        return this._selection._ranges;
    }
    /**
     * Returns an iterable that contains copies of all ranges added to the selection.
     *
     * @returns {Iterable.<module:engine/view/range~Range>}
     */
    *getRanges() {
        yield* this._selection.getRanges();
    }
    /**
     * Returns copy of the first range in the selection. First range is the one which
     * {@link module:engine/view/range~Range#start start} position {@link module:engine/view/position~Position#isBefore is before} start
     * position of all other ranges (not to confuse with the first range added to the selection).
     * Returns `null` if no ranges are added to selection.
     *
     * @returns {module:engine/view/range~Range|null}
     */
    getFirstRange() {
        return this._selection.getFirstRange();
    }
    /**
     * Returns copy of the last range in the selection. Last range is the one which {@link module:engine/view/range~Range#end end}
     * position {@link module:engine/view/position~Position#isAfter is after} end position of all other ranges (not to confuse
     * with the last range added to the selection). Returns `null` if no ranges are added to selection.
     *
     * @returns {module:engine/view/range~Range|null}
     */
    getLastRange() {
        return this._selection.getLastRange();
    }
    /**
     * Returns copy of the first position in the selection. First position is the position that
     * {@link module:engine/view/position~Position#isBefore is before} any other position in the selection ranges.
     * Returns `null` if no ranges are added to selection.
     *
     * @returns {module:engine/view/position~Position|null}
     */
    getFirstPosition() {
        return this._selection.getFirstPosition();
    }
    /**
     * Returns copy of the last position in the selection. Last position is the position that
     * {@link module:engine/view/position~Position#isAfter is after} any other position in the selection ranges.
     * Returns `null` if no ranges are added to selection.
     *
     * @returns {module:engine/view/position~Position|null}
     */
    getLastPosition() {
        return this._selection.getLastPosition();
    }
    /**
     * Returns the selected element. {@link module:engine/view/element~Element Element} is considered as selected if there is only
     * one range in the selection, and that range contains exactly one element.
     * Returns `null` if there is no selected element.
     *
     * @returns {module:engine/view/element~Element|null}
     */
    getSelectedElement() {
        return this._selection.getSelectedElement();
    }
    /**
     * Checks whether, this selection is equal to given selection. Selections are equal if they have same directions,
     * same number of ranges and all ranges from one selection equal to a range from other selection.
     *
     * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection
     * Selection to compare with.
     * @returns {Boolean} `true` if selections are equal, `false` otherwise.
     */
    isEqual(otherSelection) {
        return this._selection.isEqual(otherSelection);
    }
    /**
     * Checks whether this selection is similar to given selection. Selections are similar if they have same directions, same
     * number of ranges, and all {@link module:engine/view/range~Range#getTrimmed trimmed} ranges from one selection are
     * equal to any trimmed range from other selection.
     *
     * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection
     * Selection to compare with.
     * @returns {Boolean} `true` if selections are similar, `false` otherwise.
     */
    isSimilar(otherSelection) {
        return this._selection.isSimilar(otherSelection);
    }
    /**
     * Sets this selection's ranges and direction to the specified location based on the given
     * {@link module:engine/view/selection~Selectable selectable}.
     *
     *		// Sets selection to the given range.
     *		const range = writer.createRange( start, end );
     *		documentSelection._setTo( range );
     *
     *		// Sets selection to given ranges.
     * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( start2, end2 ) ];
     *		documentSelection._setTo( range );
     *
     *		// Sets selection to the other selection.
     *		const otherSelection = writer.createSelection();
     *		documentSelection._setTo( otherSelection );
     *
     * 		// Sets collapsed selection at the given position.
     *		const position = writer.createPositionAt( root, offset );
     *		documentSelection._setTo( position );
     *
     * 		// Sets collapsed selection at the position of given item and offset.
     *		documentSelection._setTo( paragraph, offset );
     *
     * 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.
     *
     *		documentSelection._setTo( paragraph, 'in' );
     *
     * Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends just after the item.
     *
     *		documentSelection._setTo( paragraph, 'on' );
     *
     * 		// Clears selection. Removes all ranges.
     *		documentSelection._setTo( null );
     *
     * `Selection#_setTo()` method allow passing additional options (`backward`, `fake` and `label`) as the last argument.
     *
     *		// Sets selection as backward.
     *		documentSelection._setTo( range, { backward: true } );
     *
     * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
     * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
     * represented in other way, for example by applying proper CSS class.
     *
     * Additionally fake's selection label can be provided. It will be used to des cribe fake selection in DOM
     * (and be  properly handled by screen readers).
     *
     *		// Creates fake selection with label.
     *		documentSelection._setTo( range, { fake: true, label: 'foo' } );
     *
     * @protected
     * @fires change
     * @param {module:engine/view/selection~Selectable} selectable
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
     * @param {String} [options.label] Label for the fake selection.
     */
    _setTo(...args) {
        this._selection.setTo(...args);
    }
    /**
     * Moves {@link #focus} to the specified location.
     *
     * The location can be specified in the same form as {@link module:engine/view/view~View#createPositionAt view.createPositionAt()}
     * parameters.
     *
     * @protected
     * @fires change
     * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * first parameter is a {@link module:engine/view/item~Item view item}.
     */
    _setFocus(itemOrPosition, offset) {
        this._selection.setFocus(itemOrPosition, offset);
    }
}
/**
 * Checks whether this object is of the given type.
 *
 *		docSelection.is( 'selection' ); // -> true
 *		docSelection.is( 'documentSelection' ); // -> true
 *		docSelection.is( 'view:selection' ); // -> true
 *		docSelection.is( 'view:documentSelection' ); // -> true
 *
 *		docSelection.is( 'model:documentSelection' ); // -> false
 *		docSelection.is( 'element' ); // -> false
 *		docSelection.is( 'node' ); // -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * @param {String} type
 * @returns {Boolean}
 */
DocumentSelection$1.prototype.is = function (type) {
    return type === 'selection' ||
        type == 'documentSelection' ||
        type == 'view:selection' ||
        type == 'view:documentSelection';
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * The event object passed to bubbling event callbacks. It is used to provide information about the event as well as a tool to
 * manipulate it.
 *
 * @extends module:utils/eventinfo~EventInfo
 */
class BubblingEventInfo extends EventInfo {
    /**
     * @param {Object} source The emitter.
     * @param {String} name The event name.
     * @param {module:engine/view/range~Range} startRange The view range that the bubbling should start from.
     */
    constructor(source, name, startRange) {
        super(source, name);
        /**
         * The view range that the bubbling should start from.
         *
         * @readonly
         * @member {module:engine/view/range~Range}
         */
        this.startRange = startRange;
        /**
         * The current event phase.
         *
         * @protected
         * @member {'none'|'capturing'|'atTarget'|'bubbling'}
         */
        this._eventPhase = 'none';
        /**
         * The current bubbling target.
         *
         * @protected
         * @member {module:engine/view/document~Document|module:engine/view/node~Node|null}
         */
        this._currentTarget = null;
    }
    /**
     * The current event phase.
     *
     * @readonly
     * @member {'none'|'capturing'|'atTarget'|'bubbling'}
     */
    get eventPhase() {
        return this._eventPhase;
    }
    /**
     * The current bubbling target.
     *
     * @readonly
     * @member {module:engine/view/document~Document|module:engine/view/node~Node|null}
     */
    get currentTarget() {
        return this._currentTarget;
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const contextsSymbol = Symbol('bubbling contexts');
/**
 * Bubbling emitter mixin for the view document as described in the
 * {@link ~BubblingEmitter} interface.
 *
 * @mixin BubblingEmitterMixin
 * @implements module:engine/view/observer/bubblingemittermixin~BubblingEmitter
 */
function BubblingEmitterMixin(base) {
    class Mixin extends base {
        fire(eventOrInfo, ...eventArgs) {
            try {
                const eventInfo = eventOrInfo instanceof EventInfo ? eventOrInfo : new EventInfo(this, eventOrInfo);
                const eventContexts = getBubblingContexts(this);
                if (!eventContexts.size) {
                    return;
                }
                updateEventInfo(eventInfo, 'capturing', this);
                // The capture phase of the event.
                if (fireListenerFor(eventContexts, '$capture', eventInfo, ...eventArgs)) {
                    return eventInfo.return;
                }
                const startRange = eventInfo.startRange || this.selection.getFirstRange();
                const selectedElement = startRange ? startRange.getContainedElement() : null;
                const isCustomContext = selectedElement ? Boolean(getCustomContext(eventContexts, selectedElement)) : false;
                let node = selectedElement || getDeeperRangeParent(startRange);
                updateEventInfo(eventInfo, 'atTarget', node);
                // For the not yet bubbling event trigger for $text node if selection can be there and it's not a custom context selected.
                if (!isCustomContext) {
                    if (fireListenerFor(eventContexts, '$text', eventInfo, ...eventArgs)) {
                        return eventInfo.return;
                    }
                    updateEventInfo(eventInfo, 'bubbling', node);
                }
                while (node) {
                    // Root node handling.
                    if (node.is('rootElement')) {
                        if (fireListenerFor(eventContexts, '$root', eventInfo, ...eventArgs)) {
                            return eventInfo.return;
                        }
                    }
                    // Element node handling.
                    else if (node.is('element')) {
                        if (fireListenerFor(eventContexts, node.name, eventInfo, ...eventArgs)) {
                            return eventInfo.return;
                        }
                    }
                    // Check custom contexts (i.e., a widget).
                    if (fireListenerFor(eventContexts, node, eventInfo, ...eventArgs)) {
                        return eventInfo.return;
                    }
                    node = node.parent;
                    updateEventInfo(eventInfo, 'bubbling', node);
                }
                updateEventInfo(eventInfo, 'bubbling', this);
                // Document context.
                fireListenerFor(eventContexts, '$document', eventInfo, ...eventArgs);
                return eventInfo.return;
            }
            catch (err) {
                // @if CK_DEBUG // throw err;
                /* istanbul ignore next */
                CKEditorError.rethrowUnexpectedError(err, this);
            }
        }
        _addEventListener(event, callback, options) {
            const contexts = toArray(options.context || '$document');
            const eventContexts = getBubblingContexts(this);
            for (const context of contexts) {
                let emitter = eventContexts.get(context);
                if (!emitter) {
                    emitter = new Emitter$1();
                    eventContexts.set(context, emitter);
                }
                this.listenTo(emitter, event, callback, options);
            }
        }
        _removeEventListener(event, callback) {
            const eventContexts = getBubblingContexts(this);
            for (const emitter of eventContexts.values()) {
                this.stopListening(emitter, event, callback);
            }
        }
    }
    return Mixin;
}
// Backward compatibility with `mix`.
{
    const mixin = BubblingEmitterMixin(Object);
    ['fire', '_addEventListener', '_removeEventListener'].forEach(key => {
        BubblingEmitterMixin[key] = mixin.prototype[key];
    });
}
// Update the event info bubbling fields.
//
// @param {module:utils/eventinfo~EventInfo} eventInfo The event info object to update.
// @param {'none'|'capturing'|'atTarget'|'bubbling'} eventPhase The current event phase.
// @param {module:engine/view/document~Document|module:engine/view/node~Node} currentTarget The current bubbling target.
function updateEventInfo(eventInfo, eventPhase, currentTarget) {
    if (eventInfo instanceof BubblingEventInfo) {
        eventInfo._eventPhase = eventPhase;
        eventInfo._currentTarget = currentTarget;
    }
}
// Fires the listener for the specified context. Returns `true` if event was stopped.
//
// @private
// @param {Map.<String|Function, module:utils/emittermixin~Emitter>} eventContexts
// @param {String|module:engine/view/node~Node} context
// @param {module:utils/eventinfo~EventInfo} eventInfo The `EventInfo` object.
// @param {...*} [eventArgs] Additional arguments to be passed to the callbacks.
// @returns {Boolean} True if event stop was called.
function fireListenerFor(eventContexts, context, eventInfo, ...eventArgs) {
    const emitter = typeof context == 'string' ? eventContexts.get(context) : getCustomContext(eventContexts, context);
    if (!emitter) {
        return false;
    }
    emitter.fire(eventInfo, ...eventArgs);
    return eventInfo.stop.called;
}
// Returns an emitter for a specified view node.
//
// @private
// @param {Map.<String|Function, module:utils/emittermixin~Emitter>} eventContexts
// @param {module:engine/view/node~Node} node
// @returns {module:utils/emittermixin~Emitter|null}
function getCustomContext(eventContexts, node) {
    for (const [context, emitter] of eventContexts) {
        if (typeof context == 'function' && context(node)) {
            return emitter;
        }
    }
    return null;
}
// Returns bubbling contexts map for the source (emitter).
function getBubblingContexts(source) {
    if (!source[contextsSymbol]) {
        source[contextsSymbol] = new Map();
    }
    return source[contextsSymbol];
}
// Returns the deeper parent element for the range.
function getDeeperRangeParent(range) {
    if (!range) {
        return null;
    }
    const startParent = range.start.parent;
    const endParent = range.end.parent;
    const startPath = startParent.getPath();
    const endPath = endParent.getPath();
    return startPath.length > endPath.length ? startParent : endParent;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// @if CK_DEBUG_ENGINE // const { logDocument } = require( '../dev-utils/utils' );
/**
 * Document class creates an abstract layer over the content editable area, contains a tree of view elements and
 * {@link module:engine/view/documentselection~DocumentSelection view selection} associated with this document.
 *
 * @mixes module:engine/view/observer/bubblingemittermixin~BubblingEmitterMixin
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class Document$1 extends BubblingEmitterMixin(Observable) {
    /**
     * Creates a Document instance.
     *
     * @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor The styles processor instance.
     */
    constructor(stylesProcessor) {
        super();
        /**
         * Selection done on this document.
         *
         * @readonly
         * @member {module:engine/view/documentselection~DocumentSelection} module:engine/view/document~Document#selection
         */
        this.selection = new DocumentSelection$1();
        /**
         * Roots of the view tree. Collection of the {@link module:engine/view/element~Element view elements}.
         *
         * View roots are created as a result of binding between {@link module:engine/view/document~Document#roots} and
         * {@link module:engine/model/document~Document#roots} and this is handled by
         * {@link module:engine/controller/editingcontroller~EditingController}, so to create view root we need to create
         * model root using {@link module:engine/model/document~Document#createRoot}.
         *
         * @readonly
         * @member {module:utils/collection~Collection} module:engine/view/document~Document#roots
         */
        this.roots = new Collection({ idProperty: 'rootName' });
        /**
         * The styles processor instance used by this document when normalizing styles.
         *
         * @readonly
         * @member {module:engine/view/stylesmap~StylesProcessor}
         */
        this.stylesProcessor = stylesProcessor;
        /**
         * Defines whether document is in read-only mode.
         *
         * When document is read-ony then all roots are read-only as well and caret placed inside this root is hidden.
         *
         * @observable
         * @member {Boolean} #isReadOnly
         */
        this.set('isReadOnly', false);
        /**
         * True if document is focused.
         *
         * This property is updated by the {@link module:engine/view/observer/focusobserver~FocusObserver}.
         * If the {@link module:engine/view/observer/focusobserver~FocusObserver} is disabled this property will not change.
         *
         * @readonly
         * @observable
         * @member {Boolean} module:engine/view/document~Document#isFocused
         */
        this.set('isFocused', false);
        /**
         * `true` while the user is making a selection in the document (e.g. holding the mouse button and moving the cursor).
         * When they stop selecting, the property goes back to `false`.
         *
         * This property is updated by the {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
         *
         * @readonly
         * @observable
         * @member {Boolean} module:engine/view/document~Document#isSelecting
         */
        this.set('isSelecting', false);
        /**
         * True if composition is in progress inside the document.
         *
         * This property is updated by the {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
         * If the {@link module:engine/view/observer/compositionobserver~CompositionObserver} is disabled this property will not change.
         *
         * @readonly
         * @observable
         * @member {Boolean} module:engine/view/document~Document#isComposing
         */
        this.set('isComposing', false);
        /**
         * Post-fixer callbacks registered to the view document.
         *
         * @private
         * @member {Set}
         */
        this._postFixers = new Set();
    }
    /**
     * Gets a {@link module:engine/view/document~Document#roots view root element} with the specified name. If the name is not
     * specific "main" root is returned.
     *
     * @param {String} [name='main'] Name of the root.
     * @returns {module:engine/view/rooteditableelement~RootEditableElement|null} The view root element with the specified name
     * or null when there is no root of given name.
     */
    getRoot(name = 'main') {
        return this.roots.get(name);
    }
    /**
     * Allows registering post-fixer callbacks. A post-fixers mechanism allows to update the view tree just before it is rendered
     * to the DOM.
     *
     * Post-fixers are executed right after all changes from the outermost change block were applied but
     * before the {@link module:engine/view/view~View#event:render render event} is fired. If a post-fixer callback made
     * a change, it should return `true`. When this happens, all post-fixers are fired again to check if something else should
     * not be fixed in the new document tree state.
     *
     * View post-fixers are useful when you want to apply some fixes whenever the view structure changes. Keep in mind that
     * changes executed in a view post-fixer should not break model-view mapping.
     *
     * The types of changes which should be safe:
     *
     * * adding or removing attribute from elements,
     * * changes inside of {@link module:engine/view/uielement~UIElement UI elements},
     * * {@link module:engine/controller/editingcontroller~EditingController#reconvertItem marking some of the model elements to be
     * re-converted}.
     *
     * Try to avoid changes which touch view structure:
     *
     * * you should not add or remove nor wrap or unwrap any view elements,
     * * you should not change the editor data model in a view post-fixer.
     *
     * As a parameter, a post-fixer callback receives a {@link module:engine/view/downcastwriter~DowncastWriter downcast writer}.
     *
     * Typically, a post-fixer will look like this:
     *
     *		editor.editing.view.document.registerPostFixer( writer => {
     *			if ( checkSomeCondition() ) {
     *				writer.doSomething();
     *
     *				// Let other post-fixers know that something changed.
     *				return true;
     *			}
     *		} );
     *
     * Note that nothing happens right after you register a post-fixer (e.g. execute such a code in the console).
     * That is because adding a post-fixer does not execute it.
     * The post-fixer will be executed as soon as any change in the document needs to cause its rendering.
     * If you want to re-render the editor's view after registering the post-fixer then you should do it manually by calling
     * {@link module:engine/view/view~View#forceRender `view.forceRender()`}.
     *
     * If you need to register a callback which is executed when DOM elements are already updated,
     * use {@link module:engine/view/view~View#event:render render event}.
     *
     * @param {Function} postFixer
     */
    registerPostFixer(postFixer) {
        this._postFixers.add(postFixer);
    }
    /**
     * Destroys this instance. Makes sure that all observers are destroyed and listeners removed.
     */
    destroy() {
        this.roots.map(root => root.destroy());
        this.stopListening();
    }
    /**
     * Performs post-fixer loops. Executes post-fixer callbacks as long as none of them has done any changes to the model.
     *
     * @protected
     * @param {module:engine/view/downcastwriter~DowncastWriter} writer
     */
    _callPostFixers(writer) {
        let wasFixed = false;
        do {
            for (const callback of this._postFixers) {
                wasFixed = callback(writer);
                if (wasFixed) {
                    break;
                }
            }
        } while (wasFixed);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// Default attribute priority.
const DEFAULT_PRIORITY = 10;
/**
 * Attribute elements are used to represent formatting elements in the view (think – `<b>`, `<span style="font-size: 2em">`, etc.).
 * Most often they are created when downcasting model text attributes.
 *
 * Editing engine does not define a fixed HTML DTD. This is why a feature developer needs to choose between various
 * types (container element, {@link module:engine/view/attributeelement~AttributeElement attribute element},
 * {@link module:engine/view/emptyelement~EmptyElement empty element}, etc) when developing a feature.
 *
 * To create a new attribute element instance use the
 * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement `DowncastWriter#createAttributeElement()`} method.
 *
 * @extends module:engine/view/element~Element
 */
class AttributeElement extends Element$1 {
    /**
     * Creates an attribute element.
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#createAttributeElement
     * @see module:engine/view/element~Element
     * @protected
     * @param {module:engine/view/document~Document} document The document instance to which this element belongs.
     * @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]
     * A list of nodes to be inserted into created element.
     */
    constructor(...args) {
        super(...args);
        /**
         * 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$4;
        /**
         * Element priority. Decides in what order elements are wrapped by {@link module:engine/view/downcastwriter~DowncastWriter}.
         *
         * @protected
         * @member {Number}
         */
        this._priority = DEFAULT_PRIORITY;
        /**
         * Element identifier. If set, it is used by {@link module:engine/view/element~Element#isSimilar},
         * and then two elements are considered similar if, and only if they have the same `_id`.
         *
         * @protected
         * @member {String|Number}
         */
        this._id = null;
        /**
         * Keeps all the attribute elements that have the same {@link module:engine/view/attributeelement~AttributeElement#id ids}
         * and still exist in the view tree.
         *
         * This property is managed by {@link module:engine/view/downcastwriter~DowncastWriter}.
         *
         * @protected
         * @member {Set.<module:engine/view/attributeelement~AttributeElement>|null}
         */
        this._clonesGroup = null;
    }
    /**
     * Element priority. Decides in what order elements are wrapped by {@link module:engine/view/downcastwriter~DowncastWriter}.
     *
     * @readonly
     * @type {Number}
     */
    get priority() {
        return this._priority;
    }
    /**
     * Element identifier. If set, it is used by {@link module:engine/view/element~Element#isSimilar},
     * and then two elements are considered similar if, and only if they have the same `id`.
     *
     * @readonly
     * @type {String|Number}
     */
    get id() {
        return this._id;
    }
    /**
     * Returns all {@link module:engine/view/attributeelement~AttributeElement attribute elements} that has the
     * same {@link module:engine/view/attributeelement~AttributeElement#id id} and are in the view tree (were not removed).
     *
     * Note: If this element has been removed from the tree, returned set will not include it.
     *
     * Throws {@link module:utils/ckeditorerror~CKEditorError attribute-element-get-elements-with-same-id-no-id}
     * if this element has no `id`.
     *
     * @returns {Set.<module:engine/view/attributeelement~AttributeElement>} Set containing all the attribute elements
     * with the same `id` that were added and not removed from the view tree.
     */
    getElementsWithSameId() {
        if (this.id === null) {
            /**
             * Cannot get elements with the same id for an attribute element without id.
             *
             * @error attribute-element-get-elements-with-same-id-no-id
             */
            throw new CKEditorError('attribute-element-get-elements-with-same-id-no-id', this);
        }
        return new Set(this._clonesGroup);
    }
    /**
     * Checks if this element is similar to other element.
     *
     * If none of elements has set {@link module:engine/view/attributeelement~AttributeElement#id}, then 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.
     *
     * If at least one element has {@link module:engine/view/attributeelement~AttributeElement#id} set, then both
     * elements have to have the same {@link module:engine/view/attributeelement~AttributeElement#id} value to be
     * considered similar.
     *
     * Similarity is important for {@link module:engine/view/downcastwriter~DowncastWriter}. For example:
     *
     * * two following similar elements can be merged together into one, longer element,
     * * {@link module:engine/view/downcastwriter~DowncastWriter#unwrap} checks similarity of passed element and processed element to
     * decide whether processed element should be unwrapped,
     * * etc.
     *
     * @param {module:engine/view/element~Element} otherElement
     * @returns {Boolean}
     */
    isSimilar(otherElement) {
        // If any element has an `id` set, just compare the ids.
        if (this.id !== null || otherElement.id !== null) {
            return this.id === otherElement.id;
        }
        return super.isSimilar(otherElement) && this.priority == otherElement.priority;
    }
    /**
     * Clones provided element with priority.
     *
     * @protected
     * @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 = false) {
        const cloned = super._clone(deep);
        // Clone priority too.
        cloned._priority = this._priority;
        // And id too.
        cloned._id = this._id;
        return cloned;
    }
}
AttributeElement.DEFAULT_PRIORITY = DEFAULT_PRIORITY;
/**
 * Checks whether this object is of the given.
 *
 *		attributeElement.is( 'attributeElement' ); // -> true
 *		attributeElement.is( 'element' ); // -> true
 *		attributeElement.is( 'node' ); // -> true
 *		attributeElement.is( 'view:attributeElement' ); // -> true
 *		attributeElement.is( 'view:element' ); // -> true
 *		attributeElement.is( 'view:node' ); // -> true
 *
 *		attributeElement.is( 'model:element' ); // -> false
 *		attributeElement.is( 'documentFragment' ); // -> false
 *
 * Assuming that the object being checked is an attribute element, you can also check its
 * {@link module:engine/view/attributeelement~AttributeElement#name name}:
 *
 *		attributeElement.is( 'element', 'b' ); // -> true if this is a bold element
 *		attributeElement.is( 'attributeElement', 'b' ); // -> same as above
 *		text.is( 'element', 'b' ); -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * @param {String} type Type to check.
 * @param {String} [name] Element name.
 * @returns {Boolean}
 */
AttributeElement.prototype.is = function (type, name) {
    if (!name) {
        return type === 'attributeElement' || type === 'view:attributeElement' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === 'element' || type === 'view:element' ||
            type === 'node' || type === 'view:node';
    }
    else {
        return name === this.name && (type === 'attributeElement' || type === 'view:attributeElement' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === 'element' || type === 'view:element');
    }
};
// 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 getFillerOffset$4() {
    // <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
// @returns {Number}
function nonUiChildrenCount(element) {
    return Array.from(element.getChildren()).filter(element => !element.is('uiElement')).length;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Empty element class. It is used to represent elements that cannot contain any child nodes (for example `<img>` elements).
 *
 * To create a new empty element use the
 * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement `downcastWriter#createEmptyElement()`} method.
 *
 * @extends module:engine/view/element~Element
 */
class EmptyElement extends Element$1 {
    /**
     * 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).
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#createEmptyElement
     * @protected
     * @param {module:engine/view/document~Document} document The document instance to which this element belongs.
     * @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]
     * A list of nodes to be inserted into created element.
     */
    constructor(document, name, attributes, children) {
        super(document, name, attributes, children);
        /**
         * Returns `null` because filler is not needed for EmptyElements.
         *
         * @method #getFillerOffset
         * @returns {null} Always returns null.
         */
        this.getFillerOffset = getFillerOffset$3;
    }
    /**
     * Overrides {@link module:engine/view/element~Element#_insertChild} method.
     * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-emptyelement-cannot-add` to prevent
     * adding any child nodes to EmptyElement.
     *
     * @protected
     */
    _insertChild(index, items) {
        if (items && (items instanceof Node$2 || Array.from(items).length > 0)) {
            /**
             * Cannot add children to {@link module:engine/view/emptyelement~EmptyElement}.
             *
             * @error view-emptyelement-cannot-add
             */
            throw new CKEditorError('view-emptyelement-cannot-add', [this, items]);
        }
        return 0;
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		emptyElement.is( 'emptyElement' ); // -> true
 *		emptyElement.is( 'element' ); // -> true
 *		emptyElement.is( 'node' ); // -> true
 *		emptyElement.is( 'view:emptyElement' ); // -> true
 *		emptyElement.is( 'view:element' ); // -> true
 *		emptyElement.is( 'view:node' ); // -> true
 *
 *		emptyElement.is( 'model:element' ); // -> false
 *		emptyElement.is( 'documentFragment' ); // -> false
 *
 * Assuming that the object being checked is an empty element, you can also check its
 * {@link module:engine/view/emptyelement~EmptyElement#name name}:
 *
 *		emptyElement.is( 'element', 'img' ); // -> true if this is a img element
 *		emptyElement.is( 'emptyElement', 'img' ); // -> same as above
 *		text.is( 'element', 'img' ); -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * @param {String} type Type to check.
 * @param {String} [name] Element name.
 * @returns {Boolean}
 */
EmptyElement.prototype.is = function (type, name) {
    if (!name) {
        return type === 'emptyElement' || type === 'view:emptyElement' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === 'element' || type === 'view:element' ||
            type === 'node' || type === 'view:node';
    }
    else {
        return name === this.name && (type === 'emptyElement' || type === 'view:emptyElement' ||
            type === 'element' || type === 'view:element');
    }
};
// Returns `null` because block filler is not needed for EmptyElements.
//
// @returns {null}
function getFillerOffset$3() {
    return null;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/* globals navigator:false */
/**
 * @module utils/env
 */
/**
 * Safely returns `userAgent` from browser's navigator API in a lower case.
 * If navigator API is not available it will return an empty string.
 *
 * @returns {String}
 */
function getUserAgent() {
    // In some environments navigator API might not be available.
    try {
        return navigator.userAgent.toLowerCase();
    }
    catch (e) {
        return '';
    }
}
const userAgent = getUserAgent();
/**
 * A namespace containing environment and browser information.
 *
 * @namespace
 */
const env = {
    /**
     * Indicates that the application is running on Macintosh.
     *
     * @static
     * @type {Boolean}
     */
    isMac: isMac(userAgent),
    /**
     * Indicates that the application is running on Windows.
     *
     * @static
     * @type {Boolean}
     */
    isWindows: isWindows(userAgent),
    /**
     * Indicates that the application is running in Firefox (Gecko).
     *
     * @static
     * @type {Boolean}
     */
    isGecko: isGecko(userAgent),
    /**
     * Indicates that the application is running in Safari.
     *
     * @static
     * @type {Boolean}
     */
    isSafari: isSafari(userAgent),
    /**
     * Indicates the the application is running in iOS.
     *
     * @static
     * @type {Boolean}
     */
    isiOS: isiOS(userAgent),
    /**
     * Indicates that the application is running on Android mobile device.
     *
     * @static
     * @type {Boolean}
     */
    isAndroid: isAndroid(userAgent),
    /**
     * Indicates that the application is running in a browser using the Blink engine.
     *
     * @static
     * @type {Boolean}
     */
    isBlink: isBlink(userAgent),
    /**
     * Environment features information.
     *
     * @memberOf module:utils/env~env
     * @namespace
     */
    features: {
        /**
         * Indicates that the environment supports ES2018 Unicode property escapes — like `\p{P}` or `\p{L}`.
         * More information about unicode properties might be found
         * [in Unicode Standard Annex #44](https://www.unicode.org/reports/tr44/#GC_Values_Table).
         *
         * @type {Boolean}
         */
        isRegExpUnicodePropertySupported: isRegExpUnicodePropertySupported()
    }
};
/**
 * 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;
}
/**
 * Checks if User Agent represented by the string is running on Windows.
 *
 * @param {String} userAgent **Lowercase** `navigator.userAgent` string.
 * @returns {Boolean} Whether User Agent is running on Windows or not.
 */
function isWindows(userAgent) {
    return userAgent.indexOf('windows') > -1;
}
/**
 * Checks if User Agent represented by the string is Firefox (Gecko).
 *
 * @param {String} userAgent **Lowercase** `navigator.userAgent` string.
 * @returns {Boolean} Whether User Agent is Firefox or not.
 */
function isGecko(userAgent) {
    return !!userAgent.match(/gecko\/\d+/);
}
/**
 * Checks if User Agent represented by the string is Safari.
 *
 * @param {String} userAgent **Lowercase** `navigator.userAgent` string.
 * @returns {Boolean} Whether User Agent is Safari or not.
 */
function isSafari(userAgent) {
    return userAgent.indexOf(' applewebkit/') > -1 && userAgent.indexOf('chrome') === -1;
}
/**
 * Checks if User Agent represented by the string is running in iOS.
 *
 * @param {String} userAgent **Lowercase** `navigator.userAgent` string.
 * @returns {Boolean} Whether User Agent is running in iOS or not.
 */
function isiOS(userAgent) {
    // "Request mobile site" || "Request desktop site".
    return !!userAgent.match(/iphone|ipad/i) || (isMac(userAgent) && navigator.maxTouchPoints > 0);
}
/**
 * Checks if User Agent represented by the string is Android mobile device.
 *
 * @param {String} userAgent **Lowercase** `navigator.userAgent` string.
 * @returns {Boolean} Whether User Agent is Safari or not.
 */
function isAndroid(userAgent) {
    return userAgent.indexOf('android') > -1;
}
/**
 * Checks if User Agent represented by the string is Blink engine.
 *
 * @param {String} userAgent **Lowercase** `navigator.userAgent` string.
 * @returns {Boolean} Whether User Agent is Blink engine or not.
 */
function isBlink(userAgent) {
    // The Edge browser before switching to the Blink engine used to report itself as Chrome (and "Edge/")
    // but after switching to the Blink it replaced "Edge/" with "Edg/".
    return userAgent.indexOf('chrome/') > -1 && userAgent.indexOf('edge/') < 0;
}
/**
 * Checks if the current environment supports ES2018 Unicode properties like `\p{P}` or `\p{L}`.
 * More information about unicode properties might be found
 * [in Unicode Standard Annex #44](https://www.unicode.org/reports/tr44/#GC_Values_Table).
 *
 * @returns {Boolean}
 */
function isRegExpUnicodePropertySupported() {
    let isSupported = false;
    // Feature detection for Unicode properties. Added in ES2018. Currently Firefox does not support it.
    // See https://github.com/ckeditor/ckeditor5-mention/issues/44#issuecomment-487002174.
    try {
        // Usage of regular expression literal cause error during build (ckeditor/ckeditor5-dev#534).
        isSupported = 'ć'.search(new RegExp('[\\p{L}]', 'u')) === 0;
    }
    catch (error) {
        // Firefox throws a SyntaxError when the group is unsupported.
    }
    return isSupported;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const modifiersToGlyphsMac = {
    ctrl: '⌃',
    cmd: '⌘',
    alt: '⌥',
    shift: '⇧'
};
const modifiersToGlyphsNonMac = {
    ctrl: 'Ctrl+',
    alt: 'Alt+',
    shift: 'Shift+'
};
/**
 * An 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();
const keyCodeNames = Object.fromEntries(Object.entries(keyCodes).map(([name, code]) => [code, name.charAt(0).toUpperCase() + name.slice(1)]));
/**
 * Converts a key name or {@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} A 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 included in the {@link module:utils/keyboard~keyCodes} can be used.
             *
             * @error keyboard-unknown-key
             * @param {String} key
             */
            throw new CKEditorError('keyboard-unknown-key', null, { key });
        }
    }
    else {
        keyCode = key.keyCode +
            (key.altKey ? keyCodes.alt : 0) +
            (key.ctrlKey ? keyCodes.ctrl : 0) +
            (key.shiftKey ? keyCodes.shift : 0) +
            (key.metaKey ? keyCodes.cmd : 0);
    }
    return keyCode;
}
/**
 * Parses the keystroke and returns a keystroke code that will match the code returned by
 * {@link module:utils/keyboard~getCode} for the 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).
 *
 * Note: On macOS, keystroke handling is translating the `Ctrl` key to the `Cmd` key and handling only that keystroke.
 * For example, a registered keystroke `Ctrl+A` will be translated to `Cmd+A` on macOS. To disable the translation of some keystroke,
 * use the forced modifier: `Ctrl!+A` (note the exclamation mark).
 *
 * @param {String|Array.<Number|String>} keystroke The keystroke definition.
 * @returns {Number} Keystroke code.
 */
function parseKeystroke(keystroke) {
    if (typeof keystroke == 'string') {
        keystroke = splitKeystrokeText(keystroke);
    }
    return keystroke
        .map(key => (typeof key == 'string') ? getEnvKeyCode(key) : key)
        .reduce((key, sum) => sum + key, 0);
}
/**
 * Translates any keystroke string text like `"Ctrl+A"` to an
 * environment–specific keystroke, i.e. `"⌘A"` on macOS.
 *
 * @param {String} keystroke The keystroke text.
 * @returns {String} The keystroke text specific for the environment.
 */
function getEnvKeystrokeText(keystroke) {
    let keystrokeCode = parseKeystroke(keystroke);
    const modifiersToGlyphs = Object.entries(env.isMac ? modifiersToGlyphsMac : modifiersToGlyphsNonMac);
    const modifiers = modifiersToGlyphs.reduce((modifiers, [name, glyph]) => {
        // Modifier keys are stored as a bit mask so extract those from the keystroke code.
        if ((keystrokeCode & keyCodes[name]) != 0) {
            keystrokeCode &= ~keyCodes[name];
            modifiers += glyph;
        }
        return modifiers;
    }, '');
    return modifiers + (keystrokeCode ? keyCodeNames[keystrokeCode] : '');
}
/**
 * Returns `true` if the provided key code represents one of the arrow keys.
 *
 * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
 * @returns {Boolean}
 */
function isArrowKeyCode(keyCode) {
    return keyCode == keyCodes.arrowright ||
        keyCode == keyCodes.arrowleft ||
        keyCode == keyCodes.arrowup ||
        keyCode == keyCodes.arrowdown;
}
/**
 * Returns the direction in which the {@link module:engine/model/documentselection~DocumentSelection selection}
 * will move when the provided arrow key code is pressed considering the language direction of the editor content.
 *
 * For instance, in right–to–left (RTL) content languages, pressing the left arrow means moving the selection right (forward)
 * in the model structure. Similarly, pressing the right arrow moves the selection left (backward).
 *
 * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
 * @param {module:utils/language~LanguageDirection} contentLanguageDirection The content language direction, corresponding to
 * {@link module:utils/locale~Locale#contentLanguageDirection}.
 * @returns {module:utils/keyboard~ArrowKeyCodeDirection|undefined} Localized arrow direction or `undefined` for non-arrow key codes.
 */
function getLocalizedArrowKeyCodeDirection(keyCode, contentLanguageDirection) {
    const isLtrContent = contentLanguageDirection === 'ltr';
    switch (keyCode) {
        case keyCodes.arrowleft:
            return isLtrContent ? 'left' : 'right';
        case keyCodes.arrowright:
            return isLtrContent ? 'right' : 'left';
        case keyCodes.arrowup:
            return 'up';
        case keyCodes.arrowdown:
            return 'down';
    }
}
// Converts a key name to the key code with mapping based on the env.
//
// See: {@link module:utils/keyboard~getCode}.
//
// @param {String} key The key name (see {@link module:utils/keyboard~keyCodes}).
// @returns {Number} Key code.
function getEnvKeyCode(key) {
    // Don't remap modifier key for forced modifiers.
    if (key.endsWith('!')) {
        return getCode(key.slice(0, -1));
    }
    const code = getCode(key);
    return env.isMac && code == keyCodes.ctrl ? keyCodes.cmd : code;
}
/**
 * Determines if the provided key code moves the {@link module:engine/model/documentselection~DocumentSelection selection}
 * forward or backward considering the language direction of the editor content.
 *
 * For instance, in right–to–left (RTL) languages, pressing the left arrow means moving forward
 * in the model structure. Similarly, pressing the right arrow moves the selection backward.
 *
 * @param {Number} keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
 * @param {module:utils/language~LanguageDirection} contentLanguageDirection The content language direction, corresponding to
 * {@link module:utils/locale~Locale#contentLanguageDirection}.
 * @returns {Boolean}
 */
function isForwardArrowKeyCode(keyCode, contentLanguageDirection) {
    const localizedKeyCodeDirection = getLocalizedArrowKeyCodeDirection(keyCode, contentLanguageDirection);
    return localizedKeyCodeDirection === 'down' || localizedKeyCodeDirection === 'right';
}
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,
        shift: 0x220000,
        alt: 0x440000,
        cmd: 0x880000
    };
    // 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;
    }
    // other characters
    for (const char of '`-=[];\',./\\') {
        keyCodes[char] = char.charCodeAt(0);
    }
    return keyCodes;
}
function splitKeystrokeText(keystroke) {
    return keystroke.split('+').map(key => key.trim());
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * UI element class. It should be used to represent editing UI which needs to be injected into the editing view
 * If possible, you should keep your UI outside the editing view. However, if that is not possible,
 * UI elements can be used.
 *
 * How a UI element is rendered is in your control (you pass a callback to
 * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement `downcastWriter#createUIElement()`}).
 * The editor will ignore your UI element – the selection cannot be placed in it, it is skipped (invisible) when
 * the user modifies the selection by using arrow keys and the editor does not listen to any mutations which
 * happen inside your UI elements.
 *
 * The limitation is that you cannot convert a model element to a UI element. UI elements need to be
 * created for {@link module:engine/model/markercollection~Marker markers} or as additinal elements
 * inside normal {@link module:engine/view/containerelement~ContainerElement container elements}.
 *
 * To create a new UI element use the
 * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement `downcastWriter#createUIElement()`} method.
 *
 * @extends module:engine/view/element~Element
 */
class UIElement extends Element$1 {
    /**
     * 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).
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#createUIElement
     * @protected
     * @param {module:engine/view/document~Document} document The document instance to which this element belongs.
     * @param {String} name Node name.
     * @param {Object|Iterable} [attributes] Collection of attributes.
     * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
     * A list of nodes to be inserted into created element.
     */
    constructor(...args) {
        super(...args);
        /**
         * Returns `null` because filler is not needed for UIElements.
         *
         * @method #getFillerOffset
         * @returns {null} Always returns null.
         */
        this.getFillerOffset = getFillerOffset$2;
    }
    /**
     * Overrides {@link module:engine/view/element~Element#_insertChild} method.
     * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-uielement-cannot-add` to prevent adding any child nodes
     * to UIElement.
     *
     * @protected
     */
    _insertChild(index, items) {
        if (items && (items instanceof Node$2 || Array.from(items).length > 0)) {
            /**
             * Cannot add children to {@link module:engine/view/uielement~UIElement}.
             *
             * @error view-uielement-cannot-add
             */
            throw new CKEditorError('view-uielement-cannot-add', [this, items]);
        }
        return 0;
    }
    /**
     * Renders this {@link module:engine/view/uielement~UIElement} to DOM. This method is called by
     * {@link module:engine/view/domconverter~DomConverter}.
     * Do not use inheritance to create custom rendering method, replace `render()` method instead:
     *
     *		const myUIElement = downcastWriter.createUIElement( 'span' );
     *		myUIElement.render = function( domDocument, domConverter ) {
     *			const domElement = this.toDomElement( domDocument );
     *
     *			domConverter.setContentOf( domElement, '<b>this is ui element</b>' );
     *
     *			return domElement;
     *		};
     *
     * If changes in your UI element should trigger some editor UI update you should call
     * the {@link module:core/editor/editorui~EditorUI#update `editor.ui.update()`} method
     * after rendering your UI element.
     *
     * @param {Document} domDocument
     * @param {module:engine/view/domconverter~DomConverter} domConverter Instance of the DomConverter used to optimize the output.
     * @returns {HTMLElement}
     */
    render(domDocument, domConverter) {
        // Provide basic, default output.
        return this.toDomElement(domDocument);
    }
    /**
     * Creates DOM element based on this view UIElement.
     * Note that each time this method is called new DOM element is created.
     *
     * @param {Document} domDocument
     * @returns {HTMLElement}
     */
    toDomElement(domDocument) {
        const domElement = domDocument.createElement(this.name);
        for (const key of this.getAttributeKeys()) {
            domElement.setAttribute(key, this.getAttribute(key));
        }
        return domElement;
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		uiElement.is( 'uiElement' ); // -> true
 *		uiElement.is( 'element' ); // -> true
 *		uiElement.is( 'node' ); // -> true
 *		uiElement.is( 'view:uiElement' ); // -> true
 *		uiElement.is( 'view:element' ); // -> true
 *		uiElement.is( 'view:node' ); // -> true
 *
 *		uiElement.is( 'model:element' ); // -> false
 *		uiElement.is( 'documentFragment' ); // -> false
 *
 * Assuming that the object being checked is an ui element, you can also check its
 * {@link module:engine/view/uielement~UIElement#name name}:
 *
 *		uiElement.is( 'element', 'span' ); // -> true if this is a span ui element
 *		uiElement.is( 'uiElement', 'span' ); // -> same as above
 *		text.is( 'element', 'span' ); -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * @param {String} type Type to check.
 * @param {String} [name] Element name.
 * @returns {Boolean}
 */
UIElement.prototype.is = function (type, name) {
    if (!name) {
        return type === 'uiElement' || type === 'view:uiElement' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === 'element' || type === 'view:element' ||
            type === 'node' || type === 'view:node';
    }
    else {
        return name === this.name && (type === 'uiElement' || type === 'view:uiElement' ||
            type === 'element' || type === 'view:element');
    }
};
/**
 * 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/view~View} view View controller to which the quirks handling will be injected.
 */
function injectUiElementHandling(view) {
    view.document.on('arrowKey', (evt, data) => jumpOverUiElement(evt, data, view.domConverter), { priority: 'low' });
}
// Returns `null` because block filler is not needed for UIElements.
//
// @returns {null}
function getFillerOffset$2() {
    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);
                }
            }
        }
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * The raw element class.
 *
 * The raw elements work as data containers ("wrappers", "sandboxes") but their children are not managed or
 * even recognized by the editor. This encapsulation allows integrations to maintain custom DOM structures
 * in the editor content without, for instance, worrying about compatibility with other editor features.
 * Raw elements are a perfect tool for integration with external frameworks and data sources.
 *
 * Unlike {@link module:engine/view/uielement~UIElement UI elements}, raw elements act like real editor
 * content (similar to {@link module:engine/view/containerelement~ContainerElement} or
 * {@link module:engine/view/emptyelement~EmptyElement}), they are considered by the editor selection and
 * {@link module:widget/utils~toWidget they can work as widgets}.
 *
 * To create a new raw element, use the
 * {@link module:engine/view/downcastwriter~DowncastWriter#createRawElement `downcastWriter#createRawElement()`} method.
 *
 * @extends module:engine/view/element~Element
 */
class RawElement extends Element$1 {
    /**
     * Creates a new instance of a raw element.
     *
     * Throws the `view-rawelement-cannot-add` {@link module:utils/ckeditorerror~CKEditorError CKEditorError} when the `children`
     * parameter is passed to inform that the usage of `RawElement` is incorrect (adding child nodes to `RawElement` is forbidden).
     *
     * @see module:engine/view/downcastwriter~DowncastWriter#createRawElement
     * @protected
     * @param {module:engine/view/document~Document} document The document instance to which this element belongs.
     * @param {String} name A node name.
     * @param {Object|Iterable} [attrs] The collection of attributes.
     * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
     * A list of nodes to be inserted into the created element.
     */
    constructor(...args) {
        super(...args);
        /**
         * Returns `null` because filler is not needed for raw elements.
         *
         * @method #getFillerOffset
         * @returns {null} Always returns null.
         */
        this.getFillerOffset = getFillerOffset$1;
    }
    /**
     * Overrides the {@link module:engine/view/element~Element#_insertChild} method.
     * Throws the `view-rawelement-cannot-add` {@link module:utils/ckeditorerror~CKEditorError CKEditorError} to prevent
     * adding any child nodes to a raw element.
     *
     * @protected
     */
    _insertChild(index, items) {
        if (items && (items instanceof Node$2 || Array.from(items).length > 0)) {
            /**
             * Cannot add children to a {@link module:engine/view/rawelement~RawElement} instance.
             *
             * @error view-rawelement-cannot-add
             */
            throw new CKEditorError('view-rawelement-cannot-add', [this, items]);
        }
        return 0;
    }
    render() { }
}
/**
 * Checks whether this object is of the given type or name.
 *
 *		rawElement.is( 'rawElement' ); // -> true
 *		rawElement.is( 'element' ); // -> true
 *		rawElement.is( 'node' ); // -> true
 *		rawElement.is( 'view:rawElement' ); // -> true
 *		rawElement.is( 'view:element' ); // -> true
 *		rawElement.is( 'view:node' ); // -> true
 *
 *		rawElement.is( 'model:element' ); // -> false
 *		rawElement.is( 'documentFragment' ); // -> false
 *
 * Assuming that the object being checked is a raw element, you can also check its
 * {@link module:engine/view/rawelement~RawElement#name name}:
 *
 *		rawElement.is( 'img' ); // -> true if this is an img element
 *		rawElement.is( 'rawElement', 'img' ); // -> same as above
 *		text.is( 'img' ); -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * @param {String} type The type to check when the `name` parameter is present.
 * Otherwise, it acts like the `name` parameter.
 * @param {String} [name] The element name.
 * @returns {Boolean}
 */
RawElement.prototype.is = function (type, name) {
    if (!name) {
        return type === 'rawElement' || type === 'view:rawElement' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === this.name || type === 'view:' + this.name ||
            type === 'element' || type === 'view:element' ||
            type === 'node' || type === 'view:node';
    }
    else {
        return name === this.name && (type === 'rawElement' || type === 'view:rawElement' ||
            type === 'element' || type === 'view:element');
    }
};
// Returns `null` because block filler is not needed for raw elements.
//
// @returns {null}
function getFillerOffset$1() {
    return null;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Document fragment.
 *
 * To create a new document fragment instance use the
 * {@link module:engine/view/upcastwriter~UpcastWriter#createDocumentFragment `UpcastWriter#createDocumentFragment()`}
 * method.
 */
class DocumentFragment$1 extends EmitterMixin(TypeCheckable$1) {
    /**
     * Creates new DocumentFragment instance.
     *
     * @protected
     * @param {module:engine/view/document~Document} document The document to which this document fragment belongs.
     * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
     * A list of nodes to be inserted into the created document fragment.
     */
    constructor(document, children) {
        super();
        /**
         * The document to which this document fragment belongs.
         *
         * @readonly
         * @member {module:engine/view/document~Document}
         */
        this.document = document;
        /**
         * Array of child nodes.
         *
         * @protected
         * @member {Array.<module:engine/view/node~Node>} module:engine/view/documentfragment~DocumentFragment#_children
         */
        this._children = [];
        if (children) {
            this._insertChild(0, children);
        }
    }
    /**
     * Iterable interface.
     *
     * Iterates over nodes added to this document fragment.
     *
     * @returns {Iterable.<module:engine/view/node~Node>}
     */
    [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;
    }
    /**
     * {@link module:engine/view/documentfragment~DocumentFragment#_insertChild 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/item~Item|Iterable.<module:engine/view/item~Item>} items Items to be inserted.
     * @returns {Number} Number of appended nodes.
     */
    _appendChild(items) {
        return this._insertChild(this.childCount, items);
    }
    /**
     * 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/item~Item|Iterable.<module:engine/view/item~Item>} items Items to be inserted.
     * @returns {Number} Number of inserted nodes.
     */
    _insertChild(index, items) {
        this._fireChange('children', this);
        let count = 0;
        const nodes = normalize$3(this.document, items);
        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`.
     *
     * @internal
     * @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);
    }
}
/**
 * Checks whether this object is of the given type.
 *
 *		docFrag.is( 'documentFragment' ); // -> true
 *		docFrag.is( 'view:documentFragment' ); // -> true
 *
 *		docFrag.is( 'model:documentFragment' ); // -> false
 *		docFrag.is( 'element' ); // -> false
 *		docFrag.is( 'node' ); // -> false
 *
 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 *
 * @param {String} type
 * @returns {Boolean}
 */
DocumentFragment$1.prototype.is = function (type) {
    return type === 'documentFragment' || type === 'view:documentFragment';
};
// Converts strings to Text and non-iterables to arrays.
//
// @param {String|module:engine/view/item~Item|Iterable.<String|module:engine/view/item~Item>}
// @returns {Iterable.<module:engine/view/node~Node>}
function normalize$3(document, nodes) {
    // Separate condition because string is iterable.
    if (typeof nodes == 'string') {
        return [new Text$2(document, nodes)];
    }
    if (!isIterable(nodes)) {
        nodes = [nodes];
    }
    // Array.from to enable .map() on non-arrays.
    return Array.from(nodes)
        .map(node => {
        if (typeof node == 'string') {
            return new Text$2(document, node);
        }
        if (node instanceof TextProxy$1) {
            return new Text$2(document, node.data);
        }
        return node;
    });
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * View downcast writer.
 *
 * It provides a set of methods used to manipulate view nodes.
 *
 * Do not create an instance of this writer manually. To modify a view structure, use
 * the {@link module:engine/view/view~View#change `View#change()`} block.
 *
 * The `DowncastWriter` is designed to work with semantic views which are the views that were/are being downcasted from the model.
 * To work with ordinary views (e.g. parsed from a pasted content) use the
 * {@link module:engine/view/upcastwriter~UpcastWriter upcast writer}.
 *
 * Read more about changing the view in the {@glink framework/guides/architecture/editing-engine#changing-the-view Changing the view}
 * section of the {@glink framework/guides/architecture/editing-engine Editing engine architecture} guide.
 */
class DowncastWriter {
    /**
     * @param {module:engine/view/document~Document} document The view document instance.
     */
    constructor(document) {
        /**
         * The view document instance in which this writer operates.
         *
         * @readonly
         * @type {module:engine/view/document~Document}
         */
        this.document = document;
        /**
         * Holds references to the attribute groups that share the same {@link module:engine/view/attributeelement~AttributeElement#id id}.
         * The keys are `id`s, the values are `Set`s holding {@link module:engine/view/attributeelement~AttributeElement}s.
         *
         * @private
         * @type {Map.<String,Set>}
         */
        this._cloneGroups = new Map();
        /**
         * The slot factory used by the `elementToStructure` downcast helper.
         *
         * @private
         * @type {Function|null}
         */
        this._slotFactory = null;
    }
    /**
     * Sets {@link module:engine/view/documentselection~DocumentSelection selection's} ranges and direction to the
     * specified location based on the given {@link module:engine/view/selection~Selectable selectable}.
     *
     * Usage:
     *
     *		// Sets selection to the given range.
     *		const range = writer.createRange( start, end );
     *		writer.setSelection( range );
     *
     *		// Sets backward selection to the given range.
     *		const range = writer.createRange( start, end );
     *		writer.setSelection( range );
     *
     *		// Sets selection to given ranges.
     * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( start2, end2 ) ];
     *		writer.setSelection( range );
     *
     *		// Sets selection to the other selection.
     *		const otherSelection = writer.createSelection();
     *		writer.setSelection( otherSelection );
     *
     * 		// Sets collapsed selection at the given position.
     *		const position = writer.createPositionFromPath( root, path );
     *		writer.setSelection( position );
     *
     * 		// Sets collapsed selection at the position of given item and offset.
     *		const paragraph = writer.createContainerElement( 'p' );
     *		writer.setSelection( paragraph, offset );
     *
     * 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.
     *
     * 		writer.setSelection( paragraph, 'in' );
     *
     * Creates a range on the {@link module:engine/view/item~Item item} which starts before the item and ends just after the item.
     *
     *		writer.setSelection( paragraph, 'on' );
     *
     * 		// Removes all ranges.
     *		writer.setSelection( null );
     *
     * `DowncastWriter#setSelection()` allow passing additional options (`backward`, `fake` and `label`) as the last argument.
     *
     *		// Sets selection as backward.
     *		writer.setSelection( range, { backward: true } );
     *
     *		// Sets selection as fake.
     *		// Fake selection does not render as browser native selection over selected elements and is hidden to the user.
     * 		// This way, no native selection UI artifacts are displayed to the user and selection over elements can be
     * 		// represented in other way, for example by applying proper CSS class.
     *		writer.setSelection( range, { fake: true } );
     *
     * 		// Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
     * 		// (and be  properly handled by screen readers).
     *		writer.setSelection( range, { fake: true, label: 'foo' } );
     *
     * @param {module:engine/view/selection~Selectable} selectable
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
     * @param {String} [options.label] Label for the fake selection.
     */
    setSelection(...args) {
        this.document.selection._setTo(...args);
    }
    /**
     * Moves {@link module:engine/view/documentselection~DocumentSelection#focus selection's focus} to the specified location.
     *
     * The location can be specified in the same form as {@link module:engine/view/view~View#createPositionAt view.createPositionAt()}
     * parameters.
     *
     * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * first parameter is a {@link module:engine/view/item~Item view item}.
     */
    setSelectionFocus(...args) {
        this.document.selection._setFocus(...args);
    }
    /**
     * Creates a new {@link module:engine/view/documentfragment~DocumentFragment} instance.
     *
     * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
     * A list of nodes to be inserted into the created document fragment.
     * @returns {module:engine/view/documentfragment~DocumentFragment} The created document fragment.
     */
    createDocumentFragment(children) {
        return new DocumentFragment$1(this.document, children);
    }
    /**
     * Creates a new {@link module:engine/view/text~Text text node}.
     *
     *		writer.createText( 'foo' );
     *
     * @param {String} data The text's data.
     * @returns {module:engine/view/text~Text} The created text node.
     */
    createText(data) {
        return new Text$2(this.document, data);
    }
    /**
     * Creates a new {@link module:engine/view/attributeelement~AttributeElement}.
     *
     *		writer.createAttributeElement( 'strong' );
     *		writer.createAttributeElement( 'a', { href: 'foo.bar' } );
     *
     *		// Make `<a>` element contain other attributes element so the `<a>` element is not broken.
     *		writer.createAttributeElement( 'a', { href: 'foo.bar' }, { priority: 5 } );
     *
     *		// Set `id` of a marker element so it is not joined or merged with "normal" elements.
     *		writer.createAttributeElement( 'span', { class: 'my-marker' }, { id: 'marker:my' } );
     *
     * @param {String} name Name of the element.
     * @param {Object} [attributes] Element's attributes.
     * @param {Object} [options] Element's options.
     * @param {Number} [options.priority] Element's {@link module:engine/view/attributeelement~AttributeElement#priority priority}.
     * @param {Number|String} [options.id] Element's {@link module:engine/view/attributeelement~AttributeElement#id id}.
     * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
     * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
     * @returns {module:engine/view/attributeelement~AttributeElement} Created element.
     */
    createAttributeElement(name, attributes, options = {}) {
        const attributeElement = new AttributeElement(this.document, name, attributes);
        if (typeof options.priority === 'number') {
            attributeElement._priority = options.priority;
        }
        if (options.id) {
            attributeElement._id = options.id;
        }
        if (options.renderUnsafeAttributes) {
            attributeElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
        }
        return attributeElement;
    }
    createContainerElement(name, attributes, childrenOrOptions = {}, options = {}) {
        let children = null;
        if (isPlainObject(childrenOrOptions)) {
            options = childrenOrOptions;
        }
        else {
            children = childrenOrOptions;
        }
        const containerElement = new ContainerElement(this.document, name, attributes, children);
        if (options.renderUnsafeAttributes) {
            containerElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
        }
        return containerElement;
    }
    /**
     * Creates a new {@link module:engine/view/editableelement~EditableElement}.
     *
     *		writer.createEditableElement( 'div' );
     *		writer.createEditableElement( 'div', { id: 'foo-1234' } );
     *
     * Note: The editable element is to be used in the editing pipeline. Usually, together with
     * {@link module:widget/utils~toWidgetEditable `toWidgetEditable()`}.
     *
     * @param {String} name Name of the element.
     * @param {Object} [attributes] Elements attributes.
     * @param {Object} [options] Element's options.
     * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
     * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
     * @returns {module:engine/view/editableelement~EditableElement} Created element.
     */
    createEditableElement(name, attributes, options = {}) {
        const editableElement = new EditableElement(this.document, name, attributes);
        if (options.renderUnsafeAttributes) {
            editableElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
        }
        return editableElement;
    }
    /**
     * Creates a new {@link module:engine/view/emptyelement~EmptyElement}.
     *
     *		writer.createEmptyElement( 'img' );
     *		writer.createEmptyElement( 'img', { id: 'foo-1234' } );
     *
     * @param {String} name Name of the element.
     * @param {Object} [attributes] Elements attributes.
     * @param {Object} [options] Element's options.
     * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
     * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
     * @returns {module:engine/view/emptyelement~EmptyElement} Created element.
     */
    createEmptyElement(name, attributes, options = {}) {
        const emptyElement = new EmptyElement(this.document, name, attributes);
        if (options.renderUnsafeAttributes) {
            emptyElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
        }
        return emptyElement;
    }
    /**
     * Creates a new {@link module:engine/view/uielement~UIElement}.
     *
     *		writer.createUIElement( 'span' );
     *		writer.createUIElement( 'span', { id: 'foo-1234' } );
     *
     * A custom render function can be provided as the third parameter:
     *
     *		writer.createUIElement( 'span', null, function( domDocument ) {
     *			const domElement = this.toDomElement( domDocument );
     *			domElement.innerHTML = '<b>this is ui element</b>';
     *
     *			return domElement;
     *		} );
     *
     * Unlike {@link #createRawElement raw elements}, UI elements are by no means editor content, for instance,
     * they are ignored by the editor selection system.
     *
     * You should not use UI elements as data containers. Check out {@link #createRawElement} instead.
     *
     * @param {String} name The name of the element.
     * @param {Object} [attributes] Element attributes.
     * @param {Function} [renderFunction] A custom render function.
     * @returns {module:engine/view/uielement~UIElement} The created element.
     */
    createUIElement(name, attributes, renderFunction) {
        const uiElement = new UIElement(this.document, name, attributes);
        if (renderFunction) {
            uiElement.render = renderFunction;
        }
        return uiElement;
    }
    /**
     * Creates a new {@link module:engine/view/rawelement~RawElement}.
     *
     *		writer.createRawElement( 'span', { id: 'foo-1234' }, function( domElement ) {
     *			domElement.innerHTML = '<b>This is the raw content of the raw element.</b>';
     *		} );
     *
     * Raw elements work as data containers ("wrappers", "sandboxes") but their children are not managed or
     * even recognized by the editor. This encapsulation allows integrations to maintain custom DOM structures
     * in the editor content without, for instance, worrying about compatibility with other editor features.
     * Raw elements are a perfect tool for integration with external frameworks and data sources.
     *
     * Unlike {@link #createUIElement UI elements}, raw elements act like "real" editor content (similar to
     * {@link module:engine/view/containerelement~ContainerElement} or {@link module:engine/view/emptyelement~EmptyElement}),
     * and they are considered by the editor selection.
     *
     * You should not use raw elements to render the UI in the editor content. Check out {@link #createUIElement `#createUIElement()`}
     * instead.
     *
     * @param {String} name The name of the element.
     * @param {Object} [attributes] Element attributes.
     * @param {Function} [renderFunction] A custom render function.
     * @param {Object} [options] Element's options.
     * @param {Array.<String>} [options.renderUnsafeAttributes] A list of attribute names that should be rendered in the editing
     * pipeline even though they would normally be filtered out by unsafe attribute detection mechanisms.
     * @returns {module:engine/view/rawelement~RawElement} The created element.
     */
    createRawElement(name, attributes, renderFunction, options = {}) {
        const rawElement = new RawElement(this.document, name, attributes);
        if (renderFunction) {
            rawElement.render = renderFunction;
        }
        if (options.renderUnsafeAttributes) {
            rawElement._unsafeAttributesToRender.push(...options.renderUnsafeAttributes);
        }
        return rawElement;
    }
    /**
     * Adds or overwrites the element's attribute with a specified key and value.
     *
     *		writer.setAttribute( 'href', 'http://ckeditor.com', linkElement );
     *
     * @param {String} key The attribute key.
     * @param {String} value The attribute value.
     * @param {module:engine/view/element~Element} element
     */
    setAttribute(key, value, element) {
        element._setAttribute(key, value);
    }
    /**
     * Removes attribute from the element.
     *
     *		writer.removeAttribute( 'href', linkElement );
     *
     * @param {String} key Attribute key.
     * @param {module:engine/view/element~Element} element
     */
    removeAttribute(key, element) {
        element._removeAttribute(key);
    }
    /**
     * Adds specified class to the element.
     *
     *		writer.addClass( 'foo', linkElement );
     *		writer.addClass( [ 'foo', 'bar' ], linkElement );
     *
     * @param {Array.<String>|String} className
     * @param {module:engine/view/element~Element} element
     */
    addClass(className, element) {
        element._addClass(className);
    }
    /**
     * Removes specified class from the element.
     *
     *		writer.removeClass( 'foo', linkElement );
     *		writer.removeClass( [ 'foo', 'bar' ], linkElement );
     *
     * @param {Array.<String>|String} className
     * @param {module:engine/view/element~Element} element
     */
    removeClass(className, element) {
        element._removeClass(className);
    }
    setStyle(property, value, element) {
        if (isPlainObject(property) && element === undefined) {
            value._setStyle(property);
        }
        else {
            element._setStyle(property, value);
        }
    }
    /**
     * Removes specified style from the element.
     *
     *		writer.removeStyle( 'color', element ); // Removes 'color' style.
     *		writer.removeStyle( [ 'color', 'border-top' ], element ); // Removes both 'color' and 'border-top' styles.
     *
     * **Note**: This method can work with normalized style names if
     * {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
     * See {@link module:engine/view/stylesmap~StylesMap#remove `StylesMap#remove()`} for details.
     *
     * @param {Array.<String>|String} property
     * @param {module:engine/view/element~Element} element
     */
    removeStyle(property, element) {
        element._removeStyle(property);
    }
    /**
     * Sets a custom property on element. 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
     * @param {module:engine/view/element~Element} element
     */
    setCustomProperty(key, value, element) {
        element._setCustomProperty(key, value);
    }
    /**
     * Removes a custom property stored under the given key.
     *
     * @param {String|Symbol} key
     * @param {module:engine/view/element~Element} element
     * @returns {Boolean} Returns true if property was removed.
     */
    removeCustomProperty(key, element) {
        return element._removeCustomProperty(key);
    }
    /**
     * Breaks attribute elements at the provided position or at the boundaries of a provided range. It breaks attribute elements
     * up to their first ancestor that is a container element.
     *
     * In following examples `<p>` is a container, `<b>` and `<u>` are attribute elements:
     *
     *		<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:** The difference between {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breakAttributes()} and
     * {@link module:engine/view/downcastwriter~DowncastWriter#breakContainer breakContainer()} is that `breakAttributes()` breaks all
     * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of a given `position`,
     * up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
     * `breakContainer()` assumes that a given `position` is directly in the container element and breaks that container element.
     *
     * Throws the `view-writer-invalid-range-container` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
     * when the {@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 the `view-writer-cannot-break-empty-element` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
     * when trying to break attributes inside an {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
     *
     * Throws the `view-writer-cannot-break-ui-element` {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
     * when trying to break attributes inside a {@link module:engine/view/uielement~UIElement UIElement}.
     *
     * @see module:engine/view/attributeelement~AttributeElement
     * @see module:engine/view/containerelement~ContainerElement
     * @see module:engine/view/downcastwriter~DowncastWriter#breakContainer
     * @param {module:engine/view/position~Position|module:engine/view/range~Range} positionOrRange The position where
     * to break attribute elements.
     * @returns {module:engine/view/position~Position|module:engine/view/range~Range} The new position or range, after breaking the
     * attribute elements.
     */
    breakAttributes(positionOrRange) {
        if (positionOrRange instanceof Position$1) {
            return this._breakAttributes(positionOrRange);
        }
        else {
            return this._breakAttributesRange(positionOrRange);
        }
    }
    /**
     * Breaks a {@link module:engine/view/containerelement~ContainerElement container view element} into two, at the given position.
     * The position has to be directly inside the container element and cannot be in the root. It does not break the conrainer view element
     * if the position is at the beginning or at the end of its 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:** The difference between {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breakAttributes()} and
     * {@link module:engine/view/downcastwriter~DowncastWriter#breakContainer breakContainer()} is that `breakAttributes()` breaks all
     * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of a given `position`,
     * up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
     * `breakContainer()` assumes that the given `position` is directly in the container element and breaks that container element.
     *
     * @see module:engine/view/attributeelement~AttributeElement
     * @see module:engine/view/containerelement~ContainerElement
     * @see module:engine/view/downcastwriter~DowncastWriter#breakAttributes
     * @param {module:engine/view/position~Position} position The position where to break the element.
     * @returns {module:engine/view/position~Position} The position between broken elements. If an element has not been broken,
     * the returned position is placed either before or after it.
     */
    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', this.document);
        }
        if (!element.parent) {
            /**
             * Trying to break root element.
             *
             * @error view-writer-break-root
             */
            throw new CKEditorError('view-writer-break-root', this.document);
        }
        if (position.isAtStart) {
            return Position$1._createBefore(element);
        }
        else if (!position.isAtEnd) {
            const newElement = element._clone(false);
            this.insert(Position$1._createAfter(element), newElement);
            const sourceRange = new Range$1(position, Position$1._createAt(element, 'end'));
            const targetPosition = new Position$1(newElement, 0);
            this.move(sourceRange, targetPosition);
        }
        return Position$1._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/downcastwriter~DowncastWriter#mergeAttributes mergeAttributes} and
     * {@link module:engine/view/downcastwriter~DowncastWriter#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/downcastwriter~DowncastWriter#mergeContainers
     * @param {module:engine/view/position~Position} position Merge position.
     * @returns {module:engine/view/position~Position} Position after merge.
     */
    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();
            this._removeFromClonedElementsGroup(positionParent);
            return this.mergeAttributes(new Position$1(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 position 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._appendChild(nodeAfter.getChildren());
            nodeAfter._remove();
            this._removeFromClonedElementsGroup(nodeAfter);
            // New position is located inside the first node, before new nodes.
            // Call this method recursively to merge again if needed.
            return this.mergeAttributes(new Position$1(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/downcastwriter~DowncastWriter#mergeAttributes mergeAttributes} and
     * {@link module:engine/view/downcastwriter~DowncastWriter#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/downcastwriter~DowncastWriter#mergeAttributes
     * @param {module:engine/view/position~Position} position Merge position.
     * @returns {module:engine/view/position~Position} Position after merge.
     */
    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', this.document);
        }
        const lastChild = prev.getChild(prev.childCount - 1);
        const newPosition = lastChild instanceof Text$2 ? Position$1._createAt(lastChild, 'end') : Position$1._createAt(prev, 'end');
        this.move(Range$1._createIn(next), Position$1._createAt(prev, 'end'));
        this.remove(Range$1._createOn(next));
        return newPosition;
    }
    /**
     * Inserts a 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},
     * {@link module:engine/view/rawelement~RawElement RawElements} or
     * {@link module:engine/view/uielement~UIElement UIElements}.
     *
     * @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/rawelement~RawElement|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/rawelement~RawElement|
     * module:engine/view/uielement~UIElement>} nodes Node or nodes to insert.
     * @returns {module:engine/view/range~Range} Range around inserted nodes.
     */
    insert(position, nodes) {
        nodes = isIterable(nodes) ? [...nodes] : [nodes];
        // Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text.
        validateNodesToInsert(nodes, this.document);
        // Group nodes in batches of nodes that require or do not require breaking an AttributeElements.
        const nodeGroups = nodes.reduce((groups, node) => {
            const lastGroup = groups[groups.length - 1];
            // Break attributes on nodes that do exist in the model tree so they can have attributes, other elements
            // can't have an attribute in model and won't get wrapped with an AttributeElement while down-casted.
            const breakAttributes = !node.is('uiElement');
            if (!lastGroup || lastGroup.breakAttributes != breakAttributes) {
                groups.push({
                    breakAttributes,
                    nodes: [node]
                });
            }
            else {
                lastGroup.nodes.push(node);
            }
            return groups;
        }, []);
        // Insert nodes in batches.
        let start = null;
        let end = position;
        for (const { nodes, breakAttributes } of nodeGroups) {
            const range = this._insertNodes(end, nodes, breakAttributes);
            if (!start) {
                start = range.start;
            }
            end = range.end;
        }
        // When no nodes were inserted - return collapsed range.
        if (!start) {
            return new Range$1(position);
        }
        return new Range$1(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.
     *
     * @param {module:engine/view/range~Range|module:engine/view/item~Item} rangeOrItem Range to remove from container
     * or an {@link module:engine/view/item~Item item} to remove. If range is provided, 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.
     */
    remove(rangeOrItem) {
        const range = rangeOrItem instanceof Range$1 ? rangeOrItem : Range$1._createOn(rangeOrItem);
        validateRangeContainer(range, this.document);
        // If range is collapsed - nothing to remove.
        if (range.isCollapsed) {
            return new DocumentFragment$1(this.document);
        }
        // Break attributes at range start and end.
        const { start: breakStart, end: breakEnd } = this._breakAttributesRange(range, true);
        const parentContainer = breakStart.parent;
        const count = breakEnd.offset - breakStart.offset;
        // Remove nodes in range.
        const removed = parentContainer._removeChildren(breakStart.offset, count);
        for (const node of removed) {
            this._removeFromClonedElementsGroup(node);
        }
        // Merge after removing.
        const mergePosition = this.mergeAttributes(breakStart);
        range.start = mergePosition;
        range.end = mergePosition.clone();
        // Return removed nodes.
        return new DocumentFragment$1(this.document, 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.
     *
     * @param {module:engine/view/range~Range} range Range to clear.
     * @param {module:engine/view/element~Element} element Element to remove.
     */
    clear(range, element) {
        validateRangeContainer(range, this.document);
        // 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 = Range$1._createOn(item);
                // When range starts inside Text or TextProxy element.
            }
            else if (!current.nextPosition.isAfter(range.start) && 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 = Range$1._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.
                this.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.
     *
     * @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.
     */
    move(sourceRange, targetPosition) {
        let nodes;
        if (targetPosition.isAfter(sourceRange.end)) {
            targetPosition = this._breakAttributes(targetPosition, true);
            const parent = targetPosition.parent;
            const countBefore = parent.childCount;
            sourceRange = this._breakAttributesRange(sourceRange, true);
            nodes = this.remove(sourceRange);
            targetPosition.offset += (parent.childCount - countBefore);
        }
        else {
            nodes = this.remove(sourceRange);
        }
        return this.insert(targetPosition, nodes);
    }
    /**
     * Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
     * If a collapsed range is provided, it will be wrapped only if it is equal to view selection.
     *
     * If a collapsed range was passed and is same as selection, the selection
     * will be moved to the inside of the wrapped attribute element.
     *
     * 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 {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
     *
     * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-nonselection-collapsed-range` when passed range
     * is collapsed and different than view selection.
     *
     * @param {module:engine/view/range~Range} range Range to wrap.
     * @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
     * @returns {module:engine/view/range~Range} range Range after wrapping, spanning over wrapping attribute element.
     */
    wrap(range, attribute) {
        if (!(attribute instanceof AttributeElement)) {
            throw new CKEditorError('view-writer-wrap-invalid-attribute', this.document);
        }
        validateRangeContainer(range, this.document);
        if (!range.isCollapsed) {
            // Non-collapsed range. Wrap it with the attribute element.
            return this._wrapRange(range, attribute);
        }
        else {
            // Collapsed range. Wrap position.
            let position = range.start;
            if (position.parent.is('element') && !_hasNonUiChildren(position.parent)) {
                position = position.getLastMatchingPosition(value => value.item.is('uiElement'));
            }
            position = this._wrapPosition(position, attribute);
            const viewSelection = this.document.selection;
            // If wrapping position is equal to view selection, move view selection inside wrapping attribute element.
            if (viewSelection.isCollapsed && viewSelection.getFirstPosition().isEqual(range.start)) {
                this.setSelection(position);
            }
            return new Range$1(position);
        }
    }
    /**
     * 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} attribute
     */
    unwrap(range, attribute) {
        if (!(attribute instanceof AttributeElement)) {
            /**
             * The `attribute` passed to {@link module:engine/view/downcastwriter~DowncastWriter#unwrap `DowncastWriter#unwrap()`}
             * must be an instance of {@link module:engine/view/attributeelement~AttributeElement `AttributeElement`}.
             *
             * @error view-writer-unwrap-invalid-attribute
             */
            throw new CKEditorError('view-writer-unwrap-invalid-attribute', this.document);
        }
        validateRangeContainer(range, this.document);
        // If range is collapsed - nothing to unwrap.
        if (range.isCollapsed) {
            return range;
        }
        // Break attributes at range start and end.
        const { start: breakStart, end: breakEnd } = this._breakAttributesRange(range, true);
        const parentContainer = breakStart.parent;
        // Unwrap children located between break points.
        const newRange = this._unwrapChildren(parentContainer, breakStart.offset, breakEnd.offset, attribute);
        // Merge attributes at the both ends and return a new range.
        const start = this.mergeAttributes(newRange.start);
        // If start position was merged - move end position back.
        if (!start.isEqual(newRange.start)) {
            newRange.end.offset--;
        }
        const end = this.mergeAttributes(newRange.end);
        return new Range$1(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 {String} newName New name for element.
     * @param {module:engine/view/containerelement~ContainerElement} viewElement Element to be renamed.
     * @returns {module:engine/view/containerelement~ContainerElement} Element created due to rename.
     */
    rename(newName, viewElement) {
        const newElement = new ContainerElement(this.document, newName, viewElement.getAttributes());
        this.insert(Position$1._createAfter(viewElement), newElement);
        this.move(Range$1._createIn(viewElement), Position$1._createAt(newElement, 0));
        this.remove(Range$1._createOn(viewElement));
        return newElement;
    }
    /**
     * Cleans up memory by removing obsolete cloned elements group from the writer.
     *
     * Should be used whenever all {@link module:engine/view/attributeelement~AttributeElement attribute elements}
     * with the same {@link module:engine/view/attributeelement~AttributeElement#id id} are going to be removed from the view and
     * the group will no longer be needed.
     *
     * Cloned elements group are not removed automatically in case if the group is still needed after all its elements
     * were removed from the view.
     *
     * Keep in mind that group names are equal to the `id` property of the attribute element.
     *
     * @param {String} groupName Name of the group to clear.
     */
    clearClonedElementsGroup(groupName) {
        this._cloneGroups.delete(groupName);
    }
    /**
     * 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 #createPositionBefore},
     * * {@link #createPositionAfter},
     *
     * @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * first parameter is a {@link module:engine/view/item~Item view item}.
     * @returns {module:engine/view/position~Position}
     */
    createPositionAt(itemOrPosition, offset) {
        return Position$1._createAt(itemOrPosition, 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}
     */
    createPositionAfter(item) {
        return Position$1._createAfter(item);
    }
    /**
     * 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}
     */
    createPositionBefore(item) {
        return Position$1._createBefore(item);
    }
    /**
     * Creates a range spanning from `start` position to `end` position.
     *
     * **Note:** This factory method creates its 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.
     * @returns {module:engine/view/range~Range}
     */
    createRange(...args) {
        return new Range$1(...args);
    }
    /**
     * 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}
     */
    createRangeOn(item) {
        return Range$1._createOn(item);
    }
    /**
     * 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}
     */
    createRangeIn(element) {
        return Range$1._createIn(element);
    }
    /**
     * Creates new {@link module:engine/view/selection~Selection} instance.
     *
     * 		// Creates empty selection without ranges.
     *		const selection = writer.createSelection();
     *
     *		// Creates selection at the given range.
     *		const range = writer.createRange( start, end );
     *		const selection = writer.createSelection( range );
     *
     *		// Creates selection at the given ranges
     * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
     *		const selection = writer.createSelection( ranges );
     *
     *		// Creates selection from the other selection.
     *		const otherSelection = writer.createSelection();
     *		const selection = writer.createSelection( otherSelection );
     *
     *		// Creates selection from the document selection.
     *		const selection = writer.createSelection( editor.editing.view.document.selection );
     *
     * 		// Creates selection at the given position.
     *		const position = writer.createPositionFromPath( root, path );
     *		const selection = writer.createSelection( position );
     *
     *		// Creates collapsed selection at the position of given item and offset.
     *		const paragraph = writer.createContainerElement( 'p' );
     *		const selection = writer.createSelection( paragraph, offset );
     *
     *		// 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.
     *		const selection = writer.createSelection( paragraph, 'in' );
     *
     *		// Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
     *		// just after the item.
     *		const selection = writer.createSelection( paragraph, 'on' );
     *
     * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
     *
     *		// Creates backward selection.
     *		const selection = writer.createSelection( range, { backward: true } );
     *
     * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
     * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
     * represented in other way, for example by applying proper CSS class.
     *
     * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
     * (and be  properly handled by screen readers).
     *
     *		// Creates fake selection with label.
     *		const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
     *
     * @param {module:engine/view/selection~Selectable} [selectable=null]
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
     * @param {String} [options.label] Label for the fake selection.
     * @returns {module:engine/view/selection~Selection}
     */
    createSelection(...args) {
        return new Selection$1(...args);
    }
    /**
     * Creates placeholders for child elements of the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
     * `elementToStructure()`} conversion helper.
     *
     *		const viewSlot = conversionApi.writer.createSlot();
     *		const viewPosition = conversionApi.writer.createPositionAt( viewElement, 0 );
     *
     *		conversionApi.writer.insert( viewPosition, viewSlot );
     *
     * It could be filtered down to a specific subset of children (only `<foo>` model elements in this case):
     *
     *		const viewSlot = conversionApi.writer.createSlot( node => node.is( 'element', 'foo' ) );
     *		const viewPosition = conversionApi.writer.createPositionAt( viewElement, 0 );
     *
     *		conversionApi.writer.insert( viewPosition, viewSlot );
     *
     * While providing a filtered slot, make sure to provide slots for all child nodes. A single node can not be downcasted into
     * multiple slots.
     *
     * **Note**: You should not change the order of nodes. View elements should be in the same order as model nodes.
     *
     * @param {'children'|module:engine/conversion/downcasthelpers~SlotFilter} [modeOrFilter='children'] The filter for child nodes.
     * @returns {module:engine/view/element~Element} The slot element to be placed in to the view structure while processing
     * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure `elementToStructure()`}.
     */
    createSlot(modeOrFilter) {
        if (!this._slotFactory) {
            /**
             * The `createSlot()` method is only allowed inside the `elementToStructure` downcast helper callback.
             *
             * @error view-writer-invalid-create-slot-context
             */
            throw new CKEditorError('view-writer-invalid-create-slot-context', this.document);
        }
        return this._slotFactory(this, modeOrFilter);
    }
    /**
     * Registers a slot factory.
     *
     * @protected
     * @param {Function} slotFactory The slot factory.
     */
    _registerSlotFactory(slotFactory) {
        this._slotFactory = slotFactory;
    }
    /**
     * Clears the registered slot factory.
     *
     * @protected
     */
    _clearSlotFactory() {
        this._slotFactory = null;
    }
    /**
     * Inserts a node or nodes at the specified position. Takes care of breaking attributes before insertion
     * and merging them afterwards if requested by the breakAttributes param.
     *
     * @private
     * @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/rawelement~RawElement|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/rawelement~RawElement|
     * module:engine/view/uielement~UIElement>} nodes Node or nodes to insert.
     * @param {Boolean} breakAttributes Whether attributes should be broken.
     * @returns {module:engine/view/range~Range} Range around inserted nodes.
     */
    _insertNodes(position, nodes, breakAttributes) {
        let parentElement;
        // Break attributes on nodes that do exist in the model tree so they can have attributes, other elements
        // can't have an attribute in model and won't get wrapped with an AttributeElement while down-casted.
        if (breakAttributes) {
            parentElement = getParentContainer(position);
        }
        else {
            parentElement = position.parent.is('$text') ? position.parent.parent : position.parent;
        }
        if (!parentElement) {
            /**
             * Position's parent container cannot be found.
             *
             * @error view-writer-invalid-position-container
             */
            throw new CKEditorError('view-writer-invalid-position-container', this.document);
        }
        let insertionPosition;
        if (breakAttributes) {
            insertionPosition = this._breakAttributes(position, true);
        }
        else {
            insertionPosition = position.parent.is('$text') ? breakTextNode(position) : position;
        }
        const length = parentElement._insertChild(insertionPosition.offset, nodes);
        for (const node of nodes) {
            this._addToClonedElementsGroup(node);
        }
        const endPosition = insertionPosition.getShiftedBy(length);
        const start = this.mergeAttributes(insertionPosition);
        // If start position was merged - move end position.
        if (!start.isEqual(insertionPosition)) {
            endPosition.offset--;
        }
        const end = this.mergeAttributes(endPosition);
        return new Range$1(start, end);
    }
    /**
     * Wraps children with provided `wrapElement`. Only children contained in `parent` element between
     * `startOffset` and `endOffset` will be wrapped.
     *
     * @private
     * @param {module:engine/view/element~Element} parent
     * @param {Number} startOffset
     * @param {Number} endOffset
     * @param {module:engine/view/element~Element} wrapElement
     */
    _wrapChildren(parent, startOffset, endOffset, wrapElement) {
        let i = startOffset;
        const wrapPositions = [];
        while (i < endOffset) {
            const child = parent.getChild(i);
            const isText = child.is('$text');
            const isAttribute = child.is('attributeElement');
            //
            // (In all examples, assume that `wrapElement` is `<span class="foo">` element.)
            //
            // Check if `wrapElement` can be joined with the wrapped element. One of requirements is having same name.
            // If possible, join elements.
            //
            // <p><span class="bar">abc</span></p>  -->  <p><span class="foo bar">abc</span></p>
            //
            if (isAttribute && this._wrapAttributeElement(wrapElement, child)) {
                wrapPositions.push(new Position$1(parent, i));
            }
            //
            // Wrap the child if it is not an attribute element or if it is an attribute element that should be inside
            // `wrapElement` (due to priority).
            //
            // <p>abc</p>                   -->  <p><span class="foo">abc</span></p>
            // <p><strong>abc</strong></p>  -->  <p><span class="foo"><strong>abc</strong></span></p>
            else if (isText || !isAttribute || shouldABeOutsideB(wrapElement, child)) {
                // Clone attribute.
                const newAttribute = wrapElement._clone();
                // Wrap current node with new attribute.
                child._remove();
                newAttribute._appendChild(child);
                parent._insertChild(i, newAttribute);
                this._addToClonedElementsGroup(newAttribute);
                wrapPositions.push(new Position$1(parent, i));
            }
            //
            // If other nested attribute is found and it wasn't wrapped (see above), continue wrapping inside it.
            //
            // <p><a href="foo.html">abc</a></p>  -->  <p><a href="foo.html"><span class="foo">abc</span></a></p>
            //
            else /* if ( isAttribute ) */ {
                this._wrapChildren(child, 0, child.childCount, wrapElement);
            }
            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 = this.mergeAttributes(position);
            // If nodes were merged - other merge offsets will change.
            if (!newPosition.isEqual(position)) {
                offsetChange++;
                endOffset--;
            }
        }
        return Range$1._createFromParentsAndOffsets(parent, startOffset, parent, endOffset);
    }
    /**
     * Unwraps children from provided `unwrapElement`. Only children contained in `parent` element between
     * `startOffset` and `endOffset` will be unwrapped.
     *
     * @private
     * @param {module:engine/view/element~Element} parent
     * @param {Number} startOffset
     * @param {Number} endOffset
     * @param {module:engine/view/element~Element} unwrapElement
     */
    _unwrapChildren(parent, startOffset, endOffset, unwrapElement) {
        let i = startOffset;
        const unwrapPositions = [];
        // Iterate over each element between provided offsets inside parent.
        // We don't use tree walker or range iterator because we will be removing and merging potentially multiple nodes,
        // so it could get messy. It is safer to it manually in this case.
        while (i < endOffset) {
            const child = parent.getChild(i);
            // Skip all text nodes. There should be no container element's here either.
            if (!child.is('attributeElement')) {
                i++;
                continue;
            }
            //
            // (In all examples, assume that `unwrapElement` is `<span class="foo">` element.)
            //
            // If the child is similar to the given attribute element, unwrap it - it will be completely removed.
            //
            // <p><span class="foo">abc</span>xyz</p>  -->  <p>abcxyz</p>
            //
            if (child.isSimilar(unwrapElement)) {
                const unwrapped = child.getChildren();
                const count = child.childCount;
                // Replace wrapper element with its children
                child._remove();
                parent._insertChild(i, unwrapped);
                this._removeFromClonedElementsGroup(child);
                // Save start and end position of moved items.
                unwrapPositions.push(new Position$1(parent, i), new Position$1(parent, i + count));
                // Skip elements that were unwrapped. Assuming there won't be another element to unwrap in child elements.
                i += count;
                endOffset += count - 1;
                continue;
            }
            //
            // If the child is not similar but is an attribute element, try partial unwrapping - remove the same attributes/styles/classes.
            // Partial unwrapping will happen only if the elements have the same name.
            //
            // <p><span class="foo bar">abc</span>xyz</p>  -->  <p><span class="bar">abc</span>xyz</p>
            // <p><i class="foo">abc</i>xyz</p>            -->  <p><i class="foo">abc</i>xyz</p>
            //
            if (this._unwrapAttributeElement(unwrapElement, child)) {
                unwrapPositions.push(new Position$1(parent, i), new Position$1(parent, i + 1));
                i++;
                continue;
            }
            //
            // If other nested attribute is found, look through it's children for elements to unwrap.
            //
            // <p><i><span class="foo">abc</span></i><p>  -->  <p><i>abc</i><p>
            //
            this._unwrapChildren(child, 0, child.childCount, unwrapElement);
            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 = this.mergeAttributes(position);
            // If nodes were merged - other merge offsets will change.
            if (!newPosition.isEqual(position)) {
                offsetChange++;
                endOffset--;
            }
        }
        return Range$1._createFromParentsAndOffsets(parent, startOffset, parent, endOffset);
    }
    /**
     * Helper function for `view.writer.wrap`. Wraps range with provided attribute element.
     * This method will also merge newly added attribute element 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 {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
     *
     * @private
     * @param {module:engine/view/range~Range} range
     * @param {module:engine/view/attributeelement~AttributeElement} attribute
     * @returns {module:engine/view/range~Range} New range after wrapping, spanning over wrapping attribute element.
     */
    _wrapRange(range, attribute) {
        // Break attributes at range start and end.
        const { start: breakStart, end: breakEnd } = this._breakAttributesRange(range, true);
        const parentContainer = breakStart.parent;
        // Wrap all children with attribute.
        const newRange = this._wrapChildren(parentContainer, breakStart.offset, breakEnd.offset, attribute);
        // Merge attributes at the both ends and return a new range.
        const start = this.mergeAttributes(newRange.start);
        // If start position was merged - move end position back.
        if (!start.isEqual(newRange.start)) {
            newRange.end.offset--;
        }
        const end = this.mergeAttributes(newRange.end);
        return new Range$1(start, end);
    }
    /**
     * Helper function for {@link #wrap}. Wraps position with provided attribute element.
     * This method will also merge newly added attribute element 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 {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
     *
     * @private
     * @param {module:engine/view/position~Position} position
     * @param {module:engine/view/attributeelement~AttributeElement} attribute
     * @returns {module:engine/view/position~Position} New position after wrapping.
     */
    _wrapPosition(position, attribute) {
        // Return same position when trying to wrap with attribute similar to position parent.
        if (attribute.isSimilar(position.parent)) {
            return movePositionToTextNode(position.clone());
        }
        // 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 fakeElement = this.createAttributeElement('_wrapPosition-fake-element');
        fakeElement._priority = Number.POSITIVE_INFINITY;
        fakeElement.isSimilar = () => false;
        // Insert fake element in position location.
        position.parent._insertChild(position.offset, fakeElement);
        // Range around inserted fake attribute element.
        const wrapRange = new Range$1(position, position.getShiftedBy(1));
        // Wrap fake element with attribute (it will also merge if possible).
        this.wrap(wrapRange, attribute);
        // Remove fake element and place new position there.
        const newPosition = new Position$1(fakeElement.parent, fakeElement.index);
        fakeElement._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 Text$2 && nodeAfter instanceof Text$2) {
            return mergeTextNodes(nodeBefore, nodeAfter);
        }
        // If position is next to text node - move position inside.
        return movePositionToTextNode(newPosition);
    }
    /**
     * 	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.
     *
     * 	@private
     * 	@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.
     */
    _wrapAttributeElement(wrapper, toWrap) {
        if (!canBeJoined(wrapper, toWrap)) {
            return false;
        }
        // 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)) {
                this.setAttribute(key, wrapper.getAttribute(key), toWrap);
            }
        }
        for (const key of wrapper.getStyleNames()) {
            if (!toWrap.hasStyle(key)) {
                this.setStyle(key, wrapper.getStyle(key), toWrap);
            }
        }
        for (const key of wrapper.getClassNames()) {
            if (!toWrap.hasClass(key)) {
                this.addClass(key, toWrap);
            }
        }
        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.
     *
     * @private
     * @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.
     **/
    _unwrapAttributeElement(wrapper, toUnwrap) {
        if (!canBeJoined(wrapper, toUnwrap)) {
            return false;
        }
        // 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;
            }
            this.removeAttribute(key, toUnwrap);
        }
        // Remove all wrapper's classes from unwrapped element.
        this.removeClass(Array.from(wrapper.getClassNames()), toUnwrap);
        // Remove all wrapper's styles from unwrapped element.
        this.removeStyle(Array.from(wrapper.getStyleNames()), toUnwrap);
        return true;
    }
    /**
     * Helper function used by other `DowncastWriter` methods. Breaks attribute elements at the boundaries of given range.
     *
     * @private
     * @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.
     * @returns {module:engine/view/range~Range} New range with located at break positions.
     */
    _breakAttributesRange(range, forceSplitText = false) {
        const rangeStart = range.start;
        const rangeEnd = range.end;
        validateRangeContainer(range, this.document);
        // Break at the collapsed position. Return new collapsed range.
        if (range.isCollapsed) {
            const position = this._breakAttributes(range.start, forceSplitText);
            return new Range$1(position, position);
        }
        const breakEnd = this._breakAttributes(rangeEnd, forceSplitText);
        const count = breakEnd.parent.childCount;
        const breakStart = this._breakAttributes(rangeStart, forceSplitText);
        // Calculate new break end offset.
        breakEnd.offset += breakEnd.parent.childCount - count;
        return new Range$1(breakStart, breakEnd);
    }
    /**
     * Helper function used by other `DowncastWriter` methods. Breaks attribute elements at given position.
     *
     * 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}.
     *
     * @private
     * @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.
     * @returns {module:engine/view/position~Position} New position after breaking the attributes.
     */
    _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 an `EmptyElement` instance.
             *
             * This error is thrown if
             * {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes `DowncastWriter#breakAttributes()`}
             * was executed in an incorrect position.
             *
             * @error view-writer-cannot-break-empty-element
             */
            throw new CKEditorError('view-writer-cannot-break-empty-element', this.document);
        }
        // If position is placed inside UIElement - throw an exception as we cannot break inside.
        if (position.parent.is('uiElement')) {
            /**
             * Cannot break a `UIElement` instance.
             *
             * This error is thrown if
             * {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes `DowncastWriter#breakAttributes()`}
             * was executed in an incorrect position.
             *
             * @error view-writer-cannot-break-ui-element
             */
            throw new CKEditorError('view-writer-cannot-break-ui-element', this.document);
        }
        // If position is placed inside RawElement - throw an exception as we cannot break inside.
        if (position.parent.is('rawElement')) {
            /**
             * Cannot break a `RawElement` instance.
             *
             * This error is thrown if
             * {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes `DowncastWriter#breakAttributes()`}
             * was executed in an incorrect position.
             *
             * @error view-writer-cannot-break-raw-element
             */
            throw new CKEditorError('view-writer-cannot-break-raw-element', this.document);
        }
        // There are no attributes to break and text nodes breaking is not forced.
        if (!forceSplitText && positionParent.is('$text') && isContainerOrFragment(positionParent.parent)) {
            return position.clone();
        }
        // Position's parent is container, so no attributes to break.
        if (isContainerOrFragment(positionParent)) {
            return position.clone();
        }
        // Break text and start again in new position.
        if (positionParent.is('$text')) {
            return this._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 Position$1(positionParent.parent, positionParent.index + 1);
            return this._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 Position$1(positionParent.parent, positionParent.index);
                return this._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._insertChild(offsetAfter, clonedNode);
                this._addToClonedElementsGroup(clonedNode);
                // Get nodes to move.
                const count = positionParent.childCount - positionOffset;
                const nodesToMove = positionParent._removeChildren(positionOffset, count);
                // Move nodes to cloned node.
                clonedNode._appendChild(nodesToMove);
                // Create new position to work on.
                const newPosition = new Position$1(positionParent.parent, offsetAfter);
                return this._breakAttributes(newPosition, forceSplitText);
            }
        }
    }
    /**
     * Stores the information that an {@link module:engine/view/attributeelement~AttributeElement attribute element} was
     * added to the tree. Saves the reference to the group in the given element and updates the group, so other elements
     * from the group now keep a reference to the given attribute element.
     *
     * The clones group can be obtained using {@link module:engine/view/attributeelement~AttributeElement#getElementsWithSameId}.
     *
     * Does nothing if added element has no {@link module:engine/view/attributeelement~AttributeElement#id id}.
     *
     * @private
     * @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to save.
     */
    _addToClonedElementsGroup(element) {
        // Add only if the element is in document tree.
        if (!element.root.is('rootElement')) {
            return;
        }
        // Traverse the element's children recursively to find other attribute elements that also might got inserted.
        // The loop is at the beginning so we can make fast returns later in the code.
        if (element.is('element')) {
            for (const child of element.getChildren()) {
                this._addToClonedElementsGroup(child);
            }
        }
        const id = element.id;
        if (!id) {
            return;
        }
        let group = this._cloneGroups.get(id);
        if (!group) {
            group = new Set();
            this._cloneGroups.set(id, group);
        }
        group.add(element);
        element._clonesGroup = group;
    }
    /**
     * Removes all the information about the given {@link module:engine/view/attributeelement~AttributeElement attribute element}
     * from its clones group.
     *
     * Keep in mind, that the element will still keep a reference to the group (but the group will not keep a reference to it).
     * This allows to reference the whole group even if the element was already removed from the tree.
     *
     * Does nothing if the element has no {@link module:engine/view/attributeelement~AttributeElement#id id}.
     *
     * @private
     * @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to remove.
     */
    _removeFromClonedElementsGroup(element) {
        // Traverse the element's children recursively to find other attribute elements that also got removed.
        // The loop is at the beginning so we can make fast returns later in the code.
        if (element.is('element')) {
            for (const child of element.getChildren()) {
                this._removeFromClonedElementsGroup(child);
            }
        }
        const id = element.id;
        if (!id) {
            return;
        }
        const group = this._cloneGroups.get(id);
        if (!group) {
            return;
        }
        group.delete(element);
        // Not removing group from element on purpose!
        // If other parts of code have reference to this element, they will be able to get references to other elements from the group.
    }
}
// Helper function for `view.writer.wrap`. Checks if given element has any children that are not ui elements.
function _hasNonUiChildren(parent) {
    return Array.from(parent.getChildren()).some(child => !child.is('uiElement'));
}
/**
 * The `attribute` passed to {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#wrap()`}
 * must be an instance of {@link module:engine/view/attributeelement~AttributeElement `AttributeElement`}.
 *
 * @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;
}
// Checks if first {@link module:engine/view/attributeelement~AttributeElement AttributeElement} provided to the function
// can be wrapped outside 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 Position$1(nodeBefore, nodeBefore.data.length);
    }
    const nodeAfter = position.nodeAfter;
    if (nodeAfter && nodeAfter.is('$text')) {
        return new Position$1(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 Position$1(position.parent.parent, position.parent.index + 1);
    }
    if (position.offset === 0) {
        return new Position$1(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._insertChild(position.parent.index + 1, new Text$2(position.root.document, textToMove));
    // Return new position between two newly created text nodes.
    return new Position$1(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 Position$1(t1, nodeBeforeLength);
}
const validNodesToInsert = [Text$2, AttributeElement, ContainerElement, EmptyElement, RawElement, UIElement];
// Checks if provided nodes are valid to insert.
//
// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
// contains instances that are not supported ones (see error description for valid ones.
//
// @param Iterable.<module:engine/view/text~Text|module:engine/view/element~Element> nodes
// @param {Object} errorContext
function validateNodesToInsert(nodes, errorContext) {
    for (const node of nodes) {
        if (!validNodesToInsert.some((validNode => node instanceof validNode))) { // eslint-disable-line no-use-before-define
            /**
             * One of the nodes to be inserted is of an invalid type.
             *
             * Nodes to be inserted with {@link module:engine/view/downcastwriter~DowncastWriter#insert `DowncastWriter#insert()`} should be
             * of the following types:
             *
             * * {@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/rawelement~RawElement RawElement},
             * * {@link module:engine/view/text~Text Text}.
             *
             * @error view-writer-insert-invalid-node-type
             */
            throw new CKEditorError('view-writer-insert-invalid-node-type', errorContext);
        }
        if (!node.is('$text')) {
            validateNodesToInsert(node.getChildren(), errorContext);
        }
    }
}
// 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
// @param {Object} errorContext
function validateRangeContainer(range, errorContext) {
    const startContainer = getParentContainer(range.start);
    const endContainer = getParentContainer(range.end);
    if (!startContainer || !endContainer || startContainer !== endContainer) {
        /**
         * The container of the given range is invalid.
         *
         * This may 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 the same container element or
         * a parent container for these positions cannot be found.
         *
         * Methods like {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#remove()`},
         * {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#clean()`},
         * {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#wrap()`},
         * {@link module:engine/view/downcastwriter~DowncastWriter#wrap `DowncastWriter#unwrap()`} need to be called
         * on a range that has its start and end positions located in the same container element. Both positions can be
         * nested within other elements (e.g. an attribute element) but the closest container ancestor must be the same.
         *
         * @error view-writer-invalid-range-container
         */
        throw new CKEditorError('view-writer-invalid-range-container', errorContext);
    }
}
// Checks if two attribute elements can be joined together. Elements can be joined together if, and only if
// they do not have ids specified.
//
// @private
// @param {module:engine/view/element~Element} a
// @param {module:engine/view/element~Element} b
// @returns {Boolean}
function canBeJoined(a, b) {
    return a.id === null && b.id === null;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/dom/istext
 */
/**
 * Checks if the object is a native DOM Text node.
 *
 * @param {*} obj
 * @returns {Boolean}
 */
function isText(obj) {
    return Object.prototype.toString.call(obj) == '[object Text]';
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Set of utilities related to handling block and inline fillers.
 *
 * Browsers do not allow to put caret in elements which does not have height. Because of it, we need to fill all
 * empty elements which should be selectable with elements or characters called "fillers". Unfortunately there is no one
 * universal filler, this is why two types are uses:
 *
 * * Block filler is an element which fill block elements, like `<p>`. CKEditor uses `<br>` as a block filler during the editing,
 * as browsers do natively. So instead of an empty `<p>` there will be `<p><br></p>`. The advantage of block filler is that
 * it is transparent for the selection, so when the caret is before the `<br>` and user presses right arrow he will be
 * moved to the next paragraph, not after the `<br>`. The disadvantage is that it breaks a block, so it can not be used
 * in the middle of a line of text. The {@link module:engine/view/filler~BR_FILLER `<br>` filler} can be replaced with any other
 * character in the data output, for instance {@link module:engine/view/filler~NBSP_FILLER non-breaking space} or
 * {@link module:engine/view/filler~MARKED_NBSP_FILLER marked non-breaking space}.
 *
 * * Inline filler is a filler which does not break a line of text, so it can be used inside the text, for instance in the empty
 * `<b>` surrendered by text: `foo<b></b>bar`, if we want to put the caret there. CKEditor uses a sequence of the zero-width
 * spaces as an {@link module:engine/view/filler~INLINE_FILLER inline filler} having the predetermined
 * {@link module:engine/view/filler~INLINE_FILLER_LENGTH length}. A sequence is used, instead of a single character to
 * avoid treating random zero-width spaces as the inline filler. Disadvantage of the inline filler is that it is not
 * transparent for the selection. The arrow key moves the caret between zero-width spaces characters, so the additional
 * code is needed to handle the caret.
 *
 * Both inline and block fillers are handled by the {@link module:engine/view/renderer~Renderer renderer} and are not present in the
 * view.
 *
 * @module engine/view/filler
 */
/**
 * Non-breaking space filler creator. This function creates the `&nbsp;` text node.
 * It defines how the filler is created.
 *
 * @see module:engine/view/filler~MARKED_NBSP_FILLER
 * @see module:engine/view/filler~BR_FILLER
 * @function
 */
const NBSP_FILLER = (domDocument) => domDocument.createTextNode('\u00A0');
/**
 * Marked non-breaking space filler creator. This function creates the `<span data-cke-filler="true">&nbsp;</span>` element.
 * It defines how the filler is created.
 *
 * @see module:engine/view/filler~NBSP_FILLER
 * @see module:engine/view/filler~BR_FILLER
 * @function
 */
const MARKED_NBSP_FILLER = (domDocument) => {
    const span = domDocument.createElement('span');
    span.dataset.ckeFiller = 'true';
    span.innerText = '\u00A0';
    return span;
};
/**
 * `<br>` filler creator. This function creates the `<br data-cke-filler="true">` element.
 * It defines how the filler is created.
 *
 * @see module:engine/view/filler~NBSP_FILLER
 * @see module:engine/view/filler~MARKED_NBSP_FILLER
 * @function
 */
const BR_FILLER = (domDocument) => {
    const fillerBr = domDocument.createElement('br');
    fillerBr.dataset.ckeFiller = 'true';
    return fillerBr;
};
/**
 * Length of the {@link module:engine/view/filler~INLINE_FILLER INLINE_FILLER}.
 */
const INLINE_FILLER_LENGTH = 7;
/**
 * Inline filler which is a sequence of the word joiners.
 *
 * @type {String}
 */
const INLINE_FILLER = '\u2060'.repeat(INLINE_FILLER_LENGTH);
/**
 * Checks if the node is a text node which starts with the {@link module:engine/view/filler~INLINE_FILLER inline filler}.
 *
 *		startsWithFiller( document.createTextNode( INLINE_FILLER ) ); // true
 *		startsWithFiller( document.createTextNode( INLINE_FILLER + 'foo' ) ); // true
 *		startsWithFiller( document.createTextNode( 'foo' ) ); // false
 *		startsWithFiller( document.createElement( 'p' ) ); // false
 *
 * @param {Node} domNode DOM node.
 * @returns {Boolean} True if the text node starts with the {@link module:engine/view/filler~INLINE_FILLER inline filler}.
 */
function startsWithFiller(domNode) {
    return isText(domNode) && (domNode.data.substr(0, INLINE_FILLER_LENGTH) === INLINE_FILLER);
}
/**
 * Checks if the text node contains only the {@link module:engine/view/filler~INLINE_FILLER inline filler}.
 *
 *		isInlineFiller( document.createTextNode( INLINE_FILLER ) ); // true
 *		isInlineFiller( document.createTextNode( INLINE_FILLER + 'foo' ) ); // false
 *
 * @param {Text} domText DOM text node.
 * @returns {Boolean} True if the text node contains only the {@link module:engine/view/filler~INLINE_FILLER inline filler}.
 */
function isInlineFiller(domText) {
    return domText.data.length == INLINE_FILLER_LENGTH && startsWithFiller(domText);
}
/**
 * Get string data from the text node, removing an {@link module:engine/view/filler~INLINE_FILLER inline filler} from it,
 * if text node contains it.
 *
 *		getDataWithoutFiller( document.createTextNode( INLINE_FILLER + 'foo' ) ) == 'foo' // true
 *		getDataWithoutFiller( document.createTextNode( 'foo' ) ) == 'foo' // true
 *
 * @param {Text} domText DOM text node, possible with inline filler.
 * @returns {String} Data without filler.
 */
function getDataWithoutFiller(domText) {
    if (startsWithFiller(domText)) {
        return domText.data.slice(INLINE_FILLER_LENGTH);
    }
    else {
        return domText.data;
    }
}
/**
 * Assign key observer which move cursor from the end of the inline filler to the beginning of it when
 * the left arrow is pressed, so the filler does not break navigation.
 *
 * @param {module:engine/view/view~View} view View controller instance we should inject quirks handling on.
 */
function injectQuirksHandling(view) {
    view.document.on('arrowKey', jumpOverInlineFiller, { priority: 'low' });
}
// Move cursor from the end of the inline filler to the beginning of it when, so the filler does not break navigation.
function jumpOverInlineFiller(evt, data) {
    if (data.keyCode == keyCodes.arrowleft) {
        const domSelection = data.domTarget.ownerDocument.defaultView.getSelection();
        if (domSelection.rangeCount == 1 && domSelection.getRangeAt(0).collapsed) {
            const domParent = domSelection.getRangeAt(0).startContainer;
            const domOffset = domSelection.getRangeAt(0).startOffset;
            if (startsWithFiller(domParent) && domOffset <= INLINE_FILLER_LENGTH) {
                domSelection.collapse(domParent, 0);
            }
        }
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Finds positions of the first and last change in the given string/array and generates a set of changes:
 *
 *		fastDiff( '12a', '12xyza' );
 *		// [ { index: 2, type: 'insert', values: [ 'x', 'y', 'z' ] } ]
 *
 *		fastDiff( '12a', '12aa' );
 *		// [ { index: 3, type: 'insert', values: [ 'a' ] } ]
 *
 *		fastDiff( '12xyza', '12a' );
 *		// [ { index: 2, type: 'delete', howMany: 3 } ]
 *
 *		fastDiff( [ '1', '2', 'a', 'a' ], [ '1', '2', 'a' ] );
 *		// [ { index: 3, type: 'delete', howMany: 1 } ]
 *
 *		fastDiff( [ '1', '2', 'a', 'b', 'c', '3' ], [ '2', 'a', 'b' ] );
 *		// [ { index: 0, type: 'insert', values: [ '2', 'a', 'b' ] }, { index: 3, type: 'delete', howMany: 6 } ]
 *
 * Passed arrays can contain any type of data, however to compare them correctly custom comparator function
 * should be passed as a third parameter:
 *
 *		fastDiff( [ { value: 1 }, { value: 2 } ], [ { value: 1 }, { value: 3 } ], ( a, b ) => {
 *			return a.value === b.value;
 *		} );
 *		// [ { index: 1, type: 'insert', values: [ { value: 3 } ] }, { index: 2, type: 'delete', howMany: 1 } ]
 *
 * The resulted set of changes can be applied to the input in order to transform it into the output, for example:
 *
 *		let input = '12abc3';
 *		const output = '2ab';
 *		const changes = fastDiff( input, output );
 *
 *		changes.forEach( change => {
 *			if ( change.type == 'insert' ) {
 *				input = input.substring( 0, change.index ) + change.values.join( '' ) + input.substring( change.index );
 *			} else if ( change.type == 'delete' ) {
 *				input = input.substring( 0, change.index ) + input.substring( change.index + change.howMany );
 *			}
 *		} );
 *
 *		// input equals output now
 *
 * or in case of arrays:
 *
 *		let input = [ '1', '2', 'a', 'b', 'c', '3' ];
 *		const output = [ '2', 'a', 'b' ];
 *		const changes = fastDiff( input, output );
 *
 *		changes.forEach( change => {
 *			if ( change.type == 'insert' ) {
 *				input = input.slice( 0, change.index ).concat( change.values, input.slice( change.index ) );
 *			} else if ( change.type == 'delete' ) {
 *				input = input.slice( 0, change.index ).concat( input.slice( change.index + change.howMany ) );
 *			}
 *		} );
 *
 *		// input equals output now
 *
 * By passing `true` as the fourth parameter (`atomicChanges`) the output of this function will become compatible with
 * the {@link module:utils/diff~diff `diff()`} function:
 *
 *		fastDiff( '12a', '12xyza' );
 *		// [ 'equal', 'equal', 'insert', 'insert', 'insert', 'equal' ]
 *
 * The default output format of this function is compatible with the output format of
 * {@link module:utils/difftochanges~diffToChanges `diffToChanges()`}. The `diffToChanges()` input format is, in turn,
 * compatible with the output of {@link module:utils/diff~diff `diff()`}:
 *
 *		const a = '1234';
 *		const b = '12xyz34';
 *
 *		// Both calls will return the same results (grouped changes format).
 *		fastDiff( a, b );
 *		diffToChanges( diff( a, b ) );
 *
 *		// Again, both calls will return the same results (atomic changes format).
 *		fastDiff( a, b, null, true );
 *		diff( a, b );
 *
 *
 * @param {Array|String} a Input array or string.
 * @param {Array|String} b Input array or string.
 * @param {Function} [cmp] Optional function used to compare array values, by default `===` (strict equal operator) is used.
 * @param {Boolean} [atomicChanges=false] Whether an array of `inset|delete|equal` operations should
 * be returned instead of changes set. This makes this function compatible with {@link module:utils/diff~diff `diff()`}.
 * @returns {Array} Array of changes.
 */
function fastDiff(a, b, cmp, atomicChanges = false) {
    // Set the comparator function.
    cmp = cmp || function (a, b) {
        return a === b;
    };
    // Convert the string (or any array-like object - eg. NodeList) to an array by using the slice() method because,
    // unlike Array.from(), it returns array of UTF-16 code units instead of the code points of a string.
    // One code point might be a surrogate pair of two code units. All text offsets are expected to be in code units.
    // See ckeditor/ckeditor5#3147.
    //
    // We need to make sure here that fastDiff() works identical to diff().
    const arrayA = Array.isArray(a) ? a : Array.prototype.slice.call(a);
    const arrayB = Array.isArray(b) ? b : Array.prototype.slice.call(b);
    // Find first and last change.
    const changeIndexes = findChangeBoundaryIndexes(arrayA, arrayB, cmp);
    // Transform into changes array.
    return atomicChanges ? changeIndexesToAtomicChanges(changeIndexes, arrayB.length) : changeIndexesToChanges(arrayB, changeIndexes);
}
// Finds position of the first and last change in the given arrays. For example:
//
//		const indexes = findChangeBoundaryIndexes( [ '1', '2', '3', '4' ], [ '1', '3', '4', '2', '4' ] );
//		console.log( indexes ); // { firstIndex: 1, lastIndexOld: 3, lastIndexNew: 4 }
//
// The above indexes means that in the first array the modified part is `1[23]4` and in the second array it is `1[342]4`.
// Based on such indexes, array with `insert`/`delete` operations which allows transforming first value into the second one
// can be generated.
//
// @param {Array} arr1
// @param {Array} arr2
// @param {Function} cmp Comparator function.
// @returns {Object}
// @returns {Number} return.firstIndex Index of the first change in both values (always the same for both).
// @returns {Number} result.lastIndexOld Index of the last common value in `arr1`.
// @returns {Number} result.lastIndexNew Index of the last common value in `arr2`.
function findChangeBoundaryIndexes(arr1, arr2, cmp) {
    // Find the first difference between passed values.
    const firstIndex = findFirstDifferenceIndex(arr1, arr2, cmp);
    // If arrays are equal return -1 indexes object.
    if (firstIndex === -1) {
        return { firstIndex: -1, lastIndexOld: -1, lastIndexNew: -1 };
    }
    // Remove the common part of each value and reverse them to make it simpler to find the last difference between them.
    const oldArrayReversed = cutAndReverse(arr1, firstIndex);
    const newArrayReversed = cutAndReverse(arr2, firstIndex);
    // Find the first difference between reversed values.
    // It should be treated as "how many elements from the end the last difference occurred".
    //
    // For example:
    //
    // 				initial	->	after cut	-> reversed:
    // oldValue:	'321ba'	->	'21ba'		-> 'ab12'
    // newValue:	'31xba'	->	'1xba'		-> 'abx1'
    // lastIndex:							-> 2
    //
    // So the last change occurred two characters from the end of the arrays.
    const lastIndex = findFirstDifferenceIndex(oldArrayReversed, newArrayReversed, cmp);
    // Use `lastIndex` to calculate proper offset, starting from the beginning (`lastIndex` kind of starts from the end).
    const lastIndexOld = arr1.length - lastIndex;
    const lastIndexNew = arr2.length - lastIndex;
    return { firstIndex, lastIndexOld, lastIndexNew };
}
// Returns a first index on which given arrays differ. If both arrays are the same, -1 is returned.
//
// @param {Array} arr1
// @param {Array} arr2
// @param {Function} cmp Comparator function.
// @returns {Number}
function findFirstDifferenceIndex(arr1, arr2, cmp) {
    for (let i = 0; i < Math.max(arr1.length, arr2.length); i++) {
        if (arr1[i] === undefined || arr2[i] === undefined || !cmp(arr1[i], arr2[i])) {
            return i;
        }
    }
    return -1; // Return -1 if arrays are equal.
}
// Returns a copy of the given array with `howMany` elements removed starting from the beginning and in reversed order.
//
// @param {Array} arr Array to be processed.
// @param {Number} howMany How many elements from array beginning to remove.
// @returns {Array} Shortened and reversed array.
function cutAndReverse(arr, howMany) {
    return arr.slice(howMany).reverse();
}
// Generates changes array based on change indexes from `findChangeBoundaryIndexes` function. This function will
// generate array with 0 (no changes), 1 (deletion or insertion) or 2 records (insertion and deletion).
//
// @param {Array} newArray New array for which change indexes were calculated.
// @param {Object} changeIndexes Change indexes object from `findChangeBoundaryIndexes` function.
// @returns {Array.<module:utils/difftochanges~Change>} Array of changes compatible with
// {@link module:utils/difftochanges~diffToChanges} format.
function changeIndexesToChanges(newArray, changeIndexes) {
    const result = [];
    const { firstIndex, lastIndexOld, lastIndexNew } = changeIndexes;
    // Order operations as 'insert', 'delete' array to keep compatibility with {@link module:utils/difftochanges~diffToChanges}
    // in most cases. However, 'diffToChanges' does not stick to any order so in some cases
    // (for example replacing '12345' with 'abcd') it will generate 'delete', 'insert' order.
    if (lastIndexNew - firstIndex > 0) {
        result.push({
            index: firstIndex,
            type: 'insert',
            values: newArray.slice(firstIndex, lastIndexNew)
        });
    }
    if (lastIndexOld - firstIndex > 0) {
        result.push({
            index: firstIndex + (lastIndexNew - firstIndex),
            type: 'delete',
            howMany: lastIndexOld - firstIndex
        });
    }
    return result;
}
// Generates array with set `equal|insert|delete` operations based on change indexes from `findChangeBoundaryIndexes` function.
//
// @param {Object} changeIndexes Change indexes object from `findChangeBoundaryIndexes` function.
// @param {Number} newLength Length of the new array on which `findChangeBoundaryIndexes` calculated change indexes.
// @returns {Array.<module:utils/diff~DiffResult>} Array of changes compatible with {@link module:utils/diff~diff} format.
function changeIndexesToAtomicChanges(changeIndexes, newLength) {
    const { firstIndex, lastIndexOld, lastIndexNew } = changeIndexes;
    // No changes.
    if (firstIndex === -1) {
        return Array(newLength).fill('equal');
    }
    let result = [];
    if (firstIndex > 0) {
        result = result.concat(Array(firstIndex).fill('equal'));
    }
    if (lastIndexNew - firstIndex > 0) {
        result = result.concat(Array(lastIndexNew - firstIndex).fill('insert'));
    }
    if (lastIndexOld - firstIndex > 0) {
        result = result.concat(Array(lastIndexOld - firstIndex).fill('delete'));
    }
    if (lastIndexNew < newLength) {
        result = result.concat(Array(newLength - lastIndexNew).fill('equal'));
    }
    return result;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// The following code is based on the "O(NP) Sequence Comparison Algorithm"
// by Sun Wu, Udi Manber, Gene Myers, Webb Miller.
/**
 * Calculates the difference between two arrays or strings producing an array containing a list of changes
 * necessary to transform input into output.
 *
 *		diff( 'aba', 'acca' ); // [ 'equal', 'insert', 'insert', 'delete', 'equal' ]
 *
 * This function is based on the "O(NP) Sequence Comparison Algorithm" by Sun Wu, Udi Manber, Gene Myers, Webb Miller.
 * Unfortunately, while it gives the most precise results, its to complex for longer strings/arrow (above 200 items).
 * Therefore, `diff()` automatically switches to {@link module:utils/fastdiff~fastDiff `fastDiff()`} when detecting
 * such a scenario. The return formats of both functions are identical.
 *
 * @param {Array|String} a Input array or string.
 * @param {Array|String} b Output array or string.
 * @param {Function} [cmp] Optional function used to compare array values, by default === is used.
 * @returns {Array.<module:utils/diff~DiffResult>} Array of changes.
 */
function diff(a, b, cmp) {
    // Set the comparator function.
    cmp = cmp || function (a, b) {
        return a === b;
    };
    const aLength = a.length;
    const bLength = b.length;
    // Perform `fastDiff` for longer strings/arrays (see #269).
    if (aLength > 200 || bLength > 200 || aLength + bLength > 300) {
        return diff.fastDiff(a, b, cmp, true);
    }
    // Temporary action type statics.
    let _insert, _delete;
    // Swapped the arrays to use the shorter one as the first one.
    if (bLength < aLength) {
        const tmp = a;
        a = b;
        b = tmp;
        // We swap the action types as well.
        _insert = 'delete';
        _delete = 'insert';
    }
    else {
        _insert = 'insert';
        _delete = 'delete';
    }
    const m = a.length;
    const n = b.length;
    const delta = n - m;
    // Edit scripts, for each diagonal.
    const es = {};
    // Furthest points, the furthest y we can get on each diagonal.
    const fp = {};
    function snake(k) {
        // We use -1 as an alternative below to handle initial values ( instead of filling the fp with -1 first ).
        // Furthest points (y) on the diagonal below k.
        const y1 = (fp[k - 1] !== undefined ? fp[k - 1] : -1) + 1;
        // Furthest points (y) on the diagonal above k.
        const y2 = fp[k + 1] !== undefined ? fp[k + 1] : -1;
        // The way we should go to get further.
        const dir = y1 > y2 ? -1 : 1;
        // Clone previous changes array (if any).
        if (es[k + dir]) {
            es[k] = es[k + dir].slice(0);
        }
        // Create changes array.
        if (!es[k]) {
            es[k] = [];
        }
        // Push the action.
        es[k].push(y1 > y2 ? _insert : _delete);
        // Set the beginning coordinates.
        let y = Math.max(y1, y2);
        let x = y - k;
        // Traverse the diagonal as long as the values match.
        while (x < m && y < n && cmp(a[x], b[y])) {
            x++;
            y++;
            // Push no change action.
            es[k].push('equal');
        }
        return y;
    }
    let p = 0;
    let k;
    // Traverse the graph until we reach the end of the longer string.
    do {
        // Updates furthest points and edit scripts for diagonals below delta.
        for (k = -p; k < delta; k++) {
            fp[k] = snake(k);
        }
        // Updates furthest points and edit scripts for diagonals above delta.
        for (k = delta + p; k > delta; k--) {
            fp[k] = snake(k);
        }
        // Updates furthest point and edit script for the delta diagonal.
        // note that the delta diagonal is the one which goes through the sink (m, n).
        fp[delta] = snake(delta);
        p++;
    } while (fp[delta] !== n);
    // Return the final list of edit changes.
    // We remove the first item that represents the action for the injected nulls.
    return es[delta].slice(1);
}
// Store the API in static property to easily overwrite it in tests.
// Too bad dependency injection does not work in Webpack + ES 6 (const) + Babel.
diff.fastDiff = fastDiff;/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/dom/insertat
 */
/**
 * Inserts node to the parent at given index.
 *
 * @param {Element} parentElement Parent element.
 * @param {Number} index Insertions index.
 * @param {Node} nodeToInsert Node to insert.
 */
function insertAt(parentElement, index, nodeToInsert) {
    parentElement.insertBefore(nodeToInsert, parentElement.childNodes[index] || null);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/dom/remove
 */
/**
 * Removes given node from parent.
 *
 * @param {Node} node Node to remove.
 */
function remove$1(node) {
    const parent = node.parentNode;
    if (parent) {
        parent.removeChild(node);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/* globals Node */
/**
 * @module utils/dom/iscomment
 */
/**
 * Checks whether the object is a native DOM Comment node.
 *
 * @param {*} obj
 * @returns {Boolean}
 */
function isComment(obj) {
    return obj && obj.nodeType === Node.COMMENT_NODE;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/dom/isnode
 */
/**
 * Checks if the object is a native DOM Node.
 *
 * @param {*} obj
 * @returns {Boolean}
 */
function isNode(obj) {
    if (obj) {
        if (obj.defaultView) {
            return obj instanceof obj.defaultView.Document;
        }
        else if (obj.ownerDocument && obj.ownerDocument.defaultView) {
            return obj instanceof obj.ownerDocument.defaultView.Node;
        }
    }
    return false;
}function styleInject(css, ref) {
  if ( ref === void 0 ) ref = {};
  var insertAt = ref.insertAt;

  if (!css || typeof document === 'undefined') { return; }

  var head = document.head || document.getElementsByTagName('head')[0];
  var style = document.createElement('style');
  style.type = 'text/css';

  if (insertAt === 'top') {
    if (head.firstChild) {
      head.insertBefore(style, head.firstChild);
    } else {
      head.appendChild(style);
    }
  } else {
    head.appendChild(style);
  }

  if (style.styleSheet) {
    style.styleSheet.cssText = css;
  } else {
    style.appendChild(document.createTextNode(css));
  }
}var css_248z$W = ".ck.ck-editor__editable span[data-ck-unsafe-element]{display:none}";
styleInject(css_248z$W);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Renderer is responsible for updating the DOM structure and the DOM selection based on
 * the {@link module:engine/view/renderer~Renderer#markToSync information about updated view nodes}.
 * In other words, it renders the view to the DOM.
 *
 * Its main responsibility is to make only the necessary, minimal changes to the DOM. However, unlike in many
 * virtual DOM implementations, the primary reason for doing minimal changes is not the performance but ensuring
 * that native editing features such as text composition, autocompletion, spell checking, selection's x-index are
 * affected as little as possible.
 *
 * Renderer uses {@link module:engine/view/domconverter~DomConverter} to transform view nodes and positions
 * to and from the DOM.
 */
class Renderer extends Observable {
    /**
     * Creates a renderer instance.
     *
     * @param {module:engine/view/domconverter~DomConverter} domConverter Converter instance.
     * @param {module:engine/view/documentselection~DocumentSelection} selection View selection.
     */
    constructor(domConverter, selection) {
        super();
        /**
         * Set of DOM Documents instances.
         *
         * @readonly
         * @member {Set.<Document>}
         */
        this.domDocuments = new Set();
        /**
         * Converter instance.
         *
         * @readonly
         * @member {module:engine/view/domconverter~DomConverter}
         */
        this.domConverter = domConverter;
        /**
         * Set of nodes which attributes changed and may need to be rendered.
         *
         * @readonly
         * @member {Set.<module:engine/view/node~ViewNode>}
         */
        this.markedAttributes = new Set();
        /**
         * Set of elements which child lists changed and may need to be rendered.
         *
         * @readonly
         * @member {Set.<module:engine/view/node~ViewNode>}
         */
        this.markedChildren = new Set();
        /**
         * Set of text nodes which text data changed and may need to be rendered.
         *
         * @readonly
         * @member {Set.<module:engine/view/node~ViewNode>}
         */
        this.markedTexts = new Set();
        /**
         * View selection. Renderer updates DOM selection based on the view selection.
         *
         * @readonly
         * @member {module:engine/view/documentselection~DocumentSelection}
         */
        this.selection = selection;
        /**
         * Indicates if the view document is focused and selection can be rendered. Selection will not be rendered if
         * this is set to `false`.
         *
         * @member {Boolean}
         * @observable
         */
        this.set('isFocused', false);
        /**
         * Indicates whether the user is making a selection in the document (e.g. holding the mouse button and moving the cursor).
         * When they stop selecting, the property goes back to `false`.
         *
         * Note: In some browsers, the renderer will stop rendering the selection and inline fillers while the user is making
         * a selection to avoid glitches in DOM selection
         * (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
         *
         * @member {Boolean}
         * @observable
         */
        this.set('isSelecting', false);
        // Rendering the selection and inline filler manipulation should be postponed in (non-Android) Blink until the user finishes
        // creating the selection in DOM to avoid accidental selection collapsing
        // (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
        // When the user stops selecting, all pending changes should be rendered ASAP, though.
        if (env.isBlink && !env.isAndroid) {
            this.on('change:isSelecting', () => {
                if (!this.isSelecting) {
                    this.render();
                }
            });
        }
        /**
         * The text node in which the inline filler was rendered.
         *
         * @private
         * @member {Text}
         */
        this._inlineFiller = null;
        /**
         * DOM element containing fake selection.
         *
         * @private
         * @type {null|HTMLElement}
         */
        this._fakeSelectionContainer = null;
    }
    /**
     * Marks a view node to be updated in the DOM by {@link #render `render()`}.
     *
     * Note that only view nodes whose parents have corresponding DOM elements need to be marked to be synchronized.
     *
     * @see #markedAttributes
     * @see #markedChildren
     * @see #markedTexts
     *
     * @param {module:engine/view/document~ChangeType} type Type of the change.
     * @param {module:engine/view/node~ViewNode} node ViewNode to be marked.
     */
    markToSync(type, node) {
        if (type === 'text') {
            if (this.domConverter.mapViewToDom(node.parent)) {
                this.markedTexts.add(node);
            }
        }
        else {
            // If the node has no DOM element it is not rendered yet,
            // its children/attributes do not need to be marked to be sync.
            if (!this.domConverter.mapViewToDom(node)) {
                return;
            }
            if (type === 'attributes') {
                this.markedAttributes.add(node);
            }
            else if (type === 'children') {
                this.markedChildren.add(node);
            }
            else {
                /**
                 * Unknown type passed to Renderer.markToSync.
                 *
                 * @error view-renderer-unknown-type
                 */
                throw new CKEditorError('view-renderer-unknown-type', this);
            }
        }
    }
    /**
     * Renders all buffered changes ({@link #markedAttributes}, {@link #markedChildren} and {@link #markedTexts}) and
     * the current view selection (if needed) to the DOM by applying a minimal set of changes to it.
     *
     * Renderer tries not to break the text composition (e.g. IME) and x-index of the selection,
     * so it does as little as it is needed to update the DOM.
     *
     * Renderer also handles {@link module:engine/view/filler fillers}. Especially, it checks if the inline filler is needed
     * at the selection position and adds or removes it. To prevent breaking text composition inline filler will not be
     * removed as long as the selection is in the text node which needed it at first.
     */
    render() {
        let inlineFillerPosition = null;
        const isInlineFillerRenderingPossible = env.isBlink && !env.isAndroid ? !this.isSelecting : true;
        // Refresh mappings.
        for (const element of this.markedChildren) {
            this._updateChildrenMappings(element);
        }
        // Don't manipulate inline fillers while the selection is being made in (non-Android) Blink to prevent accidental
        // DOM selection collapsing
        // (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
        if (isInlineFillerRenderingPossible) {
            // There was inline filler rendered in the DOM but it's not
            // at the selection position any more, so we can remove it
            // (cause even if it's needed, it must be placed in another location).
            if (this._inlineFiller && !this._isSelectionInInlineFiller()) {
                this._removeInlineFiller();
            }
            // If we've got the filler, let's try to guess its position in the view.
            if (this._inlineFiller) {
                inlineFillerPosition = this._getInlineFillerPosition();
            }
            // Otherwise, if it's needed, create it at the selection position.
            else if (this._needsInlineFillerAtSelection()) {
                inlineFillerPosition = this.selection.getFirstPosition();
                // Do not use `markToSync` so it will be added even if the parent is already added.
                this.markedChildren.add(inlineFillerPosition.parent);
            }
        }
        // Make sure the inline filler has any parent, so it can be mapped to view position by DomConverter.
        else if (this._inlineFiller && this._inlineFiller.parentNode) {
            // While the user is making selection, preserve the inline filler at its original position.
            inlineFillerPosition = this.domConverter.domPositionToView(this._inlineFiller);
            // While down-casting the document selection attributes, all existing empty
            // attribute elements (for selection position) are removed from the view and DOM,
            // so make sure that we were able to map filler position.
            // https://github.com/ckeditor/ckeditor5/issues/12026
            if (inlineFillerPosition && inlineFillerPosition.parent.is('$text')) {
                // The inline filler position is expected to be before the text node.
                inlineFillerPosition = Position$1._createBefore(inlineFillerPosition.parent);
            }
        }
        for (const element of this.markedAttributes) {
            this._updateAttrs(element);
        }
        for (const element of this.markedChildren) {
            this._updateChildren(element, { inlineFillerPosition });
        }
        for (const node of this.markedTexts) {
            if (!this.markedChildren.has(node.parent) && this.domConverter.mapViewToDom(node.parent)) {
                this._updateText(node, { inlineFillerPosition });
            }
        }
        // * Check whether the inline filler is required and where it really is in the DOM.
        //   At this point in most cases it will be in the DOM, but there are exceptions.
        //   For example, if the inline filler was deep in the created DOM structure, it will not be created.
        //   Similarly, if it was removed at the beginning of this function and then neither text nor children were updated,
        //   it will not be present. Fix those and similar scenarios.
        // * Don't manipulate inline fillers while the selection is being made in (non-Android) Blink to prevent accidental
        //   DOM selection collapsing
        //   (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
        if (isInlineFillerRenderingPossible) {
            if (inlineFillerPosition) {
                const fillerDomPosition = this.domConverter.viewPositionToDom(inlineFillerPosition);
                const domDocument = fillerDomPosition.parent.ownerDocument;
                if (!startsWithFiller(fillerDomPosition.parent)) {
                    // Filler has not been created at filler position. Create it now.
                    this._inlineFiller = addInlineFiller(domDocument, fillerDomPosition.parent, fillerDomPosition.offset);
                }
                else {
                    // Filler has been found, save it.
                    this._inlineFiller = fillerDomPosition.parent;
                }
            }
            else {
                // There is no filler needed.
                this._inlineFiller = null;
            }
        }
        // First focus the new editing host, then update the selection.
        // Otherwise, FF may throw an error (https://github.com/ckeditor/ckeditor5/issues/721).
        this._updateFocus();
        this._updateSelection();
        this.markedTexts.clear();
        this.markedAttributes.clear();
        this.markedChildren.clear();
    }
    /**
     * Updates mappings of view element's children.
     *
     * Children that were replaced in the view structure by similar elements (same tag name) are treated as 'replaced'.
     * This means that their mappings can be updated so the new view elements are mapped to the existing DOM elements.
     * Thanks to that these elements do not need to be re-rendered completely.
     *
     * @private
     * @param {module:engine/view/node~ViewNode} viewElement The view element whose children mappings will be updated.
     */
    _updateChildrenMappings(viewElement) {
        const domElement = this.domConverter.mapViewToDom(viewElement);
        if (!domElement) {
            // If there is no `domElement` it means that it was already removed from DOM and there is no need to process it.
            return;
        }
        // Removing nodes from the DOM as we iterate can cause `actualDomChildren`
        // (which is a live-updating `NodeList`) to get out of sync with the
        // indices that we compute as we iterate over `actions`.
        // This would produce incorrect element mappings.
        //
        // Converting live list to an array to make the list static.
        const actualDomChildren = Array.from(this.domConverter.mapViewToDom(viewElement).childNodes);
        const expectedDomChildren = Array.from(this.domConverter.viewChildrenToDom(viewElement, { withChildren: false }));
        const diff = this._diffNodeLists(actualDomChildren, expectedDomChildren);
        const actions = this._findReplaceActions(diff, actualDomChildren, expectedDomChildren);
        if (actions.indexOf('replace') !== -1) {
            const counter = { equal: 0, insert: 0, delete: 0 };
            for (const action of actions) {
                if (action === 'replace') {
                    const insertIndex = counter.equal + counter.insert;
                    const deleteIndex = counter.equal + counter.delete;
                    const viewChild = viewElement.getChild(insertIndex);
                    // UIElement and RawElement are special cases. Their children are not stored in a view (#799)
                    // so we cannot use them with replacing flow (since they use view children during rendering
                    // which will always result in rendering empty elements).
                    if (viewChild && !(viewChild.is('uiElement') || viewChild.is('rawElement'))) {
                        this._updateElementMappings(viewChild, actualDomChildren[deleteIndex]);
                    }
                    remove$1(expectedDomChildren[insertIndex]);
                    counter.equal++;
                }
                else {
                    counter[action]++;
                }
            }
        }
    }
    /**
     * Updates mappings of a given view element.
     *
     * @private
     * @param {module:engine/view/node~ViewNode} viewElement The view element whose mappings will be updated.
     * @param {ViewNode} domElement The DOM element representing the given view element.
     */
    _updateElementMappings(viewElement, domElement) {
        // Remap 'DomConverter' bindings.
        this.domConverter.unbindDomElement(domElement);
        this.domConverter.bindElements(domElement, viewElement);
        // View element may have children which needs to be updated, but are not marked, mark them to update.
        this.markedChildren.add(viewElement);
        // Because we replace new view element mapping with the existing one, the corresponding DOM element
        // will not be rerendered. The new view element may have different attributes than the previous one.
        // Since its corresponding DOM element will not be rerendered, new attributes will not be added
        // to the DOM, so we need to mark it here to make sure its attributes gets updated. See #1427 for more
        // detailed case study.
        // Also there are cases where replaced element is removed from the view structure and then has
        // its attributes changed or removed. In such cases the element will not be present in `markedAttributes`
        // and also may be the same (`element.isSimilar()`) as the reused element not having its attributes updated.
        // To prevent such situations we always mark reused element to have its attributes rerenderd (#1560).
        this.markedAttributes.add(viewElement);
    }
    /**
     * Gets the position of the inline filler based on the current selection.
     * Here, we assume that we know that the filler is needed and
     * {@link #_isSelectionInInlineFiller is at the selection position}, and, since it is needed,
     * it is somewhere at the selection position.
     *
     * Note: The filler position cannot be restored based on the filler's DOM text node, because
     * when this method is called (before rendering), the bindings will often be broken. View-to-DOM
     * bindings are only dependable after rendering.
     *
     * @private
     * @returns {module:engine/view/position~Position}
     */
    _getInlineFillerPosition() {
        const firstPos = this.selection.getFirstPosition();
        if (firstPos.parent.is('$text')) {
            return Position$1._createBefore(firstPos.parent);
        }
        else {
            return firstPos;
        }
    }
    /**
     * Returns `true` if the selection has not left the inline filler's text node.
     * If it is `true`, it means that the filler had been added for a reason and the selection did not
     * leave the filler's text node. For example, the user can be in the middle of a composition so it should not be touched.
     *
     * @private
     * @returns {Boolean} `true` if the inline filler and selection are in the same place.
     */
    _isSelectionInInlineFiller() {
        if (this.selection.rangeCount != 1 || !this.selection.isCollapsed) {
            return false;
        }
        // Note, we can't check if selection's position equals position of the
        // this._inlineFiller node, because of #663. We may not be able to calculate
        // the filler's position in the view at this stage.
        // Instead, we check it the other way – whether selection is anchored in
        // that text node or next to it.
        // Possible options are:
        // "FILLER{}"
        // "FILLERadded-text{}"
        const selectionPosition = this.selection.getFirstPosition();
        const position = this.domConverter.viewPositionToDom(selectionPosition);
        if (position && isText(position.parent) && startsWithFiller(position.parent)) {
            return true;
        }
        return false;
    }
    /**
     * Removes the inline filler.
     *
     * @private
     */
    _removeInlineFiller() {
        const domFillerNode = this._inlineFiller;
        // Something weird happened and the stored node doesn't contain the filler's text.
        if (!startsWithFiller(domFillerNode)) {
            /**
             * The inline filler node was lost. Most likely, something overwrote the filler text node
             * in the DOM.
             *
             * @error view-renderer-filler-was-lost
             */
            throw new CKEditorError('view-renderer-filler-was-lost', this);
        }
        if (isInlineFiller(domFillerNode)) {
            domFillerNode.remove();
        }
        else {
            domFillerNode.data = domFillerNode.data.substr(INLINE_FILLER_LENGTH);
        }
        this._inlineFiller = null;
    }
    /**
     * Checks if the inline {@link module:engine/view/filler filler} should be added.
     *
     * @private
     * @returns {Boolean} `true` if the inline filler should be added.
     */
    _needsInlineFillerAtSelection() {
        if (this.selection.rangeCount != 1 || !this.selection.isCollapsed) {
            return false;
        }
        const selectionPosition = this.selection.getFirstPosition();
        const selectionParent = selectionPosition.parent;
        const selectionOffset = selectionPosition.offset;
        // If there is no DOM root we do not care about fillers.
        if (!this.domConverter.mapViewToDom(selectionParent.root)) {
            return false;
        }
        if (!(selectionParent.is('element'))) {
            return false;
        }
        // Prevent adding inline filler inside elements with contenteditable=false.
        // https://github.com/ckeditor/ckeditor5-engine/issues/1170
        if (!isEditable(selectionParent)) {
            return false;
        }
        // We have block filler, we do not need inline one.
        if (selectionOffset === selectionParent.getFillerOffset()) {
            return false;
        }
        const nodeBefore = selectionPosition.nodeBefore;
        const nodeAfter = selectionPosition.nodeAfter;
        if (nodeBefore instanceof Text$2 || nodeAfter instanceof Text$2) {
            return false;
        }
        return true;
    }
    /**
     * Checks if text needs to be updated and possibly updates it.
     *
     * @private
     * @param {module:engine/view/text~Text} viewText View text to update.
     * @param {Object} options
     * @param {module:engine/view/position~Position} options.inlineFillerPosition The position where the inline
     * filler should be rendered.
     */
    _updateText(viewText, options) {
        const domText = this.domConverter.findCorrespondingDomText(viewText);
        const newDomText = this.domConverter.viewToDom(viewText);
        const actualText = domText.data;
        let expectedText = newDomText.data;
        const filler = options.inlineFillerPosition;
        if (filler && filler.parent == viewText.parent && filler.offset == viewText.index) {
            expectedText = INLINE_FILLER + expectedText;
        }
        if (actualText != expectedText) {
            const actions = fastDiff(actualText, expectedText);
            for (const action of actions) {
                if (action.type === 'insert') {
                    domText.insertData(action.index, action.values.join(''));
                }
                else { // 'delete'
                    domText.deleteData(action.index, action.howMany);
                }
            }
        }
    }
    /**
     * Checks if attribute list needs to be updated and possibly updates it.
     *
     * @private
     * @param {module:engine/view/element~Element} viewElement The view element to update.
     */
    _updateAttrs(viewElement) {
        const domElement = this.domConverter.mapViewToDom(viewElement);
        if (!domElement) {
            // If there is no `domElement` it means that 'viewElement' is outdated as its mapping was updated
            // in 'this._updateChildrenMappings()'. There is no need to process it as new view element which
            // replaced old 'viewElement' mapping was also added to 'this.markedAttributes'
            // in 'this._updateChildrenMappings()' so it will be processed separately.
            return;
        }
        const domAttrKeys = Array.from(domElement.attributes).map(attr => attr.name);
        const viewAttrKeys = viewElement.getAttributeKeys();
        // Add or overwrite attributes.
        for (const key of viewAttrKeys) {
            this.domConverter.setDomElementAttribute(domElement, key, viewElement.getAttribute(key), viewElement);
        }
        // Remove from DOM attributes which do not exists in the view.
        for (const key of domAttrKeys) {
            // All other attributes not present in the DOM should be removed.
            if (!viewElement.hasAttribute(key)) {
                this.domConverter.removeDomElementAttribute(domElement, key);
            }
        }
    }
    /**
     * Checks if elements child list needs to be updated and possibly updates it.
     *
     * @private
     * @param {module:engine/view/element~Element} viewElement View element to update.
     * @param {Object} options
     * @param {module:engine/view/position~Position} options.inlineFillerPosition The position where the inline
     * filler should be rendered.
     */
    _updateChildren(viewElement, options) {
        const domElement = this.domConverter.mapViewToDom(viewElement);
        if (!domElement) {
            // If there is no `domElement` it means that it was already removed from DOM.
            // There is no need to process it. It will be processed when re-inserted.
            return;
        }
        const inlineFillerPosition = options.inlineFillerPosition;
        const actualDomChildren = this.domConverter.mapViewToDom(viewElement).childNodes;
        const expectedDomChildren = Array.from(this.domConverter.viewChildrenToDom(viewElement, { bind: true }));
        // Inline filler element has to be created as it is present in the DOM, but not in the view. It is required
        // during diffing so text nodes could be compared correctly and also during rendering to maintain
        // proper order and indexes while updating the DOM.
        if (inlineFillerPosition && inlineFillerPosition.parent === viewElement) {
            addInlineFiller(domElement.ownerDocument, expectedDomChildren, inlineFillerPosition.offset);
        }
        const diff = this._diffNodeLists(actualDomChildren, expectedDomChildren);
        let i = 0;
        const nodesToUnbind = new Set();
        // Handle deletions first.
        // This is to prevent a situation where an element that already exists in `actualDomChildren` is inserted at a different
        // index in `actualDomChildren`. Since `actualDomChildren` is a `NodeList`, this works like move, not like an insert,
        // and it disrupts the whole algorithm. See https://github.com/ckeditor/ckeditor5/issues/6367.
        //
        // It doesn't matter in what order we remove or add nodes, as long as we remove and add correct nodes at correct indexes.
        for (const action of diff) {
            if (action === 'delete') {
                nodesToUnbind.add(actualDomChildren[i]);
                remove$1(actualDomChildren[i]);
            }
            else if (action === 'equal') {
                i++;
            }
        }
        i = 0;
        for (const action of diff) {
            if (action === 'insert') {
                insertAt(domElement, i, expectedDomChildren[i]);
                i++;
            }
            else if (action === 'equal') {
                // Force updating text nodes inside elements which did not change and do not need to be re-rendered (#1125).
                // Do it here (not in the loop above) because only after insertions the `i` index is correct.
                this._markDescendantTextToSync(this.domConverter.domToView(expectedDomChildren[i]));
                i++;
            }
        }
        // Unbind removed nodes. When node does not have a parent it means that it was removed from DOM tree during
        // comparison with the expected DOM. We don't need to check child nodes, because if child node was reinserted,
        // it was moved to DOM tree out of the removed node.
        for (const node of nodesToUnbind) {
            if (!node.parentNode) {
                this.domConverter.unbindDomElement(node);
            }
        }
    }
    /**
     * Shorthand for diffing two arrays or node lists of DOM nodes.
     *
     * @private
     * @param {Array.<ViewNode>|NodeList} actualDomChildren Actual DOM children
     * @param {Array.<ViewNode>|NodeList} expectedDomChildren Expected DOM children.
     * @returns {Array.<String>} The list of actions based on the {@link module:utils/diff~diff} function.
     */
    _diffNodeLists(actualDomChildren, expectedDomChildren) {
        actualDomChildren = filterOutFakeSelectionContainer(actualDomChildren, this._fakeSelectionContainer);
        return diff(actualDomChildren, expectedDomChildren, sameNodes.bind(null, this.domConverter));
    }
    /**
     * Finds DOM nodes that were replaced with the similar nodes (same tag name) in the view. All nodes are compared
     * within one `insert`/`delete` action group, for example:
     *
     * 		Actual DOM:		<p><b>Foo</b>Bar<i>Baz</i><b>Bax</b></p>
     * 		Expected DOM:	<p>Bar<b>123</b><i>Baz</i><b>456</b></p>
     * 		Input actions:	[ insert, insert, delete, delete, equal, insert, delete ]
     * 		Output actions:	[ insert, replace, delete, equal, replace ]
     *
     * @private
     * @param {Array.<String>} actions Actions array which is a result of the {@link module:utils/diff~diff} function.
     * @param {Array.<ViewNode>|NodeList} actualDom Actual DOM children
     * @param {Array.<ViewNode>} expectedDom Expected DOM children.
     * @returns {Array.<String>} Actions array modified with the `replace` actions.
     */
    _findReplaceActions(actions, actualDom, expectedDom) {
        // If there is no both 'insert' and 'delete' actions, no need to check for replaced elements.
        if (actions.indexOf('insert') === -1 || actions.indexOf('delete') === -1) {
            return actions;
        }
        let newActions = [];
        let actualSlice = [];
        let expectedSlice = [];
        const counter = { equal: 0, insert: 0, delete: 0 };
        for (const action of actions) {
            if (action === 'insert') {
                expectedSlice.push(expectedDom[counter.equal + counter.insert]);
            }
            else if (action === 'delete') {
                actualSlice.push(actualDom[counter.equal + counter.delete]);
            }
            else { // equal
                newActions = newActions.concat(diff(actualSlice, expectedSlice, areSimilar).map(x => x === 'equal' ? 'replace' : x));
                newActions.push('equal');
                // Reset stored elements on 'equal'.
                actualSlice = [];
                expectedSlice = [];
            }
            counter[action]++;
        }
        return newActions.concat(diff(actualSlice, expectedSlice, areSimilar).map(x => x === 'equal' ? 'replace' : x));
    }
    /**
     * Marks text nodes to be synchronized.
     *
     * If a text node is passed, it will be marked. If an element is passed, all descendant text nodes inside it will be marked.
     *
     * @private
     * @param {module:engine/view/node~ViewNode} viewNode View node to sync.
     */
    _markDescendantTextToSync(viewNode) {
        if (!viewNode) {
            return;
        }
        if (viewNode.is('$text')) {
            this.markedTexts.add(viewNode);
        }
        else if (viewNode.is('element')) {
            for (const child of viewNode.getChildren()) {
                this._markDescendantTextToSync(child);
            }
        }
    }
    /**
     * Checks if the selection needs to be updated and possibly updates it.
     *
     * @private
     */
    _updateSelection() {
        // Block updating DOM selection in (non-Android) Blink while the user is selecting to prevent accidental selection collapsing.
        // Note: Structural changes in DOM must trigger selection rendering, though. Nodes the selection was anchored
        // to, may disappear in DOM which would break the selection (e.g. in real-time collaboration scenarios).
        // https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723
        if (env.isBlink && !env.isAndroid && this.isSelecting && !this.markedChildren.size) {
            return;
        }
        // If there is no selection - remove DOM and fake selections.
        if (this.selection.rangeCount === 0) {
            this._removeDomSelection();
            this._removeFakeSelection();
            return;
        }
        const domRoot = this.domConverter.mapViewToDom(this.selection.editableElement);
        // Do nothing if there is no focus, or there is no DOM element corresponding to selection's editable element.
        if (!this.isFocused || !domRoot) {
            return;
        }
        // Render selection.
        if (this.selection.isFake) {
            this._updateFakeSelection(domRoot);
        }
        else {
            this._removeFakeSelection();
            this._updateDomSelection(domRoot);
        }
    }
    /**
     * Updates the fake selection.
     *
     * @private
     * @param {HTMLElement} domRoot A valid DOM root where the fake selection container should be added.
     */
    _updateFakeSelection(domRoot) {
        const domDocument = domRoot.ownerDocument;
        if (!this._fakeSelectionContainer) {
            this._fakeSelectionContainer = createFakeSelectionContainer(domDocument);
        }
        const container = this._fakeSelectionContainer;
        // Bind fake selection container with the current selection *position*.
        this.domConverter.bindFakeSelection(container, this.selection);
        if (!this._fakeSelectionNeedsUpdate(domRoot)) {
            return;
        }
        if (!container.parentElement || container.parentElement != domRoot) {
            domRoot.appendChild(container);
        }
        container.textContent = this.selection.fakeSelectionLabel || '\u00A0';
        const domSelection = domDocument.getSelection();
        const domRange = domDocument.createRange();
        domSelection.removeAllRanges();
        domRange.selectNodeContents(container);
        domSelection.addRange(domRange);
    }
    /**
     * Updates the DOM selection.
     *
     * @private
     * @param {HTMLElement} domRoot A valid DOM root where the DOM selection should be rendered.
     */
    _updateDomSelection(domRoot) {
        const domSelection = domRoot.ownerDocument.defaultView.getSelection();
        // Let's check whether DOM selection needs updating at all.
        if (!this._domSelectionNeedsUpdate(domSelection)) {
            return;
        }
        // Multi-range selection is not available in most browsers, and, at least in Chrome, trying to
        // set such selection, that is not continuous, throws an error. Because of that, we will just use anchor
        // and focus of view selection.
        // Since we are not supporting multi-range selection, we also do not need to check if proper editable is
        // selected. If there is any editable selected, it is okay (editable is taken from selection anchor).
        const anchor = this.domConverter.viewPositionToDom(this.selection.anchor);
        const focus = this.domConverter.viewPositionToDom(this.selection.focus);
        domSelection.collapse(anchor.parent, anchor.offset);
        domSelection.extend(focus.parent, focus.offset);
        // Firefox–specific hack (https://github.com/ckeditor/ckeditor5-engine/issues/1439).
        if (env.isGecko) {
            fixGeckoSelectionAfterBr(focus, domSelection);
        }
    }
    /**
     * Checks whether a given DOM selection needs to be updated.
     *
     * @private
     * @param {Selection} domSelection The DOM selection to check.
     * @returns {Boolean}
     */
    _domSelectionNeedsUpdate(domSelection) {
        if (!this.domConverter.isDomSelectionCorrect(domSelection)) {
            // Current DOM selection is in incorrect position. We need to update it.
            return true;
        }
        const oldViewSelection = domSelection && this.domConverter.domSelectionToView(domSelection);
        if (oldViewSelection && this.selection.isEqual(oldViewSelection)) {
            return false;
        }
        // If selection is not collapsed, it does not need to be updated if it is similar.
        if (!this.selection.isCollapsed && this.selection.isSimilar(oldViewSelection)) {
            // Selection did not changed and is correct, do not update.
            return false;
        }
        // Selections are not similar.
        return true;
    }
    /**
     * Checks whether the fake selection needs to be updated.
     *
     * @private
     * @param {HTMLElement} domRoot A valid DOM root where a new fake selection container should be added.
     * @returns {Boolean}
     */
    _fakeSelectionNeedsUpdate(domRoot) {
        const container = this._fakeSelectionContainer;
        const domSelection = domRoot.ownerDocument.getSelection();
        // Fake selection needs to be updated if there's no fake selection container, or the container currently sits
        // in a different root.
        if (!container || container.parentElement !== domRoot) {
            return true;
        }
        // Make sure that the selection actually is within the fake selection.
        if (domSelection.anchorNode !== container && !container.contains(domSelection.anchorNode)) {
            return true;
        }
        return container.textContent !== this.selection.fakeSelectionLabel;
    }
    /**
     * Removes the DOM selection.
     *
     * @private
     */
    _removeDomSelection() {
        for (const doc of this.domDocuments) {
            const domSelection = doc.getSelection();
            if (domSelection.rangeCount) {
                const activeDomElement = doc.activeElement;
                const viewElement = this.domConverter.mapDomToView(activeDomElement);
                if (activeDomElement && viewElement) {
                    domSelection.removeAllRanges();
                }
            }
        }
    }
    /**
     * Removes the fake selection.
     *
     * @private
     */
    _removeFakeSelection() {
        const container = this._fakeSelectionContainer;
        if (container) {
            container.remove();
        }
    }
    /**
     * Checks if focus needs to be updated and possibly updates it.
     *
     * @private
     */
    _updateFocus() {
        if (this.isFocused) {
            const editable = this.selection.editableElement;
            if (editable) {
                this.domConverter.focus(editable);
            }
        }
    }
}
// Checks if provided element is editable.
//
// @private
// @param {module:engine/view/element~Element} element
// @returns {Boolean}
function isEditable(element) {
    if (element.getAttribute('contenteditable') == 'false') {
        return false;
    }
    const parent = element.findAncestor(element => element.hasAttribute('contenteditable'));
    return !parent || parent.getAttribute('contenteditable') == 'true';
}
// Adds inline filler at a given position.
//
// The position can be given as an array of DOM nodes and an offset in that array,
// or a DOM parent element and an offset in that element.
//
// @private
// @param {Document} domDocument
// @param {Element|Array.<ViewNode>} domParentOrArray
// @param {Number} offset
// @returns {Text} The DOM text node that contains an inline filler.
function addInlineFiller(domDocument, domParentOrArray, offset) {
    const childNodes = domParentOrArray instanceof Array ? domParentOrArray : domParentOrArray.childNodes;
    const nodeAfterFiller = childNodes[offset];
    if (isText(nodeAfterFiller)) {
        nodeAfterFiller.data = INLINE_FILLER + nodeAfterFiller.data;
        return nodeAfterFiller;
    }
    else {
        const fillerNode = domDocument.createTextNode(INLINE_FILLER);
        if (Array.isArray(domParentOrArray)) {
            childNodes.splice(offset, 0, fillerNode);
        }
        else {
            insertAt(domParentOrArray, offset, fillerNode);
        }
        return fillerNode;
    }
}
// Whether two DOM nodes should be considered as similar.
// Nodes are considered similar if they have the same tag name.
//
// @private
// @param {ViewNode} node1
// @param {ViewNode} node2
// @returns {Boolean}
function areSimilar(node1, node2) {
    return isNode(node1) && isNode(node2) &&
        !isText(node1) && !isText(node2) &&
        !isComment(node1) && !isComment(node2) &&
        node1.tagName.toLowerCase() === node2.tagName.toLowerCase();
}
// Whether two dom nodes should be considered as the same.
// Two nodes which are considered the same are:
//
//		* Text nodes with the same text.
//		* Element nodes represented by the same object.
//		* Two block filler elements.
//
// @private
// @param {String} blockFillerMode Block filler mode, see {@link module:engine/view/domconverter~DomConverter#blockFillerMode}.
// @param {ViewNode} node1
// @param {ViewNode} node2
// @returns {Boolean}
function sameNodes(domConverter, actualDomChild, expectedDomChild) {
    // Elements.
    if (actualDomChild === expectedDomChild) {
        return true;
    }
    // Texts.
    else if (isText(actualDomChild) && isText(expectedDomChild)) {
        return actualDomChild.data === expectedDomChild.data;
    }
    // Block fillers.
    else if (domConverter.isBlockFiller(actualDomChild) &&
        domConverter.isBlockFiller(expectedDomChild)) {
        return true;
    }
    // Not matching types.
    return false;
}
// The following is a Firefox–specific hack (https://github.com/ckeditor/ckeditor5-engine/issues/1439).
// When the native DOM selection is at the end of the block and preceded by <br /> e.g.
//
//		<p>foo<br/>[]</p>
//
// which happens a lot when using the soft line break, the browser fails to (visually) move the
// caret to the new line. A quick fix is as simple as force–refreshing the selection with the same range.
function fixGeckoSelectionAfterBr(focus, domSelection) {
    const parent = focus.parent;
    // This fix works only when the focus point is at the very end of an element.
    // There is no point in running it in cases unrelated to the browser bug.
    if (parent.nodeType != Node.ELEMENT_NODE || focus.offset != parent.childNodes.length - 1) {
        return;
    }
    const childAtOffset = parent.childNodes[focus.offset];
    // To stay on the safe side, the fix being as specific as possible, it targets only the
    // selection which is at the very end of the element and preceded by <br />.
    if (childAtOffset && childAtOffset.tagName == 'BR') {
        domSelection.addRange(domSelection.getRangeAt(0));
    }
}
function filterOutFakeSelectionContainer(domChildList, fakeSelectionContainer) {
    const childList = Array.from(domChildList);
    if (childList.length == 0 || !fakeSelectionContainer) {
        return childList;
    }
    const last = childList[childList.length - 1];
    if (last == fakeSelectionContainer) {
        childList.pop();
    }
    return childList;
}
// Creates a fake selection container for a given document.
//
// @private
// @param {Document} domDocument
// @returns {HTMLElement}
function createFakeSelectionContainer(domDocument) {
    const container = domDocument.createElement('div');
    container.className = 'ck-fake-selection-container';
    Object.assign(container.style, {
        position: 'fixed',
        top: 0,
        left: '-9999px',
        // See https://github.com/ckeditor/ckeditor5/issues/752.
        width: '42px'
    });
    // Fill it with a text node so we can update it later.
    container.textContent = '\u00A0';
    return container;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/dom/indexof
 */
/**
 * Returns index of the node in the parent element.
 *
 * @param {Node} node Node which index is tested.
 * @returns {Number} Index of the node in the parent element. Returns 0 if node has no parent.
 */
function indexOf(node) {
    let index = 0;
    while (node.previousSibling) {
        node = node.previousSibling;
        index++;
    }
    return index;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/* globals Node */
/**
 * @module utils/dom/getancestors
 */
/**
 * Returns all ancestors of given DOM node, starting from the top-most (root). Includes the given node itself. If the
 * node is a part of `DocumentFragment` that `DocumentFragment` will be returned. In contrary, if the node is
 * appended to a `Document`, that `Document` will not be returned (algorithms operating on DOM tree care for `Document#documentElement`
 * at most, which will be returned).
 *
 * @param {Node} node DOM node.
 * @returns {Array.<Node|DocumentFragment>} Array of given `node` parents.
 */
function getAncestors(node) {
    const nodes = [];
    let currentNode = node;
    // We are interested in `Node`s `DocumentFragment`s only.
    while (currentNode && currentNode.nodeType != Node.DOCUMENT_NODE) {
        nodes.unshift(currentNode);
        currentNode = currentNode.parentNode;
    }
    return nodes;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const BR_FILLER_REF = BR_FILLER(global$2.document); // eslint-disable-line new-cap
const NBSP_FILLER_REF = NBSP_FILLER(global$2.document); // eslint-disable-line new-cap
const MARKED_NBSP_FILLER_REF = MARKED_NBSP_FILLER(global$2.document); // eslint-disable-line new-cap
const UNSAFE_ATTRIBUTE_NAME_PREFIX = 'data-ck-unsafe-attribute-';
const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
/**
 * `DomConverter` is a set of tools to do transformations between DOM nodes and view nodes. It also handles
 * {@link module:engine/view/domconverter~DomConverter#bindElements bindings} between these nodes.
 *
 * An instance of the DOM converter is available under
 * {@link module:engine/view/view~View#domConverter `editor.editing.view.domConverter`}.
 *
 * The DOM converter does not check which nodes should be rendered (use {@link module:engine/view/renderer~Renderer}), does not keep the
 * state of a tree nor keeps the synchronization between the tree view and the DOM tree (use {@link module:engine/view/document~Document}).
 *
 * The DOM converter keeps DOM elements to view element bindings, so when the converter gets destroyed, the bindings are lost.
 * Two converters will keep separate binding maps, so one tree view can be bound with two DOM trees.
 */
class DomConverter {
    /**
     * Creates a DOM converter.
     *
     * @param {module:engine/view/document~Document} document The view document instance.
     * @param {Object} options An object with configuration options.
     * @param {module:engine/view/filler~BlockFillerMode} [options.blockFillerMode] The type of the block filler to use.
     * Default value depends on the options.renderingMode:
     *  'nbsp' when options.renderingMode == 'data',
     *  'br' when options.renderingMode == 'editing'.
     * @param {'data'|'editing'} [options.renderingMode='editing'] Whether to leave the View-to-DOM conversion result unchanged
     * or improve editing experience by filtering out interactive data.
     */
    constructor(document, options = {}) {
        /**
         * @readonly
         * @type {module:engine/view/document~Document}
         */
        this.document = document;
        /**
         * Whether to leave the View-to-DOM conversion result unchanged or improve editing experience by filtering out interactive data.
         *
         * @member {'data'|'editing'} module:engine/view/domconverter~DomConverter#renderingMode
         */
        this.renderingMode = options.renderingMode || 'editing';
        /**
         * The mode of a block filler used by the DOM converter.
         *
         * @member {'br'|'nbsp'|'markedNbsp'} module:engine/view/domconverter~DomConverter#blockFillerMode
         */
        this.blockFillerMode = options.blockFillerMode || (this.renderingMode === 'editing' ? 'br' : 'nbsp');
        /**
         * Elements which are considered pre-formatted elements.
         *
         * @readonly
         * @member {Array.<String>} module:engine/view/domconverter~DomConverter#preElements
         */
        this.preElements = ['pre'];
        /**
         * Elements which are considered block elements (and hence should be filled with a
         * {@link #isBlockFiller block filler}).
         *
         * Whether an element is considered a block element also affects handling of trailing whitespaces.
         *
         * You can extend this array if you introduce support for block elements which are not yet recognized here.
         *
         * @readonly
         * @member {Array.<String>} module:engine/view/domconverter~DomConverter#blockElements
         */
        this.blockElements = [
            'address', 'article', 'aside', 'blockquote', 'caption', 'center', 'dd', 'details', 'dir', 'div',
            'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header',
            'hgroup', 'legend', 'li', 'main', 'menu', 'nav', 'ol', 'p', 'pre', 'section', 'summary', 'table', 'tbody',
            'td', 'tfoot', 'th', 'thead', 'tr', 'ul'
        ];
        /**
         * A list of elements that exist inline (in text) but their inner structure cannot be edited because
         * of the way they are rendered by the browser. They are mostly HTML form elements but there are other
         * elements such as `<img>` or `<iframe>` that also have non-editable children or no children whatsoever.
         *
         * Whether an element is considered an inline object has an impact on white space rendering (trimming)
         * around (and inside of it). In short, white spaces in text nodes next to inline objects are not trimmed.
         *
         * You can extend this array if you introduce support for inline object elements which are not yet recognized here.
         *
         * @readonly
         * @member {Array.<String>} module:engine/view/domconverter~DomConverter#inlineObjectElements
         */
        this.inlineObjectElements = [
            'object', 'iframe', 'input', 'button', 'textarea', 'select', 'option', 'video', 'embed', 'audio', 'img', 'canvas'
        ];
        /**
         * A list of elements which may affect the editing experience. To avoid this, those elements are replaced with
         * `<span data-ck-unsafe-element="[element name]"></span>` while rendering in the editing mode.
         *
         * @readonly
         * @member {Array.<String>} module:engine/view/domconverter~DomConverter#unsafeElements
         */
        this.unsafeElements = ['script', 'style'];
        /**
         * The DOM Document used to create DOM nodes.
         *
         * @type {Document}
         * @private
         */
        this._domDocument = this.renderingMode === 'editing' ? global$2.document : global$2.document.implementation.createHTMLDocument('');
        /**
         * The DOM-to-view mapping.
         *
         * @private
         * @member {WeakMap} module:engine/view/domconverter~DomConverter#_domToViewMapping
         */
        this._domToViewMapping = new WeakMap();
        /**
         * The view-to-DOM mapping.
         *
         * @private
         * @member {WeakMap} module:engine/view/domconverter~DomConverter#_viewToDomMapping
         */
        this._viewToDomMapping = new WeakMap();
        /**
         * Holds the mapping between fake selection containers and corresponding view selections.
         *
         * @private
         * @member {WeakMap} module:engine/view/domconverter~DomConverter#_fakeSelectionMapping
         */
        this._fakeSelectionMapping = new WeakMap();
        /**
         * Matcher for view elements whose content should be treated as raw data
         * and not processed during the conversion from DOM nodes to view elements.
         *
         * @private
         * @type {module:engine/view/matcher~Matcher}
         */
        this._rawContentElementMatcher = new Matcher();
        /**
         * A set of encountered raw content DOM nodes. It is used for preventing left trimming of the following text node.
         *
         * @private
         * @type {WeakSet.<Node>}
         */
        this._encounteredRawContentDomNodes = new WeakSet();
    }
    /**
     * Binds a given DOM element that represents fake selection to a **position** of a
     * {@link module:engine/view/documentselection~DocumentSelection document selection}.
     * Document selection copy is stored and can be retrieved by the
     * {@link module:engine/view/domconverter~DomConverter#fakeSelectionToView} method.
     *
     * @param {HTMLElement} domElement
     * @param {module:engine/view/documentselection~DocumentSelection} viewDocumentSelection
     */
    bindFakeSelection(domElement, viewDocumentSelection) {
        this._fakeSelectionMapping.set(domElement, new Selection$1(viewDocumentSelection));
    }
    /**
     * Returns a {@link module:engine/view/selection~Selection view selection} instance corresponding to a given
     * DOM element that represents fake selection. Returns `undefined` if binding to the given DOM element does not exist.
     *
     * @param {HTMLElement} domElement
     * @returns {module:engine/view/selection~Selection|undefined}
     */
    fakeSelectionToView(domElement) {
        return this._fakeSelectionMapping.get(domElement);
    }
    /**
     * Binds DOM and view elements, so it will be possible to get corresponding elements using
     * {@link module:engine/view/domconverter~DomConverter#mapDomToView} and
     * {@link module:engine/view/domconverter~DomConverter#mapViewToDom}.
     *
     * @param {HTMLElement} domElement The DOM element to bind.
     * @param {module:engine/view/element~Element} viewElement The view element to bind.
     */
    bindElements(domElement, viewElement) {
        this._domToViewMapping.set(domElement, viewElement);
        this._viewToDomMapping.set(viewElement, domElement);
    }
    /**
     * Unbinds a given DOM element from the view element it was bound to. Unbinding is deep, meaning that all children of
     * the DOM element will be unbound too.
     *
     * @param {HTMLElement} domElement The DOM element to unbind.
     */
    unbindDomElement(domElement) {
        const viewElement = this._domToViewMapping.get(domElement);
        if (viewElement) {
            this._domToViewMapping.delete(domElement);
            this._viewToDomMapping.delete(viewElement);
            for (const child of Array.from(domElement.children)) {
                this.unbindDomElement(child);
            }
        }
    }
    /**
     * Binds DOM and view document fragments, so it will be possible to get corresponding document fragments using
     * {@link module:engine/view/domconverter~DomConverter#mapDomToView} and
     * {@link module:engine/view/domconverter~DomConverter#mapViewToDom}.
     *
     * @param {DocumentFragment} domFragment The DOM document fragment to bind.
     * @param {module:engine/view/documentfragment~DocumentFragment} viewFragment The view document fragment to bind.
     */
    bindDocumentFragments(domFragment, viewFragment) {
        this._domToViewMapping.set(domFragment, viewFragment);
        this._viewToDomMapping.set(viewFragment, domFragment);
    }
    /**
     * Decides whether a given pair of attribute key and value should be passed further down the pipeline.
     *
     * @param {String} attributeKey
     * @param {String} attributeValue
     * @param {String} elementName Element name in lower case.
     * @returns {Boolean}
     */
    shouldRenderAttribute(attributeKey, attributeValue, elementName) {
        if (this.renderingMode === 'data') {
            return true;
        }
        attributeKey = attributeKey.toLowerCase();
        if (attributeKey.startsWith('on')) {
            return false;
        }
        if (attributeKey === 'srcdoc' &&
            attributeValue.match(/\bon\S+\s*=|javascript:|<\s*\/*script/i)) {
            return false;
        }
        if (elementName === 'img' &&
            (attributeKey === 'src' || attributeKey === 'srcset')) {
            return true;
        }
        if (elementName === 'source' && attributeKey === 'srcset') {
            return true;
        }
        if (attributeValue.match(/^\s*(javascript:|data:(image\/svg|text\/x?html))/i)) {
            return false;
        }
        return true;
    }
    /**
     * Set `domElement`'s content using provided `html` argument. Apply necessary filtering for the editing pipeline.
     *
     * @param {Element} domElement DOM element that should have `html` set as its content.
     * @param {String} html Textual representation of the HTML that will be set on `domElement`.
     */
    setContentOf(domElement, html) {
        // For data pipeline we pass the HTML as-is.
        if (this.renderingMode === 'data') {
            domElement.innerHTML = html;
            return;
        }
        const document = new DOMParser().parseFromString(html, 'text/html');
        const fragment = document.createDocumentFragment();
        const bodyChildNodes = document.body.childNodes;
        while (bodyChildNodes.length > 0) {
            fragment.appendChild(bodyChildNodes[0]);
        }
        const treeWalker = document.createTreeWalker(fragment, NodeFilter.SHOW_ELEMENT);
        const nodes = [];
        let currentNode;
        // eslint-disable-next-line no-cond-assign
        while (currentNode = treeWalker.nextNode()) {
            nodes.push(currentNode);
        }
        for (const currentNode of nodes) {
            // Go through nodes to remove those that are prohibited in editing pipeline.
            for (const attributeName of currentNode.getAttributeNames()) {
                this.setDomElementAttribute(currentNode, attributeName, currentNode.getAttribute(attributeName));
            }
            const elementName = currentNode.tagName.toLowerCase();
            // There are certain nodes, that should be renamed to <span> in editing pipeline.
            if (this._shouldRenameElement(elementName)) {
                _logUnsafeElement(elementName);
                currentNode.replaceWith(this._createReplacementDomElement(elementName, currentNode));
            }
        }
        // Empty the target element.
        while (domElement.firstChild) {
            domElement.firstChild.remove();
        }
        domElement.append(fragment);
    }
    /**
     * Converts the view to the DOM. For all text nodes, not bound elements and document fragments new items will
     * be created. For bound elements and document fragments the method will return corresponding items.
     *
     * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} viewNode
     * View node or document fragment to transform.
     * @param {Object} [options] Conversion options.
     * @param {Boolean} [options.bind=false] Determines whether new elements will be bound.
     * @param {Boolean} [options.withChildren=true] If `true`, node's and document fragment's children will be converted too.
     * @returns {Node|DocumentFragment} Converted node or DocumentFragment.
     */
    viewToDom(viewNode, options = {}) {
        if (viewNode.is('$text')) {
            const textData = this._processDataFromViewText(viewNode);
            return this._domDocument.createTextNode(textData);
        }
        else {
            if (this.mapViewToDom(viewNode)) {
                return this.mapViewToDom(viewNode);
            }
            let domElement;
            if (viewNode.is('documentFragment')) {
                // Create DOM document fragment.
                domElement = this._domDocument.createDocumentFragment();
                if (options.bind) {
                    this.bindDocumentFragments(domElement, viewNode);
                }
            }
            else if (viewNode.is('uiElement')) {
                if (viewNode.name === '$comment') {
                    domElement = this._domDocument.createComment(viewNode.getCustomProperty('$rawContent'));
                }
                else {
                    // UIElement has its own render() method (see #799).
                    domElement = viewNode.render(this._domDocument, this);
                }
                if (options.bind) {
                    this.bindElements(domElement, viewNode);
                }
                return domElement;
            }
            else {
                // Create DOM element.
                if (this._shouldRenameElement(viewNode.name)) {
                    _logUnsafeElement(viewNode.name);
                    domElement = this._createReplacementDomElement(viewNode.name);
                }
                else if (viewNode.hasAttribute('xmlns')) {
                    domElement = this._domDocument.createElementNS(viewNode.getAttribute('xmlns'), viewNode.name);
                }
                else {
                    domElement = this._domDocument.createElement(viewNode.name);
                }
                // RawElement take care of their children in RawElement#render() method which can be customized
                // (see https://github.com/ckeditor/ckeditor5/issues/4469).
                if (viewNode.is('rawElement')) {
                    viewNode.render(domElement, this);
                }
                if (options.bind) {
                    this.bindElements(domElement, viewNode);
                }
                // Copy element's attributes.
                for (const key of viewNode.getAttributeKeys()) {
                    this.setDomElementAttribute(domElement, key, viewNode.getAttribute(key), viewNode);
                }
            }
            if (options.withChildren !== false) {
                for (const child of this.viewChildrenToDom(viewNode, options)) {
                    domElement.appendChild(child);
                }
            }
            return domElement;
        }
    }
    /**
     * Sets the attribute on a DOM element.
     *
     * **Note**: To remove the attribute, use {@link #removeDomElementAttribute}.
     *
     * @param {HTMLElement} domElement The DOM element the attribute should be set on.
     * @param {String} key The name of the attribute.
     * @param {String} value The value of the attribute.
     * @param {module:engine/view/element~Element} [relatedViewElement] The view element related to the `domElement` (if there is any).
     * It helps decide whether the attribute set is unsafe. For instance, view elements created via the
     * {@link module:engine/view/downcastwriter~DowncastWriter} methods can allow certain attributes that would normally be filtered out.
     */
    setDomElementAttribute(domElement, key, value, relatedViewElement) {
        const shouldRenderAttribute = this.shouldRenderAttribute(key, value, domElement.tagName.toLowerCase()) ||
            relatedViewElement && relatedViewElement.shouldRenderUnsafeAttribute(key);
        if (!shouldRenderAttribute) {
            logWarning('domconverter-unsafe-attribute-detected', { domElement, key, value });
        }
        // The old value was safe but the new value is unsafe.
        if (domElement.hasAttribute(key) && !shouldRenderAttribute) {
            domElement.removeAttribute(key);
        }
        // The old value was unsafe (but prefixed) but the new value will be safe (will be unprefixed).
        else if (domElement.hasAttribute(UNSAFE_ATTRIBUTE_NAME_PREFIX + key) && shouldRenderAttribute) {
            domElement.removeAttribute(UNSAFE_ATTRIBUTE_NAME_PREFIX + key);
        }
        // If the attribute should not be rendered, rename it (instead of removing) to give developers some idea of what
        // is going on (https://github.com/ckeditor/ckeditor5/issues/10801).
        domElement.setAttribute(shouldRenderAttribute ? key : UNSAFE_ATTRIBUTE_NAME_PREFIX + key, value);
    }
    /**
     * Removes an attribute from a DOM element.
     *
     * **Note**: To set the attribute, use {@link #setDomElementAttribute}.
     *
     * @param {HTMLElement} domElement The DOM element the attribute should be removed from.
     * @param {String} key The name of the attribute.
     */
    removeDomElementAttribute(domElement, key) {
        // See #_createReplacementDomElement() to learn what this is.
        if (key == UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE) {
            return;
        }
        domElement.removeAttribute(key);
        // See setDomElementAttribute() to learn what this is.
        domElement.removeAttribute(UNSAFE_ATTRIBUTE_NAME_PREFIX + key);
    }
    /**
     * Converts children of the view element to DOM using the
     * {@link module:engine/view/domconverter~DomConverter#viewToDom} method.
     * Additionally, this method adds block {@link module:engine/view/filler filler} to the list of children, if needed.
     *
     * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElement Parent view element.
     * @param {Object} options See {@link module:engine/view/domconverter~DomConverter#viewToDom} options parameter.
     * @returns {Iterable.<Node>} DOM nodes.
     */
    *viewChildrenToDom(viewElement, options = {}) {
        const fillerPositionOffset = viewElement.getFillerOffset && viewElement.getFillerOffset();
        let offset = 0;
        for (const childView of viewElement.getChildren()) {
            if (fillerPositionOffset === offset) {
                yield this._getBlockFiller();
            }
            const transparentRendering = childView.is('element') &&
                childView.getCustomProperty('dataPipeline:transparentRendering');
            if (transparentRendering && this.renderingMode == 'data') {
                yield* this.viewChildrenToDom(childView, options);
            }
            else {
                if (transparentRendering) {
                    /**
                     * The `dataPipeline:transparentRendering` flag is supported only in the data pipeline.
                     *
                     * @error domconverter-transparent-rendering-unsupported-in-editing-pipeline
                     */
                    logWarning('domconverter-transparent-rendering-unsupported-in-editing-pipeline', { viewElement: childView });
                }
                yield this.viewToDom(childView, options);
            }
            offset++;
        }
        if (fillerPositionOffset === offset) {
            yield this._getBlockFiller();
        }
    }
    /**
     * Converts view {@link module:engine/view/range~Range} to DOM range.
     * Inline and block {@link module:engine/view/filler fillers} are handled during the conversion.
     *
     * @param {module:engine/view/range~Range} viewRange View range.
     * @returns {Range} DOM range.
     */
    viewRangeToDom(viewRange) {
        const domStart = this.viewPositionToDom(viewRange.start);
        const domEnd = this.viewPositionToDom(viewRange.end);
        const domRange = this._domDocument.createRange();
        domRange.setStart(domStart.parent, domStart.offset);
        domRange.setEnd(domEnd.parent, domEnd.offset);
        return domRange;
    }
    /**
     * Converts view {@link module:engine/view/position~Position} to DOM parent and offset.
     *
     * Inline and block {@link module:engine/view/filler fillers} are handled during the conversion.
     * If the converted position is directly before inline filler it is moved inside the filler.
     *
     * @param {module:engine/view/position~Position} viewPosition View position.
     * @returns {Object|null} position DOM position or `null` if view position could not be converted to DOM.
     * @returns {Node} position.parent DOM position parent.
     * @returns {Number} position.offset DOM position offset.
     */
    viewPositionToDom(viewPosition) {
        const viewParent = viewPosition.parent;
        if (viewParent.is('$text')) {
            const domParent = this.findCorrespondingDomText(viewParent);
            if (!domParent) {
                // Position is in a view text node that has not been rendered to DOM yet.
                return null;
            }
            let offset = viewPosition.offset;
            if (startsWithFiller(domParent)) {
                offset += INLINE_FILLER_LENGTH;
            }
            return { parent: domParent, offset };
        }
        else {
            // viewParent is instance of ViewElement.
            let domParent, domBefore, domAfter;
            if (viewPosition.offset === 0) {
                domParent = this.mapViewToDom(viewParent);
                if (!domParent) {
                    // Position is in a view element that has not been rendered to DOM yet.
                    return null;
                }
                domAfter = domParent.childNodes[0];
            }
            else {
                const nodeBefore = viewPosition.nodeBefore;
                domBefore = nodeBefore.is('$text') ?
                    this.findCorrespondingDomText(nodeBefore) :
                    this.mapViewToDom(nodeBefore);
                if (!domBefore) {
                    // Position is after a view element that has not been rendered to DOM yet.
                    return null;
                }
                domParent = domBefore.parentNode;
                domAfter = domBefore.nextSibling;
            }
            // If there is an inline filler at position return position inside the filler. We should never return
            // the position before the inline filler.
            if (isText(domAfter) && startsWithFiller(domAfter)) {
                return { parent: domAfter, offset: INLINE_FILLER_LENGTH };
            }
            const offset = domBefore ? indexOf(domBefore) + 1 : 0;
            return { parent: domParent, offset };
        }
    }
    /**
     * Converts DOM to view. For all text nodes, not bound elements and document fragments new items will
     * be created. For bound elements and document fragments function will return corresponding items. For
     * {@link module:engine/view/filler fillers} `null` will be returned.
     * For all DOM elements rendered by {@link module:engine/view/uielement~UIElement} that UIElement will be returned.
     *
     * @param {Node|DocumentFragment} domNode DOM node or document fragment to transform.
     * @param {Object} [options] Conversion options.
     * @param {Boolean} [options.bind=false] Determines whether new elements will be bound.
     * @param {Boolean} [options.withChildren=true] If `true`, node's and document fragment's children will be converted too.
     * @param {Boolean} [options.keepOriginalCase=false] If `false`, node's tag name will be converted to lower case.
     * @param {Boolean} [options.skipComments=false] If `false`, comment nodes will be converted to `$comment`
     * {@link module:engine/view/uielement~UIElement view UI elements}.
     * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} Converted node or document fragment
     * or `null` if DOM node is a {@link module:engine/view/filler filler} or the given node is an empty text node.
     */
    domToView(domNode, options = {}) {
        if (this.isBlockFiller(domNode)) {
            return null;
        }
        // When node is inside a UIElement or a RawElement return that parent as it's view representation.
        const hostElement = this.getHostViewElement(domNode);
        if (hostElement) {
            return hostElement;
        }
        if (isComment(domNode) && options.skipComments) {
            return null;
        }
        if (isText(domNode)) {
            if (isInlineFiller(domNode)) {
                return null;
            }
            else {
                const textData = this._processDataFromDomText(domNode);
                return textData === '' ? null : new Text$2(this.document, textData);
            }
        }
        else {
            if (this.mapDomToView(domNode)) {
                return this.mapDomToView(domNode);
            }
            let viewElement;
            if (this.isDocumentFragment(domNode)) {
                // Create view document fragment.
                viewElement = new DocumentFragment$1(this.document);
                if (options.bind) {
                    this.bindDocumentFragments(domNode, viewElement);
                }
            }
            else {
                // Create view element.
                viewElement = this._createViewElement(domNode, options);
                if (options.bind) {
                    this.bindElements(domNode, viewElement);
                }
                // Copy element's attributes.
                const attrs = domNode.attributes;
                if (attrs) {
                    for (let l = attrs.length, i = 0; i < l; i++) {
                        viewElement._setAttribute(attrs[i].name, attrs[i].value);
                    }
                }
                // Treat this element's content as a raw data if it was registered as such.
                // Comment node is also treated as an element with raw data.
                if (this._isViewElementWithRawContent(viewElement, options) || isComment(domNode)) {
                    const rawContent = isComment(domNode) ? domNode.data : domNode.innerHTML;
                    viewElement._setCustomProperty('$rawContent', rawContent);
                    // Store a DOM node to prevent left trimming of the following text node.
                    this._encounteredRawContentDomNodes.add(domNode);
                    return viewElement;
                }
            }
            if (options.withChildren !== false) {
                for (const child of this.domChildrenToView(domNode, options)) {
                    viewElement._appendChild(child);
                }
            }
            return viewElement;
        }
    }
    /**
     * Converts children of the DOM element to view nodes using
     * the {@link module:engine/view/domconverter~DomConverter#domToView} method.
     * Additionally this method omits block {@link module:engine/view/filler filler}, if it exists in the DOM parent.
     *
     * @param {HTMLElement} domElement Parent DOM element.
     * @param {Object} options See {@link module:engine/view/domconverter~DomConverter#domToView} options parameter.
     * @returns {Iterable.<module:engine/view/node~Node>} View nodes.
     */
    *domChildrenToView(domElement, options) {
        for (let i = 0; i < domElement.childNodes.length; i++) {
            const domChild = domElement.childNodes[i];
            const viewChild = this.domToView(domChild, options);
            if (viewChild !== null) {
                yield viewChild;
            }
        }
    }
    /**
     * Converts DOM selection to view {@link module:engine/view/selection~Selection}.
     * Ranges which cannot be converted will be omitted.
     *
     * @param {Selection} domSelection DOM selection.
     * @returns {module:engine/view/selection~Selection} View selection.
     */
    domSelectionToView(domSelection) {
        // DOM selection might be placed in fake selection container.
        // If container contains fake selection - return corresponding view selection.
        if (domSelection.rangeCount === 1) {
            let container = domSelection.getRangeAt(0).startContainer;
            // The DOM selection might be moved to the text node inside the fake selection container.
            if (isText(container)) {
                container = container.parentNode;
            }
            const viewSelection = this.fakeSelectionToView(container);
            if (viewSelection) {
                return viewSelection;
            }
        }
        const isBackward = this.isDomSelectionBackward(domSelection);
        const viewRanges = [];
        for (let i = 0; i < domSelection.rangeCount; i++) {
            // DOM Range have correct start and end, no matter what is the DOM Selection direction. So we don't have to fix anything.
            const domRange = domSelection.getRangeAt(i);
            const viewRange = this.domRangeToView(domRange);
            if (viewRange) {
                viewRanges.push(viewRange);
            }
        }
        return new Selection$1(viewRanges, { backward: isBackward });
    }
    /**
     * Converts DOM Range to view {@link module:engine/view/range~Range}.
     * If the start or end position can not be converted `null` is returned.
     *
     * @param {Range} domRange DOM range.
     * @returns {module:engine/view/range~Range|null} View range.
     */
    domRangeToView(domRange) {
        const viewStart = this.domPositionToView(domRange.startContainer, domRange.startOffset);
        const viewEnd = this.domPositionToView(domRange.endContainer, domRange.endOffset);
        if (viewStart && viewEnd) {
            return new Range$1(viewStart, viewEnd);
        }
        return null;
    }
    /**
     * Converts DOM parent and offset to view {@link module:engine/view/position~Position}.
     *
     * If the position is inside a {@link module:engine/view/filler filler} which has no corresponding view node,
     * position of the filler will be converted and returned.
     *
     * If the position is inside DOM element rendered by {@link module:engine/view/uielement~UIElement}
     * that position will be converted to view position before that UIElement.
     *
     * If structures are too different and it is not possible to find corresponding position then `null` will be returned.
     *
     * @param {Node} domParent DOM position parent.
     * @param {Number} [domOffset=0] DOM position offset. You can skip it when converting the inline filler node.
     * @returns {module:engine/view/position~Position} viewPosition View position.
     */
    domPositionToView(domParent, domOffset = 0) {
        if (this.isBlockFiller(domParent)) {
            return this.domPositionToView(domParent.parentNode, indexOf(domParent));
        }
        // If position is somewhere inside UIElement or a RawElement - return position before that element.
        const viewElement = this.mapDomToView(domParent);
        if (viewElement && (viewElement.is('uiElement') || viewElement.is('rawElement'))) {
            return Position$1._createBefore(viewElement);
        }
        if (isText(domParent)) {
            if (isInlineFiller(domParent)) {
                return this.domPositionToView(domParent.parentNode, indexOf(domParent));
            }
            const viewParent = this.findCorrespondingViewText(domParent);
            let offset = domOffset;
            if (!viewParent) {
                return null;
            }
            if (startsWithFiller(domParent)) {
                offset -= INLINE_FILLER_LENGTH;
                offset = offset < 0 ? 0 : offset;
            }
            return new Position$1(viewParent, offset);
        }
        // domParent instanceof HTMLElement.
        else {
            if (domOffset === 0) {
                const viewParent = this.mapDomToView(domParent);
                if (viewParent) {
                    return new Position$1(viewParent, 0);
                }
            }
            else {
                const domBefore = domParent.childNodes[domOffset - 1];
                const viewBefore = isText(domBefore) ?
                    this.findCorrespondingViewText(domBefore) :
                    this.mapDomToView(domBefore);
                // TODO #663
                if (viewBefore && viewBefore.parent) {
                    return new Position$1(viewBefore.parent, viewBefore.index + 1);
                }
            }
            return null;
        }
    }
    /**
     * Returns corresponding view {@link module:engine/view/element~Element Element} or
     * {@link module:engine/view/documentfragment~DocumentFragment} for provided DOM element or
     * document fragment. If there is no view item {@link module:engine/view/domconverter~DomConverter#bindElements bound}
     * to the given DOM - `undefined` is returned.
     *
     * For all DOM elements rendered by a {@link module:engine/view/uielement~UIElement} or
     * a {@link module:engine/view/rawelement~RawElement}, the parent `UIElement` or `RawElement` will be returned.
     *
     * @param {DocumentFragment|Element} domElementOrDocumentFragment DOM element or document fragment.
     * @returns {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|undefined}
     * Corresponding view element, document fragment or `undefined` if no element was bound.
     */
    mapDomToView(domElementOrDocumentFragment) {
        const hostElement = this.getHostViewElement(domElementOrDocumentFragment);
        return hostElement || this._domToViewMapping.get(domElementOrDocumentFragment);
    }
    /**
     * Finds corresponding text node. Text nodes are not {@link module:engine/view/domconverter~DomConverter#bindElements bound},
     * corresponding text node is returned based on the sibling or parent.
     *
     * If the directly previous sibling is a {@link module:engine/view/domconverter~DomConverter#bindElements bound} element, it is used
     * to find the corresponding text node.
     *
     * If this is a first child in the parent and the parent is a {@link module:engine/view/domconverter~DomConverter#bindElements bound}
     * element, it is used to find the corresponding text node.
     *
     * For all text nodes rendered by a {@link module:engine/view/uielement~UIElement} or
     * a {@link module:engine/view/rawelement~RawElement}, the parent `UIElement` or `RawElement` will be returned.
     *
     * Otherwise `null` is returned.
     *
     * Note that for the block or inline {@link module:engine/view/filler filler} this method returns `null`.
     *
     * @param {Text} domText DOM text node.
     * @returns {module:engine/view/text~Text|null} Corresponding view text node or `null`, if it was not possible to find a
     * corresponding node.
     */
    findCorrespondingViewText(domText) {
        if (isInlineFiller(domText)) {
            return null;
        }
        // If DOM text was rendered by a UIElement or a RawElement - return this parent element.
        const hostElement = this.getHostViewElement(domText);
        if (hostElement) {
            return hostElement;
        }
        const previousSibling = domText.previousSibling;
        // Try to use previous sibling to find the corresponding text node.
        if (previousSibling) {
            if (!(this.isElement(previousSibling))) {
                // The previous is text or comment.
                return null;
            }
            const viewElement = this.mapDomToView(previousSibling);
            if (viewElement) {
                const nextSibling = viewElement.nextSibling;
                // It might be filler which has no corresponding view node.
                if (nextSibling instanceof Text$2) {
                    return nextSibling;
                }
                else {
                    return null;
                }
            }
        }
        // Try to use parent to find the corresponding text node.
        else {
            const viewElement = this.mapDomToView(domText.parentNode);
            if (viewElement) {
                const firstChild = viewElement.getChild(0);
                // It might be filler which has no corresponding view node.
                if (firstChild instanceof Text$2) {
                    return firstChild;
                }
                else {
                    return null;
                }
            }
        }
        return null;
    }
    /**
     * Returns corresponding DOM item for provided {@link module:engine/view/element~Element Element} or
     * {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment}.
     * To find a corresponding text for {@link module:engine/view/text~Text view Text instance}
     * use {@link #findCorrespondingDomText}.
     *
     * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewNode
     * View element or document fragment.
     * @returns {Node|DocumentFragment|undefined} Corresponding DOM node or document fragment.
     */
    mapViewToDom(documentFragmentOrElement) {
        return this._viewToDomMapping.get(documentFragmentOrElement);
    }
    /**
     * Finds corresponding text node. Text nodes are not {@link module:engine/view/domconverter~DomConverter#bindElements bound},
     * corresponding text node is returned based on the sibling or parent.
     *
     * If the directly previous sibling is a {@link module:engine/view/domconverter~DomConverter#bindElements bound} element, it is used
     * to find the corresponding text node.
     *
     * If this is a first child in the parent and the parent is a {@link module:engine/view/domconverter~DomConverter#bindElements bound}
     * element, it is used to find the corresponding text node.
     *
     * Otherwise `null` is returned.
     *
     * @param {module:engine/view/text~Text} viewText View text node.
     * @returns {Text|null} Corresponding DOM text node or `null`, if it was not possible to find a corresponding node.
     */
    findCorrespondingDomText(viewText) {
        const previousSibling = viewText.previousSibling;
        // Try to use previous sibling to find the corresponding text node.
        if (previousSibling && this.mapViewToDom(previousSibling)) {
            return this.mapViewToDom(previousSibling).nextSibling;
        }
        // If this is a first node, try to use parent to find the corresponding text node.
        if (!previousSibling && viewText.parent && this.mapViewToDom(viewText.parent)) {
            return this.mapViewToDom(viewText.parent).childNodes[0];
        }
        return null;
    }
    /**
     * Focuses DOM editable that is corresponding to provided {@link module:engine/view/editableelement~EditableElement}.
     *
     * @param {module:engine/view/editableelement~EditableElement} viewEditable
     */
    focus(viewEditable) {
        const domEditable = this.mapViewToDom(viewEditable);
        if (domEditable && domEditable.ownerDocument.activeElement !== domEditable) {
            // Save the scrollX and scrollY positions before the focus.
            const { scrollX, scrollY } = global$2.window;
            const scrollPositions = [];
            // Save all scrollLeft and scrollTop values starting from domEditable up to
            // document#documentElement.
            forEachDomElementAncestor(domEditable, node => {
                const { scrollLeft, scrollTop } = node;
                scrollPositions.push([scrollLeft, scrollTop]);
            });
            domEditable.focus();
            // Restore scrollLeft and scrollTop values starting from domEditable up to
            // document#documentElement.
            // https://github.com/ckeditor/ckeditor5-engine/issues/951
            // https://github.com/ckeditor/ckeditor5-engine/issues/957
            forEachDomElementAncestor(domEditable, node => {
                const [scrollLeft, scrollTop] = scrollPositions.shift();
                node.scrollLeft = scrollLeft;
                node.scrollTop = scrollTop;
            });
            // Restore the scrollX and scrollY positions after the focus.
            // https://github.com/ckeditor/ckeditor5-engine/issues/951
            global$2.window.scrollTo(scrollX, scrollY);
        }
    }
    /**
     * Returns `true` when `node.nodeType` equals `Node.ELEMENT_NODE`.
     *
     * @param {Node} node Node to check.
     * @returns {Boolean}
     */
    isElement(node) {
        return node && node.nodeType == Node.ELEMENT_NODE;
    }
    /**
     * Returns `true` when `node.nodeType` equals `Node.DOCUMENT_FRAGMENT_NODE`.
     *
     * @param {Node} node Node to check.
     * @returns {Boolean}
     */
    isDocumentFragment(node) {
        return node && node.nodeType == Node.DOCUMENT_FRAGMENT_NODE;
    }
    /**
     * Checks if the node is an instance of the block filler for this DOM converter.
     *
     *		const converter = new DomConverter( viewDocument, { blockFillerMode: 'br' } );
     *
     *		converter.isBlockFiller( BR_FILLER( document ) ); // true
     *		converter.isBlockFiller( NBSP_FILLER( document ) ); // false
     *
     * **Note:**: For the `'nbsp'` mode the method also checks context of a node so it cannot be a detached node.
     *
     * **Note:** A special case in the `'nbsp'` mode exists where the `<br>` in `<p><br></p>` is treated as a block filler.
     *
     * @param {Node} domNode DOM node to check.
     * @returns {Boolean} True if a node is considered a block filler for given mode.
     */
    isBlockFiller(domNode) {
        if (this.blockFillerMode == 'br') {
            return domNode.isEqualNode(BR_FILLER_REF);
        }
        // Special case for <p><br></p> in which <br> should be treated as filler even when we are not in the 'br' mode. See ckeditor5#5564.
        if (domNode.tagName === 'BR' &&
            hasBlockParent(domNode, this.blockElements) &&
            domNode.parentNode.childNodes.length === 1) {
            return true;
        }
        // If not in 'br' mode, try recognizing both marked and regular nbsp block fillers.
        return domNode.isEqualNode(MARKED_NBSP_FILLER_REF) || isNbspBlockFiller(domNode, this.blockElements);
    }
    /**
     * Returns `true` if given selection is a backward selection, that is, if it's `focus` is before `anchor`.
     *
     * @param {Selection} DOM Selection instance to check.
     * @returns {Boolean}
     */
    isDomSelectionBackward(selection) {
        if (selection.isCollapsed) {
            return false;
        }
        // Since it takes multiple lines of code to check whether a "DOM Position" is before/after another "DOM Position",
        // we will use the fact that range will collapse if it's end is before it's start.
        const range = this._domDocument.createRange();
        range.setStart(selection.anchorNode, selection.anchorOffset);
        range.setEnd(selection.focusNode, selection.focusOffset);
        const backward = range.collapsed;
        range.detach();
        return backward;
    }
    /**
     * Returns a parent {@link module:engine/view/uielement~UIElement} or {@link module:engine/view/rawelement~RawElement}
     * that hosts the provided DOM node. Returns `null` if there is no such parent.
     *
     * @param {Node} domNode
     * @returns {module:engine/view/uielement~UIElement|module:engine/view/rawelement~RawElement|null}
     */
    getHostViewElement(domNode) {
        const ancestors = getAncestors(domNode);
        // Remove domNode from the list.
        ancestors.pop();
        while (ancestors.length) {
            const domNode = ancestors.pop();
            const viewNode = this._domToViewMapping.get(domNode);
            if (viewNode && (viewNode.is('uiElement') || viewNode.is('rawElement'))) {
                return viewNode;
            }
        }
        return null;
    }
    /**
     * Checks if the given selection's boundaries are at correct places.
     *
     * The following places are considered as incorrect for selection boundaries:
     *
     * * before or in the middle of an inline filler sequence,
     * * inside a DOM element which represents {@link module:engine/view/uielement~UIElement a view UI element},
     * * inside a DOM element which represents {@link module:engine/view/rawelement~RawElement a view raw element}.
     *
     * @param {Selection} domSelection The DOM selection object to be checked.
     * @returns {Boolean} `true` if the given selection is at a correct place, `false` otherwise.
     */
    isDomSelectionCorrect(domSelection) {
        return this._isDomSelectionPositionCorrect(domSelection.anchorNode, domSelection.anchorOffset) &&
            this._isDomSelectionPositionCorrect(domSelection.focusNode, domSelection.focusOffset);
    }
    /**
     * Registers a {@link module:engine/view/matcher~MatcherPattern} for view elements whose content should be treated as raw data
     * and not processed during the conversion from DOM nodes to view elements.
     *
     * This is affecting how {@link module:engine/view/domconverter~DomConverter#domToView} and
     * {@link module:engine/view/domconverter~DomConverter#domChildrenToView} process DOM nodes.
     *
     * The raw data can be later accessed by a
     * {@link module:engine/view/element~Element#getCustomProperty custom property of a view element} called `"$rawContent"`.
     *
     * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching a view element whose content should
     * be treated as raw data.
     */
    registerRawContentMatcher(pattern) {
        this._rawContentElementMatcher.add(pattern);
    }
    /**
     * Returns the block {@link module:engine/view/filler filler} node based on the current {@link #blockFillerMode} setting.
     *
     * @private
     * @returns {Node} filler
     */
    _getBlockFiller() {
        switch (this.blockFillerMode) {
            case 'nbsp':
                return NBSP_FILLER(this._domDocument); // eslint-disable-line new-cap
            case 'markedNbsp':
                return MARKED_NBSP_FILLER(this._domDocument); // eslint-disable-line new-cap
            case 'br':
                return BR_FILLER(this._domDocument); // eslint-disable-line new-cap
        }
    }
    /**
     * Checks if the given DOM position is a correct place for selection boundary. See {@link #isDomSelectionCorrect}.
     *
     * @private
     * @param {Element} domParent Position parent.
     * @param {Number} offset Position offset.
     * @returns {Boolean} `true` if given position is at a correct place for selection boundary, `false` otherwise.
     */
    _isDomSelectionPositionCorrect(domParent, offset) {
        // If selection is before or in the middle of inline filler string, it is incorrect.
        if (isText(domParent) && startsWithFiller(domParent) && offset < INLINE_FILLER_LENGTH) {
            // Selection in a text node, at wrong position (before or in the middle of filler).
            return false;
        }
        if (this.isElement(domParent) && startsWithFiller(domParent.childNodes[offset])) {
            // Selection in an element node, before filler text node.
            return false;
        }
        const viewParent = this.mapDomToView(domParent);
        // The position is incorrect when anchored inside a UIElement or a RawElement.
        // Note: In case of UIElement and RawElement, mapDomToView() returns a parent element for any DOM child
        // so there's no need to perform any additional checks.
        if (viewParent && (viewParent.is('uiElement') || viewParent.is('rawElement'))) {
            return false;
        }
        return true;
    }
    /**
     * Takes text data from a given {@link module:engine/view/text~Text#data} and processes it so
     * it is correctly displayed in the DOM.
     *
     * Following changes are done:
     *
     * * a space at the beginning is changed to `&nbsp;` if this is the first text node in its container
     * element or if a previous text node ends with a space character,
     * * space at the end of the text node is changed to `&nbsp;` if there are two spaces at the end of a node or if next node
     * starts with a space or if it is the last text node in its container,
     * * remaining spaces are replaced to a chain of spaces and `&nbsp;` (e.g. `'x   x'` becomes `'x &nbsp; x'`).
     *
     * Content of {@link #preElements} is not processed.
     *
     * @private
     * @param {module:engine/view/text~Text} node View text node to process.
     * @returns {String} Processed text data.
     */
    _processDataFromViewText(node) {
        let data = node.data;
        // If any of node ancestors has a name which is in `preElements` array, then currently processed
        // view text node is (will be) in preformatted element. We should not change whitespaces then.
        if (node.getAncestors().some(parent => this.preElements.includes(parent.name))) {
            return data;
        }
        // 1. Replace the first space with a nbsp if the previous node ends with a space or there is no previous node
        // (container element boundary).
        if (data.charAt(0) == ' ') {
            const prevNode = this._getTouchingInlineViewNode(node, false);
            const prevEndsWithSpace = prevNode && prevNode.is('$textProxy') && this._nodeEndsWithSpace(prevNode);
            if (prevEndsWithSpace || !prevNode) {
                data = '\u00A0' + data.substr(1);
            }
        }
        // 2. Replace the last space with nbsp if there are two spaces at the end or if the next node starts with space or there is no
        // next node (container element boundary).
        //
        // Keep in mind that Firefox prefers $nbsp; before tag, not inside it:
        //
        // Foo <span>&nbsp;bar</span>  <-- bad.
        // Foo&nbsp;<span> bar</span>  <-- good.
        //
        // More here: https://github.com/ckeditor/ckeditor5-engine/issues/1747.
        if (data.charAt(data.length - 1) == ' ') {
            const nextNode = this._getTouchingInlineViewNode(node, true);
            const nextStartsWithSpace = nextNode && nextNode.is('$textProxy') && nextNode.data.charAt(0) == ' ';
            if (data.charAt(data.length - 2) == ' ' || !nextNode || nextStartsWithSpace) {
                data = data.substr(0, data.length - 1) + '\u00A0';
            }
        }
        // 3. Create space+nbsp pairs.
        return data.replace(/ {2}/g, ' \u00A0');
    }
    /**
     * Checks whether given node ends with a space character after changing appropriate space characters to `&nbsp;`s.
     *
     * @private
     * @param {module:engine/view/text~Text} node Node to check.
     * @returns {Boolean} `true` if given `node` ends with space, `false` otherwise.
     */
    _nodeEndsWithSpace(node) {
        if (node.getAncestors().some(parent => this.preElements.includes(parent.name))) {
            return false;
        }
        const data = this._processDataFromViewText(node);
        return data.charAt(data.length - 1) == ' ';
    }
    /**
     * Takes text data from native `Text` node and processes it to a correct {@link module:engine/view/text~Text view text node} data.
     *
     * Following changes are done:
     *
     * * multiple whitespaces are replaced to a single space,
     * * space at the beginning of a text node is removed if it is the first text node in its container
     * element or if the previous text node ends with a space character,
     * * space at the end of the text node is removed if there are two spaces at the end of a node or if next node
     * starts with a space or if it is the last text node in its container
     * * nbsps are converted to spaces.
     *
     * @param {Node} node DOM text node to process.
     * @returns {String} Processed data.
     * @private
     */
    _processDataFromDomText(node) {
        let data = node.data;
        if (_hasDomParentOfType(node, this.preElements)) {
            return getDataWithoutFiller(node);
        }
        // Change all consecutive whitespace characters (from the [ \n\t\r] set –
        // see https://github.com/ckeditor/ckeditor5-engine/issues/822#issuecomment-311670249) to a single space character.
        // That's how multiple whitespaces are treated when rendered, so we normalize those whitespaces.
        // We're replacing 1+ (and not 2+) to also normalize singular \n\t\r characters (#822).
        data = data.replace(/[ \n\t\r]{1,}/g, ' ');
        const prevNode = this._getTouchingInlineDomNode(node, false);
        const nextNode = this._getTouchingInlineDomNode(node, true);
        const shouldLeftTrim = this._checkShouldLeftTrimDomText(node, prevNode);
        const shouldRightTrim = this._checkShouldRightTrimDomText(node, nextNode);
        // If the previous dom text node does not exist or it ends by whitespace character, remove space character from the beginning
        // of this text node. Such space character is treated as a whitespace.
        if (shouldLeftTrim) {
            data = data.replace(/^ /, '');
        }
        // If the next text node does not exist remove space character from the end of this text node.
        if (shouldRightTrim) {
            data = data.replace(/ $/, '');
        }
        // At the beginning and end of a block element, Firefox inserts normal space + <br> instead of non-breaking space.
        // This means that the text node starts/end with normal space instead of non-breaking space.
        // This causes a problem because the normal space would be removed in `.replace` calls above. To prevent that,
        // the inline filler is removed only after the data is initially processed (by the `.replace` above). See ckeditor5#692.
        data = getDataWithoutFiller(new Text(data));
        // At this point we should have removed all whitespaces from DOM text data.
        //
        // Now, We will reverse the process that happens in `_processDataFromViewText`.
        //
        // We have to change &nbsp; chars, that were in DOM text data because of rendering reasons, to spaces.
        // First, change all ` \u00A0` pairs (space + &nbsp;) to two spaces. DOM converter changes two spaces from model/view to
        // ` \u00A0` to ensure proper rendering. Since here we convert back, we recognize those pairs and change them back to `  `.
        data = data.replace(/ \u00A0/g, '  ');
        const isNextNodeInlineObjectElement = nextNode && this.isElement(nextNode) && nextNode.tagName != 'BR';
        const isNextNodeStartingWithSpace = nextNode && isText(nextNode) && nextNode.data.charAt(0) == ' ';
        // Then, let's change the last nbsp to a space.
        if (/( |\u00A0)\u00A0$/.test(data) || !nextNode || isNextNodeInlineObjectElement || isNextNodeStartingWithSpace) {
            data = data.replace(/\u00A0$/, ' ');
        }
        // Then, change &nbsp; character that is at the beginning of the text node to space character.
        // We do that replacement only if this is the first node or the previous node ends on whitespace character.
        if (shouldLeftTrim || prevNode && this.isElement(prevNode) && prevNode.tagName != 'BR') {
            data = data.replace(/^\u00A0/, ' ');
        }
        // At this point, all whitespaces should be removed and all &nbsp; created for rendering reasons should be
        // changed to normal space. All left &nbsp; are &nbsp; inserted intentionally.
        return data;
    }
    /**
     * Helper function which checks if a DOM text node, preceded by the given `prevNode` should
     * be trimmed from the left side.
     *
     * @private
     * @param {Node} node
     * @param {Node} prevNode Either DOM text or `<br>` or one of `#inlineObjectElements`.
     */
    _checkShouldLeftTrimDomText(node, prevNode) {
        if (!prevNode) {
            return true;
        }
        if (this.isElement(prevNode)) {
            return prevNode.tagName === 'BR';
        }
        // Shouldn't left trim if previous node is a node that was encountered as a raw content node.
        if (this._encounteredRawContentDomNodes.has(node.previousSibling)) {
            return false;
        }
        return /[^\S\u00A0]/.test(prevNode.data.charAt(prevNode.data.length - 1));
    }
    /**
     * Helper function which checks if a DOM text node, succeeded by the given `nextNode` should
     * be trimmed from the right side.
     *
     * @private
     * @param {Node} node
     * @param {Node} nextNode Either DOM text or `<br>` or one of `#inlineObjectElements`.
     */
    _checkShouldRightTrimDomText(node, nextNode) {
        if (nextNode) {
            return false;
        }
        return !startsWithFiller(node);
    }
    /**
     * Helper function. For given {@link module:engine/view/text~Text view text node}, it finds previous or next sibling
     * that is contained in the same container element. If there is no such sibling, `null` is returned.
     *
     * @private
     * @param {module:engine/view/text~Text} node Reference node.
     * @param {Boolean} getNext
     * @returns {module:engine/view/text~Text|module:engine/view/element~Element|null} Touching text node, an inline object
     * or `null` if there is no next or previous touching text node.
     */
    _getTouchingInlineViewNode(node, getNext) {
        const treeWalker = new TreeWalker$1({
            startPosition: getNext ? Position$1._createAfter(node) : Position$1._createBefore(node),
            direction: getNext ? 'forward' : 'backward'
        });
        for (const value of treeWalker) {
            // Found an inline object (for example an image).
            if (value.item.is('element') && this.inlineObjectElements.includes(value.item.name)) {
                return value.item;
            }
            // ViewContainerElement is found on a way to next ViewText node, so given `node` was first/last
            // text node in its container element.
            else if (value.item.is('containerElement')) {
                return null;
            }
            // <br> found – it works like a block boundary, so do not scan further.
            else if (value.item.is('element', 'br')) {
                return null;
            }
            // Found a text node in the same container element.
            else if (value.item.is('$textProxy')) {
                return value.item;
            }
        }
        return null;
    }
    /**
     * Helper function. For the given text node, it finds the closest touching node which is either
     * a text, `<br>` or an {@link #inlineObjectElements inline object}.
     *
     * If no such node is found, `null` is returned.
     *
     * For instance, in the following DOM structure:
     *
     *		<p>foo<b>bar</b><br>bom</p>
     *
     * * `foo` doesn't have its previous touching inline node (`null` is returned),
     * * `foo`'s next touching inline node is `bar`
     * * `bar`'s next touching inline node is `<br>`
     *
     * This method returns text nodes and `<br>` elements because these types of nodes affect how
     * spaces in the given text node need to be converted.
     *
     * @private
     * @param {Text} node
     * @param {Boolean} getNext
     * @returns {Text|Element|null}
     */
    _getTouchingInlineDomNode(node, getNext) {
        if (!node.parentNode) {
            return null;
        }
        const stepInto = getNext ? 'firstChild' : 'lastChild';
        const stepOver = getNext ? 'nextSibling' : 'previousSibling';
        let skipChildren = true;
        let returnNode = node;
        do {
            if (!skipChildren && returnNode[stepInto]) {
                returnNode = returnNode[stepInto];
            }
            else if (returnNode[stepOver]) {
                returnNode = returnNode[stepOver];
                skipChildren = false;
            }
            else {
                returnNode = returnNode.parentNode;
                skipChildren = true;
            }
            if (!returnNode || this._isBlockElement(returnNode)) {
                return null;
            }
        } while (!(isText(returnNode) || returnNode.tagName == 'BR' || this._isInlineObjectElement(returnNode)));
        return returnNode;
    }
    /**
     * Returns `true` if a DOM node belongs to {@link #blockElements}. `false` otherwise.
     *
     * @private
     * @param {Node} node
     * @returns {Boolean}
     */
    _isBlockElement(node) {
        return this.isElement(node) && this.blockElements.includes(node.tagName.toLowerCase());
    }
    /**
     * Returns `true` if a DOM node belongs to {@link #inlineObjectElements}. `false` otherwise.
     *
     * @private
     * @param {Node} node
     * @returns {Boolean}
     */
    _isInlineObjectElement(node) {
        return this.isElement(node) && this.inlineObjectElements.includes(node.tagName.toLowerCase());
    }
    /**
     * Creates view element basing on the node type.
     *
     * @private
     * @param {Node} node DOM node to check.
     * @param {Object} options Conversion options. See {@link module:engine/view/domconverter~DomConverter#domToView} options parameter.
     * @returns {Element}
     */
    _createViewElement(node, options) {
        if (isComment(node)) {
            return new UIElement(this.document, '$comment');
        }
        const viewName = options.keepOriginalCase ? node.tagName : node.tagName.toLowerCase();
        return new Element$1(this.document, viewName);
    }
    /**
     * Checks if view element's content should be treated as a raw data.
     *
     * @private
     * @param {Element} viewElement View element to check.
     * @param {Object} options Conversion options. See {@link module:engine/view/domconverter~DomConverter#domToView} options parameter.
     * @returns {Boolean}
     */
    _isViewElementWithRawContent(viewElement, options) {
        return options.withChildren !== false && !!this._rawContentElementMatcher.match(viewElement);
    }
    /**
     * Checks whether a given element name should be renamed in a current rendering mode.
     *
     * @private
     * @param {String} elementName The name of view element.
     * @returns {Boolean}
     */
    _shouldRenameElement(elementName) {
        const name = elementName.toLowerCase();
        return this.renderingMode === 'editing' && this.unsafeElements.includes(name);
    }
    /**
     * Return a <span> element with a special attribute holding the name of the original element.
     * Optionally, copy all the attributes of the original element if that element is provided.
     *
     * @private
     * @param {String} elementName The name of view element.
     * @param {Element} [originalDomElement] The original DOM element to copy attributes and content from.
     * @returns {Element}
     */
    _createReplacementDomElement(elementName, originalDomElement) {
        const newDomElement = this._domDocument.createElement('span');
        // Mark the span replacing a script as hidden.
        newDomElement.setAttribute(UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE, elementName);
        if (originalDomElement) {
            while (originalDomElement.firstChild) {
                newDomElement.appendChild(originalDomElement.firstChild);
            }
            for (const attributeName of originalDomElement.getAttributeNames()) {
                newDomElement.setAttribute(attributeName, originalDomElement.getAttribute(attributeName));
            }
        }
        return newDomElement;
    }
}
// Helper function.
// Used to check if given native `Element` or `Text` node has parent with tag name from `types` array.
//
// @param {Node} node
// @param {Array.<String>} types
// @returns {Boolean} `true` if such parent exists or `false` if it does not.
function _hasDomParentOfType(node, types) {
    const parents = getAncestors(node);
    return parents.some(parent => parent.tagName && types.includes(parent.tagName.toLowerCase()));
}
// A helper that executes given callback for each DOM node's ancestor, starting from the given node
// and ending in document#documentElement.
//
// @param {Node} node
// @param {Function} callback A callback to be executed for each ancestor.
function forEachDomElementAncestor(element, callback) {
    let node = element;
    while (node) {
        callback(node);
        node = node.parentElement;
    }
}
// Checks if given node is a nbsp block filler.
//
// A &nbsp; is a block filler only if it is a single child of a block element.
//
// @param {Node} domNode DOM node.
// @param {Array.<String>} blockElements
// @returns {Boolean}
function isNbspBlockFiller(domNode, blockElements) {
    const isNBSP = domNode.isEqualNode(NBSP_FILLER_REF);
    return isNBSP && hasBlockParent(domNode, blockElements) && domNode.parentNode.childNodes.length === 1;
}
// Checks if domNode has block parent.
//
// @param {Node} domNode DOM node.
// @param {Array.<String>} blockElements
// @returns {Boolean}
function hasBlockParent(domNode, blockElements) {
    const parent = domNode.parentNode;
    return !!parent && !!parent.tagName && blockElements.includes(parent.tagName.toLowerCase());
}
// Log to console the information about element that was replaced.
// Check UNSAFE_ELEMENTS for all recognized unsafe elements.
//
// @param {String} elementName The name of the view element
function _logUnsafeElement(elementName) {
    if (elementName === 'script') {
        logWarning('domconverter-unsafe-script-element-detected');
    }
    if (elementName === 'style') {
        logWarning('domconverter-unsafe-style-element-detected');
    }
}
/**
 * While rendering the editor content, the {@link module:engine/view/domconverter~DomConverter} detected a `<script>` element that may
 * disrupt the editing experience. To avoid this, the `<script>` element was replaced with `<span data-ck-unsafe-element="script"></span>`.
 *
 * @error domconverter-unsafe-script-element-detected
 */
/**
 * While rendering the editor content, the {@link module:engine/view/domconverter~DomConverter} detected a `<style>` element that may affect
 * the editing experience. To avoid this, the `<style>` element was replaced with `<span data-ck-unsafe-element="style"></span>`.
 *
 * @error domconverter-unsafe-style-element-detected
 */
/**
 * The {@link module:engine/view/domconverter~DomConverter} detected an interactive attribute in the
 * {@glink framework/guides/architecture/editing-engine#editing-pipeline editing pipeline}. For the best
 * editing experience, the attribute was renamed to `data-ck-unsafe-attribute-[original attribute name]`.
 *
 * If you are the author of the plugin that generated this attribute and you want it to be preserved
 * in the editing pipeline, you can configure this when creating the element
 * using {@link module:engine/view/downcastwriter~DowncastWriter} during the
 * {@glink framework/guides/architecture/editing-engine#conversion model–view conversion}. Methods such as
 * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement},
 * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement}, or
 * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement}
 * accept an option that will disable filtering of specific attributes:
 *
 *		const paragraph = writer.createContainerElement( 'p',
 *			{
 *				class: 'clickable-paragraph',
 *				onclick: 'alert( "Paragraph clicked!" )'
 *			},
 *			{
 *				// Make sure the "onclick" attribute will pass through.
 *				renderUnsafeAttributes: [ 'onclick' ]
 *			}
 *		);
 *
 * @error domconverter-unsafe-attribute-detected
 * @param {HTMLElement} domElement The DOM element the attribute was set on.
 * @param {String} key The original name of the attribute
 * @param {String} value The value of the original attribute
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/dom/iswindow
 */
/**
 * Checks if the object is a native DOM Window.
 *
 * @param {*} obj
 * @returns {Boolean}
 */
function isWindow(obj) {
    const stringifiedObject = Object.prototype.toString.apply(obj);
    // Returns `true` for the `window` object in browser environments.
    if (stringifiedObject == '[object Window]') {
        return true;
    }
    // Returns `true` for the `window` object in the Electron environment.
    if (stringifiedObject == '[object global]') {
        return true;
    }
    return false;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Mixin that injects the DOM events API into its host. It provides the API
 * compatible with {@link module:utils/emittermixin~EmitterMixin}.
 *
 * DOM emitter mixin is by default available in the {@link module:ui/view~View} class,
 * but it can also be mixed into any other class:
 *
 *		import mix from '../utils/mix.js';
 *		import DomEmitterMixin from '../utils/dom/emittermixin.js';
 *		import { Emitter } from '../utils/emittermixin.js';
 *
 *		class SomeView extends DomEmitterMixin( Emitter ) {}
 *
 *		const view = new SomeView();
 *		view.listenTo( domElement, ( evt, domEvt ) => {
 *			console.log( evt, domEvt );
 *		} );
 *
 * @mixin EmitterMixin
 * @mixes module:utils/emittermixin~EmitterMixin
 * @implements module:utils/dom/emittermixin~Emitter
 */
function DomEmitterMixin(base) {
    class Mixin extends base {
        listenTo(emitter, event, callback, options = {}) {
            // Check if emitter is an instance of DOM Node. If so, use corresponding ProxyEmitter (or create one if not existing).
            if (isNode(emitter) || isWindow(emitter)) {
                const proxyOptions = {
                    capture: !!options.useCapture,
                    passive: !!options.usePassive
                };
                const proxyEmitter = this._getProxyEmitter(emitter, proxyOptions) || new ProxyEmitter(emitter, proxyOptions);
                this.listenTo(proxyEmitter, event, callback, options);
            }
            else {
                // Execute parent class method with Emitter (or ProxyEmitter) instance.
                Emitter$1.prototype.listenTo.call(this, emitter, event, callback, options);
            }
        }
        stopListening(emitter, event, callback) {
            // Check if the emitter is an instance of DOM Node. If so, forward the call to the corresponding ProxyEmitters.
            if (isNode(emitter) || isWindow(emitter)) {
                const proxyEmitters = this._getAllProxyEmitters(emitter);
                for (const proxy of proxyEmitters) {
                    this.stopListening(proxy, event, callback);
                }
            }
            else {
                // Execute parent class method with Emitter (or ProxyEmitter) instance.
                Emitter$1.prototype.stopListening.call(this, emitter, event, callback);
            }
        }
        /**
         * Retrieves ProxyEmitter instance for given DOM Node residing in this Host and given options.
         *
         * @private
         * @param {Node|Window} node DOM Node of the ProxyEmitter.
         * @param {Object} [options] Additional options.
         * @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered
         * listener before being dispatched to any EventTarget beneath it in the DOM tree.
         * @param {Boolean} [options.usePassive=false] Indicates that the function specified by listener will never call preventDefault()
         * and prevents blocking browser's main thread by this event handler.
         * @returns {module:utils/dom/emittermixin~ProxyEmitter|null} ProxyEmitter instance bound to the DOM Node.
         */
        _getProxyEmitter(node, options) {
            return _getEmitterListenedTo(this, getProxyEmitterId(node, options));
        }
        /**
         * Retrieves all the ProxyEmitter instances for given DOM Node residing in this Host.
         *
         * @private
         * @param {Node|Window} node DOM Node of the ProxyEmitter.
         * @returns {Array.<module:utils/dom/emittermixin~ProxyEmitter>}
         */
        _getAllProxyEmitters(node) {
            return [
                { capture: false, passive: false },
                { capture: false, passive: true },
                { capture: true, passive: false },
                { capture: true, passive: true }
            ].map(options => this._getProxyEmitter(node, options)).filter(proxy => !!proxy);
        }
    }
    return Mixin;
}
const Emitter = DomEmitterMixin(Emitter$1);
// Backward compatibility with `mix`
([
    '_getProxyEmitter', '_getAllProxyEmitters',
    'on', 'once', 'off', 'listenTo',
    'stopListening', 'fire', 'delegate', 'stopDelegating',
    '_addEventListener', '_removeEventListener'
]).forEach(key => {
    DomEmitterMixin[key] = Emitter.prototype[key];
});
/**
 * Creates a ProxyEmitter instance. Such an instance is a bridge between a DOM Node firing events
 * and any Host listening to them. It is backwards compatible with {@link module:utils/emittermixin~EmitterMixin#on}.
 * There is a separate instance for each combination of modes (useCapture & usePassive). The mode is concatenated with
 * UID stored in HTMLElement to give each instance unique identifier.
 *
 *                                  listenTo( click, ... )
 *                    +-----------------------------------------+
 *                    |              stopListening( ... )       |
 *     +----------------------------+                           |             addEventListener( click, ... )
 *     | Host                       |                           |   +---------------------------------------------+
 *     +----------------------------+                           |   |       removeEventListener( click, ... )     |
 *     | _listeningTo: {            |                +----------v-------------+                                   |
 *     |   UID+mode: {              |                | ProxyEmitter           |                                   |
 *     |     emitter: ProxyEmitter, |                +------------------------+                      +------------v----------+
 *     |     callbacks: {           |                | events: {              |                      | Node (HTMLElement)    |
 *     |       click: [ callbacks ] |                |   click: [ callbacks ] |                      +-----------------------+
 *     |     }                      |                | },                     |                      | data-ck-expando: UID  |
 *     |   }                        |                | _domNode: Node,        |                      +-----------------------+
 *     | }                          |                | _domListeners: {},     |                                   |
 *     | +------------------------+ |                | _emitterId: UID+mode   |                                   |
 *     | | DomEmitterMixin        | |                +--------------^---------+                                   |
 *     | +------------------------+ |                           |   |                                             |
 *     +--------------^-------------+                           |   +---------------------------------------------+
 *                    |                                         |                  click (DOM Event)
 *                    +-----------------------------------------+
 *                                fire( click, DOM Event )
 *
 * @mixes module:utils/emittermixin~EmitterMixin
 * @implements module:utils/dom/emittermixin~Emitter
 * @private
 */
class ProxyEmitter extends Emitter$1 {
    /**
     * @param {Node|Window} node DOM Node that fires events.
     * @param {Object} [options] Additional options.
     * @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered
     * listener before being dispatched to any EventTarget beneath it in the DOM tree.
     * @param {Boolean} [options.usePassive=false] Indicates that the function specified by listener will never call preventDefault()
     * and prevents blocking browser's main thread by this event handler.
     */
    constructor(node, options) {
        super();
        // Set emitter ID to match DOM Node "expando" property.
        _setEmitterId(this, getProxyEmitterId(node, options));
        // Remember the DOM Node this ProxyEmitter is bound to.
        this._domNode = node;
        // And given options.
        this._options = options;
    }
    /**
     * Registers a callback function to be executed when an event is fired.
     *
     * It attaches a native DOM listener to the DOM Node. When fired,
     * a corresponding Emitter event will also fire with DOM Event object as an argument.
     *
     * **Note**: This is automatically called by the
     * {@link module:utils/emittermixin~EmitterMixin#listenTo `EmitterMixin#listenTo()`}.
     *
     * @method module:utils/dom/emittermixin~ProxyEmitter#attach
     * @param {String} event The name of the event.
     */
    attach(event) {
        // If the DOM Listener for given event already exist it is pointless
        // to attach another one.
        if (this._domListeners && this._domListeners[event]) {
            return;
        }
        const domListener = this._createDomListener(event);
        // Attach the native DOM listener to DOM Node.
        this._domNode.addEventListener(event, domListener, this._options);
        if (!this._domListeners) {
            this._domListeners = {};
        }
        // Store the native DOM listener in this ProxyEmitter. It will be helpful
        // when stopping listening to the event.
        this._domListeners[event] = domListener;
    }
    /**
     * Stops executing the callback on the given event.
     *
     * **Note**: This is automatically called by the
     * {@link module:utils/emittermixin~EmitterMixin#stopListening `EmitterMixin#stopListening()`}.
     *
     * @method module:utils/dom/emittermixin~ProxyEmitter#detach
     * @param {String} event The name of the event.
     */
    detach(event) {
        let events;
        // Remove native DOM listeners which are orphans. If no callbacks
        // are awaiting given event, detach native DOM listener from DOM Node.
        // See: {@link attach}.
        if (this._domListeners[event] && (!(events = this._events[event]) || !events.callbacks.length)) {
            this._domListeners[event].removeListener();
        }
    }
    /**
     * Adds callback to emitter for given event.
     *
     * @protected
     * @method module:utils/dom/emittermixin~ProxyEmitter#_addEventListener
     * @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.
     */
    _addEventListener(event, callback, options) {
        this.attach(event);
        Emitter$1.prototype._addEventListener.call(this, event, callback, options);
    }
    /**
     * Removes callback from emitter for given event.
     *
     * @protected
     * @method module:utils/dom/emittermixin~ProxyEmitter#_removeEventListener
     * @param {String} event The name of the event.
     * @param {Function} callback The function to stop being called.
     */
    _removeEventListener(event, callback) {
        Emitter$1.prototype._removeEventListener.call(this, event, callback);
        this.detach(event);
    }
    /**
     * Creates a native DOM listener callback. When the native DOM event
     * is fired it will fire corresponding event on this ProxyEmitter.
     * Note: A native DOM Event is passed as an argument.
     *
     * @private
     * @method module:utils/dom/emittermixin~ProxyEmitter#_createDomListener
     * @param {String} event The name of the event.
     * @returns {Function} The DOM listener callback.
     */
    _createDomListener(event) {
        const domListener = (domEvt) => {
            this.fire(event, domEvt);
        };
        // Supply the DOM listener callback with a function that will help
        // detach it from the DOM Node, when it is no longer necessary.
        // See: {@link detach}.
        domListener.removeListener = () => {
            this._domNode.removeEventListener(event, domListener, this._options);
            delete this._domListeners[event];
        };
        return domListener;
    }
}
// Gets an unique DOM Node identifier. The identifier will be set if not defined.
//
// @private
// @param {Node} node
// @returns {String} UID for given DOM Node.
function getNodeUID(node) {
    return node['data-ck-expando'] || (node['data-ck-expando'] = uid());
}
// Gets id of the ProxyEmitter for the given node.
//
// Combines DOM Node identifier and additional options.
//
// @private
// @param {Node} node
// @param {Object} options Additional options.
// @returns {String} ProxyEmitter id.
function getProxyEmitterId(node, options) {
    let id = getNodeUID(node);
    for (const option of Object.keys(options).sort()) {
        if (options[option]) {
            id += '-' + option;
        }
    }
    return id;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Abstract base observer class. Observers are classes which listen to DOM events, do the preliminary
 * processing and fire events on the {@link module:engine/view/document~Document} objects.
 * Observers can also add features to the view, for instance by updating its status or marking elements
 * which need a refresh on DOM events.
 *
 * @abstract
 */
class Observer extends Emitter {
    /**
     * Creates an instance of the observer.
     *
     * @param {module:engine/view/view~View} view
     */
    constructor(view) {
        super();
        /**
         * An instance of the view controller.
         *
         * @readonly
         * @member {module:engine/view/view~View}
         */
        this.view = view;
        /**
         * A reference to the {@link module:engine/view/document~Document} object.
         *
         * @readonly
         * @member {module:engine/view/document~Document}
         */
        this.document = view.document;
        /**
         * The state of the observer. If it is disabled, no events will be fired.
         *
         * @readonly
         * @member {Boolean}
         */
        this.isEnabled = false;
    }
    /**
     * Enables the observer. This method is called when the observer is registered to the
     * {@link module:engine/view/view~View} and after {@link module:engine/view/view~View#forceRender rendering}
     * (all observers are {@link #disable disabled} before rendering).
     *
     * A typical use case for disabling observers is that mutation observers need to be disabled for the rendering.
     * However, a child class may not need to be disabled, so it can implement an empty method.
     *
     * @see module:engine/view/observer/observer~Observer#disable
     */
    enable() {
        this.isEnabled = true;
    }
    /**
     * Disables the observer. This method is called before
     * {@link module:engine/view/view~View#forceRender rendering} to prevent firing events during rendering.
     *
     * @see module:engine/view/observer/observer~Observer#enable
     */
    disable() {
        this.isEnabled = false;
    }
    /**
     * Disables and destroys the observer, among others removes event listeners created by the observer.
     */
    destroy() {
        this.disable();
        this.stopListening();
    }
    /**
     * Checks whether a given DOM event should be ignored (should not be turned into a synthetic view document event).
     *
     * Currently, an event will be ignored only if its target or any of its ancestors has the `data-cke-ignore-events` attribute.
     * This attribute can be used inside the structures generated by
     * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement `DowncastWriter#createUIElement()`} to ignore events
     * fired within a UI that should be excluded from CKEditor 5's realms.
     *
     * @param {Node} domTarget The DOM event target to check (usually an element, sometimes a text node and
     * potentially sometimes a document, too).
     * @returns {Boolean} Whether this event should be ignored by the observer.
     */
    checkShouldIgnoreEventFromTarget(domTarget) {
        if (domTarget && domTarget.nodeType === 3) {
            domTarget = domTarget.parentNode;
        }
        if (!domTarget || domTarget.nodeType !== 1) {
            return false;
        }
        return domTarget.matches('[data-cke-ignore-events], [data-cke-ignore-events] *');
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Mutation observer class observes changes in the DOM, fires {@link module:engine/view/document~Document#event:mutations} event, mark view
 * elements as changed and call {@link module:engine/view/renderer~Renderer#render}.
 * Because all mutated nodes are marked as "to be rendered" and the
 * {@link module:engine/view/renderer~Renderer#render} is called, all changes will be reverted, unless the mutation will be handled by the
 * {@link module:engine/view/document~Document#event:mutations} event listener. It means user will see only handled changes, and the editor
 * will block all changes which are not handled.
 *
 * Mutation Observer also take care of reducing number of mutations which are fired. It removes duplicates and
 * mutations on elements which do not have corresponding view elements. Also
 * {@link module:engine/view/observer/mutationobserver~MutatedText text mutation} is fired only if parent element do not change child list.
 *
 * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
 *
 * @extends module:engine/view/observer/observer~Observer
 */
class MutationObserver extends Observer {
    constructor(view) {
        super(view);
        /**
         * Native mutation observer config.
         *
         * @private
         * @member {Object}
         */
        this._config = {
            childList: true,
            characterData: true,
            characterDataOldValue: true,
            subtree: true
        };
        /**
         * Reference to the {@link module:engine/view/view~View#domConverter}.
         *
         * @member {module:engine/view/domconverter~DomConverter}
         */
        this.domConverter = view.domConverter;
        /**
         * Reference to the {@link module:engine/view/view~View#_renderer}.
         *
         * @member {module:engine/view/renderer~Renderer}
         */
        this.renderer = view._renderer;
        /**
         * Observed DOM elements.
         *
         * @private
         * @member {Array.<HTMLElement>}
         */
        this._domElements = [];
        /**
         * Native mutation observer.
         *
         * @private
         * @member {MutationObserver}
         */
        this._mutationObserver = new window.MutationObserver(this._onMutations.bind(this));
    }
    /**
     * Synchronously fires {@link module:engine/view/document~Document#event:mutations} event with all mutations in record queue.
     * At the same time empties the queue so mutations will not be fired twice.
     */
    flush() {
        this._onMutations(this._mutationObserver.takeRecords());
    }
    /**
     * @inheritDoc
     */
    observe(domElement) {
        this._domElements.push(domElement);
        if (this.isEnabled) {
            this._mutationObserver.observe(domElement, this._config);
        }
    }
    /**
     * @inheritDoc
     */
    enable() {
        super.enable();
        for (const domElement of this._domElements) {
            this._mutationObserver.observe(domElement, this._config);
        }
    }
    /**
     * @inheritDoc
     */
    disable() {
        super.disable();
        this._mutationObserver.disconnect();
    }
    /**
     * @inheritDoc
     */
    destroy() {
        super.destroy();
        this._mutationObserver.disconnect();
    }
    /**
     * Handles mutations. Deduplicates, mark view elements to sync, fire event and call render.
     *
     * @private
     * @param {Array.<Object>} domMutations Array of native mutations.
     */
    _onMutations(domMutations) {
        // As a result of this.flush() we can have an empty collection.
        if (domMutations.length === 0) {
            return;
        }
        const domConverter = this.domConverter;
        // Use map and set for deduplication.
        const mutatedTexts = new Map();
        const mutatedElements = new Set();
        // Handle `childList` mutations first, so we will be able to check if the `characterData` mutation is in the
        // element with changed structure anyway.
        for (const mutation of domMutations) {
            if (mutation.type === 'childList') {
                const element = domConverter.mapDomToView(mutation.target);
                // Do not collect mutations from UIElements and RawElements.
                if (element && (element.is('uiElement') || element.is('rawElement'))) {
                    continue;
                }
                if (element && !this._isBogusBrMutation(mutation)) {
                    mutatedElements.add(element);
                }
            }
        }
        // Handle `characterData` mutations later, when we have the full list of nodes which changed structure.
        for (const mutation of domMutations) {
            const element = domConverter.mapDomToView(mutation.target);
            // Do not collect mutations from UIElements and RawElements.
            if (element && (element.is('uiElement') || element.is('rawElement'))) {
                continue;
            }
            if (mutation.type === 'characterData') {
                const text = domConverter.findCorrespondingViewText(mutation.target);
                if (text && !mutatedElements.has(text.parent)) {
                    // Use text as a key, for deduplication. If there will be another mutation on the same text element
                    // we will have only one in the map.
                    mutatedTexts.set(text, {
                        type: 'text',
                        oldText: text.data,
                        newText: getDataWithoutFiller(mutation.target),
                        node: text
                    });
                }
                // When we added first letter to the text node which had only inline filler, for the DOM it is mutation
                // on text, but for the view, where filler text node did not existed, new text node was created, so we
                // need to fire 'children' mutation instead of 'text'.
                else if (!text && startsWithFiller(mutation.target)) {
                    mutatedElements.add(domConverter.mapDomToView(mutation.target.parentNode));
                }
            }
        }
        // Now we build the list of mutations to fire and mark elements. We did not do it earlier to avoid marking the
        // same node multiple times in case of duplication.
        // List of mutations we will fire.
        const viewMutations = [];
        for (const mutatedText of mutatedTexts.values()) {
            this.renderer.markToSync('text', mutatedText.node);
            viewMutations.push(mutatedText);
        }
        for (const viewElement of mutatedElements) {
            const domElement = domConverter.mapViewToDom(viewElement);
            const viewChildren = Array.from(viewElement.getChildren());
            const newViewChildren = Array.from(domConverter.domChildrenToView(domElement, { withChildren: false }));
            // It may happen that as a result of many changes (sth was inserted and then removed),
            // both elements haven't really changed. #1031
            if (!isEqualWith(viewChildren, newViewChildren, sameNodes)) {
                this.renderer.markToSync('children', viewElement);
                viewMutations.push({
                    type: 'children',
                    oldChildren: viewChildren,
                    newChildren: newViewChildren,
                    node: viewElement
                });
            }
        }
        // Retrieve `domSelection` using `ownerDocument` of one of mutated nodes.
        // There should not be simultaneous mutation in multiple documents, so it's fine.
        const domSelection = domMutations[0].target.ownerDocument.getSelection();
        let viewSelection = null;
        if (domSelection && domSelection.anchorNode) {
            // If `domSelection` is inside a dom node that is already bound to a view node from view tree, get
            // corresponding selection in the view and pass it together with `viewMutations`. The `viewSelection` may
            // be used by features handling mutations.
            // Only one range is supported.
            const viewSelectionAnchor = domConverter.domPositionToView(domSelection.anchorNode, domSelection.anchorOffset);
            const viewSelectionFocus = domConverter.domPositionToView(domSelection.focusNode, domSelection.focusOffset);
            // Anchor and focus has to be properly mapped to view.
            if (viewSelectionAnchor && viewSelectionFocus) {
                viewSelection = new Selection$1(viewSelectionAnchor);
                viewSelection.setFocus(viewSelectionFocus);
            }
        }
        // In case only non-relevant mutations were recorded it skips the event and force render (#5600).
        if (viewMutations.length) {
            this.document.fire('mutations', viewMutations, viewSelection);
            // If nothing changes on `mutations` event, at this point we have "dirty DOM" (changed) and de-synched
            // view (which has not been changed). In order to "reset DOM" we render the view again.
            this.view.forceRender();
        }
        function sameNodes(child1, child2) {
            // First level of comparison (array of children vs array of children) – use the Lodash's default behavior.
            if (Array.isArray(child1)) {
                return;
            }
            // Elements.
            if (child1 === child2) {
                return true;
            }
            // Texts.
            else if (child1.is('$text') && child2.is('$text')) {
                return child1.data === child2.data;
            }
            // Not matching types.
            return false;
        }
    }
    /**
     * Checks if mutation was generated by the browser inserting bogus br on the end of the block element.
     * Such mutations are generated while pressing space or performing native spellchecker correction
     * on the end of the block element in Firefox browser.
     *
     * @private
     * @param {Object} mutation Native mutation object.
     * @returns {Boolean}
     */
    _isBogusBrMutation(mutation) {
        let addedNode = null;
        // Check if mutation added only one node on the end of its parent.
        if (mutation.nextSibling === null && mutation.removedNodes.length === 0 && mutation.addedNodes.length == 1) {
            addedNode = this.domConverter.domToView(mutation.addedNodes[0], {
                withChildren: false
            });
        }
        return addedNode && addedNode.is('element', 'br');
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Information about a DOM event in context of the {@link module:engine/view/document~Document}.
 * It wraps the native event, which usually should not be used as the wrapper contains
 * additional data (like key code for keyboard events).
 */
class DomEventData {
    /**
     * @param {module:engine/view/view~View} view The instance of the view controller.
     * @param {Event} domEvent The DOM event.
     * @param {Object} [additionalData] Additional properties that the instance should contain.
     */
    constructor(view, domEvent, additionalData) {
        /**
         * Instance of the view controller.
         *
         * @readonly
         * @member {module:engine/view/view~View} module:engine/view/observer/observer~Observer.DomEvent#view
         */
        this.view = view;
        /**
         * The instance of the document.
         *
         * @readonly
         * @member {module:engine/view/document~Document} module:engine/view/observer/observer~Observer.DomEvent#document
         */
        this.document = view.document;
        /**
         * The DOM event.
         *
         * @readonly
         * @member {Event} module:engine/view/observer/observer~Observer.DomEvent#domEvent
         */
        this.domEvent = domEvent;
        /**
         * The DOM target.
         *
         * @readonly
         * @member {HTMLElement} module:engine/view/observer/observer~Observer.DomEvent#target
         */
        this.domTarget = domEvent.target;
        extend(this, additionalData);
    }
    /**
     * The tree view element representing the target.
     *
     * @readonly
     * @type module:engine/view/element~Element
     */
    get target() {
        return this.view.domConverter.mapDomToView(this.domTarget);
    }
    /**
     * Prevents the native's event default action.
     */
    preventDefault() {
        this.domEvent.preventDefault();
    }
    /**
     * Stops native event propagation.
     */
    stopPropagation() {
        this.domEvent.stopPropagation();
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Base class for DOM event observers. This class handles
 * {@link module:engine/view/observer/observer~Observer#observe adding} listeners to DOM elements,
 * {@link module:engine/view/observer/observer~Observer#disable disabling} and
 * {@link module:engine/view/observer/observer~Observer#enable re-enabling} events.
 * Child class needs to define
 * {@link module:engine/view/observer/domeventobserver~DomEventObserver#domEventType DOM event type} and
 * {@link module:engine/view/observer/domeventobserver~DomEventObserver#onDomEvent callback}.
 *
 * For instance:
 *
 *		class ClickObserver extends DomEventObserver {
 *			// It can also be defined as a normal property in the constructor.
 *			get domEventType() {
 *				return 'click';
 *			}
 *
 *			onDomEvent( domEvent ) {
 *				this.fire( 'click', domEvent );
 *			}
 *		}
 *
 * @extends module:engine/view/observer/observer~Observer
 */
class DomEventObserver extends Observer {
    /**
     * @inheritDoc
     */
    constructor(view) {
        super(view);
        /**
         * If set to `true` DOM events will be listened on the capturing phase.
         * Default value is `false`.
         *
         * @member {Boolean}
         */
        this.useCapture = false;
    }
    /**
     * @inheritDoc
     */
    observe(domElement) {
        const types = typeof this.domEventType == 'string' ? [this.domEventType] : this.domEventType;
        types.forEach(type => {
            this.listenTo(domElement, type, (eventInfo, domEvent) => {
                if (this.isEnabled && !this.checkShouldIgnoreEventFromTarget(domEvent.target)) {
                    this.onDomEvent(domEvent);
                }
            }, { useCapture: this.useCapture });
        });
    }
    /**
     * Calls `Document#fire()` if observer {@link #isEnabled is enabled}.
     *
     * @see module:utils/emittermixin~EmitterMixin#fire
     * @param {String} eventType The event type (name).
     * @param {Event} domEvent The DOM event.
     * @param {Object} [additionalData] The additional data which should extend the
     * {@link module:engine/view/observer/domeventdata~DomEventData event data} object.
     */
    fire(eventType, domEvent, additionalData) {
        if (this.isEnabled) {
            this.document.fire(eventType, new DomEventData(this.view, domEvent, additionalData));
        }
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Observer for events connected with pressing keyboard keys.
 *
 * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
 *
 * @extends module:engine/view/observer/domeventobserver~DomEventObserver
 */
class KeyObserver extends DomEventObserver {
    constructor(view) {
        super(view);
        this.domEventType = ['keydown', 'keyup'];
    }
    onDomEvent(domEvt) {
        const data = {
            keyCode: domEvt.keyCode,
            altKey: domEvt.altKey,
            ctrlKey: domEvt.ctrlKey,
            shiftKey: domEvt.shiftKey,
            metaKey: domEvt.metaKey,
            get keystroke() {
                return getCode(this);
            }
        };
        this.fire(domEvt.type, domEvt, data);
    }
}
/**
 * Fired when a key has been pressed.
 *
 * Introduced by {@link module:engine/view/observer/keyobserver~KeyObserver}.
 *
 * Note that because {@link module:engine/view/observer/keyobserver~KeyObserver} is attached by the
 * {@link module:engine/view/view~View} this event is available by default.
 *
 * @see module:engine/view/observer/keyobserver~KeyObserver
 * @event module:engine/view/document~Document#event:keydown
 * @param {module:engine/view/observer/keyobserver~KeyEventData} keyEventData
 */
/**
 * Fired when a key has been released.
 *
 * Introduced by {@link module:engine/view/observer/keyobserver~KeyObserver}.
 *
 * Note that because {@link module:engine/view/observer/keyobserver~KeyObserver} is attached by the
 * {@link module:engine/view/view~View} this event is available by default.
 *
 * @see module:engine/view/observer/keyobserver~KeyObserver
 * @event module:engine/view/document~Document#event:keyup
 * @param {module:engine/view/observer/keyobserver~KeyEventData} keyEventData
 */
/**
 * The value of both events - {@link module:engine/view/document~Document#event:keydown} and
 * {@link module:engine/view/document~Document#event:keyup}.
 *
 * @class module:engine/view/observer/keyobserver~KeyEventData
 * @extends module:engine/view/observer/domeventdata~DomEventData
 * @implements module:utils/keyboard~KeystrokeInfo
 */
/**
 * Code of the whole keystroke. See {@link module:utils/keyboard~getCode}.
 *
 * @readonly
 * @member {Number} module:engine/view/observer/keyobserver~KeyEventData#keystroke
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Fake selection observer class. If view selection is fake it is placed in dummy DOM container. This observer listens
 * on {@link module:engine/view/document~Document#event:keydown keydown} events and handles moving fake view selection to the correct place
 * if arrow keys are pressed.
 * Fires {@link module:engine/view/document~Document#event:selectionChange selectionChange event} simulating natural behaviour of
 * {@link module:engine/view/observer/selectionobserver~SelectionObserver SelectionObserver}.
 *
 * @extends module:engine/view/observer/observer~Observer
 */
class FakeSelectionObserver extends Observer {
    /**
     * Creates new FakeSelectionObserver instance.
     *
     * @param {module:engine/view/view~View} view
     */
    constructor(view) {
        super(view);
        /**
         * Fires debounced event `selectionChangeDone`. It uses `lodash#debounce` method to delay function call.
         *
         * @private
         * @param {Object} data Selection change data.
         * @method #_fireSelectionChangeDoneDebounced
         */
        this._fireSelectionChangeDoneDebounced = debounce(data => {
            this.document.fire('selectionChangeDone', data);
        }, 200);
    }
    /**
     * @inheritDoc
     */
    observe() {
        const document = this.document;
        document.on('arrowKey', (eventInfo, data) => {
            const selection = document.selection;
            if (selection.isFake && this.isEnabled) {
                // Prevents default key down handling - no selection change will occur.
                data.preventDefault();
            }
        }, { context: '$capture' });
        document.on('arrowKey', (eventInfo, data) => {
            const selection = document.selection;
            if (selection.isFake && this.isEnabled) {
                this._handleSelectionMove(data.keyCode);
            }
        }, { priority: 'lowest' });
    }
    /**
     * @inheritDoc
     */
    destroy() {
        super.destroy();
        this._fireSelectionChangeDoneDebounced.cancel();
    }
    /**
     * Handles collapsing view selection according to given key code. If left or up key is provided - new selection will be
     * collapsed to left. If right or down key is pressed - new selection will be collapsed to right.
     *
     * This method fires {@link module:engine/view/document~Document#event:selectionChange} and
     * {@link module:engine/view/document~Document#event:selectionChangeDone} events imitating behaviour of
     * {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
     *
     * @private
     * @param {Number} keyCode
     * @fires module:engine/view/document~Document#event:selectionChange
     * @fires module:engine/view/document~Document#event:selectionChangeDone
     */
    _handleSelectionMove(keyCode) {
        const selection = this.document.selection;
        const newSelection = new Selection$1(selection.getRanges(), { backward: selection.isBackward, fake: false });
        // Left or up arrow pressed - move selection to start.
        if (keyCode == keyCodes.arrowleft || keyCode == keyCodes.arrowup) {
            newSelection.setTo(newSelection.getFirstPosition());
        }
        // Right or down arrow pressed - move selection to end.
        if (keyCode == keyCodes.arrowright || keyCode == keyCodes.arrowdown) {
            newSelection.setTo(newSelection.getLastPosition());
        }
        const data = {
            oldSelection: selection,
            newSelection,
            domSelection: null
        };
        // Fire dummy selection change event.
        this.document.fire('selectionChange', data);
        // Call` #_fireSelectionChangeDoneDebounced` every time when `selectionChange` event is fired.
        // This function is debounced what means that `selectionChangeDone` event will be fired only when
        // defined int the function time will elapse since the last time the function was called.
        // So `selectionChangeDone` will be fired when selection will stop changing.
        this._fireSelectionChangeDoneDebounced(data);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Selection observer class observes selection changes in the document. If a selection changes on the document this
 * observer checks if there are any mutations and if the DOM selection is different from the
 * {@link module:engine/view/document~Document#selection view selection}. The selection observer fires
 * {@link module:engine/view/document~Document#event:selectionChange} event only if a selection change was the only change in the document
 * and the DOM selection is different then the view selection.
 *
 * This observer also manages the {@link module:engine/view/document~Document#isSelecting} property of the view document.
 *
 * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
 *
 * @see module:engine/view/observer/mutationobserver~MutationObserver
 * @extends module:engine/view/observer/observer~Observer
 */
class SelectionObserver extends Observer {
    constructor(view) {
        super(view);
        /**
         * Instance of the mutation observer. Selection observer calls
         * {@link module:engine/view/observer/mutationobserver~MutationObserver#flush} to ensure that the mutations will be handled
         * before the {@link module:engine/view/document~Document#event:selectionChange} event is fired.
         *
         * @readonly
         * @member {module:engine/view/observer/mutationobserver~MutationObserver}
         * module:engine/view/observer/selectionobserver~SelectionObserver#mutationObserver
         */
        this.mutationObserver = view.getObserver(MutationObserver);
        /**
         * Reference to the view {@link module:engine/view/documentselection~DocumentSelection} object used to compare
         * new selection with it.
         *
         * @readonly
         * @member {module:engine/view/documentselection~DocumentSelection}
         * module:engine/view/observer/selectionobserver~SelectionObserver#selection
         */
        this.selection = this.document.selection;
        /* eslint-disable max-len */
        /**
         * Reference to the {@link module:engine/view/view~View#domConverter}.
         *
         * @readonly
         * @member {module:engine/view/domconverter~DomConverter} module:engine/view/observer/selectionobserver~SelectionObserver#domConverter
         */
        /* eslint-enable max-len */
        this.domConverter = view.domConverter;
        /**
         * A set of documents which have added `selectionchange` listener to avoid adding a listener twice to the same
         * document.
         *
         * @private
         * @member {WeakSet.<Document>} module:engine/view/observer/selectionobserver~SelectionObserver#_documents
         */
        this._documents = new WeakSet();
        /**
         * Fires debounced event `selectionChangeDone`. It uses `lodash#debounce` method to delay function call.
         *
         * @private
         * @param {Object} data Selection change data.
         * @method #_fireSelectionChangeDoneDebounced
         */
        this._fireSelectionChangeDoneDebounced = debounce(data => {
            this.document.fire('selectionChangeDone', data);
        }, 200);
        /**
         * When called, starts clearing the {@link #_loopbackCounter} counter in time intervals. When the number of selection
         * changes exceeds a certain limit within the interval of time, the observer will not fire `selectionChange` but warn about
         * possible infinite selection loop.
         *
         * @private
         * @member {Number} #_clearInfiniteLoopInterval
         */
        this._clearInfiniteLoopInterval = setInterval(() => this._clearInfiniteLoop(), 1000);
        /**
         * Unlocks the `isSelecting` state of the view document in case the selection observer did not record this fact
         * correctly (for whatever reason). It is a safeguard (paranoid check), that returns document to the normal state
         * after a certain period of time (debounced, postponed by each selectionchange event).
         *
         * @private
         * @method #_documentIsSelectingInactivityTimeoutDebounced
         */
        this._documentIsSelectingInactivityTimeoutDebounced = debounce(() => (this.document.isSelecting = false), 5000);
        /**
         * Private property to check if the code does not enter infinite loop.
         *
         * @private
         * @member {Number} module:engine/view/observer/selectionobserver~SelectionObserver#_loopbackCounter
         */
        this._loopbackCounter = 0;
    }
    /**
     * @inheritDoc
     */
    observe(domElement) {
        const domDocument = domElement.ownerDocument;
        const startDocumentIsSelecting = () => {
            this.document.isSelecting = true;
            // Let's activate the safety timeout each time the document enters the "is selecting" state.
            this._documentIsSelectingInactivityTimeoutDebounced();
        };
        const endDocumentIsSelecting = () => {
            this.document.isSelecting = false;
            // The safety timeout can be canceled when the document leaves the "is selecting" state.
            this._documentIsSelectingInactivityTimeoutDebounced.cancel();
        };
        // The document has the "is selecting" state while the user keeps making (extending) the selection
        // (e.g. by holding the mouse button and moving the cursor). The state resets when they either released
        // the mouse button or interrupted the process by pressing or releasing any key.
        this.listenTo(domElement, 'selectstart', startDocumentIsSelecting, { priority: 'highest' });
        this.listenTo(domElement, 'keydown', endDocumentIsSelecting, { priority: 'highest' });
        this.listenTo(domElement, 'keyup', endDocumentIsSelecting, { priority: 'highest' });
        // Add document-wide listeners only once. This method could be called for multiple editing roots.
        if (this._documents.has(domDocument)) {
            return;
        }
        this.listenTo(domDocument, 'mouseup', endDocumentIsSelecting, { priority: 'highest' });
        this.listenTo(domDocument, 'selectionchange', (evt, domEvent) => {
            this._handleSelectionChange(domEvent, domDocument);
            // Defer the safety timeout when the selection changes (e.g. the user keeps extending the selection
            // using their mouse).
            this._documentIsSelectingInactivityTimeoutDebounced();
        });
        this._documents.add(domDocument);
    }
    /**
     * @inheritDoc
     */
    destroy() {
        super.destroy();
        clearInterval(this._clearInfiniteLoopInterval);
        this._fireSelectionChangeDoneDebounced.cancel();
        this._documentIsSelectingInactivityTimeoutDebounced.cancel();
    }
    /**
     * Selection change listener. {@link module:engine/view/observer/mutationobserver~MutationObserver#flush Flush} mutations, check if
     * a selection changes and fires {@link module:engine/view/document~Document#event:selectionChange} event on every change
     * and {@link module:engine/view/document~Document#event:selectionChangeDone} when a selection stop changing.
     *
     * @private
     * @param {Event} domEvent DOM event.
     * @param {Document} domDocument DOM document.
     */
    _handleSelectionChange(domEvent, domDocument) {
        if (!this.isEnabled) {
            return;
        }
        const domSelection = domDocument.defaultView.getSelection();
        if (this.checkShouldIgnoreEventFromTarget(domSelection.anchorNode)) {
            return;
        }
        // Ensure the mutation event will be before selection event on all browsers.
        this.mutationObserver.flush();
        // If there were mutations then the view will be re-rendered by the mutation observer and the selection
        // will be updated, so the selections will equal and the event will not be fired, as expected.
        const newViewSelection = this.domConverter.domSelectionToView(domSelection);
        // Do not convert selection change if the new view selection has no ranges in it.
        //
        // It means that the DOM selection is in some way incorrect. Ranges that were in the DOM selection could not be
        // converted to the view. This happens when the DOM selection was moved outside of the editable element.
        if (newViewSelection.rangeCount == 0) {
            this.view.hasDomSelection = false;
            return;
        }
        this.view.hasDomSelection = true;
        if (this.selection.isEqual(newViewSelection) && this.domConverter.isDomSelectionCorrect(domSelection)) {
            return;
        }
        // Ensure we are not in the infinite loop (#400).
        // This counter is reset each second. 60 selection changes in 1 second is enough high number
        // to be very difficult (impossible) to achieve using just keyboard keys (during normal editor use).
        if (++this._loopbackCounter > 60) {
            // Selection change observer detected an infinite rendering loop.
            // Most probably you try to put the selection in the position which is not allowed
            // by the browser and browser fixes it automatically what causes `selectionchange` event on
            // which a loopback through a model tries to re-render the wrong selection and again.
            //
            // @if CK_DEBUG // console.warn( 'Selection change observer detected an infinite rendering loop.' );
            return;
        }
        if (this.selection.isSimilar(newViewSelection)) {
            // If selection was equal and we are at this point of algorithm, it means that it was incorrect.
            // Just re-render it, no need to fire any events, etc.
            this.view.forceRender();
        }
        else {
            const data = {
                oldSelection: this.selection,
                newSelection: newViewSelection,
                domSelection: domSelection
            };
            // Prepare data for new selection and fire appropriate events.
            this.document.fire('selectionChange', data);
            // Call `#_fireSelectionChangeDoneDebounced` every time when `selectionChange` event is fired.
            // This function is debounced what means that `selectionChangeDone` event will be fired only when
            // defined int the function time will elapse since the last time the function was called.
            // So `selectionChangeDone` will be fired when selection will stop changing.
            this._fireSelectionChangeDoneDebounced(data);
        }
    }
    /**
     * Clears `SelectionObserver` internal properties connected with preventing infinite loop.
     *
     * @protected
     */
    _clearInfiniteLoop() {
        this._loopbackCounter = 0;
    }
}
/**
 * Fired when selection stops changing.
 *
 * Introduced by {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
 *
 * Note that because {@link module:engine/view/observer/selectionobserver~SelectionObserver} is attached by the
 * {@link module:engine/view/view~View} this event is available by default.
 *
 * @see module:engine/view/observer/selectionobserver~SelectionObserver
 * @event module:engine/view/document~Document#event:selectionChangeDone
 * @param {Object} data
 * @param {module:engine/view/documentselection~DocumentSelection} data.oldSelection Old View selection which is
 * {@link module:engine/view/document~Document#selection}.
 * @param {module:engine/view/selection~Selection} data.newSelection New View selection which is converted DOM selection.
 * @param {Selection} data.domSelection Native DOM selection.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * {@link module:engine/view/document~Document#event:focus Focus}
 * and {@link module:engine/view/document~Document#event:blur blur} events observer.
 * Focus observer handle also {@link module:engine/view/rooteditableelement~RootEditableElement#isFocused isFocused} property of the
 * {@link module:engine/view/rooteditableelement~RootEditableElement root elements}.
 *
 * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
 *
 * @extends module:engine/view/observer/domeventobserver~DomEventObserver
 */
class FocusObserver extends DomEventObserver {
    constructor(view) {
        super(view);
        this.domEventType = ['focus', 'blur'];
        this.useCapture = true;
        const document = this.document;
        document.on('focus', () => {
            document.isFocused = true;
            // Unfortunately native `selectionchange` event is fired asynchronously.
            // We need to wait until `SelectionObserver` handle the event and then render. Otherwise rendering will
            // overwrite new DOM selection with selection from the view.
            // See https://github.com/ckeditor/ckeditor5-engine/issues/795 for more details.
            // Long timeout is needed to solve #676 and https://github.com/ckeditor/ckeditor5-engine/issues/1157 issues.
            //
            // Using `view.change()` instead of `view.forceRender()` to prevent double rendering
            // in a situation where `selectionchange` already caused selection change.
            this._renderTimeoutId = setTimeout(() => view.change(() => { }), 50);
        });
        document.on('blur', (evt, data) => {
            const selectedEditable = document.selection.editableElement;
            if (selectedEditable === null || selectedEditable === data.target) {
                document.isFocused = false;
                // Re-render the document to update view elements
                // (changing document.isFocused already marked view as changed since last rendering).
                view.change(() => { });
            }
        });
        /**
         * Identifier of the timeout currently used by focus listener to delay rendering execution.
         *
         * @private
         * @member {Number} #_renderTimeoutId
         */
    }
    onDomEvent(domEvent) {
        this.fire(domEvent.type, domEvent);
    }
    /**
     * @inheritDoc
     */
    destroy() {
        if (this._renderTimeoutId) {
            clearTimeout(this._renderTimeoutId);
        }
        super.destroy();
    }
}
/**
 * Fired when one of the editables gets focus.
 *
 * Introduced by {@link module:engine/view/observer/focusobserver~FocusObserver}.
 *
 * Note that because {@link module:engine/view/observer/focusobserver~FocusObserver} is attached by the
 * {@link module:engine/view/view~View} this event is available by default.
 *
 * @see module:engine/view/observer/focusobserver~FocusObserver
 * @event module:engine/view/document~Document#event:focus
 * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
 */
/**
 * Fired when one of the editables loses focus.
 *
 * Introduced by {@link module:engine/view/observer/focusobserver~FocusObserver}.
 *
 * Note that because {@link module:engine/view/observer/focusobserver~FocusObserver} is attached by the
 * {@link module:engine/view/view~View} this event is available by default.
 *
 * @see module:engine/view/observer/focusobserver~FocusObserver
 * @event module:engine/view/document~Document#event:blur
 * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * {@link module:engine/view/document~Document#event:compositionstart Compositionstart},
 * {@link module:engine/view/document~Document#event:compositionupdate compositionupdate} and
 * {@link module:engine/view/document~Document#event:compositionend compositionend} events observer.
 *
 * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
 *
 * @extends module:engine/view/observer/domeventobserver~DomEventObserver
 */
class CompositionObserver extends DomEventObserver {
    constructor(view) {
        super(view);
        this.domEventType = ['compositionstart', 'compositionupdate', 'compositionend'];
        const document = this.document;
        document.on('compositionstart', () => {
            document.isComposing = true;
        });
        document.on('compositionend', () => {
            document.isComposing = false;
        });
    }
    onDomEvent(domEvent) {
        this.fire(domEvent.type, domEvent);
    }
}
/**
 * Fired when composition starts inside one of the editables.
 *
 * Introduced by {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
 *
 * Note that because {@link module:engine/view/observer/compositionobserver~CompositionObserver} is attached by the
 * {@link module:engine/view/view~View} this event is available by default.
 *
 * @see module:engine/view/observer/compositionobserver~CompositionObserver
 * @event module:engine/view/document~Document#event:compositionstart
 * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
 */
/**
 * Fired when composition is updated inside one of the editables.
 *
 * Introduced by {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
 *
 * Note that because {@link module:engine/view/observer/compositionobserver~CompositionObserver} is attached by the
 * {@link module:engine/view/view~View} this event is available by default.
 *
 * @see module:engine/view/observer/compositionobserver~CompositionObserver
 * @event module:engine/view/document~Document#event:compositionupdate
 * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
 */
/**
 * Fired when composition ends inside one of the editables.
 *
 * Introduced by {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
 *
 * Note that because {@link module:engine/view/observer/compositionobserver~CompositionObserver} is attached by the
 * {@link module:engine/view/view~View} this event is available by default.
 *
 * @see module:engine/view/observer/compositionobserver~CompositionObserver
 * @event module:engine/view/document~Document#event:compositionend
 * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Observer for events connected with data input.
 *
 * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
 *
 * @extends module:engine/view/observer/domeventobserver~DomEventObserver
 */
class InputObserver extends DomEventObserver {
    constructor(view) {
        super(view);
        this.domEventType = ['beforeinput'];
    }
    onDomEvent(domEvent) {
        this.fire(domEvent.type, domEvent);
    }
}
/**
 * Fired before browser inputs (or deletes) some data.
 *
 * This event is available only on browsers which support DOM `beforeinput` event.
 *
 * Introduced by {@link module:engine/view/observer/inputobserver~InputObserver}.
 *
 * Note that because {@link module:engine/view/observer/inputobserver~InputObserver} is attached by the
 * {@link module:engine/view/view~View} this event is available by default.
 *
 * @see module:engine/view/observer/inputobserver~InputObserver
 * @event module:engine/view/document~Document#event:beforeinput
 * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/elementreplacer
 */
/**
 * Utility class allowing to hide existing HTML elements or replace them with given ones in a way that doesn't remove
 * the original elements from the DOM.
 */
class ElementReplacer {
    constructor() {
        this._replacedElements = [];
    }
    /**
     * Hides the `element` and, if specified, inserts the the given element next to it.
     *
     * The effect of this method can be reverted by {@link #restore}.
     *
     * @param {HTMLElement} element The element to replace.
     * @param {HTMLElement} [newElement] The replacement element. If not passed, then the `element` will just be hidden.
     */
    replace(element, newElement) {
        this._replacedElements.push({ element, newElement });
        element.style.display = 'none';
        if (newElement) {
            element.parentNode.insertBefore(newElement, element.nextSibling);
        }
    }
    /**
     * Restores what {@link #replace} did.
     */
    restore() {
        this._replacedElements.forEach(({ element, newElement }) => {
            element.style.display = '';
            if (newElement) {
                newElement.remove();
            }
        });
        this._replacedElements = [];
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Creates element with attributes and children.
 *
 *		createElement( document, 'p' ); // <p>
 *		createElement( document, 'p', { class: 'foo' } ); // <p class="foo">
 *		createElement( document, 'p', null, 'foo' ); // <p>foo</p>
 *		createElement( document, 'p', null, [ 'foo', createElement( document, 'img' ) ] ); // <p>foo<img></p>
 *
 * @param {Document} doc Document used to create element.
 * @param {String} name Name of the element.
 * @param {Object} [attributes] Object keys will become attributes keys and object values will became attributes values.
 * @param {Node|String|Iterable.<Node|String>} [children] Child or any iterable of children. Strings will be automatically turned
 * into Text nodes.
 * @returns {Element} Created element.
 */
function createElement(doc, name, attributes = {}, children = []) {
    const namespace = attributes && attributes.xmlns;
    const element = namespace ? doc.createElementNS(namespace, name) : doc.createElement(name);
    for (const key in attributes) {
        element.setAttribute(key, attributes[key]);
    }
    if (isString(children) || !isIterable(children)) {
        children = [children];
    }
    for (let child of children) {
        if (isString(child)) {
            child = doc.createTextNode(child);
        }
        element.appendChild(child);
    }
    return element;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/* globals HTMLTextAreaElement */
/**
 * @module utils/dom/getdatafromelement
 */
/**
 * Gets data from a given source element.
 *
 * @param {HTMLElement} el The element from which the data will be retrieved.
 * @returns {String} The data string.
 */
function getDataFromElement(el) {
    if (el instanceof HTMLTextAreaElement) {
        return el.value;
    }
    return el.innerHTML;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/dom/isrange
 */
/**
 * Checks if the object is a native DOM Range.
 *
 * @param {*} obj
 * @returns {Boolean}
 */
function isRange(obj) {
    return Object.prototype.toString.apply(obj) == '[object Range]';
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/dom/getborderwidths
 */
/**
 * Returns an object containing CSS border widths of a specified HTML element.
 *
 * @param {HTMLElement} element An element which has CSS borders.
 * @returns {module:utils/dom/getborderwidths~BorderWidths} An object containing `top`, `left`, `right` and `bottom` properties
 * with numerical values of the `border-[top,left,right,bottom]-width` CSS styles.
 */
function getBorderWidths(element) {
    // Call getComputedStyle on the window the element document belongs to.
    const style = element.ownerDocument.defaultView.getComputedStyle(element);
    return {
        top: parseInt(style.borderTopWidth, 10),
        right: parseInt(style.borderRightWidth, 10),
        bottom: parseInt(style.borderBottomWidth, 10),
        left: parseInt(style.borderLeftWidth, 10)
    };
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const rectProperties = ['top', 'right', 'bottom', 'left', 'width', 'height'];
/**
 * A helper class representing a `ClientRect` object, e.g. value returned by
 * the native `object.getBoundingClientRect()` method. Provides a set of methods
 * to manipulate the rect and compare it against other rect instances.
 */
class Rect {
    /**
     * Creates an instance of rect.
     *
     *		// Rect of an HTMLElement.
     *		const rectA = new Rect( document.body );
     *
     *		// Rect of a DOM Range.
     *		const rectB = new Rect( document.getSelection().getRangeAt( 0 ) );
     *
     *		// Rect of a window (web browser viewport).
     *		const rectC = new Rect( window );
     *
     *		// Rect out of an object.
     *		const rectD = new Rect( { top: 0, right: 10, bottom: 10, left: 0, width: 10, height: 10 } );
     *
     *		// Rect out of another Rect instance.
     *		const rectE = new Rect( rectD );
     *
     *		// Rect out of a ClientRect.
     *		const rectF = new Rect( document.body.getClientRects().item( 0 ) );
     *
     * **Note**: By default a rect of an HTML element includes its CSS borders and scrollbars (if any)
     * ant the rect of a `window` includes scrollbars too. Use {@link #excludeScrollbarsAndBorders}
     * to get the inner part of the rect.
     *
     * @param {module:utils/dom/rect~RectSource} source A source object to create the rect.
     */
    constructor(source) {
        const isSourceRange = isRange(source);
        Object.defineProperty(this, '_source', {
            // If the source is a Rect instance, copy it's #_source.
            value: source._source || source,
            writable: true,
            enumerable: false
        });
        if (isDomElement(source) || isSourceRange) {
            // The `Rect` class depends on `getBoundingClientRect` and `getClientRects` DOM methods. If the source
            // of a rect in an HTML element or a DOM range but it does not belong to any rendered DOM tree, these methods
            // will fail to obtain the geometry and the rect instance makes little sense to the features using it.
            // To get rid of this warning make sure the source passed to the constructor is a descendant of `window.document.body`.
            // @if CK_DEBUG // const sourceNode = isSourceRange ? source.startContainer : source;
            // @if CK_DEBUG // if ( !sourceNode.ownerDocument || !sourceNode.ownerDocument.body.contains( sourceNode ) ) {
            // @if CK_DEBUG // 	console.warn(
            // @if CK_DEBUG // 		'rect-source-not-in-dom: The source of this rect does not belong to any rendered DOM tree.',
            // @if CK_DEBUG // 		{ source } );
            // @if CK_DEBUG // }
            if (isSourceRange) {
                const rangeRects = Rect.getDomRangeRects(source);
                copyRectProperties(this, Rect.getBoundingRect(rangeRects));
            }
            else {
                copyRectProperties(this, source.getBoundingClientRect());
            }
        }
        else if (isWindow(source)) {
            const { innerWidth, innerHeight } = source;
            copyRectProperties(this, {
                top: 0,
                right: innerWidth,
                bottom: innerHeight,
                left: 0,
                width: innerWidth,
                height: innerHeight
            });
        }
        else {
            copyRectProperties(this, source);
        }
    }
    /**
     * Returns a clone of the rect.
     *
     * @returns {module:utils/dom/rect~Rect} A cloned rect.
     */
    clone() {
        return new Rect(this);
    }
    /**
     * Moves the rect so that its upper–left corner lands in desired `[ x, y ]` location.
     *
     * @param {Number} x Desired horizontal location.
     * @param {Number} y Desired vertical location.
     * @returns {module:utils/dom/rect~Rect} A rect which has been moved.
     */
    moveTo(x, y) {
        this.top = y;
        this.right = x + this.width;
        this.bottom = y + this.height;
        this.left = x;
        return this;
    }
    /**
     * Moves the rect in–place by a dedicated offset.
     *
     * @param {Number} x A horizontal offset.
     * @param {Number} y A vertical offset
     * @returns {module:utils/dom/rect~Rect} A rect which has been moved.
     */
    moveBy(x, y) {
        this.top += y;
        this.right += x;
        this.left += x;
        this.bottom += y;
        return this;
    }
    /**
     * Returns a new rect a a result of intersection with another rect.
     *
     * @param {module:utils/dom/rect~Rect} anotherRect
     * @returns {module:utils/dom/rect~Rect|null}
     */
    getIntersection(anotherRect) {
        const rect = {
            top: Math.max(this.top, anotherRect.top),
            right: Math.min(this.right, anotherRect.right),
            bottom: Math.min(this.bottom, anotherRect.bottom),
            left: Math.max(this.left, anotherRect.left),
            width: 0,
            height: 0
        };
        rect.width = rect.right - rect.left;
        rect.height = rect.bottom - rect.top;
        if (rect.width < 0 || rect.height < 0) {
            return null;
        }
        else {
            return new Rect(rect);
        }
    }
    /**
     * Returns the area of intersection with another rect.
     *
     * @param {module:utils/dom/rect~Rect} anotherRect
     * @returns {Number} Area of intersection.
     */
    getIntersectionArea(anotherRect) {
        const rect = this.getIntersection(anotherRect);
        if (rect) {
            return rect.getArea();
        }
        else {
            return 0;
        }
    }
    /**
     * Returns the area of the rect.
     *
     * @returns {Number}
     */
    getArea() {
        return this.width * this.height;
    }
    /**
     * Returns a new rect, a part of the original rect, which is actually visible to the user,
     * e.g. an original rect cropped by parent element rects which have `overflow` set in CSS
     * other than `"visible"`.
     *
     * If there's no such visible rect, which is when the rect is limited by one or many of
     * the ancestors, `null` is returned.
     *
     * @returns {module:utils/dom/rect~Rect|null} A visible rect instance or `null`, if there's none.
     */
    getVisible() {
        const source = this._source;
        let visibleRect = this.clone();
        // There's no ancestor to crop <body> with the overflow.
        if (!isBody(source)) {
            let parent = source.parentNode || source.commonAncestorContainer;
            // Check the ancestors all the way up to the <body>.
            while (parent && !isBody(parent)) {
                const parentRect = new Rect(parent);
                const intersectionRect = visibleRect.getIntersection(parentRect);
                if (intersectionRect) {
                    if (intersectionRect.getArea() < visibleRect.getArea()) {
                        // Reduce the visible rect to the intersection.
                        visibleRect = intersectionRect;
                    }
                }
                else {
                    // There's no intersection, the rect is completely invisible.
                    return null;
                }
                parent = parent.parentNode;
            }
        }
        return visibleRect;
    }
    /**
     * Checks if all property values ({@link #top}, {@link #left}, {@link #right},
     * {@link #bottom}, {@link #width} and {@link #height}) are the equal in both rect
     * instances.
     *
     * @param {module:utils/dom/rect~Rect} anotherRect A rect instance to compare with.
     * @returns {Boolean} `true` when Rects are equal. `false` otherwise.
     */
    isEqual(anotherRect) {
        for (const prop of rectProperties) {
            if (this[prop] !== anotherRect[prop]) {
                return false;
            }
        }
        return true;
    }
    /**
     * Checks whether a rect fully contains another rect instance.
     *
     * @param {module:utils/dom/rect~Rect} anotherRect
     * @returns {Boolean} `true` if contains, `false` otherwise.
     */
    contains(anotherRect) {
        const intersectRect = this.getIntersection(anotherRect);
        return !!(intersectRect && intersectRect.isEqual(anotherRect));
    }
    /**
     * Excludes scrollbars and CSS borders from the rect.
     *
     * * Borders are removed when {@link #_source} is an HTML element.
     * * Scrollbars are excluded from HTML elements and the `window`.
     *
     * @returns {module:utils/dom/rect~Rect} A rect which has been updated.
     */
    excludeScrollbarsAndBorders() {
        const source = this._source;
        let scrollBarWidth, scrollBarHeight, direction;
        if (isWindow(source)) {
            scrollBarWidth = source.innerWidth - source.document.documentElement.clientWidth;
            scrollBarHeight = source.innerHeight - source.document.documentElement.clientHeight;
            direction = source.getComputedStyle(source.document.documentElement).direction;
        }
        else {
            const borderWidths = getBorderWidths(source);
            scrollBarWidth = source.offsetWidth - source.clientWidth - borderWidths.left - borderWidths.right;
            scrollBarHeight = source.offsetHeight - source.clientHeight - borderWidths.top - borderWidths.bottom;
            direction = source.ownerDocument.defaultView.getComputedStyle(source).direction;
            this.left += borderWidths.left;
            this.top += borderWidths.top;
            this.right -= borderWidths.right;
            this.bottom -= borderWidths.bottom;
            this.width = this.right - this.left;
            this.height = this.bottom - this.top;
        }
        this.width -= scrollBarWidth;
        if (direction === 'ltr') {
            this.right -= scrollBarWidth;
        }
        else {
            this.left += scrollBarWidth;
        }
        this.height -= scrollBarHeight;
        this.bottom -= scrollBarHeight;
        return this;
    }
    /**
     * Returns an array of rects of the given native DOM Range.
     *
     * @param {Range} range A native DOM range.
     * @returns {Array.<module:utils/dom/rect~Rect>} DOM Range rects.
     */
    static getDomRangeRects(range) {
        const rects = [];
        // Safari does not iterate over ClientRectList using for...of loop.
        const clientRects = Array.from(range.getClientRects());
        if (clientRects.length) {
            for (const rect of clientRects) {
                rects.push(new Rect(rect));
            }
        }
        // If there's no client rects for the Range, use parent container's bounding rect
        // instead and adjust rect's width to simulate the actual geometry of such range.
        // https://github.com/ckeditor/ckeditor5-utils/issues/153
        // https://github.com/ckeditor/ckeditor5-ui/issues/317
        else {
            let startContainer = range.startContainer;
            if (isText(startContainer)) {
                startContainer = startContainer.parentNode;
            }
            const rect = new Rect(startContainer.getBoundingClientRect());
            rect.right = rect.left;
            rect.width = 0;
            rects.push(rect);
        }
        return rects;
    }
    /**
     * Returns a bounding rectangle that contains all the given `rects`.
     *
     * @param {Iterable.<module:utils/dom/rect~Rect>} rects A list of rectangles that should be contained in the result rectangle.
     * @returns {module:utils/dom/rect~Rect|null} Bounding rectangle or `null` if no `rects` were given.
     */
    static getBoundingRect(rects) {
        const boundingRectData = {
            left: Number.POSITIVE_INFINITY,
            top: Number.POSITIVE_INFINITY,
            right: Number.NEGATIVE_INFINITY,
            bottom: Number.NEGATIVE_INFINITY,
            width: 0,
            height: 0
        };
        let rectangleCount = 0;
        for (const rect of rects) {
            rectangleCount++;
            boundingRectData.left = Math.min(boundingRectData.left, rect.left);
            boundingRectData.top = Math.min(boundingRectData.top, rect.top);
            boundingRectData.right = Math.max(boundingRectData.right, rect.right);
            boundingRectData.bottom = Math.max(boundingRectData.bottom, rect.bottom);
        }
        if (rectangleCount == 0) {
            return null;
        }
        boundingRectData.width = boundingRectData.right - boundingRectData.left;
        boundingRectData.height = boundingRectData.bottom - boundingRectData.top;
        return new Rect(boundingRectData);
    }
}
// Acquires all the rect properties from the passed source.
//
// @private
// @param {module:utils/dom/rect~Rect} rect
// @param {module:utils/dom/rect~RectLike} source
function copyRectProperties(rect, source) {
    for (const p of rectProperties) {
        rect[p] = source[p];
    }
}
// Checks if provided object is a <body> HTML element.
//
// @private
// @param {*} value
// @returns {Boolean}
function isBody(value) {
    if (!isDomElement(value)) {
        return false;
    }
    return value === value.ownerDocument.body;
}
// Checks if provided object is a DOM Element.
//
// It's just a wrapper for lodash `isElement` but with type guard.
//
// @private
// @param {*} value
// @returns {Boolean}
function isDomElement(value) {
    return isElement(value);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * A helper class which instances allow performing custom actions when native DOM elements are resized.
 *
 *		const editableElement = editor.editing.view.getDomRoot();
 *
 *		const observer = new ResizeObserver( editableElement, entry => {
 *			console.log( 'The editable element has been resized in DOM.' );
 *			console.log( entry.target ); // -> editableElement
 *			console.log( entry.contentRect.width ); // -> e.g. '423px'
 *		} );
 *
 * It uses the [native DOM resize observer](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
 * under the hood.
 */
class ResizeObserver {
    /**
     * Creates an instance of the `ResizeObserver` class.
     *
     * @param {Element} element A DOM element that is to be observed for resizing. Note that
     * the element must be visible (i.e. not detached from DOM) for the observer to work.
     * @param {Function} callback A function called when the observed element was resized. It passes
     * the [`ResizeObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
     * object with information about the resize event.
     */
    constructor(element, callback) {
        // **Note**: For the maximum performance, this class ensures only a single instance of the native
        // observer is used no matter how many instances of this class were created.
        if (!ResizeObserver._observerInstance) {
            ResizeObserver._createObserver();
        }
        this._element = element;
        this._callback = callback;
        ResizeObserver._addElementCallback(element, callback);
        ResizeObserver._observerInstance.observe(element);
    }
    /**
     * Destroys the observer which disables the `callback` passed to the {@link #constructor}.
     */
    destroy() {
        ResizeObserver._deleteElementCallback(this._element, this._callback);
    }
    /**
     * Registers a new resize callback for the DOM element.
     *
     * @private
     * @static
     * @param {Element} element
     * @param {Function} callback
     */
    static _addElementCallback(element, callback) {
        if (!ResizeObserver._elementCallbacks) {
            ResizeObserver._elementCallbacks = new Map();
        }
        let callbacks = ResizeObserver._elementCallbacks.get(element);
        if (!callbacks) {
            callbacks = new Set();
            ResizeObserver._elementCallbacks.set(element, callbacks);
        }
        callbacks.add(callback);
    }
    /**
     * Removes a resize callback from the DOM element. If no callbacks are left
     * for the element, it removes the element from the native observer.
     *
     * @private
     * @static
     * @param {Element} element
     * @param {Function} callback
     */
    static _deleteElementCallback(element, callback) {
        const callbacks = ResizeObserver._getElementCallbacks(element);
        // Remove the element callback. Check if exist first in case someone
        // called destroy() twice.
        if (callbacks) {
            callbacks.delete(callback);
            // If no callbacks left for the element, also remove the element.
            if (!callbacks.size) {
                ResizeObserver._elementCallbacks.delete(element);
                ResizeObserver._observerInstance.unobserve(element);
            }
        }
        if (ResizeObserver._elementCallbacks && !ResizeObserver._elementCallbacks.size) {
            ResizeObserver._observerInstance = null;
            ResizeObserver._elementCallbacks = null;
        }
    }
    /**
     * Returns are registered resize callbacks for the DOM element.
     *
     * @private
     * @static
     * @param {Element} element
     * @returns {Set.<Function>|null|undefined}
     */
    static _getElementCallbacks(element) {
        if (!ResizeObserver._elementCallbacks) {
            return null;
        }
        return ResizeObserver._elementCallbacks.get(element);
    }
    /**
     * Creates the single native observer shared across all `ResizeObserver` instances.
     *
     * @private
     * @static
     */
    static _createObserver() {
        ResizeObserver._observerInstance = new global$2.window.ResizeObserver(entries => {
            for (const entry of entries) {
                const callbacks = ResizeObserver._getElementCallbacks(entry.target);
                if (callbacks) {
                    for (const callback of callbacks) {
                        callback(entry);
                    }
                }
            }
        });
    }
}
/**
 * The single native observer instance shared across all {@link module:utils/dom/resizeobserver~ResizeObserver} instances.
 *
 * @static
 * @private
 * @readonly
 * @property {Object|null}
 */
ResizeObserver._observerInstance = null;
/**
 * A mapping of native DOM elements and their callbacks shared across all
 * {@link module:utils/dom/resizeobserver~ResizeObserver} instances.
 *
 * @static
 * @private
 * @property {Map.<Element,Set>|null}
 */
ResizeObserver._elementCallbacks = null;/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/dom/setdatainelement
 */
/* globals HTMLTextAreaElement */
/**
 * Sets data in a given element.
 *
 * @param {HTMLElement} el The element in which the data will be set.
 * @param {String} data The data string.
 */
function setDataInElement(el, data) {
    if (el instanceof HTMLTextAreaElement) {
        el.value = data;
    }
    el.innerHTML = data;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/dom/tounit
 */
/**
 * Returns a helper function, which adds a desired trailing
 * `unit` to the passed value.
 *
 * @param {String} unit An unit like "px" or "em".
 * @returns {module:utils/dom/tounit~helper}
 */
function toUnit(unit) {
    /**
     * A function, which adds a pre–defined trailing `unit`
     * to the passed `value`.
     *
     * @function helper
     * @param {String|Number} value A value to be given the unit.
     * @returns {String} A value with the trailing unit.
     */
    return value => value + unit;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/dom/isvisible
 */
/**
 * Checks whether the element is visible to the user in DOM:
 *
 * * connected to the root of the document,
 * * has no `display: none`,
 * * has no ancestors with `display: none`.
 *
 * **Note**: This helper does not check whether the element is hidden by cropping, overflow, etc..
 * To check that, use {@link module:utils/dom/rect~Rect} instead.
 *
 * @param {HTMLElement|null|undefined} element
 * @returns {Boolean}
 */
function isVisible(element) {
    return !!(element && element.getClientRects && element.getClientRects().length);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Makes any page `HTMLElement` or `Range` (`target`) visible inside the browser viewport.
 * This helper will scroll all `target` ancestors and the web browser viewport to reveal the target to
 * the user. If the `target` is already visible, nothing will happen.
 *
 * @param {Object} options
 * @param {HTMLElement|Range} options.target A target, which supposed to become visible to the user.
 * @param {Number} [options.viewportOffset] An offset from the edge of the viewport (in pixels)
 * the `target` will be moved by when the viewport is scrolled. It enhances the user experience
 * by keeping the `target` some distance from the edge of the viewport and thus making it easier to
 * read or edit by the user.
 */
function scrollViewportToShowTarget({ target, viewportOffset = 0 }) {
    const targetWindow = getWindow(target);
    let currentWindow = targetWindow;
    let currentFrame = null;
    // Iterate over all windows, starting from target's parent window up to window#top.
    while (currentWindow) {
        let firstAncestorToScroll;
        // Let's scroll target's ancestors first to reveal it. Then, once the ancestor scrolls
        // settled down, the algorithm can eventually scroll the viewport of the current window.
        //
        // Note: If the current window is target's **original** window (e.g. the first one),
        // start scrolling the closest parent of the target. If not, scroll the closest parent
        // of an iframe that resides in the current window.
        if (currentWindow == targetWindow) {
            firstAncestorToScroll = getParentElement(target);
        }
        else {
            firstAncestorToScroll = getParentElement(currentFrame);
        }
        // Scroll the target's ancestors first. Once done, scrolling the viewport is easy.
        scrollAncestorsToShowRect(firstAncestorToScroll, () => {
            // Note: If the target does not belong to the current window **directly**,
            // i.e. it resides in an iframe belonging to the window, obtain the target's rect
            // in the coordinates of the current window. By default, a Rect returns geometry
            // relative to the current window's viewport. To make it work in a parent window,
            // it must be shifted.
            return getRectRelativeToWindow(target, currentWindow);
        });
        // Obtain the rect of the target after it has been scrolled within its ancestors.
        // It's time to scroll the viewport.
        const targetRect = getRectRelativeToWindow(target, currentWindow);
        scrollWindowToShowRect(currentWindow, targetRect, viewportOffset);
        if (currentWindow.parent != currentWindow) {
            // Keep the reference to the <iframe> element the "previous current window" was
            // rendered within. It will be useful to re–calculate the rect of the target
            // in the parent window's relative geometry. The target's rect must be shifted
            // by it's iframe's position.
            currentFrame = currentWindow.frameElement;
            currentWindow = currentWindow.parent;
            // If the current window has some parent but frameElement is inaccessible, then they have
            // different domains/ports and, due to security reasons, accessing and scrolling
            // the parent window won't be possible.
            // See https://github.com/ckeditor/ckeditor5/issues/930.
            if (!currentFrame) {
                return;
            }
        }
        else {
            currentWindow = null;
        }
    }
}
/**
 * Makes any page `HTMLElement` or `Range` (target) visible within its scrollable ancestors,
 * e.g. if they have `overflow: scroll` CSS style.
 *
 * @param {HTMLElement|Range} target A target, which supposed to become visible to the user.
 */
function scrollAncestorsToShowTarget(target) {
    const targetParent = getParentElement(target);
    scrollAncestorsToShowRect(targetParent, () => {
        return new Rect(target);
    });
}
// Makes a given rect visible within its parent window.
//
// Note: Avoid the situation where the caret is still in the viewport, but totally
// at the edge of it. In such situation, if it moved beyond the viewport in the next
// action e.g. after paste, the scrolling would move it to the viewportOffset level
// and it all would look like the caret visually moved up/down:
//
// 1.
//		| foo[]
//		|                                    <--- N px of space below the caret
//		+---------------------------------...
//
// 2. *paste*
// 3.
//		|
//		|
//		+-foo-----------------------------...
//		  bar[]                              <--- caret below viewport, scrolling...
//
// 4. *scrolling*
// 5.
//		|
//		| foo
//		| bar[]                              <--- caret precisely at the edge
//		+---------------------------------...
//
// To prevent this, this method checks the rects moved by the viewportOffset to cover
// the upper/lower edge of the viewport. It makes sure if the action repeats, there's
// no twitching – it's a purely visual improvement:
//
// 5. (after fix)
//		|
//		| foo
//		| bar[]
//		|                                    <--- N px of space below the caret
//		+---------------------------------...
//
// @private
// @param {Window} window A window which is scrolled to reveal the rect.
// @param {module:utils/dom/rect~Rect} rect A rect which is to be revealed.
// @param {Number} viewportOffset See scrollViewportToShowTarget.
function scrollWindowToShowRect(window, rect, viewportOffset) {
    const targetShiftedDownRect = rect.clone().moveBy(0, viewportOffset);
    const targetShiftedUpRect = rect.clone().moveBy(0, -viewportOffset);
    const viewportRect = new Rect(window).excludeScrollbarsAndBorders();
    const rects = [targetShiftedUpRect, targetShiftedDownRect];
    if (!rects.every(rect => viewportRect.contains(rect))) {
        let { scrollX, scrollY } = window;
        if (isAbove(targetShiftedUpRect, viewportRect)) {
            scrollY -= viewportRect.top - rect.top + viewportOffset;
        }
        else if (isBelow(targetShiftedDownRect, viewportRect)) {
            scrollY += rect.bottom - viewportRect.bottom + viewportOffset;
        }
        // TODO: Web browsers scroll natively to place the target in the middle
        // of the viewport. It's not a very popular case, though.
        if (isLeftOf(rect, viewportRect)) {
            scrollX -= viewportRect.left - rect.left + viewportOffset;
        }
        else if (isRightOf(rect, viewportRect)) {
            scrollX += rect.right - viewportRect.right + viewportOffset;
        }
        window.scrollTo(scrollX, scrollY);
    }
}
// Recursively scrolls element ancestors to visually reveal a rect.
//
// @private
// @param {HTMLElement} A parent The first ancestors to start scrolling.
// @param {Function} getRect A function which returns the Rect, which is to be revealed.
function scrollAncestorsToShowRect(parent, getRect) {
    const parentWindow = getWindow(parent);
    let parentRect, targetRect;
    while (parent != parentWindow.document.body) {
        targetRect = getRect();
        parentRect = new Rect(parent).excludeScrollbarsAndBorders();
        if (!parentRect.contains(targetRect)) {
            if (isAbove(targetRect, parentRect)) {
                parent.scrollTop -= parentRect.top - targetRect.top;
            }
            else if (isBelow(targetRect, parentRect)) {
                parent.scrollTop += targetRect.bottom - parentRect.bottom;
            }
            if (isLeftOf(targetRect, parentRect)) {
                parent.scrollLeft -= parentRect.left - targetRect.left;
            }
            else if (isRightOf(targetRect, parentRect)) {
                parent.scrollLeft += targetRect.right - parentRect.right;
            }
        }
        parent = parent.parentNode;
    }
}
// Determines if a given `Rect` extends beyond the bottom edge of the second `Rect`.
//
// @private
// @param {module:utils/dom/rect~Rect} firstRect
// @param {module:utils/dom/rect~Rect} secondRect
// @returns {Boolean}
function isBelow(firstRect, secondRect) {
    return firstRect.bottom > secondRect.bottom;
}
// Determines if a given `Rect` extends beyond the top edge of the second `Rect`.
//
// @private
// @param {module:utils/dom/rect~Rect} firstRect
// @param {module:utils/dom/rect~Rect} secondRect
// @returns {Boolean}
function isAbove(firstRect, secondRect) {
    return firstRect.top < secondRect.top;
}
// Determines if a given `Rect` extends beyond the left edge of the second `Rect`.
//
// @private
// @param {module:utils/dom/rect~Rect} firstRect
// @param {module:utils/dom/rect~Rect} secondRect
// @returns {Boolean}
function isLeftOf(firstRect, secondRect) {
    return firstRect.left < secondRect.left;
}
// Determines if a given `Rect` extends beyond the right edge of the second `Rect`.
//
// @private
// @param {module:utils/dom/rect~Rect} firstRect
// @param {module:utils/dom/rect~Rect} secondRect
// @returns {Boolean}
function isRightOf(firstRect, secondRect) {
    return firstRect.right > secondRect.right;
}
// Returns the closest window of an element or range.
//
// @private
// @param {HTMLElement|Range} elementOrRange
// @returns {Window}
function getWindow(elementOrRange) {
    if (isRange(elementOrRange)) {
        return elementOrRange.startContainer.ownerDocument.defaultView;
    }
    else {
        return elementOrRange.ownerDocument.defaultView;
    }
}
// Returns the closest parent of an element or DOM range.
//
// @private
// @param {HTMLElement|Range} elementOrRange
// @returns {HTMLelement}
function getParentElement(elementOrRange) {
    if (isRange(elementOrRange)) {
        let parent = elementOrRange.commonAncestorContainer;
        // If a Range is attached to the Text, use the closest element ancestor.
        if (isText(parent)) {
            parent = parent.parentNode;
        }
        return parent;
    }
    else {
        return elementOrRange.parentNode;
    }
}
// Returns the rect of an element or range residing in an iframe.
// The result rect is relative to the geometry of the passed window instance.
//
// @private
// @param {HTMLElement|Range} target Element or range which rect should be returned.
// @param {Window} relativeWindow A window the rect should be relative to.
// @returns {module:utils/dom/rect~Rect}
function getRectRelativeToWindow(target, relativeWindow) {
    const targetWindow = getWindow(target);
    const rect = new Rect(target);
    if (targetWindow === relativeWindow) {
        return rect;
    }
    else {
        let currentWindow = targetWindow;
        while (currentWindow != relativeWindow) {
            const frame = currentWindow.frameElement;
            const frameRect = new Rect(frame).excludeScrollbarsAndBorders();
            rect.moveBy(frameRect.left, frameRect.top);
            currentWindow = currentWindow.parent;
        }
    }
    return rect;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/first
 */
/**
 * Returns first item of the given `iterator`.
 *
 * @param {Iterator.<*>} iterator
 * @returns {*}
 */
function first(iterator) {
    const iteratorItem = iterator.next();
    if (iteratorItem.done) {
        return null;
    }
    return iteratorItem.value;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Allows observing a group of `Element`s whether at least one of them is focused.
 *
 * Used by the {@link module:core/editor/editor~Editor} in order to track whether the focus is still within the application,
 * or were used outside of its UI.
 *
 * **Note** `focus` and `blur` listeners use event capturing, so it is only needed to register wrapper `Element`
 * which contain other `focusable` elements. But note that this wrapper element has to be focusable too
 * (have e.g. `tabindex="-1"`).
 *
 * Check out the {@glink framework/guides/deep-dive/ui/focus-tracking "Deep dive into focus tracking" guide} to learn more.
 *
 * @mixes module:utils/dom/emittermixin~EmitterMixin
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class FocusTracker extends DomEmitterMixin(Observable) {
    constructor() {
        super();
        this.set('isFocused', false);
        this.set('focusedElement', null);
        this._elements = new Set();
        this._nextEventLoopTimeout = null;
    }
    /**
     * Starts tracking the specified element.
     *
     * @param {Element} element
     */
    add(element) {
        if (this._elements.has(element)) {
            /**
             * This element is already tracked by {@link module:utils/focustracker~FocusTracker}.
             *
             * @error focustracker-add-element-already-exist
             */
            throw new CKEditorError('focustracker-add-element-already-exist', this);
        }
        this.listenTo(element, 'focus', () => this._focus(element), { useCapture: true });
        this.listenTo(element, 'blur', () => this._blur(), { useCapture: true });
        this._elements.add(element);
    }
    /**
     * Stops tracking the specified element and stops listening on this element.
     *
     * @param {Element} element
     */
    remove(element) {
        if (element === this.focusedElement) {
            this._blur();
        }
        if (this._elements.has(element)) {
            this.stopListening(element);
            this._elements.delete(element);
        }
    }
    /**
     * Destroys the focus tracker by:
     * - Disabling all event listeners attached to tracked elements.
     * - Removing all tracked elements that were previously added.
     */
    destroy() {
        this.stopListening();
    }
    /**
     * Stores currently focused element and set {#isFocused} as `true`.
     *
     * @private
     * @param {Element} element Element which has been focused.
     */
    _focus(element) {
        clearTimeout(this._nextEventLoopTimeout);
        this.focusedElement = element;
        this.isFocused = true;
    }
    /**
     * Clears currently focused element and set {@link #isFocused} as `false`.
     * This method uses `setTimeout` to change order of fires `blur` and `focus` events.
     *
     * @private
     * @fires blur
     */
    _blur() {
        clearTimeout(this._nextEventLoopTimeout);
        this._nextEventLoopTimeout = setTimeout(() => {
            this.focusedElement = null;
            this.isFocused = false;
        }, 0);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Keystroke handler allows registering callbacks for given keystrokes.
 *
 * The most frequent use of this class is through the {@link module:core/editor/editor~Editor#keystrokes `editor.keystrokes`}
 * property. It allows listening to keystrokes executed in the editing view:
 *
 *		editor.keystrokes.set( 'Ctrl+A', ( keyEvtData, cancel ) => {
 *			console.log( 'Ctrl+A has been pressed' );
 *			cancel();
 *		} );
 *
 * However, this utility class can be used in various part of the UI. For instance, a certain {@link module:ui/view~View}
 * can use it like this:
 *
 *		class MyView extends View {
 *			constructor() {
 *				this.keystrokes = new KeystrokeHandler();
 *
 * 				this.keystrokes.set( 'tab', handleTabKey );
 *			}
 *
 *			render() {
 *				super.render();
 *
 *				this.keystrokes.listenTo( this.element );
 *			}
 *		}
 *
 * That keystroke handler will listen to `keydown` events fired in this view's main element.
 *
 */
class KeystrokeHandler {
    /**
     * Creates an instance of the keystroke handler.
     */
    constructor() {
        this._listener = Object.create(DomEmitterMixin);
    }
    /**
     * Starts listening for `keydown` events from a given emitter.
     *
     * @param {module:utils/emittermixin~Emitter|HTMLElement|Window} emitter
     */
    listenTo(emitter) {
        // The #_listener works here as a kind of dispatcher. It groups the events coming from the same
        // keystroke so the listeners can be attached to them with different priorities.
        //
        // E.g. all the keystrokes with the `keyCode` of 42 coming from the `emitter` are propagated
        // as a `_keydown:42` event by the `_listener`. If there's a callback created by the `set`
        // method for this 42 keystroke, it listens to the `_listener#_keydown:42` event only and interacts
        // only with other listeners of this particular event, thus making it possible to prioritize
        // the listeners and safely cancel execution, when needed. Instead of duplicating the Emitter logic,
        // the KeystrokeHandler re–uses it to do its job.
        this._listener.listenTo(emitter, 'keydown', (evt, keyEvtData) => {
            this._listener.fire('_keydown:' + getCode(keyEvtData), keyEvtData);
        });
    }
    /**
     * Registers a handler for the specified keystroke.
     *
     * @param {String|Array.<String|Number>} keystroke Keystroke defined in a format accepted by
     * the {@link module:utils/keyboard~parseKeystroke} function.
     * @param {Function} callback A function called with the
     * {@link module:engine/view/observer/keyobserver~KeyEventData key event data} object and
     * a helper funcion to call both `preventDefault()` and `stopPropagation()` on the underlying event.
     * @param {Object} [options={}] Additional options.
     * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of the keystroke
     * callback. The higher the priority value the sooner the callback will be executed. Keystrokes having the same priority
     * are called in the order they were added.
     */
    set(keystroke, callback, options = {}) {
        const keyCode = parseKeystroke(keystroke);
        const priority = options.priority;
        // Execute the passed callback on KeystrokeHandler#_keydown.
        // TODO: https://github.com/ckeditor/ckeditor5-utils/issues/144
        this._listener.listenTo(this._listener, '_keydown:' + keyCode, (evt, keyEvtData) => {
            callback(keyEvtData, () => {
                // Stop the event in the DOM: no listener in the web page
                // will be triggered by this event.
                keyEvtData.preventDefault();
                keyEvtData.stopPropagation();
                // Stop the event in the KeystrokeHandler: no more callbacks
                // will be executed for this keystroke.
                evt.stop();
            });
            // Mark this keystroke as handled by the callback. See: #press.
            evt.return = true;
        }, { priority });
    }
    /**
     * Triggers a keystroke handler for a specified key combination, if such a keystroke was {@link #set defined}.
     *
     * @param {module:engine/view/observer/keyobserver~KeyEventData} keyEvtData Key event data.
     * @returns {Boolean} Whether the keystroke was handled.
     */
    press(keyEvtData) {
        return !!this._listener.fire('_keydown:' + getCode(keyEvtData), keyEvtData);
    }
    /**
     * Destroys the keystroke handler.
     */
    destroy() {
        this._listener.stopListening();
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */var index$8=/*#__PURE__*/Object.freeze({__proto__:null,env:env,diff:diff,mix:mix,EmitterMixin:EmitterMixin,ObservableMixin:ObservableMixin,CKEditorError:CKEditorError,logError:logError,logWarning:logWarning,ElementReplacer:ElementReplacer,createElement:createElement,DomEmitterMixin:DomEmitterMixin,global:global$2,getDataFromElement:getDataFromElement,Rect:Rect,ResizeObserver:ResizeObserver,setDataInElement:setDataInElement,toUnit:toUnit,isVisible:isVisible,Locale:Locale,Collection:Collection,first:first,FocusTracker:FocusTracker,KeystrokeHandler:KeystrokeHandler,toArray:toArray,toMap:toMap,priorities:priorities,uid:uid,version:version,scrollViewportToShowTarget:scrollViewportToShowTarget,scrollAncestorsToShowTarget:scrollAncestorsToShowTarget,keyCodes:keyCodes,getCode:getCode,parseKeystroke:parseKeystroke,getEnvKeystrokeText:getEnvKeystrokeText,isArrowKeyCode:isArrowKeyCode,getLocalizedArrowKeyCodeDirection:getLocalizedArrowKeyCodeDirection,isForwardArrowKeyCode:isForwardArrowKeyCode,getLanguageDirection:getLanguageDirection});/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Arrow keys observer introduces the {@link module:engine/view/document~Document#event:arrowKey `Document#arrowKey`} event.
 *
 * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
 *
 * @extends module:engine/view/observer/observer~Observer
 */
class ArrowKeysObserver extends Observer {
    /**
     * @inheritDoc
     */
    constructor(view) {
        super(view);
        this.document.on('keydown', (event, data) => {
            if (this.isEnabled && isArrowKeyCode(data.keyCode)) {
                const eventInfo = new BubblingEventInfo(this.document, 'arrowKey', this.document.selection.getFirstRange());
                this.document.fire(eventInfo, data);
                if (eventInfo.stop.called) {
                    event.stop();
                }
            }
        });
    }
    /**
     * @inheritDoc
     */
    observe() { }
}
/**
 * Event fired when the user presses an arrow keys.
 *
 * Introduced by {@link module:engine/view/observer/arrowkeysobserver~ArrowKeysObserver}.
 *
 * Note that because {@link module:engine/view/observer/arrowkeysobserver~ArrowKeysObserver} is attached by the
 * {@link module:engine/view/view~View} this event is available by default.
 *
 * @event module:engine/view/document~Document#event:arrowKey
 * @param {module:engine/view/observer/domeventdata~DomEventData} data
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Tab observer introduces the {@link module:engine/view/document~Document#event:tab `Document#tab`} event.
 *
 * Note that because {@link module:engine/view/observer/tabobserver~TabObserver} is attached by the
 * {@link module:engine/view/view~View}, this event is available by default.
 *
 * @extends module:engine/view/observer/observer~Observer
 */
class TabObserver extends Observer {
    /**
     * @inheritDoc
     */
    constructor(view) {
        super(view);
        const doc = this.document;
        doc.on('keydown', (evt, data) => {
            if (!this.isEnabled ||
                data.keyCode != keyCodes.tab ||
                data.ctrlKey) {
                return;
            }
            const event = new BubblingEventInfo(doc, 'tab', doc.selection.getFirstRange());
            doc.fire(event, data);
            if (event.stop.called) {
                evt.stop();
            }
        });
    }
    /**
     * @inheritDoc
     */
    observe() { }
}
/**
 * Event fired when the user presses a tab key.
 *
 * Introduced by {@link module:engine/view/observer/tabobserver~TabObserver}.
 *
 * Note that because {@link module:engine/view/observer/tabobserver~TabObserver} is attached by the
 * {@link module:engine/view/view~View}, this event is available by default.
 *
 * @event module:engine/view/document~Document#event:tab
 *
 * @param {module:engine/view/observer/domeventdata~DomEventData} data
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Editor's view controller class. Its main responsibility is DOM - View management for editing purposes, to provide
 * abstraction over the DOM structure and events and hide all browsers quirks.
 *
 * View controller renders view document to DOM whenever view structure changes. To determine when view can be rendered,
 * all changes need to be done using the {@link module:engine/view/view~View#change} method, using
 * {@link module:engine/view/downcastwriter~DowncastWriter}:
 *
 *		view.change( writer => {
 *			writer.insert( position, writer.createText( 'foo' ) );
 *		} );
 *
 * View controller also register {@link module:engine/view/observer/observer~Observer observers} which observes changes
 * on DOM and fire events on the {@link module:engine/view/document~Document Document}.
 * Note that the following observers are added by the class constructor and are always available:
 *
 * * {@link module:engine/view/observer/mutationobserver~MutationObserver},
 * * {@link module:engine/view/observer/selectionobserver~SelectionObserver},
 * * {@link module:engine/view/observer/focusobserver~FocusObserver},
 * * {@link module:engine/view/observer/keyobserver~KeyObserver},
 * * {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}.
 * * {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
 * * {@link module:engine/view/observer/arrowkeysobserver~ArrowKeysObserver}.
 * * {@link module:engine/view/observer/tabobserver~TabObserver}.
 *
 * This class also {@link module:engine/view/view~View#attachDomRoot binds the DOM and the view elements}.
 *
 * If you do not need full a DOM - view management, and only want to transform a tree of view elements to a tree of DOM
 * elements you do not need this controller. You can use the {@link module:engine/view/domconverter~DomConverter DomConverter} instead.
 *
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class View$1 extends Observable {
    /**
     * @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor The styles processor instance.
     */
    constructor(stylesProcessor) {
        super();
        /**
         * Instance of the {@link module:engine/view/document~Document} associated with this view controller.
         *
         * @readonly
         * @type {module:engine/view/document~Document}
         */
        this.document = new Document$1(stylesProcessor);
        /**
         * Instance of the {@link module:engine/view/domconverter~DomConverter domConverter} used by
         * {@link module:engine/view/view~View#_renderer renderer}
         * and {@link module:engine/view/observer/observer~Observer observers}.
         *
         * @readonly
         * @type {module:engine/view/domconverter~DomConverter}
         */
        this.domConverter = new DomConverter(this.document);
        /**
         * Roots of the DOM tree. Map on the `HTMLElement`s with roots names as keys.
         *
         * @readonly
         * @type {Map.<String, HTMLElement>}
         */
        this.domRoots = new Map();
        /**
         * Used to prevent calling {@link #forceRender} and {@link #change} during rendering view to the DOM.
         *
         * @readonly
         * @member {Boolean} #isRenderingInProgress
         */
        this.set('isRenderingInProgress', false);
        /**
         * Informs whether the DOM selection is inside any of the DOM roots managed by the view.
         *
         * @readonly
         * @member {Boolean} #hasDomSelection
         */
        this.set('hasDomSelection', false);
        /**
         * Instance of the {@link module:engine/view/renderer~Renderer renderer}.
         *
         * @protected
         * @type {module:engine/view/renderer~Renderer}
         */
        this._renderer = new Renderer(this.domConverter, this.document.selection);
        this._renderer.bind('isFocused', 'isSelecting').to(this.document, 'isFocused', 'isSelecting');
        /**
         * A DOM root attributes cache. It saves the initial values of DOM root attributes before the DOM element
         * is {@link module:engine/view/view~View#attachDomRoot attached} to the view so later on, when
         * the view is destroyed ({@link module:engine/view/view~View#detachDomRoot}), they can be easily restored.
         * This way, the DOM element can go back to the (clean) state as if the editing view never used it.
         *
         * @private
         * @member {WeakMap.<HTMLElement,Object>}
         */
        this._initialDomRootAttributes = new WeakMap();
        /**
         * Map of registered {@link module:engine/view/observer/observer~Observer observers}.
         *
         * @private
         * @type {Map.<Function, module:engine/view/observer/observer~Observer>}
         */
        this._observers = new Map();
        /**
         * Is set to `true` when {@link #change view changes} are currently in progress.
         *
         * @private
         * @type {Boolean}
         */
        this._ongoingChange = false;
        /**
         * Used to prevent calling {@link #forceRender} and {@link #change} during rendering view to the DOM.
         *
         * @private
         * @type {Boolean}
         */
        this._postFixersInProgress = false;
        /**
         * Internal flag to temporary disable rendering. See the usage in the {@link #_disableRendering}.
         *
         * @private
         * @type {Boolean}
         */
        this._renderingDisabled = false;
        /**
         * Internal flag that disables rendering when there are no changes since the last rendering.
         * It stores information about changed selection and changed elements from attached document roots.
         *
         * @private
         * @type {Boolean}
         */
        this._hasChangedSinceTheLastRendering = false;
        /**
         * DowncastWriter instance used in {@link #change change method} callbacks.
         *
         * @private
         * @type {module:engine/view/downcastwriter~DowncastWriter}
         */
        this._writer = new DowncastWriter(this.document);
        // Add default observers.
        this.addObserver(MutationObserver);
        this.addObserver(SelectionObserver);
        this.addObserver(FocusObserver);
        this.addObserver(KeyObserver);
        this.addObserver(FakeSelectionObserver);
        this.addObserver(CompositionObserver);
        this.addObserver(ArrowKeysObserver);
        this.addObserver(TabObserver);
        if (env.isAndroid) {
            this.addObserver(InputObserver);
        }
        // Inject quirks handlers.
        injectQuirksHandling(this);
        injectUiElementHandling(this);
        // Use 'normal' priority so that rendering is performed as first when using that priority.
        this.on('render', () => {
            this._render();
            // Informs that layout has changed after render.
            this.document.fire('layoutChanged');
            // Reset the `_hasChangedSinceTheLastRendering` flag after rendering.
            this._hasChangedSinceTheLastRendering = false;
        });
        // Listen to the document selection changes directly.
        this.listenTo(this.document.selection, 'change', () => {
            this._hasChangedSinceTheLastRendering = true;
        });
        // Trigger re-render if only the focus changed.
        this.listenTo(this.document, 'change:isFocused', () => {
            this._hasChangedSinceTheLastRendering = true;
        });
    }
    /**
     * Attaches a DOM root element to the view element and enable all observers on that element.
     * Also {@link module:engine/view/renderer~Renderer#markToSync mark element} to be synchronized
     * with the view what means that all child nodes will be removed and replaced with content of the view root.
     *
     * This method also will change view element name as the same as tag name of given dom root.
     * Name is always transformed to lower case.
     *
     * **Note:** Use {@link #detachDomRoot `detachDomRoot()`} to revert this action.
     *
     * @param {Element} domRoot DOM root element.
     * @param {String} [name='main'] Name of the root.
     */
    attachDomRoot(domRoot, name = 'main') {
        const viewRoot = this.document.getRoot(name);
        // Set view root name the same as DOM root tag name.
        viewRoot._name = domRoot.tagName.toLowerCase();
        const initialDomRootAttributes = {};
        // 1. Copy and cache the attributes to remember the state of the element before attaching.
        //    The cached attributes will be restored in detachDomRoot() so the element goes to the
        //    clean state as if the editing view never used it.
        // 2. Apply the attributes using the view writer, so they all go under the control of the engine.
        //    The editing view takes over the attribute management completely because various
        //    features (e.g. addPlaceholder()) require dynamic changes of those attributes and they
        //    cannot be managed by the engine and the UI library at the same time.
        for (const { name, value } of Array.from(domRoot.attributes)) {
            initialDomRootAttributes[name] = value;
            // Do not use writer.setAttribute() for the class attribute. The EditableUIView class
            // and its descendants could have already set some using the writer.addClass() on the view
            // document root. They haven't been rendered yet so they are not present in the DOM root.
            // Using writer.setAttribute( 'class', ... ) would override them completely.
            if (name === 'class') {
                this._writer.addClass(value.split(' '), viewRoot);
            }
            else {
                this._writer.setAttribute(name, value, viewRoot);
            }
        }
        this._initialDomRootAttributes.set(domRoot, initialDomRootAttributes);
        const updateContenteditableAttribute = () => {
            this._writer.setAttribute('contenteditable', (!viewRoot.isReadOnly).toString(), viewRoot);
            if (viewRoot.isReadOnly) {
                this._writer.addClass('ck-read-only', viewRoot);
            }
            else {
                this._writer.removeClass('ck-read-only', viewRoot);
            }
        };
        // Set initial value.
        updateContenteditableAttribute();
        this.domRoots.set(name, domRoot);
        this.domConverter.bindElements(domRoot, viewRoot);
        this._renderer.markToSync('children', viewRoot);
        this._renderer.markToSync('attributes', viewRoot);
        this._renderer.domDocuments.add(domRoot.ownerDocument);
        viewRoot.on('change:children', (evt, node) => this._renderer.markToSync('children', node));
        viewRoot.on('change:attributes', (evt, node) => this._renderer.markToSync('attributes', node));
        viewRoot.on('change:text', (evt, node) => this._renderer.markToSync('text', node));
        viewRoot.on('change:isReadOnly', () => this.change(updateContenteditableAttribute));
        viewRoot.on('change', () => {
            this._hasChangedSinceTheLastRendering = true;
        });
        for (const observer of this._observers.values()) {
            observer.observe(domRoot, name);
        }
    }
    /**
     * Detaches a DOM root element from the view element and restores its attributes to the state before
     * {@link #attachDomRoot `attachDomRoot()`}.
     *
     * @param {String} name Name of the root to detach.
     */
    detachDomRoot(name) {
        const domRoot = this.domRoots.get(name);
        // Remove all root attributes so the DOM element is "bare".
        Array.from(domRoot.attributes).forEach(({ name }) => domRoot.removeAttribute(name));
        const initialDomRootAttributes = this._initialDomRootAttributes.get(domRoot);
        // Revert all view root attributes back to the state before attachDomRoot was called.
        for (const attribute in initialDomRootAttributes) {
            domRoot.setAttribute(attribute, initialDomRootAttributes[attribute]);
        }
        this.domRoots.delete(name);
        this.domConverter.unbindDomElement(domRoot);
    }
    /**
     * Gets DOM root element.
     *
     * @param {String} [name='main']  Name of the root.
     * @returns {Element} DOM root element instance.
     */
    getDomRoot(name = 'main') {
        return this.domRoots.get(name);
    }
    /**
     * Creates observer of the given type if not yet created, {@link module:engine/view/observer/observer~Observer#enable enables} it
     * and {@link module:engine/view/observer/observer~Observer#observe attaches} to all existing and future
     * {@link #domRoots DOM roots}.
     *
     * Note: Observers are recognized by their constructor (classes). A single observer will be instantiated and used only
     * when registered for the first time. This means that features and other components can register a single observer
     * multiple times without caring whether it has been already added or not.
     *
     * @param {Function} Observer The constructor of an observer to add.
     * Should create an instance inheriting from {@link module:engine/view/observer/observer~Observer}.
     * @returns {module:engine/view/observer/observer~Observer} Added observer instance.
     */
    addObserver(ObserverConstructor) {
        let observer = this._observers.get(ObserverConstructor);
        if (observer) {
            return observer;
        }
        observer = new ObserverConstructor(this);
        this._observers.set(ObserverConstructor, observer);
        for (const [name, domElement] of this.domRoots) {
            observer.observe(domElement, name);
        }
        observer.enable();
        return observer;
    }
    /**
     * Returns observer of the given type or `undefined` if such observer has not been added yet.
     *
     * @param {Function} Observer The constructor of an observer to get.
     * @returns {module:engine/view/observer/observer~Observer|undefined} Observer instance or undefined.
     */
    getObserver(ObserverConstructor) {
        return this._observers.get(ObserverConstructor);
    }
    /**
     * Disables all added observers.
     */
    disableObservers() {
        for (const observer of this._observers.values()) {
            observer.disable();
        }
    }
    /**
     * Enables all added observers.
     */
    enableObservers() {
        for (const observer of this._observers.values()) {
            observer.enable();
        }
    }
    /**
     * Scrolls the page viewport and {@link #domRoots} with their ancestors to reveal the
     * caret, if not already visible to the user.
     */
    scrollToTheSelection() {
        const range = this.document.selection.getFirstRange();
        if (range) {
            scrollViewportToShowTarget({
                target: this.domConverter.viewRangeToDom(range),
                viewportOffset: 20
            });
        }
    }
    /**
     * It will focus DOM element representing {@link module:engine/view/editableelement~EditableElement EditableElement}
     * that is currently having selection inside.
     */
    focus() {
        if (!this.document.isFocused) {
            const editable = this.document.selection.editableElement;
            if (editable) {
                this.domConverter.focus(editable);
                this.forceRender();
            }
        }
    }
    /**
     * The `change()` method is the primary way of changing the view. You should use it to modify any node in the view tree.
     * It makes sure that after all changes are made the view is rendered to the DOM (assuming that the view will be changed
     * inside the callback). It prevents situations when the DOM is updated when the view state is not yet correct. It allows
     * to nest calls one inside another and still performs a single rendering after all those changes are made.
     * It also returns the return value of its callback.
     *
     *		const text = view.change( writer => {
     *			const newText = writer.createText( 'foo' );
     *			writer.insert( position1, newText );
     *
     *			view.change( writer => {
     *				writer.insert( position2, writer.createText( 'bar' ) );
     *			} );
     *
     * 			writer.remove( range );
     *
     * 			return newText;
     *		} );
     *
     * When the outermost change block is done and rendering to the DOM is over the
     * {@link module:engine/view/view~View#event:render `View#render`} event is fired.
     *
     * This method throws a `applying-view-changes-on-rendering` error when
     * the change block is used after rendering to the DOM has started.
     *
     * @param {Function} callback Callback function which may modify the view.
     * @returns {*} Value returned by the callback.
     */
    change(callback) {
        if (this.isRenderingInProgress || this._postFixersInProgress) {
            /**
             * Thrown when there is an attempt to make changes to the view tree when it is in incorrect state. This may
             * cause some unexpected behaviour and inconsistency between the DOM and the view.
             * This may be caused by:
             *
             * * calling {@link #change} or {@link #forceRender} during rendering process,
             * * calling {@link #change} or {@link #forceRender} inside of
             *   {@link module:engine/view/document~Document#registerPostFixer post-fixer function}.
             *
             * @error cannot-change-view-tree
             */
            throw new CKEditorError('cannot-change-view-tree', this);
        }
        try {
            // Recursive call to view.change() method - execute listener immediately.
            if (this._ongoingChange) {
                return callback(this._writer);
            }
            // This lock will assure that all recursive calls to view.change() will end up in same block - one "render"
            // event for all nested calls.
            this._ongoingChange = true;
            const callbackResult = callback(this._writer);
            this._ongoingChange = false;
            // This lock is used by editing controller to render changes from outer most model.change() once. As plugins might call
            // view.change() inside model.change() block - this will ensures that postfixers and rendering are called once after all
            // changes. Also, we don't need to render anything if there're no changes since last rendering.
            if (!this._renderingDisabled && this._hasChangedSinceTheLastRendering) {
                this._postFixersInProgress = true;
                this.document._callPostFixers(this._writer);
                this._postFixersInProgress = false;
                this.fire('render');
            }
            return callbackResult;
        }
        catch (err) {
            // @if CK_DEBUG // throw err;
            /* istanbul ignore next */
            CKEditorError.rethrowUnexpectedError(err, this);
        }
    }
    /**
     * Forces rendering {@link module:engine/view/document~Document view document} to DOM. If any view changes are
     * currently in progress, rendering will start after all {@link #change change blocks} are processed.
     *
     * Note that this method is dedicated for special cases. All view changes should be wrapped in the {@link #change}
     * block and the view will automatically check whether it needs to render DOM or not.
     *
     * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `applying-view-changes-on-rendering` when
     * trying to re-render when rendering to DOM has already started.
     */
    forceRender() {
        this._hasChangedSinceTheLastRendering = true;
        this.change(() => { });
    }
    /**
     * Destroys this instance. Makes sure that all observers are destroyed and listeners removed.
     */
    destroy() {
        for (const observer of this._observers.values()) {
            observer.destroy();
        }
        this.document.destroy();
        this.stopListening();
    }
    /**
     * 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 #createPositionBefore},
     * * {@link #createPositionAfter},
     *
     * @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * first parameter is a {@link module:engine/view/item~Item view item}.
     */
    createPositionAt(itemOrPosition, offset) {
        return Position$1._createAt(itemOrPosition, 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}
     */
    createPositionAfter(item) {
        return Position$1._createAfter(item);
    }
    /**
     * 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}
     */
    createPositionBefore(item) {
        return Position$1._createBefore(item);
    }
    /**
     * Creates a range spanning from `start` position to `end` position.
     *
     * **Note:** This factory method 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.
     * @returns {module:engine/view/range~Range}
     */
    createRange(...args) {
        return new Range$1(...args);
    }
    /**
     * 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}
     */
    createRangeOn(item) {
        return Range$1._createOn(item);
    }
    /**
     * 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}
     */
    createRangeIn(element) {
        return Range$1._createIn(element);
    }
    /**
     Creates new {@link module:engine/view/selection~Selection} instance.
     *
     * 		// Creates empty selection without ranges.
     *		const selection = view.createSelection();
     *
     *		// Creates selection at the given range.
     *		const range = view.createRange( start, end );
     *		const selection = view.createSelection( range );
     *
     *		// Creates selection at the given ranges
     * 		const ranges = [ view.createRange( start1, end2 ), view.createRange( star2, end2 ) ];
     *		const selection = view.createSelection( ranges );
     *
     *		// Creates selection from the other selection.
     *		const otherSelection = view.createSelection();
     *		const selection = view.createSelection( otherSelection );
     *
     *		// Creates selection from the document selection.
     *		const selection = view.createSelection( editor.editing.view.document.selection );
     *
     * 		// Creates selection at the given position.
     *		const position = view.createPositionFromPath( root, path );
     *		const selection = view.createSelection( position );
     *
     *		// Creates collapsed selection at the position of given item and offset.
     *		const paragraph = view.createContainerElement( 'paragraph' );
     *		const selection = view.createSelection( paragraph, offset );
     *
     *		// 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.
     *		const selection = view.createSelection( paragraph, 'in' );
     *
     *		// Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
     *		// just after the item.
     *		const selection = view.createSelection( paragraph, 'on' );
     *
     * `Selection`'s factory method allow passing additional options (`backward`, `fake` and `label`) as the last argument.
     *
     *		// Creates backward selection.
     *		const selection = view.createSelection( range, { backward: true } );
     *
     * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
     * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
     * represented in other way, for example by applying proper CSS class.
     *
     * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
     * (and be  properly handled by screen readers).
     *
     *		// Creates fake selection with label.
     *		const selection = view.createSelection( range, { fake: true, label: 'foo' } );
     *
     * @param {module:engine/view/selection~Selectable} [selectable=null]
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
     * @param {String} [options.label] Label for the fake selection.
     * @returns {module:engine/view/selection~Selection}
     */
    createSelection(...args) {
        return new Selection$1(...args);
    }
    /**
     * Disables or enables rendering. If the flag is set to `true` then the rendering will be disabled.
     * If the flag is set to `false` and if there was some change in the meantime, then the rendering action will be performed.
     *
     * @protected
     * @internal
     * @param {Boolean} flag A flag indicates whether the rendering should be disabled.
     */
    _disableRendering(flag) {
        this._renderingDisabled = flag;
        if (flag == false) {
            // Render when you stop blocking rendering.
            this.change(() => { });
        }
    }
    /**
     * Renders all changes. In order to avoid triggering the observers (e.g. mutations) all observers are disabled
     * before rendering and re-enabled after that.
     *
     * @private
     */
    _render() {
        this.isRenderingInProgress = true;
        this.disableObservers();
        this._renderer.render();
        this.enableObservers();
        this.isRenderingInProgress = false;
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
class TypeCheckable {
    /* istanbul ignore next */
    is() {
        // There are a lot of overloads above.
        // Overriding method in derived classes remove them and only `is( type: string ): boolean` is visible which we don't want.
        // One option would be to copy them all to all classes, but that's ugly.
        // It's best when TypeScript compiler doesn't see those overloads, except the one in the top base class.
        // To overload a method, but not let the compiler see it, do after class definition:
        // `MyClass.prototype.is = function( type: string ) {...}`
        throw new Error('is() method is abstract');
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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/writer~Writer Writer API}.
 *
 * Changes done by `Node` methods, like {@link module:engine/model/element~Element#_insertChild _insertChild} 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/writer~Writer#insert inserting} that node to the model tree.
 *
 * Be aware that using {@link module:engine/model/writer~Writer#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$1 extends TypeCheckable {
    /**
     * 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) {
        super();
        /**
         * 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);
    }
    /**
     * {@link module:engine/model/document~Document Document} that owns this root element.
     *
     * @readonly
     * @type {module:engine/model/document~Document|null}
     */
    get document() {
        return null;
    }
    /**
     * 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', this);
        }
        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', this);
        }
        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() {
        // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this
        let root = this;
        while (root.parent) {
            root = root.parent;
        }
        return root;
    }
    /**
     * Returns true if the node is in a tree rooted in the document (is a descendant of one of its roots).
     *
     * @returns {Boolean}
     */
    isAttached() {
        return this.root.is('rootElement');
    }
    /**
     * 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 = [];
        // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this
        let node = 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 = {}) {
        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];
    }
    /**
     * Returns whether this node is before given node. `false` is returned if nodes are in different trees (for example,
     * in different {@link module:engine/model/documentfragment~DocumentFragment}s).
     *
     * @param {module:engine/model/node~Node} node Node to compare with.
     * @returns {Boolean}
     */
    isBefore(node) {
        // Given node is not before this node if they are same.
        if (this == node) {
            return false;
        }
        // Return `false` if it is impossible to compare nodes.
        if (this.root !== node.root) {
            return false;
        }
        const thisPath = this.getPath();
        const nodePath = node.getPath();
        const result = compareArrays(thisPath, nodePath);
        switch (result) {
            case 'prefix':
                return true;
            case 'extension':
                return false;
            default:
                return thisPath[result] < nodePath[result];
        }
    }
    /**
     * Returns whether this node is after given node. `false` is returned if nodes are in different trees (for example,
     * in different {@link module:engine/model/documentfragment~DocumentFragment}s).
     *
     * @param {module:engine/model/node~Node} node Node to compare with.
     * @returns {Boolean}
     */
    isAfter(node) {
        // Given node is not before this node if they are same.
        if (this == node) {
            return false;
        }
        // Return `false` if it is impossible to compare nodes.
        if (this.root !== node.root) {
            return false;
        }
        // In other cases, just check if the `node` is before, and return the opposite.
        return !this.isBefore(node);
    }
    /**
     * 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 {Iterable.<String>}
     */
    getAttributeKeys() {
        return this._attrs.keys();
    }
    /**
     * Converts `Node` to plain object and returns it.
     *
     * @returns {Object} `Node` converted to plain object.
     */
    toJSON() {
        const json = {};
        // Serializes attributes to the object.
        // attributes = { a: 'foo', b: 1, c: true }.
        if (this._attrs.size) {
            json.attributes = Array.from(this._attrs).reduce((result, attr) => {
                result[attr[0]] = attr[1];
                return result;
            }, {});
        }
        return json;
    }
    /**
     * Creates a copy of this node, that is a node with exactly same attributes, and returns it.
     *
     * @internal
     * @protected
     * @returns {module:engine/model/node~Node} Node with same attributes as this node.
     */
    _clone(_deep) {
        return new Node$1(this._attrs);
    }
    /**
     * Removes this node from it's parent.
     *
     * @internal
     * @see module:engine/model/writer~Writer#remove
     * @protected
     */
    _remove() {
        this.parent._removeChildren(this.index);
    }
    /**
     * Sets attribute on the node. If attribute with the same key already is set, it's value is overwritten.
     *
     * @see module:engine/model/writer~Writer#setAttribute
     * @internal
     * @protected
     * @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.
     *
     * @see module:engine/model/writer~Writer#setAttributes
     * @internal
     * @protected
     * @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.
     *
     * @see module:engine/model/writer~Writer#removeAttribute
     * @internal
     * @protected
     * @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.
     *
     * @see module:engine/model/writer~Writer#clearAttributes
     * @internal
     * @protected
     */
    _clearAttributes() {
        this._attrs.clear();
    }
}
/**
 * Checks whether this object is of the given type.
 *
 * This method is useful when processing model objects that are of unknown type. For example, a function
 * may return a {@link module:engine/model/documentfragment~DocumentFragment} or a {@link module:engine/model/node~Node}
 * that can be either a text node or an element. This method can be used to check what kind of object is returned.
 *
 *		someObject.is( 'element' ); // -> true if this is an element
 *		someObject.is( 'node' ); // -> true if this is a node (a text node or an element)
 *		someObject.is( 'documentFragment' ); // -> true if this is a document fragment
 *
 * Since this method is also available on a range of view objects, you can prefix the type of the object with
 * `model:` or `view:` to check, for example, if this is the model's or view's element:
 *
 *		modelElement.is( 'model:element' ); // -> true
 *		modelElement.is( 'view:element' ); // -> false
 *
 * By using this method it is also possible to check a name of an element:
 *
 *		imageElement.is( 'element', 'imageBlock' ); // -> true
 *		imageElement.is( 'element', 'imageBlock' ); // -> same as above
 *		imageElement.is( 'model:element', 'imageBlock' ); // -> same as above, but more precise
 *
 * The list of model objects which implement the `is()` method:
 *
 * * {@link module:engine/model/node~Node#is `Node#is()`}
 * * {@link module:engine/model/text~Text#is `Text#is()`}
 * * {@link module:engine/model/element~Element#is `Element#is()`}
 * * {@link module:engine/model/rootelement~RootElement#is `RootElement#is()`}
 * * {@link module:engine/model/position~Position#is `Position#is()`}
 * * {@link module:engine/model/liveposition~LivePosition#is `LivePosition#is()`}
 * * {@link module:engine/model/range~Range#is `Range#is()`}
 * * {@link module:engine/model/liverange~LiveRange#is `LiveRange#is()`}
 * * {@link module:engine/model/documentfragment~DocumentFragment#is `DocumentFragment#is()`}
 * * {@link module:engine/model/selection~Selection#is `Selection#is()`}
 * * {@link module:engine/model/documentselection~DocumentSelection#is `DocumentSelection#is()`}
 * * {@link module:engine/model/markercollection~Marker#is `Marker#is()`}
 * * {@link module:engine/model/textproxy~TextProxy#is `TextProxy#is()`}
 *
 * @method #is
 * @param {String} type Type to check.
 * @returns {Boolean}
 */
Node$1.prototype.is = function (type) {
    return type === 'node' || type === 'model:node';
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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 {
    /**
     * Creates an empty node list.
     *
     * @protected
     * @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);
        }
    }
    /**
     * Iterable interface.
     *
     * 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
     * {@link 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 model-nodelist-index-out-of-bounds
             */
            throw new CKEditorError('model-nodelist-index-out-of-bounds', this);
        }
        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 model-nodelist-offset-out-of-bounds
             * @param {Number} offset
             * @param {module:engine/model/nodelist~NodeList} nodeList Stringified node list.
             */
            throw new CKEditorError('model-nodelist-offset-out-of-bounds', this, {
                offset,
                nodeList: this
            });
        }
        return this.length;
    }
    /**
     * Inserts given nodes at given index.
     *
     * @internal
     * @protected
     * @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$1)) {
                /**
                 * Trying to insert an object which is not a Node instance.
                 *
                 * @error model-nodelist-insertnodes-not-node
                 */
                throw new CKEditorError('model-nodelist-insertnodes-not-node', this);
            }
        }
        this._nodes.splice(index, 0, ...nodes);
    }
    /**
     * Removes one or more nodes starting at the given index.
     *
     * @internal
     * @protected
     * @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());
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// @if CK_DEBUG_ENGINE // const { convertMapToStringifiedObject } = require( '../dev-utils/utils' );
/**
 * 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.
 *
 * @extends module:engine/model/node~Node
 */
class Text$1 extends Node$1 {
    /**
     * Creates a text node.
     *
     * **Note:** Constructor of this class shouldn't be used directly in the code.
     * Use the {@link module:engine/model/writer~Writer#createText} method instead.
     *
     * @protected
     * @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.
         *
         * @protected
         * @type {String}
         */
        this._data = data || '';
    }
    /**
     * @inheritDoc
     */
    get offsetSize() {
        return this.data.length;
    }
    /**
     * Returns a text data contained in the node.
     *
     * @readonly
     * @type {String}
     */
    get data() {
        return this._data;
    }
    /**
     * 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 copy of this text node and returns it. Created text node has same text data and attributes as original text node.
     *
     * @internal
     * @protected
     * @returns {module:engine/model/text~Text} `Text` instance created using given plain object.
     */
    _clone() {
        return new Text$1(this.data, this.getAttributes());
    }
    /**
     * 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$1(json.data, json.attributes);
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		text.is( '$text' ); // -> true
 *		text.is( 'node' ); // -> true
 *		text.is( 'model:$text' ); // -> true
 *		text.is( 'model:node' ); // -> true
 *
 *		text.is( 'view:$text' ); // -> false
 *		text.is( 'documentSelection' ); // -> false
 *
 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 *
 * **Note:** Until version 20.0.0 this method wasn't accepting `'$text'` type. The legacy `'text'` type is still
 * accepted for backward compatibility.
 *
 * @param {String} type Type to check.
 * @returns {Boolean}
 */
Text$1.prototype.is = function (type) {
    return type === '$text' || type === 'model:$text' ||
        // This are legacy values kept for backward compatibility.
        type === 'text' || type === 'model:text' ||
        // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
        type === 'node' || type === 'model:node';
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// @if CK_DEBUG_ENGINE // const { convertMapToStringifiedObject } = require( '../dev-utils/utils' );
/**
 * `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 extends TypeCheckable {
    /**
     * 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) {
        super();
        /**
         * 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', this);
        }
        if (length < 0 || offsetInText + length > textNode.offsetSize) {
            /**
             * Given `length` value is incorrect.
             *
             * @error model-textproxy-wrong-length
             */
            throw new CKEditorError('model-textproxy-wrong-length', this);
        }
        /**
         * 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|null}
     */
    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|null}
     */
    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;
    }
    /**
     * 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 = {}) {
        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 {Iterable.<String>}
     */
    getAttributeKeys() {
        return this.textNode.getAttributeKeys();
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		textProxy.is( '$textProxy' ); // -> true
 *		textProxy.is( 'model:$textProxy' ); // -> true
 *
 *		textProxy.is( 'view:$textProxy' ); // -> false
 *		textProxy.is( 'range' ); // -> false
 *
 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 *
 * **Note:** Until version 20.0.0 this method wasn't accepting `'$textProxy'` type. The legacy `'textProxt'` type is still
 * accepted for backward compatibility.
 *
 * @param {String} type Type to check.
 * @returns {Boolean}
 */
TextProxy.prototype.is = function (type) {
    return type === '$textProxy' || type === 'model:$textProxy' ||
        // This are legacy values kept for backward compatibility.
        type === 'textProxy' || type === 'model:textProxy';
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// @if CK_DEBUG_ENGINE // const { stringifyMap, convertMapToStringifiedObject, convertMapToTags } = require( '../dev-utils/utils' );
/**
 * 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.
 *
 * @extends module:engine/model/node~Node
 */
class Element extends Node$1 {
    /**
     * Creates a model element.
     *
     * **Note:** Constructor of this class shouldn't be used directly in the code.
     * Use the {@link module:engine/model/writer~Writer#createElement} method instead.
     *
     * @protected
     * @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.
         *
         * @readonly
         * @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();
        if (children) {
            this._insertChild(0, children);
        }
    }
    /**
     * Number of this element's children.
     *
     * @readonly
     * @type {Number}
     */
    get childCount() {
        return this._children.length;
    }
    /**
     * Sum of {@link 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;
    }
    /**
     * Gets the child at the given index.
     *
     * @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 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|null} 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
     * {@link 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|null} Child node's starting offset.
     */
    getChildStartOffset(node) {
        return this._children.getNodeStartOffset(node);
    }
    /**
     * 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);
    }
    /**
     * 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) {
        // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this
        let node = this;
        for (const index of relativePath) {
            node = node.getChild(node.offsetToIndex(index));
        }
        return node;
    }
    /**
     * Returns the parent element of the given name. Returns null if the element is not inside the desired parent.
     *
     * @param {String} parentName The name of the parent element to find.
     * @param {Object} [options] Options object.
     * @param {Boolean} [options.includeSelf=false] When set to `true` this node will be also included while searching.
     * @returns {module:engine/model/element~Element|null}
     */
    findAncestor(parentName, options = {}) {
        let parent = options.includeSelf ? this : this.parent;
        while (parent) {
            if (parent.name === parentName) {
                return parent;
            }
            parent = parent.parent;
        }
        return null;
    }
    /**
     * 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 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 returned.
     *
     * @internal
     * @protected
     * @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)) : undefined;
        return new Element(this.name, this.getAttributes(), children);
    }
    /**
     * {@link module:engine/model/element~Element#_insertChild Inserts} one or more nodes at the end of this element.
     *
     * @see module:engine/model/writer~Writer#append
     * @internal
     * @protected
     * @param {String|module:engine/model/item~Item|Iterable.<String|module:engine/model/item~Item>} nodes Nodes to be inserted.
     */
    _appendChild(nodes) {
        this._insertChild(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.
     *
     * @see module:engine/model/writer~Writer#insert
     * @internal
     * @protected
     * @param {Number} index Index at which nodes should be inserted.
     * @param {String|module:engine/model/item~Item|Iterable.<String|module:engine/model/item~Item>} items Items to be inserted.
     */
    _insertChild(index, items) {
        const nodes = normalize$2(items);
        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`.
     *
     * @see module:engine/model/writer~Writer#remove
     * @protected
     * @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;
    }
    /**
     * 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;
        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.fromJSON(child));
                }
                else {
                    // Otherwise, it is a Text node.
                    children.push(Text$1.fromJSON(child));
                }
            }
        }
        return new Element(json.name, json.attributes, children);
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		element.is( 'element' ); // -> true
 *		element.is( 'node' ); // -> true
 *		element.is( 'model:element' ); // -> true
 *		element.is( 'model:node' ); // -> true
 *
 *		element.is( 'view:element' ); // -> false
 *		element.is( 'documentSelection' ); // -> false
 *
 * Assuming that the object being checked is an element, you can also check its
 * {@link module:engine/model/element~Element#name name}:
 *
 *		element.is( 'element', 'imageBlock' ); // -> true if this is an <imageBlock> element
 *		element.is( 'element', 'imageBlock' ); // -> same as above
 *		text.is( 'element', 'imageBlock' ); -> false
 *
 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 *
 * @param {String} type Type to check.
 * @param {String} [name] Element name.
 * @returns {Boolean}
 */
Element.prototype.is = function (type, name) {
    if (!name) {
        return type === 'element' || type === 'model:element' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === 'node' || type === 'model:node';
    }
    return name === this.name && (type === 'element' || type === 'model:element');
};
// Converts strings to Text and non-iterables to arrays.
//
// @param {String|module:engine/model/item~Item|Iterable.<String|module:engine/model/item~Item>}
// @returns {Iterable.<module:engine/model/node~Node>}
function normalize$2(nodes) {
    // Separate condition because string is iterable.
    if (typeof nodes == 'string') {
        return [new Text$1(nodes)];
    }
    if (!isIterable(nodes)) {
        nodes = [nodes];
    }
    // Array.from to enable .map() on non-arrays.
    return Array.from(nodes)
        .map(node => {
        if (typeof node == 'string') {
            return new Text$1(node);
        }
        if (node instanceof TextProxy) {
            return new Text$1(node.data, node.getAttributes());
        }
        return node;
    });
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Position iterator class. It allows to iterate forward and backward over the document.
 */
class 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|null} [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', null);
        }
        const direction = options.direction || 'forward';
        if (direction != 'forward' && direction != 'backward') {
            /**
             * Only `backward` and `forward` direction allowed.
             *
             * @error model-tree-walker-unknown-direction
             */
            throw new CKEditorError('model-tree-walker-unknown-direction', options, { 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|null} 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 = options.startPosition.clone();
        }
        else {
            this.position = Position._createAt(this.boundaries[this.direction == 'backward' ? 'end' : 'start']);
        }
        // Reset position stickiness in case it was set to other value, as the stickiness is kept after cloning.
        this.position.stickiness = 'toNone';
        /**
         * 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;
    }
    /**
     * Iterable interface.
     *
     * @returns {Iterable.<module:engine/model/treewalker~TreeWalkerValue>}
     */
    [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;
        }
    }
    /**
     * Gets the next tree walker's value.
     *
     * @returns {IteratorResult} Next tree walker's value.
     */
    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 {IteratorResult} return.value Information about taken step.
     */
    _next() {
        const previousPosition = this.position;
        const position = this.position.clone();
        const parent = this._visitedParent;
        // We are at the end of the root.
        if (parent.parent === null && position.offset === parent.maxOffset) {
            return { done: true, value: undefined };
        }
        // We reached the walker boundary.
        if (parent === this._boundaryEndParent && position.offset == this.boundaries.end.offset) {
            return { done: true, value: undefined };
        }
        // Get node just after the current position.
        // Use a highly optimized version instead of checking the text node first and then getting the node after. See #6582.
        const textNodeAtPosition = getTextNodeAtPosition(position, parent);
        const node = textNodeAtPosition ? textNodeAtPosition : getNodeAfterPosition(position, parent, textNodeAtPosition);
        if (node instanceof 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$1) {
            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(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 {IteratorResulte} return.value Information about taken step.
     */
    _previous() {
        const previousPosition = this.position;
        const position = this.position.clone();
        const parent = this._visitedParent;
        // We are at the beginning of the root.
        if (parent.parent === null && position.offset === 0) {
            return { done: true, value: undefined };
        }
        // We reached the walker boundary.
        if (parent == this._boundaryStartParent && position.offset == this.boundaries.start.offset) {
            return { done: true, value: undefined };
        }
        // Get node just before the current position.
        // Use a highly optimized version instead of checking the text node first and then getting the node before. See #6582.
        const positionParent = position.parent;
        const textNodeAtPosition = getTextNodeAtPosition(position, positionParent);
        const node = textNodeAtPosition ? textNodeAtPosition : getNodeBeforePosition(position, positionParent, textNodeAtPosition);
        if (node instanceof 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$1) {
            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(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
        }
    };
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Represents a position in the model tree.
 *
 * A position is represented by its {@link module:engine/model/position~Position#root} and
 * a {@link module:engine/model/position~Position#path} in that root.
 *
 * You can create position instances via its constructor or the `createPosition*()` factory methods of
 * {@link module:engine/model/model~Model} and {@link module:engine/model/writer~Writer}.
 *
 * **Note:** Position is based on offsets, not indexes. This means that a position between two text nodes
 * `foo` and `bar` has offset `3`, not `1`. See {@link module:engine/model/position~Position#path} for more information.
 *
 * Since a position in the 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 places.
 * This requirement is important for operational transformation algorithms.
 *
 * Also, {@link module:engine/model/operation/operation~Operation operations}
 * kept in the {@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 the document has changed.
 *
 * When changes are applied to the 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 extends TypeCheckable {
    /**
     * 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}.
     * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
     * See {@link module:engine/model/position~PositionStickiness}.
     */
    constructor(root, path, stickiness = 'toNone') {
        super();
        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', root);
        }
        if (!(path instanceof Array) || path.length === 0) {
            /**
             * Position path must be an array with at least one item.
             *
             * @error model-position-path-incorrect-format
             * @param path
             */
            throw new CKEditorError('model-position-path-incorrect-format', root, { path });
        }
        // Normalize the root and path when element (not root) is passed.
        if (root.is('rootElement')) {
            path = path.slice();
        }
        else {
            path = [...root.getPath(), ...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 ]
         *
         * @readonly
         * @member {Array.<Number>} module:engine/model/position~Position#path
         */
        this.path = path;
        /**
         * Position stickiness. See {@link module:engine/model/position~PositionStickiness}.
         *
         * @member {module:engine/model/position~PositionStickiness} module:engine/model/position~Position#stickiness
         */
        this.stickiness = stickiness;
    }
    /**
     * 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 this.path[this.path.length - 1];
    }
    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|module:engine/model/documentfragment~DocumentFragment}
     */
    get parent() {
        let parent = this.root;
        for (let i = 0; i < this.path.length - 1; i++) {
            parent = parent.getChild(parent.offsetToIndex(this.path[i]));
            if (!parent) {
                /**
                 * The position's path is incorrect. This means that a position does not point to
                 * a correct place in the tree and hence, some of its methods and getters cannot work correctly.
                 *
                 * **Note**: Unlike DOM and view positions, in the model, the
                 * {@link module:engine/model/position~Position#parent position's parent} is always an element or a document fragment.
                 * The last offset in the {@link module:engine/model/position~Position#path position's path} is the point in this element
                 * where this position points.
                 *
                 * Read more about model positions and offsets in
                 * the {@glink framework/guides/architecture/editing-engine#indexes-and-offsets Editing engine architecture guide}.
                 *
                 * @error model-position-path-incorrect
                 * @param {module:engine/model/position~Position} position The incorrect position.
                 */
                throw new CKEditorError('model-position-path-incorrect', this, { position: this });
            }
        }
        if (parent.is('$text')) {
            throw new CKEditorError('model-position-path-incorrect', this, { position: this });
        }
        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() {
        return getTextNodeAtPosition(this, this.parent);
    }
    /**
     * Node directly after this position or `null` if this position is in text node.
     *
     * @readonly
     * @type {module:engine/model/node~Node|null}
     */
    get nodeAfter() {
        // Cache the parent and reuse for performance reasons. See #6579 and #6582.
        const parent = this.parent;
        return getNodeAfterPosition(this, parent, getTextNodeAtPosition(this, parent));
    }
    /**
     * Node directly before this position or `null` if this position is in text node.
     *
     * @readonly
     * @type {module:engine/model/node~Node|null}
     */
    get nodeBefore() {
        // Cache the parent and reuse for performance reasons. See #6579 and #6582.
        const parent = this.parent;
        return getNodeBeforePosition(this, parent, getTextNodeAtPosition(this, parent));
    }
    /**
     * 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.
     *
     * This method is safe to use it on non-existing positions (for example during operational transformation).
     *
     * @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:
                return this.path[result] < otherPosition.path[result] ? 'before' : '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(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 is safe to use it on non-existing positions (for example during operational transformation).
     *
     * @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/element~Element|module:engine/model/documentfragment~DocumentFragment>} Array with ancestors.
     */
    getAncestors() {
        const parent = this.parent;
        if (parent.is('documentFragment')) {
            return [parent];
        }
        else {
            return parent.getAncestors({ includeSelf: true });
        }
    }
    /**
     * Returns the parent element of the given name. Returns null if the position is not inside the desired parent.
     *
     * @param {String} parentName The name of the parent element to find.
     * @returns {module:engine/model/element~Element|null}
     */
    findAncestor(parentName) {
        const parent = this.parent;
        if (parent.is('element')) {
            return parent.findAncestor(parentName, { includeSelf: true });
        }
        return null;
    }
    /**
     * Returns the slice of two position {@link #path paths} which is identical. The {@link #root roots}
     * of these two paths must be identical.
     *
     * This method is safe to use it on non-existing positions (for example during operational transformation).
     *
     * @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).
     *
     * This method is safe to use it on non-existing positions (for example during operational transformation).
     *
     * @param {Number} shift Offset shift. Can be a negative value.
     * @returns {module:engine/model/position~Position} Shifted position.
     */
    getShiftedBy(shift) {
        const shifted = this.clone();
        const offset = shifted.offset + shift;
        shifted.offset = offset < 0 ? 0 : offset;
        return shifted;
    }
    /**
     * Checks whether this position is after given position.
     *
     * This method is safe to use it on non-existing positions (for example during operational transformation).
     *
     * @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.
     *		}
     *
     * This method is safe to use it on non-existing positions (for example during operational transformation).
     *
     * @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.
     *
     * This method is safe to use it on non-existing positions (for example during operational transformation).
     *
     * @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.
     *
     * @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._createAt(this);
                right = Position._createAt(otherPosition);
                break;
            case 'after':
                left = Position._createAt(otherPosition);
                right = Position._createAt(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);
            }
        }
        // TypeScript compiler thinks that the flow can reach the end of this function without `return` and requires `undefined` or `void`
        // as the return type. This `throw` convinces the compiler that the flow won't pass till the end.
        /* istanbul ignore next */
        throw new Error('unreachable code');
    }
    /**
     * Checks if two positions are in the same parent.
     *
     * This method is safe to use it on non-existing positions (for example during operational transformation).
     *
     * @param {module:engine/model/position~Position} position Position to compare with.
     * @returns {Boolean} `true` if positions have the same parent, `false` otherwise.
     */
    hasSameParentAs(position) {
        if (this.root !== position.root) {
            return false;
        }
        const thisParentPath = this.getParentPath();
        const posParentPath = position.getParentPath();
        return compareArrays(thisParentPath, posParentPath) == 'same';
    }
    /**
     * Returns a copy of this position that is transformed by given `operation`.
     *
     * The new position's parameters are updated accordingly to the effect of the `operation`.
     *
     * For example, if `n` nodes are inserted before the position, the returned position {@link ~Position#offset} will be
     * increased by `n`. If the position was in a merged element, it will be accordingly moved to the new element, etc.
     *
     * This method is safe to use it on non-existing positions (for example during operational transformation).
     *
     * @param {module:engine/model/operation/operation~Operation} operation Operation to transform by.
     * @returns {module:engine/model/position~Position} Transformed position.
     */
    getTransformedByOperation(operation) {
        let result;
        switch (operation.type) {
            case 'insert':
                result = this._getTransformedByInsertOperation(operation);
                break;
            case 'move':
            case 'remove':
            case 'reinsert':
                result = this._getTransformedByMoveOperation(operation);
                break;
            case 'split':
                result = this._getTransformedBySplitOperation(operation);
                break;
            case 'merge':
                result = this._getTransformedByMergeOperation(operation);
                break;
            default:
                result = Position._createAt(this);
                break;
        }
        return result;
    }
    /**
     * Returns a copy of this position transformed by an insert operation.
     *
     * @internal
     * @protected
     * @param {module:engine/model/operation/insertoperation~InsertOperation} operation
     * @returns {module:engine/model/position~Position}
     */
    _getTransformedByInsertOperation(operation) {
        return this._getTransformedByInsertion(operation.position, operation.howMany);
    }
    /**
     * Returns a copy of this position transformed by a move operation.
     *
     * @internal
     * @protected
     * @param {module:engine/model/operation/moveoperation~MoveOperation} operation
     * @returns {module:engine/model/position~Position}
     */
    _getTransformedByMoveOperation(operation) {
        return this._getTransformedByMove(operation.sourcePosition, operation.targetPosition, operation.howMany);
    }
    /**
     * Returns a copy of this position transformed by a split operation.
     *
     * @internal
     * @protected
     * @param {module:engine/model/operation/splitoperation~SplitOperation} operation
     * @returns {module:engine/model/position~Position}
     */
    _getTransformedBySplitOperation(operation) {
        const movedRange = operation.movedRange;
        const isContained = movedRange.containsPosition(this) ||
            (movedRange.start.isEqual(this) && this.stickiness == 'toNext');
        if (isContained) {
            return this._getCombined(operation.splitPosition, operation.moveTargetPosition);
        }
        else {
            if (operation.graveyardPosition) {
                return this._getTransformedByMove(operation.graveyardPosition, operation.insertionPosition, 1);
            }
            else {
                return this._getTransformedByInsertion(operation.insertionPosition, 1);
            }
        }
    }
    /**
     * Returns a copy of this position transformed by merge operation.
     *
     * @internal
     * @protected
     * @param {module:engine/model/operation/mergeoperation~MergeOperation} operation
     * @returns {module:engine/model/position~Position}
     */
    _getTransformedByMergeOperation(operation) {
        const movedRange = operation.movedRange;
        const isContained = movedRange.containsPosition(this) || movedRange.start.isEqual(this);
        let pos;
        if (isContained) {
            pos = this._getCombined(operation.sourcePosition, operation.targetPosition);
            if (operation.sourcePosition.isBefore(operation.targetPosition)) {
                // Above happens during OT when the merged element is moved before the merged-to element.
                pos = pos._getTransformedByDeletion(operation.deletionPosition, 1);
            }
        }
        else if (this.isEqual(operation.deletionPosition)) {
            pos = Position._createAt(operation.deletionPosition);
        }
        else {
            pos = this._getTransformedByMove(operation.deletionPosition, operation.graveyardPosition, 1);
        }
        return pos;
    }
    /**
     * 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.
     *
     * @internal
     * @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._createAt(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`.
     *
     * @internal
     * @protected
     * @param {module:engine/model/position~Position} insertPosition Position where nodes are inserted.
     * @param {Number} howMany How many nodes are inserted.
     * @returns {module:engine/model/position~Position} Transformed position.
     */
    _getTransformedByInsertion(insertPosition, howMany) {
        const transformed = Position._createAt(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 && this.stickiness != 'toPrevious')) {
                // 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`.
     *
     * @internal
     * @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`.
     * @returns {module:engine/model/position~Position} Transformed position.
     */
    _getTransformedByMove(sourcePosition, targetPosition, howMany) {
        // Update target position, as it could be affected by nodes removal.
        targetPosition = targetPosition._getTransformedByDeletion(sourcePosition, howMany);
        if (sourcePosition.isEqual(targetPosition)) {
            // If `targetPosition` is equal to `sourcePosition` this isn't really any move. Just return position as it is.
            return Position._createAt(this);
        }
        // Moving a range removes nodes from their original position. We acknowledge this by proper transformation.
        const transformed = this._getTransformedByDeletion(sourcePosition, howMany);
        const isMoved = transformed === null ||
            (sourcePosition.isEqual(this) && this.stickiness == 'toNext') ||
            (sourcePosition.getShiftedBy(howMany).isEqual(this) && this.stickiness == 'toPrevious');
        if (isMoved) {
            // 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.
            return 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.
            return transformed._getTransformedByInsertion(targetPosition, howMany);
        }
    }
    /**
     * 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 = model.createPositionFromPath( root, [ 2, 3, 1 ] );
     *		let source = model.createPositionFromPath( root, [ 2, 2 ] );
     *		let target = model.createPositionFromPath( 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 ]`.
     *
     * @internal
     * @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._createAt(target);
        combined.stickiness = this.stickiness;
        // 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, ...this.path.slice(i + 1)];
        return combined;
    }
    /**
     * @inheritDoc
     */
    toJSON() {
        return {
            root: this.root.toJSON(),
            path: Array.from(this.path),
            stickiness: this.stickiness
        };
    }
    /**
     * Returns a new position that is equal to current position.
     *
     * @returns {module:engine/model/position~Position}
     */
    clone() {
        return new this.constructor(this.root, this.path, this.stickiness);
    }
    /**
     * 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 factory methods such as:
     *
     * * {@link module:engine/model/position~Position._createBefore},
     * * {@link module:engine/model/position~Position._createAfter}.
     *
     * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when the
     * first parameter is a {@link module:engine/model/item~Item model item}.
     * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness. Used only when the
     * first parameter is a {@link module:engine/model/item~Item model item}.
     * @protected
     * @internal
     */
    static _createAt(itemOrPosition, offset, stickiness = 'toNone') {
        if (itemOrPosition instanceof Position) {
            return new Position(itemOrPosition.root, itemOrPosition.path, itemOrPosition.stickiness);
        }
        else {
            const node = itemOrPosition;
            if (offset == 'end') {
                offset = node.maxOffset;
            }
            else if (offset == 'before') {
                return this._createBefore(node, stickiness);
            }
            else if (offset == 'after') {
                return this._createAfter(node, stickiness);
            }
            else if (offset !== 0 && !offset) {
                /**
                 * {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}
                 * requires the offset to be specified when the first parameter is a model item.
                 *
                 * @error model-createpositionat-offset-required
                 */
                throw new CKEditorError('model-createpositionat-offset-required', [this, itemOrPosition]);
            }
            if (!node.is('element') && !node.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', [this, itemOrPosition]);
            }
            const path = node.getPath();
            path.push(offset);
            return new this(node.root, path, stickiness);
        }
    }
    /**
     * 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.
     * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
     * @returns {module:engine/model/position~Position}
     * @protected
     * @internal
     */
    static _createAfter(item, stickiness) {
        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', [this, item], { root: item });
        }
        return this._createAt(item.parent, item.endOffset, stickiness);
    }
    /**
     * 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.
     * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
     * @returns {module:engine/model/position~Position}
     * @protected
     * @internal
     */
    static _createBefore(item, stickiness) {
        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', item, { root: item });
        }
        return this._createAt(item.parent, item.startOffset, stickiness);
    }
    /**
     * Creates a `Position` instance from given plain object (i.e. parsed JSON string).
     *
     * @param {Object} json Plain object to be converted to `Position`.
     * @param {module:engine/model/document~Document} doc Document object that will be position owner.
     * @returns {module:engine/model/position~Position} `Position` instance created using given plain object.
     */
    static fromJSON(json, doc) {
        if (json.root === '$graveyard') {
            const pos = new Position(doc.graveyard, json.path);
            pos.stickiness = json.stickiness;
            return pos;
        }
        if (!doc.getRoot(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', doc, { rootName: json.root });
        }
        return new Position(doc.getRoot(json.root), json.path, json.stickiness);
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		position.is( 'position' ); // -> true
 *		position.is( 'model:position' ); // -> true
 *
 *		position.is( 'view:position' ); // -> false
 *		position.is( 'documentSelection' ); // -> false
 *
 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 *
 * @param {String} type
 * @returns {Boolean}
 */
Position.prototype.is = function (type) {
    return type === 'position' || type === 'model:position';
};
/**
 * Returns a text node at the given position.
 *
 * This is a helper function optimized to reuse the position parent instance for performance reasons.
 *
 * Normally, you should use {@link module:engine/model/position~Position#textNode `Position#textNode`}.
 * If you start hitting performance issues with {@link module:engine/model/position~Position#parent `Position#parent`}
 * check if your algorithm does not access it multiple times (which can happen directly or indirectly via other position properties).
 *
 * See https://github.com/ckeditor/ckeditor5/issues/6579.
 *
 * See also:
 *
 * * {@link module:engine/model/position~getNodeAfterPosition}
 * * {@link module:engine/model/position~getNodeBeforePosition}
 *
 * @param {module:engine/model/position~Position} position
 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} positionParent The parent of the
 * given position.
 * @returns {module:engine/model/text~Text|null}
 */
function getTextNodeAtPosition(position, positionParent) {
    const node = positionParent.getChild(positionParent.offsetToIndex(position.offset));
    if (node && node.is('$text') && node.startOffset < position.offset) {
        return node;
    }
    return null;
}
/**
 * Returns the node after the given position.
 *
 * This is a helper function optimized to reuse the position parent instance and the calculation of the text node at the
 * specific position for performance reasons.
 *
 * Normally, you should use {@link module:engine/model/position~Position#nodeAfter `Position#nodeAfter`}.
 * If you start hitting performance issues with {@link module:engine/model/position~Position#parent `Position#parent`} and/or
 * {@link module:engine/model/position~Position#textNode `Position#textNode`}
 * check if your algorithm does not access those properties multiple times
 * (which can happen directly or indirectly via other position properties).
 *
 * See https://github.com/ckeditor/ckeditor5/issues/6579 and https://github.com/ckeditor/ckeditor5/issues/6582.
 *
 * See also:
 *
 * * {@link module:engine/model/position~getTextNodeAtPosition}
 * * {@link module:engine/model/position~getNodeBeforePosition}
 *
 * @param {module:engine/model/position~Position} position
 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} positionParent The parent of the
 * given position.
 * @param {module:engine/model/text~Text|null} textNode Text node at the given position.
 * @returns {module:engine/model/node~Node|null}
 */
function getNodeAfterPosition(position, positionParent, textNode) {
    if (textNode !== null) {
        return null;
    }
    return positionParent.getChild(positionParent.offsetToIndex(position.offset));
}
/**
 * Returns the node before the given position.
 *
 * Refer to {@link module:engine/model/position~getNodeBeforePosition} for documentation on when to use this util method.
 *
 * See also:
 *
 * * {@link module:engine/model/position~getTextNodeAtPosition}
 * * {@link module:engine/model/position~getNodeAfterPosition}
 *
 * @param {module:engine/model/position~Position} position
 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} positionParent The parent of the
 * given position.
 * @param {module:engine/model/text~Text|null} textNode Text node at the given position.
 * @returns {module:engine/model/node~Node|null}
 */
function getNodeBeforePosition(position, positionParent, textNode) {
    if (textNode !== null) {
        return null;
    }
    return positionParent.getChild(positionParent.offsetToIndex(position.offset) - 1);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Represents a range in the model tree.
 *
 * A range is defined by its {@link module:engine/model/range~Range#start} and {@link module:engine/model/range~Range#end}
 * positions.
 *
 * You can create range instances via its constructor or the `createRange*()` factory methods of
 * {@link module:engine/model/model~Model} and {@link module:engine/model/writer~Writer}.
 */
class Range extends TypeCheckable {
    /**
     * Creates a range spanning from `start` position to `end` position.
     *
     * @param {module:engine/model/position~Position} start The start position.
     * @param {module:engine/model/position~Position|null} [end] The end position. If not set,
     * the range will be collapsed at the `start` position.
     */
    constructor(start, end) {
        super();
        /**
         * Start position.
         *
         * @readonly
         * @member {module:engine/model/position~Position}
         */
        this.start = Position._createAt(start);
        /**
         * End position.
         *
         * @readonly
         * @member {module:engine/model/position~Position}
         */
        this.end = end ? Position._createAt(end) : Position._createAt(start);
        // If the range is collapsed, treat in a similar way as a position and set its boundaries stickiness to 'toNone'.
        // In other case, make the boundaries stick to the "inside" of the range.
        this.start.stickiness = this.isCollapsed ? 'toNone' : 'toNext';
        this.end.stickiness = this.isCollapsed ? 'toNone' : 'toPrevious';
    }
    /**
     * Iterable interface.
     *
     * 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 {Iterator.<module:engine/model/treewalker~TreeWalkerValue>}
     */
    *[Symbol.iterator]() {
        yield* new 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() {
        const startParentPath = this.start.getParentPath();
        const endParentPath = this.end.getParentPath();
        return compareArrays(startParentPath, endParentPath) == 'same';
    }
    /**
     * 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._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 = model.createRange(
     *			model.createPositionFromPath( root, [ 2, 7 ] ),
     *			model.createPositionFromPath( root, [ 4, 0, 1 ] )
     *		);
     *		let otherRange = model.createRange( model.createPositionFromPath( root, [ 1 ] ), model.createPositionFromPath( root, [ 5 ] ) );
     *		let transformed = range.getDifference( otherRange );
     *		// transformed array has no ranges because `otherRange` contains `range`
     *
     *		otherRange = model.createRange( model.createPositionFromPath( root, [ 1 ] ), model.createPositionFromPath( root, [ 3 ] ) );
     *		transformed = range.getDifference( otherRange );
     *		// transformed array has one range: from [ 3 ] to [ 4, 0, 1 ]
     *
     *		otherRange = model.createRange( model.createPositionFromPath( root, [ 3 ] ), model.createPositionFromPath( 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(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(otherRange.end, this.end));
            }
        }
        else {
            // Ranges do not intersect, return the original range.
            ranges.push(new Range(this.start, this.end));
        }
        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 = model.createRange(
     *			model.createPositionFromPath( root, [ 2, 7 ] ),
     *			model.createPositionFromPath( root, [ 4, 0, 1 ] )
     *		);
     *		let otherRange = model.createRange( model.createPositionFromPath( root, [ 1 ] ), model.createPositionFromPath( root, [ 2 ] ) );
     *		let transformed = range.getIntersection( otherRange ); // null - ranges have no common part
     *
     *		otherRange = model.createRange( model.createPositionFromPath( root, [ 3 ] ), model.createPositionFromPath( 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(commonRangeStart, commonRangeEnd);
        }
        // Ranges do not intersect, so they do not have common part.
        return null;
    }
    /**
     * Returns a range created by joining this {@link ~Range range} with the given {@link ~Range range}.
     * If ranges have no common part, returns `null`.
     *
     * Examples:
     *
     *		let range = model.createRange(
     *			model.createPositionFromPath( root, [ 2, 7 ] ),
     *			model.createPositionFromPath( root, [ 4, 0, 1 ] )
     *		);
     *		let otherRange = model.createRange(
     *			model.createPositionFromPath( root, [ 1 ] ),
     *			model.createPositionFromPath( root, [ 2 ] )
     *		);
     *		let transformed = range.getJoined( otherRange ); // null - ranges have no common part
     *
     *		otherRange = model.createRange(
     *			model.createPositionFromPath( root, [ 3 ] ),
     *			model.createPositionFromPath( root, [ 5 ] )
     *		);
     *		transformed = range.getJoined( otherRange ); // range from [ 2, 7 ] to [ 5 ]
     *
     * @param {module:engine/model/range~Range} otherRange Range to be joined.
     * @param {Boolean} [loose=false] Whether the intersection check is loose or strict. If the check is strict (`false`),
     * ranges are tested for intersection or whether start/end positions are equal. If the check is loose (`true`),
     * compared range is also checked if it's {@link module:engine/model/position~Position#isTouching touching} current range.
     * @returns {module:engine/model/range~Range|null} A sum of given ranges or `null` if ranges have no common part.
     */
    getJoined(otherRange, loose = false) {
        let shouldJoin = this.isIntersecting(otherRange);
        if (!shouldJoin) {
            if (this.start.isBefore(otherRange.start)) {
                shouldJoin = loose ? this.end.isTouching(otherRange.start) : this.end.isEqual(otherRange.start);
            }
            else {
                shouldJoin = loose ? otherRange.end.isTouching(this.start) : otherRange.end.isEqual(this.start);
            }
        }
        if (!shouldJoin) {
            return null;
        }
        let startPosition = this.start;
        let endPosition = this.end;
        if (otherRange.start.isBefore(startPosition)) {
            startPosition = otherRange.start;
        }
        if (otherRange.end.isAfter(endPosition)) {
            endPosition = otherRange.end;
        }
        return new Range(startPosition, endPosition);
    }
    /**
     * 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._createAt(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(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(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.
     *
     * For example, to iterate over all items in the entire document root:
     *
     *		// Create a range spanning over the entire root content:
     *		const range = editor.model.createRangeIn( editor.model.document.getRoot() );
     *
     *		// Iterate over all items in this range:
     *		for ( const value of range.getWalker() ) {
     *			console.log( value.item );
     *		}
     *
     * @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]
     * @returns {module:engine/model/treewalker~TreeWalker}
     */
    getWalker(options = {}) {
        options.boundaries = this;
        return new 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.
     *
     * @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(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(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 `operation`.
     *
     * **Note:** transformation may break one range into multiple ranges (for example, 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/operation/operation~Operation} operation Operation to transform range by.
     * @returns {Array.<module:engine/model/range~Range>} Range which is the result of transformation.
     */
    getTransformedByOperation(operation) {
        switch (operation.type) {
            case 'insert':
                return this._getTransformedByInsertOperation(operation);
            case 'move':
            case 'remove':
            case 'reinsert':
                return this._getTransformedByMoveOperation(operation);
            case 'split':
                return [this._getTransformedBySplitOperation(operation)];
            case 'merge':
                return [this._getTransformedByMergeOperation(operation)];
        }
        return [new Range(this.start, this.end)];
    }
    /**
     * Returns a range that is a result of transforming this range by multiple `operations`.
     *
     * @see ~Range#getTransformedByOperation
     * @param {Iterable.<module:engine/model/operation/operation~Operation>} operations Operations to transform the range by.
     * @returns {Array.<module:engine/model/range~Range>} Range which is the result of transformation.
     */
    getTransformedByOperations(operations) {
        const ranges = [new Range(this.start, this.end)];
        for (const operation of operations) {
            for (let i = 0; i < ranges.length; i++) {
                const result = ranges[i].getTransformedByOperation(operation);
                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 an {@link module:engine/model/element~Element Element} contained by the range.
     * The element will be returned when it is the **only** node within the range and **fully–contained**
     * at the same time.
     *
     * @returns {module:engine/model/element~Element|null}
     */
    getContainedElement() {
        if (this.isCollapsed) {
            return null;
        }
        const nodeAfterStart = this.start.nodeAfter;
        const nodeBeforeEnd = this.end.nodeBefore;
        if (nodeAfterStart && nodeAfterStart.is('element') && nodeAfterStart === nodeBeforeEnd) {
            return nodeAfterStart;
        }
        return null;
    }
    /**
     * Converts `Range` to plain object and returns it.
     *
     * @returns {Object} `Node` converted to plain object.
     */
    toJSON() {
        return {
            start: this.start.toJSON(),
            end: this.end.toJSON()
        };
    }
    /**
     * Returns a new range that is equal to current range.
     *
     * @returns {module:engine/model/range~Range}
     */
    clone() {
        return new this.constructor(this.start, this.end);
    }
    /**
     * Returns a result of transforming a copy of this range by insert operation.
     *
     * One or more ranges may be returned as a result of this transformation.
     *
     * @internal
     * @protected
     * @param {module:engine/model/operation/insertoperation~InsertOperation} operation
     * @returns {Array.<module:engine/model/range~Range>}
     */
    _getTransformedByInsertOperation(operation, spread = false) {
        return this._getTransformedByInsertion(operation.position, operation.howMany, spread);
    }
    /**
     * Returns a result of transforming a copy of this range by move operation.
     *
     * One or more ranges may be returned as a result of this transformation.
     *
     * @internal
     * @protected
     * @param {module:engine/model/operation/moveoperation~MoveOperation} operation
     * @returns {Array.<module:engine/model/range~Range>}
     */
    _getTransformedByMoveOperation(operation, spread = false) {
        const sourcePosition = operation.sourcePosition;
        const howMany = operation.howMany;
        const targetPosition = operation.targetPosition;
        return this._getTransformedByMove(sourcePosition, targetPosition, howMany, spread);
    }
    /**
     * Returns a result of transforming a copy of this range by split operation.
     *
     * Always one range is returned. The transformation is done in a way to not break the range.
     *
     * @internal
     * @protected
     * @param {module:engine/model/operation/splitoperation~SplitOperation} operation
     * @returns {module:engine/model/range~Range}
     */
    _getTransformedBySplitOperation(operation) {
        const start = this.start._getTransformedBySplitOperation(operation);
        let end = this.end._getTransformedBySplitOperation(operation);
        if (this.end.isEqual(operation.insertionPosition)) {
            end = this.end.getShiftedBy(1);
        }
        // Below may happen when range contains graveyard element used by split operation.
        if (start.root != end.root) {
            // End position was next to the moved graveyard element and was moved with it.
            // Fix it by using old `end` which has proper `root`.
            end = this.end.getShiftedBy(-1);
        }
        return new Range(start, end);
    }
    /**
     * Returns a result of transforming a copy of this range by merge operation.
     *
     * Always one range is returned. The transformation is done in a way to not break the range.
     *
     * @internal
     * @protected
     * @param {module:engine/model/operation/mergeoperation~MergeOperation} operation
     * @returns {module:engine/model/range~Range}
     */
    _getTransformedByMergeOperation(operation) {
        // Special case when the marker is set on "the closing tag" of an element. Marker can be set like that during
        // transformations, especially when a content of a few block elements were removed. For example:
        //
        // {} is the transformed range, [] is the removed range.
        // <p>F[o{o</p><p>B}ar</p><p>Xy]z</p>
        //
        // <p>Fo{o</p><p>B}ar</p><p>z</p>
        // <p>F{</p><p>B}ar</p><p>z</p>
        // <p>F{</p>}<p>z</p>
        // <p>F{}z</p>
        //
        if (this.start.isEqual(operation.targetPosition) && this.end.isEqual(operation.deletionPosition)) {
            return new Range(this.start);
        }
        let start = this.start._getTransformedByMergeOperation(operation);
        let end = this.end._getTransformedByMergeOperation(operation);
        if (start.root != end.root) {
            // This happens when the end position was next to the merged (deleted) element.
            // Then, the end position was moved to the graveyard root. In this case we need to fix
            // the range cause its boundaries would be in different roots.
            end = this.end.getShiftedBy(-1);
        }
        if (start.isAfter(end)) {
            // This happens in three following cases:
            //
            // Case 1: Merge operation source position is before the target position (due to some transformations, OT, etc.)
            //         This means that start can be moved before the end of the range.
            //
            // Before: <p>a{a</p><p>b}b</p><p>cc</p>
            // Merge:  <p>b}b</p><p>cca{a</p>
            // Fix:    <p>{b}b</p><p>ccaa</p>
            //
            // Case 2: Range start is before merged node but not directly.
            //         Result should include all nodes that were in the original range.
            //
            // Before: <p>aa</p>{<p>cc</p><p>b}b</p>
            // Merge:  <p>aab}b</p>{<p>cc</p>
            // Fix:    <p>aa{bb</p><p>cc</p>}
            //
            //         The range is expanded by an additional `b` letter but it is better than dropping the whole `cc` paragraph.
            //
            // Case 3: Range start is directly before merged node.
            //         Resulting range should include only nodes from the merged element:
            //
            // Before: <p>aa</p>{<p>b}b</p><p>cc</p>
            // Merge:  <p>aab}b</p>{<p>cc</p>
            // Fix:    <p>aa{b}b</p><p>cc</p>
            //
            if (operation.sourcePosition.isBefore(operation.targetPosition)) {
                // Case 1.
                start = Position._createAt(end);
                start.offset = 0;
            }
            else {
                if (!operation.deletionPosition.isEqual(start)) {
                    // Case 2.
                    end = operation.deletionPosition;
                }
                // In both case 2 and 3 start is at the end of the merge-to element.
                start = operation.targetPosition;
            }
            return new Range(start, end);
        }
        return new Range(start, end);
    }
    /**
     * 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 = model.createRange(
     *			model.createPositionFromPath( root, [ 2, 7 ] ),
     *			model.createPositionFromPath( root, [ 4, 0, 1 ] )
     *		);
     *		let transformed = range._getTransformedByInsertion( model.createPositionFromPath( root, [ 1 ] ), 2 );
     *		// transformed array has one range from [ 4, 7 ] to [ 6, 0, 1 ]
     *
     *		transformed = range._getTransformedByInsertion( model.createPositionFromPath( root, [ 4, 0, 0 ] ), 4 );
     *		// transformed array has one range from [ 2, 7 ] to [ 4, 0, 5 ]
     *
     *		transformed = range._getTransformedByInsertion( model.createPositionFromPath( root, [ 3, 2 ] ), 4 );
     *		// transformed array has one range, which is equal to original range
     *
     *		transformed = range._getTransformedByInsertion( model.createPositionFromPath( root, [ 3, 2 ] ), 4, true );
     *		// transformed array has two ranges: from [ 2, 7 ] to [ 3, 2 ] and from [ 3, 6 ] to [ 4, 0, 1 ]
     *
     * @internal
     * @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`.
     * @returns {Array.<module:engine/model/range~Range>} Result of the transformation.
     */
    _getTransformedByInsertion(insertPosition, howMany, spread = 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(this.start, insertPosition),
                new Range(insertPosition.getShiftedBy(howMany), this.end._getTransformedByInsertion(insertPosition, howMany))
            ];
        }
        else {
            const range = new Range(this.start, this.end);
            range.start = range.start._getTransformedByInsertion(insertPosition, howMany);
            range.end = range.end._getTransformedByInsertion(insertPosition, howMany);
            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`.
     *
     * @internal
     * @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.
     * @param {Boolean} [spread=false] Whether the range should be spread if the move points inside the range.
     * @returns {Array.<module:engine/model/range~Range>} Result of the transformation.
     */
    _getTransformedByMove(sourcePosition, targetPosition, howMany, spread = false) {
        // Special case for transforming a collapsed range. Just transform it like a position.
        if (this.isCollapsed) {
            const newPos = this.start._getTransformedByMove(sourcePosition, targetPosition, howMany);
            return [new Range(newPos)];
        }
        // Special case for transformation when a part of the range is moved towards the range.
        //
        // Examples:
        //
        // <div><p>ab</p><p>c[d</p></div><p>e]f</p> --> <div><p>ab</p></div><p>c[d</p><p>e]f</p>
        // <p>e[f</p><div><p>a]b</p><p>cd</p></div> --> <p>e[f</p><p>a]b</p><div><p>cd</p></div>
        //
        // Without this special condition, the default algorithm leaves an "artifact" range from one of `differenceSet` parts:
        //
        // <div><p>ab</p><p>c[d</p></div><p>e]f</p> --> <div><p>ab</p>{</div>}<p>c[d</p><p>e]f</p>
        //
        // This special case is applied only if the range is to be kept together (not spread).
        const moveRange = Range._createFromPositionAndShift(sourcePosition, howMany);
        const insertPosition = targetPosition._getTransformedByDeletion(sourcePosition, howMany);
        if (this.containsPosition(targetPosition) && !spread) {
            if (moveRange.containsPosition(this.start) || moveRange.containsPosition(this.end)) {
                const start = this.start._getTransformedByMove(sourcePosition, targetPosition, howMany);
                const end = this.end._getTransformedByMove(sourcePosition, targetPosition, howMany);
                return [new Range(start, end)];
            }
        }
        // Default algorithm.
        let result;
        const differenceSet = this.getDifference(moveRange);
        let difference = null;
        const common = this.getIntersection(moveRange);
        if (differenceSet.length == 1) {
            // `moveRange` and this range may intersect but may be separate.
            difference = new 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(this.start, this.end._getTransformedByDeletion(sourcePosition, howMany));
        } // else, `moveRange` contains this range.
        if (difference) {
            result = difference._getTransformedByInsertion(insertPosition, howMany, common !== null || spread);
        }
        else {
            result = [];
        }
        if (common) {
            const transformedCommon = new Range(common.start._getCombined(moveRange.start, insertPosition), common.end._getCombined(moveRange.start, insertPosition));
            if (result.length == 2) {
                result.splice(1, 0, transformedCommon);
            }
            else {
                result.push(transformedCommon);
            }
        }
        return result;
    }
    /**
     * Returns a copy of this range that is transformed by deletion of `howMany` nodes from `deletePosition`.
     *
     * If the deleted range is intersecting with the transformed range, the transformed range will be shrank.
     *
     * If the deleted range contains transformed range, `null` will be returned.
     *
     * @internal
     * @protected
     * @param {module:engine/model/position~Position} deletionPosition Position from which nodes are removed.
     * @param {Number} howMany How many nodes are removed.
     * @returns {module:engine/model/range~Range|null} Result of the transformation.
     */
    _getTransformedByDeletion(deletePosition, howMany) {
        let newStart = this.start._getTransformedByDeletion(deletePosition, howMany);
        let newEnd = this.end._getTransformedByDeletion(deletePosition, howMany);
        if (newStart == null && newEnd == null) {
            return null;
        }
        if (newStart == null) {
            newStart = deletePosition;
        }
        if (newEnd == null) {
            newEnd = deletePosition;
        }
        return new Range(newStart, newEnd);
    }
    /**
     * 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.
     *
     * @internal
     * @protected
     * @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 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.
     *
     * @internal
     * @protected
     * @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 new this(Position._createAt(element, 0), Position._createAt(element, element.maxOffset));
    }
    /**
     * Creates a range that starts before given {@link module:engine/model/item~Item model item} and ends after it.
     *
     * @internal
     * @protected
     * @param {module:engine/model/item~Item} item
     * @returns {module:engine/model/range~Range}
     */
    static _createOn(item) {
        return this._createFromPositionAndShift(Position._createBefore(item), item.offsetSize);
    }
    /**
     * 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.
     *
     * @internal
     * @protected
     * @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', null);
        }
        else if (ranges.length == 1) {
            return ranges[0].clone();
        }
        // 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.
        if (refIndex > 0) {
            // eslint-disable-next-line no-constant-condition
            for (let i = refIndex - 1; true; i++) {
                if (ranges[i].end.isEqual(result.start)) {
                    result.start = Position._createAt(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._createAt(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/range~Range} `Range` instance created using given plain object.
     */
    static fromJSON(json, doc) {
        return new this(Position.fromJSON(json.start, doc), Position.fromJSON(json.end, doc));
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		range.is( 'range' ); // -> true
 *		range.is( 'model:range' ); // -> true
 *
 *		range.is( 'view:range' ); // -> false
 *		range.is( 'documentSelection' ); // -> false
 *
 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 *
 * @param {String} type
 * @returns {Boolean}
 */
Range.prototype.is = function (type) {
    return type === 'range' || type === 'model:range';
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Maps elements, positions and markers between the {@link module:engine/view/document~Document view} and
 * the {@link module:engine/model/model model}.
 *
 * The instance of the Mapper used for the editing pipeline is available in
 * {@link module:engine/controller/editingcontroller~EditingController#mapper `editor.editing.mapper`}.
 *
 * Mapper uses 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 the complex model to/from view relations, you may provide custom callbacks for the
 * {@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 the {@link module:engine/conversion/mapper~Mapper#toViewPosition toViewPosition}
 * and {@link module:engine/conversion/mapper~Mapper#toModelPosition toModelPosition} methods. `Mapper` adds its own default callbacks
 * with `'lowest'` priority. To override default `Mapper` mapping, add custom callback with higher priority and
 * stop the event.
 * @mixes module:utils/emittermixin~EmitterMixin
 */
class Mapper extends Emitter$1 {
    /**
     * Creates an instance of the mapper.
     */
    constructor() {
        super();
        /**
         * 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();
        /**
         * Model marker name to view elements mapping.
         *
         * Keys are `String`s while values are `Set`s with {@link module:engine/view/element~Element view elements}.
         * One marker (name) can be mapped to multiple elements.
         *
         * @private
         * @member {Map}
         */
        this._markerNameToElements = new Map();
        /**
         * View element to model marker names mapping.
         *
         * This is reverse to {@link ~Mapper#_markerNameToElements} map.
         *
         * @private
         * @member {Map}
         */
        this._elementToMarkerNames = new Map();
        /**
         * The map of removed view elements with their current root (used for deferred unbinding).
         *
         * @private
         * @member {Map.<module:engine/view/element~Element,module:engine/view/documentfragment~DocumentFragment>}
         */
        this._deferredBindingRemovals = new Map();
        /**
         * Stores marker names of markers which have changed due to unbinding a view element (so it is assumed that the view element
         * has been removed, moved or renamed).
         *
         * @private
         * @member {Set.<module:engine/model/markercollection~Marker>}
         */
        this._unboundMarkerNames = new Set();
        // 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);
            if (!viewContainer) {
                /**
                 * A model position could not be mapped to the view because the parent of the model position
                 * does not have a mapped view element (might have not been converted yet or it has no converter).
                 *
                 * Make sure that the model element is correctly converted to the view.
                 *
                 * @error mapping-model-position-view-parent-not-found
                 */
                throw new CKEditorError('mapping-model-position-view-parent-not-found', this, { modelPosition: data.modelPosition });
            }
            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;
            }
            const viewBlock = this.findMappedViewAncestor(data.viewPosition);
            const modelParent = this._viewToModelMapping.get(viewBlock);
            const modelOffset = this._toModelOffset(data.viewPosition.parent, data.viewPosition.offset, viewBlock);
            data.modelPosition = Position._createAt(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 the given {@link module:engine/view/element~Element view element} from the map.
     *
     * **Note:** view-to-model binding will be removed, if it existed. However, corresponding model-to-view binding
     * will be removed only if model element is still bound to the passed `viewElement`.
     *
     * This behavior allows for re-binding model element to another view element without fear of losing the new binding
     * when the previously bound view element is unbound.
     *
     * @param {module:engine/view/element~Element} viewElement View element to unbind.
     * @param {Object} [options={}] The options object.
     * @param {Boolean} [options.defer=false] Controls whether the binding should be removed immediately or deferred until a
     * {@link #flushDeferredBindings `flushDeferredBindings()`} call.
     */
    unbindViewElement(viewElement, options = {}) {
        const modelElement = this.toModelElement(viewElement);
        if (this._elementToMarkerNames.has(viewElement)) {
            for (const markerName of this._elementToMarkerNames.get(viewElement)) {
                this._unboundMarkerNames.add(markerName);
            }
        }
        if (options.defer) {
            this._deferredBindingRemovals.set(viewElement, viewElement.root);
        }
        else {
            this._viewToModelMapping.delete(viewElement);
            if (this._modelToViewMapping.get(modelElement) == viewElement) {
                this._modelToViewMapping.delete(modelElement);
            }
        }
    }
    /**
     * Unbinds the given {@link module:engine/model/element~Element model element} from the map.
     *
     * **Note:** the model-to-view binding will be removed, if it existed. However, the corresponding view-to-model binding
     * will be removed only if the view element is still bound to the passed `modelElement`.
     *
     * This behavior lets for re-binding view element to another model element without fear of losing the new binding
     * when the previously bound model element is unbound.
     *
     * @param {module:engine/model/element~Element} modelElement Model element to unbind.
     */
    unbindModelElement(modelElement) {
        const viewElement = this.toViewElement(modelElement);
        this._modelToViewMapping.delete(modelElement);
        if (this._viewToModelMapping.get(viewElement) == modelElement) {
            this._viewToModelMapping.delete(viewElement);
        }
    }
    /**
     * Binds the given marker name with the given {@link module:engine/view/element~Element view element}. The element
     * will be added to the current set of elements bound with the given marker name.
     *
     * @param {module:engine/view/element~Element} element Element to bind.
     * @param {String} name Marker name.
     */
    bindElementToMarker(element, name) {
        const elements = this._markerNameToElements.get(name) || new Set();
        elements.add(element);
        const names = this._elementToMarkerNames.get(element) || new Set();
        names.add(name);
        this._markerNameToElements.set(name, elements);
        this._elementToMarkerNames.set(element, names);
    }
    /**
     * Unbinds an element from given marker name.
     *
     * @param {module:engine/view/element~Element} element Element to unbind.
     * @param {String} name Marker name.
     */
    unbindElementFromMarkerName(element, name) {
        const nameToElements = this._markerNameToElements.get(name);
        if (nameToElements) {
            nameToElements.delete(element);
            if (nameToElements.size == 0) {
                this._markerNameToElements.delete(name);
            }
        }
        const elementToNames = this._elementToMarkerNames.get(element);
        if (elementToNames) {
            elementToNames.delete(name);
            if (elementToNames.size == 0) {
                this._elementToMarkerNames.delete(element);
            }
        }
    }
    /**
     * Returns all marker names of markers which have changed due to unbinding a view element (so it is assumed that the view element
     * has been removed, moved or renamed) since the last flush. After returning, the marker names list is cleared.
     *
     * @returns {Array.<String>}
     */
    flushUnboundMarkerNames() {
        const markerNames = Array.from(this._unboundMarkerNames);
        this._unboundMarkerNames.clear();
        return markerNames;
    }
    /**
     * Unbinds all deferred binding removals of view elements that in the meantime were not re-attached to some root or document fragment.
     *
     * See: {@link #unbindViewElement `unbindViewElement()`}.
     */
    flushDeferredBindings() {
        for (const [viewElement, root] of this._deferredBindingRemovals) {
            // Unbind it only if it wasn't re-attached to some root or document fragment.
            if (viewElement.root == root) {
                this.unbindViewElement(viewElement);
            }
        }
        this._deferredBindingRemovals = new Map();
    }
    /**
     * Removes all model to view and view to model bindings.
     */
    clearBindings() {
        this._modelToViewMapping = new WeakMap();
        this._viewToModelMapping = new WeakMap();
        this._markerNameToElements = new Map();
        this._elementToMarkerNames = new Map();
        this._unboundMarkerNames = new Set();
        this._deferredBindingRemovals = new Map();
    }
    toModelElement(viewElement) {
        return this._viewToModelMapping.get(viewElement);
    }
    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(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 Range$1(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.
     * @param {Object} [options] Additional options for position mapping process.
     * @param {Boolean} [options.isPhantom=false] Should be set to `true` if the model position to map is pointing to a place
     * in model tree which no longer exists. For example, it could be an end of a removed model range.
     * @returns {module:engine/view/position~Position} Corresponding view position.
     */
    toViewPosition(modelPosition, options = {}) {
        const data = {
            modelPosition,
            mapper: this,
            isPhantom: options.isPhantom
        };
        this.fire('modelToViewPosition', data);
        return data.viewPosition;
    }
    /**
     * Gets all view elements bound to the given marker name.
     *
     * @param {String} name Marker name.
     * @returns {Set.<module:engine/view/element~Element>|null} View elements bound with the given marker name or `null`
     * if no elements are bound to the given marker name.
     */
    markerNameToElements(name) {
        const boundElements = this._markerNameToElements.get(name);
        if (!boundElements) {
            return null;
        }
        const elements = new Set();
        for (const element of boundElements) {
            if (element.is('attributeElement')) {
                for (const clone of element.getElementsWithSameId()) {
                    elements.add(clone);
                }
            }
            else {
                elements.add(element);
            }
        }
        return elements;
    }
    /**
     * Registers a callback that evaluates the length in the model of a view element with the 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 the view element in the 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);
    }
    /**
     * For the given `viewPosition`, finds and returns the closest ancestor of this position that has a mapping to
     * the model.
     *
     * @param {module:engine/view/position~Position} viewPosition Position for which a mapped ancestor should be found.
     * @returns {module:engine/view/element~Element}
     */
    findMappedViewAncestor(viewPosition) {
        let parent = viewPosition.parent;
        while (!this._viewToModelMapping.has(parent)) {
            parent = parent.parent;
        }
        return parent;
    }
    /**
     * 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;
    }
    /**
     * Gets the length of the view element in the model.
     *
     * The length is calculated as follows:
     * * if a {@link #registerViewToModelLength length mapping callback} is provided for the given `viewNode`, it is used to
     * evaluate the 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 its
     * {@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 non-mapped {@link module:engine/view/element~Element element} is equal to the length of its children.
     *
     * Examples:
     *
     *		foo                          -> 3 // Text length is equal to its 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 in 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
     *
     * @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: the offset in the model equals the offset in the text.
        if (viewParent.is('$text')) {
            return new Position$1(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 Position$1(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 the text nodes over positions next to text nodes, if the view position was next to a text node,
     * it moves it into the text node instead.
     *
     *		<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 the text node.
     * @returns {module:engine/view/position~Position} Position in the text node if possible.
     */
    _moveViewPositionToTextNode(viewPosition) {
        // If the position is just after a text node, put it at the end of that text node.
        // If the position is just before a text node, put it at the beginning of that text node.
        const nodeBefore = viewPosition.nodeBefore;
        const nodeAfter = viewPosition.nodeAfter;
        if (nodeBefore instanceof Text$2) {
            return new Position$1(nodeBefore, nodeBefore.data.length);
        }
        else if (nodeAfter instanceof Text$2) {
            return new Position$1(nodeAfter, 0);
        }
        // Otherwise, just return the given position.
        return viewPosition;
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Manages a list of consumable values for the {@link module:engine/model/item~Item model items}.
 *
 * Consumables are various aspects of the model. A model item can be broken down into separate, single properties that might be
 * taken into consideration when converting that item.
 *
 * `ModelConsumable` is used by {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} while analyzing the 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 its attributes). All those parts are saved in `ModelConsumable`. Then,
 * during conversion, when the given part of a model item is converted (i.e. the view element has been inserted into the view,
 * but without attributes), the consumable value is removed from `ModelConsumable`.
 *
 * For model items, `ModelConsumable` stores consumable values of one of following types: `insert`, `addattribute:<attributeKey>`,
 * `changeattributes:<attributeKey>`, `removeattributes:<attributeKey>`.
 *
 * In most cases, it is enough to let th {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}
 * gather consumable values, so there is no need to use
 * the {@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/downcasthelpers default downcast 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, the others should not, because they would duplicate the results. Using `ModelConsumable` helps to avoid
 * this situation, because callbacks should only convert these values that were not yet consumed from `ModelConsumable`.
 *
 * Consuming multiple values in a single callback:
 *
 *		// Converter for custom `imageBlock` element that might have a `caption` element inside which changes
 *		// how the image is displayed in the view:
 *		//
 *		// Model:
 *		//
 *		// [imageBlock]
 *		//   └─ [caption]
 *		//       └─ foo
 *		//
 *		// View:
 *		//
 *		// <figure>
 *		//   ├─ <img />
 *		//   └─ <caption>
 *		//       └─ foo
 *		modelConversionDispatcher.on( 'insert:imageBlock', ( evt, data, conversionApi ) => {
 *			// First, consume the `imageBlock` element.
 *			conversionApi.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 );
 *			const viewWriter = conversionApi.writer;
 *
 *			// Check if the `imageBlock` 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 ( conversionApi.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 {
    /**
     * 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 a parent
         * of that `TextProxy` and the start and end indices of that `TextProxy`. This allows identification of the `TextProxy`
         * instances that point to the same part of the model but are different instances. Each distinct `TextProxy`
         * is given a unique `Symbol` which is then registered as consumable. This process is transparent for the `ModelConsumable`
         * API user because whenever `TextProxy` is added, tested, consumed or reverted, the internal mechanisms of
         * `ModelConsumable` translate `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 a 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( 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. Will be normalized to a proper form, that is either `<word>` or `<part>:<part>`.
     * Second colon and everything after will be cut. Passing event name is a safe and good practice.
     */
    add(item, type) {
        type = _normalizeConsumableType(type);
        if (item instanceof TextProxy) {
            item = this._getSymbolForTextProxy(item);
        }
        if (!this._consumable.has(item)) {
            this._consumable.set(item, new Map());
        }
        this._consumable.get(item).set(type, true);
    }
    /**
     * Removes a given consumable value from a 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( 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. Will be normalized to a proper form, that is either `<word>` or `<part>:<part>`.
     * Second colon and everything after will be cut. Passing event name is a safe and good practice.
     * @returns {Boolean} `true` if consumable value was available and was consumed, `false` otherwise.
     */
    consume(item, type) {
        type = _normalizeConsumableType(type);
        if (item instanceof 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 a given type connected with a 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( 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. Will be normalized to a proper form, that is either `<word>` or `<part>:<part>`.
     * Second colon and everything after will be cut. Passing event name is a safe and good practice.
     * @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) {
        type = _normalizeConsumableType(type);
        if (item instanceof 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 a 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( 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) {
        type = _normalizeConsumableType(type);
        if (item instanceof 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;
    }
    /**
     * Verifies if all events from the specified group were consumed.
     *
     * @param {String} eventGroup The events group to verify.
     */
    verifyAllConsumed(eventGroup) {
        const items = [];
        for (const [item, consumables] of this._consumable) {
            for (const [event, canConsume] of consumables) {
                const eventPrefix = event.split(':')[0];
                if (canConsume && eventGroup == eventPrefix) {
                    items.push({
                        event,
                        item: item.name || item.description
                    });
                }
            }
        }
        if (items.length) {
            /**
             * Some of the {@link module:engine/model/item~Item model items} were not consumed while downcasting the model to view.
             *
             * This might be the effect of:
             *
             * * A missing converter for some model elements. Make sure that you registered downcast converters for all model elements.
             * * A custom converter that does not consume converted items. Make sure that you
             * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} all model elements that you converted
             * from the model to the view.
             * * A custom converter that called `event.stop()`. When providing a custom converter, keep in mind that you should not stop
             * the event. If you stop it then the default converter at the `lowest` priority will not trigger the conversion of this node's
             * attributes and child nodes.
             *
             * @error conversion-model-consumable-not-consumed
             * @param {Array.<module:engine/model/item~Item>} items Items that were not consumed.
             */
            throw new CKEditorError('conversion-model-consumable-not-consumed', null, { items });
        }
    }
    /**
     * Gets a unique symbol for the 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.
     *
     * @internal
     * @protected
     * @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);
        }
        return symbol;
    }
    /**
     * Adds a symbol for the given {@link module:engine/model/textproxy~TextProxy} instance.
     *
     * Used internally to correctly consume `TextProxy` instances.
     *
     * @private
     * @param {module:engine/model/textproxy~TextProxy} textProxy Text proxy instance.
     * @returns {Symbol} Symbol generated for given `TextProxy`.
     */
    _addSymbolForTextProxy(textProxy) {
        const start = textProxy.startOffset;
        const end = textProxy.endOffset;
        const parent = textProxy.parent;
        const symbol = Symbol('$textProxy:' + textProxy.data);
        let startMap;
        let 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;
    }
}
// Returns a normalized consumable type name from the given string. A normalized consumable type name is a string that has
// at most one colon, for example: `insert` or `addMarker:highlight`. If a string to normalize has more "parts" (more colons),
// the further parts are dropped, for example: `addattribute:bold:$text` -> `addattributes:bold`.
//
// @param {String} type Consumable type.
// @returns {String} Normalized consumable type.
function _normalizeConsumableType(type) {
    const parts = type.split(':');
    // For inserts allow passing event name, it's stored in the context of a specified element so the element name is not needed.
    if (parts[0] == 'insert') {
        return parts[0];
    }
    // Markers are identified by the whole name (otherwise we would consume the whole markers group).
    if (parts[0] == 'addMarker' || parts[0] == 'removeMarker') {
        return type;
    }
    return parts.length > 1 ? parts[0] + ':' + parts[1] : parts[0];
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * The downcast dispatcher is a central point of downcasting (conversion from the model to the view), which is a process of reacting
 * to changes in the model and firing a set of events. The callbacks listening to these events are called converters. The
 * converters' role is to convert the model changes to changes in view (for example, adding view nodes or
 * changing attributes on view elements).
 *
 * During the conversion process, downcast dispatcher fires events basing on the state of the model and prepares
 * data for these events. It is important to understand that the events are connected with the changes done on the model,
 * for example: "a node has been inserted" or "an attribute has changed". This is in contrary to upcasting (a view-to-model conversion)
 * where you convert the view state (view nodes) to a model tree.
 *
 * The events are prepared basing on a diff created by the {@link module:engine/model/differ~Differ Differ}, which buffers them
 * and then passes to the downcast dispatcher as a diff between the old model state and the new model state.
 *
 * Note that because the changes are converted, there is a need to have a mapping between the model structure and the view structure.
 * To map positions and elements during the downcast (a model-to-view conversion), use {@link module:engine/conversion/mapper~Mapper}.
 *
 * Downcast dispatcher fires the following events for model tree changes:
 *
 * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert`} &ndash;
 * If a range of nodes was inserted to the model tree.
 * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove `remove`} &ndash;
 * If a range of nodes was removed from the model tree.
 * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute`} &ndash;
 * If an attribute was added, changed or removed from a model node.
 *
 * For {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert`}
 * and {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute`},
 * the downcast dispatcher generates {@link module:engine/conversion/modelconsumable~ModelConsumable consumables}.
 * These are used to have control over which changes have already been consumed. It is useful when some converters
 * overwrite others or convert multiple changes (for example, it converts an insertion of an element and also converts that
 * element's attributes during the insertion).
 *
 * Additionally, downcast dispatcher fires events for {@link module:engine/model/markercollection~Marker marker} changes:
 *
 * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker `addMarker`} &ndash; If a marker was added.
 * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker `removeMarker`} &ndash; If a marker was
 * removed.
 *
 * Note that changing a marker is done through removing the marker from the old range and adding it to the new range,
 * so both of these events are fired.
 *
 * Finally, a downcast dispatcher also handles firing events for the {@link module:engine/model/selection model selection}
 * conversion:
 *
 * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:selection `selection`}
 * &ndash; Converts the selection from the model to the view.
 * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute`}
 * &ndash; Fired for every selection attribute.
 * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker `addMarker`}
 * &ndash; Fired for every marker that contains a selection.
 *
 * Unlike the model tree and the markers, the events for selection are not fired for changes but for a selection state.
 *
 * When providing custom listeners for a downcast dispatcher, remember to check whether a given change has not been
 * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} yet.
 *
 * When providing custom listeners for a downcast dispatcher, keep in mind that you **should not** stop the event. If you stop it,
 * then the default converter at the `lowest` priority will not trigger the conversion of this node's attributes and child nodes.
 *
 * When providing custom listeners for a downcast dispatcher, remember to use the provided
 * {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} to apply changes to the view document.
 *
 * You can read more about conversion in the following guide:
 *
 * * {@glink framework/guides/deep-dive/conversion/downcast Downcast conversion}
 *
 * An example of a custom converter for the downcast dispatcher:
 *
 *		// You will convert inserting a "paragraph" model element into the model.
 *		downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
 *			// Remember to check whether the change has not been consumed yet and consume it.
 *			if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
 *				return;
 *			}
 *
 *			// Translate the position in the model to a position in the view.
 *			const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
 *
 *			// Create a <p> element that will be inserted into the view at the `viewPosition`.
 *			const viewElement = conversionApi.writer.createContainerElement( 'p' );
 *
 *			// Bind the newly created view element to the model element so positions will map accordingly in the future.
 *			conversionApi.mapper.bindElements( data.item, viewElement );
 *
 *			// Add the newly created view element to the view.
 *			conversionApi.writer.insert( viewPosition, viewElement );
 *		} );
 */
class DowncastDispatcher extends Emitter$1 {
    /**
     * Creates a downcast dispatcher instance.
     *
     * @see module:engine/conversion/downcastdispatcher~DowncastConversionApi
     * @param {Object} conversionApi Additional properties for an interface that will be passed to events fired
     * by the downcast dispatcher.
     */
    constructor(conversionApi) {
        super();
        /**
         * A template for an interface passed by the dispatcher to the event callbacks.
         *
         * @protected
         * @member {module:engine/conversion/downcastdispatcher~DowncastConversionApi}
         */
        this._conversionApi = { dispatcher: this, ...conversionApi };
        /**
         * A map of already fired events for a given `ModelConsumable`.
         *
         * @private
         * @member {WeakMap.<module:engine/conversion/downcastdispatcher~DowncastConversionApi,Map>}
         */
        this._firedEventsMap = new WeakMap();
    }
    /**
     * Converts changes buffered in the given {@link module:engine/model/differ~Differ model differ}
     * and fires conversion events based on it.
     *
     * @fires insert
     * @fires remove
     * @fires attribute
     * @fires addMarker
     * @fires removeMarker
     * @fires reduceChanges
     * @param {module:engine/model/differ~Differ} differ The differ object with buffered changes.
     * @param {module:engine/model/markercollection~MarkerCollection} markers Markers related to the model fragment to convert.
     * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
     */
    convertChanges(differ, markers, writer) {
        const conversionApi = this._createConversionApi(writer, differ.getRefreshedItems());
        // Before the view is updated, remove markers which have changed.
        for (const change of differ.getMarkersToRemove()) {
            this._convertMarkerRemove(change.name, change.range, conversionApi);
        }
        // Let features modify the change list (for example to allow reconversion).
        const changes = this._reduceChanges(differ.getChanges());
        // Convert changes that happened on model tree.
        for (const entry of changes) {
            if (entry.type === 'insert') {
                this._convertInsert(Range._createFromPositionAndShift(entry.position, entry.length), conversionApi);
            }
            else if (entry.type === 'reinsert') {
                this._convertReinsert(Range._createFromPositionAndShift(entry.position, entry.length), conversionApi);
            }
            else if (entry.type === 'remove') {
                this._convertRemove(entry.position, entry.length, entry.name, conversionApi);
            }
            else {
                // Defaults to 'attribute' change.
                this._convertAttribute(entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue, conversionApi);
            }
        }
        for (const markerName of conversionApi.mapper.flushUnboundMarkerNames()) {
            const markerRange = markers.get(markerName).getRange();
            this._convertMarkerRemove(markerName, markerRange, conversionApi);
            this._convertMarkerAdd(markerName, markerRange, conversionApi);
        }
        // After the view is updated, convert markers which have changed.
        for (const change of differ.getMarkersToAdd()) {
            this._convertMarkerAdd(change.name, change.range, conversionApi);
        }
        // Remove mappings for all removed view elements.
        conversionApi.mapper.flushDeferredBindings();
        // Verify if all insert consumables were consumed.
        conversionApi.consumable.verifyAllConsumed('insert');
    }
    /**
     * Starts a conversion of a model range and the provided markers.
     *
     * @fires insert
     * @fires attribute
     * @fires addMarker
     * @param {module:engine/model/range~Range} range The inserted range.
     * @param {Map<String,module:engine/model/range~Range>} markers The map of markers that should be down-casted.
     * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
     * @param {Object} [options] Optional options object passed to `convertionApi.options`.
     */
    convert(range, markers, writer, options = {}) {
        const conversionApi = this._createConversionApi(writer, undefined, options);
        this._convertInsert(range, conversionApi);
        for (const [name, range] of markers) {
            this._convertMarkerAdd(name, range, conversionApi);
        }
        // Verify if all insert consumables were consumed.
        conversionApi.consumable.verifyAllConsumed('insert');
    }
    /**
     * Starts the model selection conversion.
     *
     * Fires events for a given {@link module:engine/model/selection~Selection selection} to start the selection conversion.
     *
     * @fires selection
     * @fires addMarker
     * @fires attribute
     * @param {module:engine/model/selection~Selection} selection The selection to convert.
     * @param {module:engine/model/markercollection~MarkerCollection} markers Markers connected with the converted model.
     * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify the view document.
     */
    convertSelection(selection, markers, writer) {
        const markersAtSelection = Array.from(markers.getMarkersAtPosition(selection.getFirstPosition()));
        const conversionApi = this._createConversionApi(writer);
        this._addConsumablesForSelection(conversionApi.consumable, selection, markersAtSelection);
        this.fire('selection', { selection }, conversionApi);
        if (!selection.isCollapsed) {
            return;
        }
        for (const marker of markersAtSelection) {
            const markerRange = marker.getRange();
            if (!shouldMarkerChangeBeConverted(selection.getFirstPosition(), marker, conversionApi.mapper)) {
                continue;
            }
            const data = {
                item: selection,
                markerName: marker.name,
                markerRange
            };
            if (conversionApi.consumable.test(selection, 'addMarker:' + marker.name)) {
                this.fire(`addMarker:${marker.name}`, data, conversionApi);
            }
        }
        for (const key of selection.getAttributeKeys()) {
            const data = {
                item: selection,
                range: selection.getFirstRange(),
                attributeKey: key,
                attributeOldValue: null,
                attributeNewValue: selection.getAttribute(key)
            };
            // Do not fire event if the attribute has been consumed.
            if (conversionApi.consumable.test(selection, 'attribute:' + data.attributeKey)) {
                this.fire(`attribute:${data.attributeKey}:$text`, data, conversionApi);
            }
        }
    }
    /**
     * Fires insertion conversion of a range of nodes.
     *
     * For each node in the range, {@link #event:insert `insert` event is fired}. For each attribute on each node,
     * {@link #event:attribute `attribute` event is fired}.
     *
     * @protected
     * @fires insert
     * @fires attribute
     * @param {module:engine/model/range~Range} range The inserted range.
     * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
     * @param {Object} [options]
     * @param {Boolean} [options.doNotAddConsumables=false] Whether the ModelConsumable should not get populated
     * for items in the provided range.
     */
    _convertInsert(range, conversionApi, options = {}) {
        if (!options.doNotAddConsumables) {
            // Collect a list of things that can be consumed, consisting of nodes and their attributes.
            this._addConsumablesForInsert(conversionApi.consumable, Array.from(range));
        }
        // Fire a separate insert event for each node and text fragment contained in the range.
        for (const data of Array.from(range.getWalker({ shallow: true })).map(walkerValueToEventData)) {
            this._testAndFire('insert', data, conversionApi);
        }
    }
    /**
     * Fires conversion of a single node removal. Fires {@link #event:remove remove event} with provided data.
     *
     * @protected
     * @param {module:engine/model/position~Position} position Position from which node was removed.
     * @param {Number} length Offset size of removed node.
     * @param {String} name Name of removed node.
     * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
     */
    _convertRemove(position, length, name, conversionApi) {
        this.fire(`remove:${name}`, { position, length }, conversionApi);
    }
    /**
     * Starts a conversion of an attribute change on a given `range`.
     *
     * For each node in the given `range`, {@link #event:attribute attribute event} is fired with the passed data.
     *
     * @protected
     * @fires attribute
     * @param {module:engine/model/range~Range} range Changed range.
     * @param {String} key Key of the attribute that has changed.
     * @param {*} oldValue Attribute value before the change or `null` if the attribute has not been set before.
     * @param {*} newValue New attribute value or `null` if the attribute has been removed.
     * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
     */
    _convertAttribute(range, key, oldValue, newValue, conversionApi) {
        // Create a list with attributes to consume.
        this._addConsumablesForRange(conversionApi.consumable, range, `attribute:${key}`);
        // Create a separate attribute event for each node in the range.
        for (const value of range) {
            const data = {
                item: value.item,
                range: Range._createFromPositionAndShift(value.previousPosition, value.length),
                attributeKey: key,
                attributeOldValue: oldValue,
                attributeNewValue: newValue
            };
            this._testAndFire(`attribute:${key}`, data, conversionApi);
        }
    }
    /**
     * Fires re-insertion conversion (with a `reconversion` flag passed to `insert` events)
     * of a range of elements (only elements on the range depth, without children).
     *
     * For each node in the range on its depth (without children), {@link #event:insert `insert` event} is fired.
     * For each attribute on each node, {@link #event:attribute `attribute` event} is fired.
     *
     * @protected
     * @fires insert
     * @fires attribute
     * @param {module:engine/model/range~Range} range The range to reinsert.
     * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
     */
    _convertReinsert(range, conversionApi) {
        // Convert the elements - without converting children.
        const walkerValues = Array.from(range.getWalker({ shallow: true }));
        // Collect a list of things that can be consumed, consisting of nodes and their attributes.
        this._addConsumablesForInsert(conversionApi.consumable, walkerValues);
        // Fire a separate insert event for each node and text fragment contained shallowly in the range.
        for (const data of walkerValues.map(walkerValueToEventData)) {
            this._testAndFire('insert', { ...data, reconversion: true }, conversionApi);
        }
    }
    /**
     * Converts the added marker. Fires the {@link #event:addMarker `addMarker`} event for each item
     * in the marker's range. If the range is collapsed, a single event is dispatched. See the event description for more details.
     *
     * @protected
     * @fires addMarker
     * @param {String} markerName Marker name.
     * @param {module:engine/model/range~Range} markerRange The marker range.
     * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
     */
    _convertMarkerAdd(markerName, markerRange, conversionApi) {
        // Do not convert if range is in graveyard.
        if (markerRange.root.rootName == '$graveyard') {
            return;
        }
        // In markers' case, event name == consumable name.
        const eventName = `addMarker:${markerName}`;
        //
        // First, fire an event for the whole marker.
        //
        conversionApi.consumable.add(markerRange, eventName);
        this.fire(eventName, { markerName, markerRange }, conversionApi);
        //
        // Do not fire events for each item inside the range if the range got consumed.
        // Also consume the whole marker consumable if it wasn't consumed.
        //
        if (!conversionApi.consumable.consume(markerRange, eventName)) {
            return;
        }
        //
        // Then, fire an event for each item inside the marker range.
        //
        this._addConsumablesForRange(conversionApi.consumable, markerRange, eventName);
        for (const item of markerRange.getItems()) {
            // Do not fire event for already consumed items.
            if (!conversionApi.consumable.test(item, eventName)) {
                continue;
            }
            const data = { item, range: Range._createOn(item), markerName, markerRange };
            this.fire(eventName, data, conversionApi);
        }
    }
    /**
     * Fires the conversion of the marker removal. Fires the {@link #event:removeMarker `removeMarker`} event with the provided data.
     *
     * @protected
     * @fires removeMarker
     * @param {String} markerName Marker name.
     * @param {module:engine/model/range~Range} markerRange The marker range.
     * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
     */
    _convertMarkerRemove(markerName, markerRange, conversionApi) {
        // Do not convert if range is in graveyard.
        if (markerRange.root.rootName == '$graveyard') {
            return;
        }
        this.fire(`removeMarker:${markerName}`, { markerName, markerRange }, conversionApi);
    }
    /**
     * Fires the reduction of changes buffered in the {@link module:engine/model/differ~Differ `Differ`}.
     *
     * Features can replace selected {@link module:engine/model/differ~DiffItem `DiffItem`}s with `reinsert` entries to trigger
     * reconversion. The {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
     * `DowncastHelpers.elementToStructure()`} is using this event to trigger reconversion.
     *
     * @private
     * @fires reduceChanges
     * @param {Iterable.<module:engine/model/differ~DiffItem>} changes
     * @returns {Iterable.<module:engine/model/differ~DiffItem>}
     */
    _reduceChanges(changes) {
        const data = { changes };
        this.fire('reduceChanges', data);
        return data.changes;
    }
    /**
     * Populates provided {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from a given range,
     * assuming that the range has just been inserted to the model.
     *
     * @private
     * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable The consumable.
     * @param {Iterable.<module:engine/model/treewalker~TreeWalkerValue>} walkerValues The walker values for the inserted range.
     * @returns {module:engine/conversion/modelconsumable~ModelConsumable} The values to consume.
     */
    _addConsumablesForInsert(consumable, walkerValues) {
        for (const value of walkerValues) {
            const item = value.item;
            // Add consumable if it wasn't there yet.
            if (consumable.test(item, 'insert') === null) {
                consumable.add(item, 'insert');
                for (const key of item.getAttributeKeys()) {
                    consumable.add(item, 'attribute:' + key);
                }
            }
        }
        return consumable;
    }
    /**
     * Populates provided {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume for a given range.
     *
     * @private
     * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable The consumable.
     * @param {module:engine/model/range~Range} range The affected range.
     * @param {String} type Consumable type.
     * @returns {module:engine/conversion/modelconsumable~ModelConsumable} The values to consume.
     */
    _addConsumablesForRange(consumable, range, type) {
        for (const item of range.getItems()) {
            consumable.add(item, type);
        }
        return consumable;
    }
    /**
     * Populates provided {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values.
     *
     * @private
     * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable The consumable.
     * @param {module:engine/model/selection~Selection} selection The selection to create the consumable from.
     * @param {Iterable.<module:engine/model/markercollection~Marker>} markers Markers that contain the selection.
     * @returns {module:engine/conversion/modelconsumable~ModelConsumable} The values to consume.
     */
    _addConsumablesForSelection(consumable, selection, markers) {
        consumable.add(selection, 'selection');
        for (const marker of markers) {
            consumable.add(selection, 'addMarker:' + marker.name);
        }
        for (const key of selection.getAttributeKeys()) {
            consumable.add(selection, 'attribute:' + key);
        }
        return consumable;
    }
    /**
     * Tests whether given event wasn't already fired and if so, fires it.
     *
     * @private
     * @fires insert
     * @fires attribute
     * @param {String} type Event type.
     * @param {Object} data Event data.
     * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
     */
    _testAndFire(type, data, conversionApi) {
        const eventName = getEventName(type, data);
        const itemKey = data.item.is('$textProxy') ? conversionApi.consumable._getSymbolForTextProxy(data.item) : data.item;
        const eventsFiredForConversion = this._firedEventsMap.get(conversionApi);
        const eventsFiredForItem = eventsFiredForConversion.get(itemKey);
        if (!eventsFiredForItem) {
            eventsFiredForConversion.set(itemKey, new Set([eventName]));
        }
        else if (!eventsFiredForItem.has(eventName)) {
            eventsFiredForItem.add(eventName);
        }
        else {
            return;
        }
        this.fire(eventName, data, conversionApi);
    }
    /**
     * Fires not already fired events for setting attributes on just inserted item.
     *
     * @private
     * @param {module:engine/model/item~Item} item The model item to convert attributes for.
     * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
     */
    _testAndFireAddAttributes(item, conversionApi) {
        const data = {
            item,
            range: Range._createOn(item)
        };
        for (const key of data.item.getAttributeKeys()) {
            data.attributeKey = key;
            data.attributeOldValue = null;
            data.attributeNewValue = data.item.getAttribute(key);
            this._testAndFire(`attribute:${key}`, data, conversionApi);
        }
    }
    /**
     * Builds an instance of the {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi} from a template and a given
     * {@link module:engine/view/downcastwriter~DowncastWriter `DowncastWriter`} and options object.
     *
     * @private
     * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify the view document.
     * @param {Set.<module:engine/model/element~Element>} [refreshedItems] A set of model elements that should not reuse their
     * previous view representations.
     * @param {Object} [options] Optional options passed to `convertionApi.options`.
     * @return {module:engine/conversion/downcastdispatcher~DowncastConversionApi} The conversion API object.
     */
    _createConversionApi(writer, refreshedItems = new Set(), options = {}) {
        const conversionApi = {
            ...this._conversionApi,
            consumable: new ModelConsumable(),
            writer,
            options,
            convertItem: item => this._convertInsert(Range._createOn(item), conversionApi),
            convertChildren: element => this._convertInsert(Range._createIn(element), conversionApi, { doNotAddConsumables: true }),
            convertAttributes: item => this._testAndFireAddAttributes(item, conversionApi),
            canReuseView: viewElement => !refreshedItems.has(conversionApi.mapper.toModelElement(viewElement))
        };
        this._firedEventsMap.set(conversionApi, new Map());
        return conversionApi;
    }
}
// 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;
}
function getEventName(type, data) {
    const name = data.item.is('element') ? data.item.name : '$text';
    return `${type}:${name}`;
}
function walkerValueToEventData(value) {
    const item = value.item;
    const itemRange = Range._createFromPositionAndShift(value.previousPosition, value.length);
    return {
        item,
        range: itemRange
    };
}
/**
 * The {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} instance.
 *
 * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #dispatcher
 */
/**
 * Stores the information about what parts of a processed model item are still waiting to be handled. After a piece of a model item was
 * converted, an appropriate consumable value should be {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}.
 *
 * @member {module:engine/conversion/modelconsumable~ModelConsumable} #consumable
 */
/**
 * The {@link module:engine/conversion/mapper~Mapper} instance.
 *
 * @member {module:engine/conversion/mapper~Mapper} #mapper
 */
/**
 * The {@link module:engine/model/schema~Schema} instance set for the model that is downcast.
 *
 * @member {module:engine/model/schema~Schema} #schema
 */
/**
 * The {@link module:engine/view/downcastwriter~DowncastWriter} instance used to manipulate the data during conversion.
 *
 * @member {module:engine/view/downcastwriter~DowncastWriter} #writer
 */
/**
 * Triggers conversion of a specified item.
 * This conversion is triggered within (as a separate process of) the parent conversion.
 *
 * @method #convertItem
 * @param {module:engine/model/item~Item} item The model item to trigger nested insert conversion on.
 */
/**
 * Triggers conversion of children of a specified element.
 *
 * @method #convertChildren
 * @param {module:engine/model/element~Element} element The model element to trigger children insert conversion on.
 */
/**
 * Triggers conversion of attributes of a specified item.
 *
 * @method #convertAttributes
 * @param {module:engine/model/item~Item} item The model item to trigger attribute conversion on.
 */
/**
 * An object with an additional configuration which can be used during the conversion process. Available only for data downcast conversion.
 *
 * @member {Object} #options
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Selection is a set of {@link module:engine/model/range~Range ranges}. It has a direction specified by its
 * {@link module:engine/model/selection~Selection#anchor anchor} and {@link module:engine/model/selection~Selection#focus focus}
 * (it can be {@link module:engine/model/selection~Selection#isBackward forward or backward}).
 * Additionally, selection may have its own attributes (think – whether text typed in in this selection
 * should have those attributes – e.g. whether you type a bolded text).
 *
 * @mixes module:utils/emittermixin~EmitterMixin
 */
class Selection extends EmitterMixin(TypeCheckable) {
    /**
     * Creates a new selection instance based on the given {@link module:engine/model/selection~Selectable selectable}
     * or creates an empty selection if no arguments were passed.
     *
     *		// Creates empty selection without ranges.
     *		const selection = writer.createSelection();
     *
     *		// Creates selection at the given range.
     *		const range = writer.createRange( start, end );
     *		const selection = writer.createSelection( range );
     *
     *		// Creates selection at the given ranges
     *		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
     *		const selection = writer.createSelection( ranges );
     *
     *		// Creates selection from the other selection.
     *		// Note: It doesn't copies selection attributes.
     *		const otherSelection = writer.createSelection();
     *		const selection = writer.createSelection( otherSelection );
     *
     *		// Creates selection from the given document selection.
     *		// Note: It doesn't copies selection attributes.
     *		const documentSelection = model.document.selection;
     *		const selection = writer.createSelection( documentSelection );
     *
     *		// Creates selection at the given position.
     *		const position = writer.createPositionFromPath( root, path );
     *		const selection = writer.createSelection( position );
     *
     *		// Creates selection at the given offset in the given element.
     *		const paragraph = writer.createElement( 'paragraph' );
     *		const selection = writer.createSelection( paragraph, offset );
     *
     *		// 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.
     *		const selection = writer.createSelection( paragraph, 'in' );
     *
     *		// Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends
     *		// just after the item.
     *		const selection = writer.createSelection( paragraph, 'on' );
     *
     * Selection's constructor allow passing additional options (`'backward'`) as the last argument.
     *
     *		// Creates backward selection.
     *		const selection = writer.createSelection( range, { backward: true } );
     *
     * @param {module:engine/model/selection~Selectable} [selectable]
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     */
    constructor(...args) {
        super();
        /**
         * Specifies whether the last added range was added as a backward or forward range.
         *
         * @private
         * @type {Boolean}
         */
        this._lastRangeBackward = false;
        /**
         * Stores selection ranges.
         *
         * @protected
         * @type {Array.<module:engine/model/range~Range>}
         */
        this._ranges = [];
        /**
         * List of attributes set on current selection.
         *
         * @protected
         * @type {Map.<String,*>}
         */
        this._attrs = new Map();
        if (args.length) {
            this.setTo(...args);
        }
    }
    /**
     * Selection anchor. Anchor is the position from which the selection was started. If a user is making a selection
     * by dragging the mouse, the anchor is where the user pressed the mouse button (the beginning of the selection).
     *
     * Anchor and {@link #focus} define the direction of the selection, which is important
     * when expanding/shrinking selection. The focus moves, while the anchor should remain in the same place.
     *
     * Anchor is always set to the {@link module:engine/model/range~Range#start start} or
     * {@link module:engine/model/range~Range#end end} position of the last of selection's ranges. Whether it is
     * the `start` or `end` depends on the specified `options.backward`. See the {@link #setTo `setTo()`} method.
     *
     * May be set to `null` if there are no ranges in the selection.
     *
     * @see #focus
     * @readonly
     * @type {module:engine/model/position~Position|null}
     */
    get anchor() {
        if (this._ranges.length > 0) {
            const range = this._ranges[this._ranges.length - 1];
            return this._lastRangeBackward ? range.end : range.start;
        }
        return null;
    }
    /**
     * Selection focus. Focus is the position where the selection ends. If a user is making a selection
     * by dragging the mouse, the focus is where the mouse cursor is.
     *
     * May be set to `null` if there are no ranges in the selection.
     *
     * @see #anchor
     * @readonly
     * @type {module:engine/model/position~Position|null}
     */
    get focus() {
        if (this._ranges.length > 0) {
            const range = this._ranges[this._ranges.length - 1];
            return this._lastRangeBackward ? range.start : range.end;
        }
        return null;
    }
    /**
     * Whether the selection is collapsed. Selection is collapsed when there is exactly one range in it
     * and it is collapsed.
     *
     * @readonly
     * @type {Boolean}
     */
    get isCollapsed() {
        const length = this._ranges.length;
        if (length === 1) {
            return this._ranges[0].isCollapsed;
        }
        else {
            return false;
        }
    }
    /**
     * Returns the number of ranges in the selection.
     *
     * @readonly
     * @type {Number}
     */
    get rangeCount() {
        return this._ranges.length;
    }
    /**
     * Specifies whether the selection's {@link #focus} precedes the selection's {@link #anchor}.
     *
     * @readonly
     * @type {Boolean}
     */
    get isBackward() {
        return !this.isCollapsed && this._lastRangeBackward;
    }
    /**
     * Checks whether this selection is equal to the given selection. Selections are equal if they have the same directions,
     * the same number of ranges and all ranges from one selection equal to ranges from the another selection.
     *
     * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} otherSelection
     * Selection to compare with.
     * @returns {Boolean} `true` if selections are equal, `false` otherwise.
     */
    isEqual(otherSelection) {
        if (this.rangeCount != otherSelection.rangeCount) {
            return false;
        }
        else if (this.rangeCount === 0) {
            return true;
        }
        if (!this.anchor.isEqual(otherSelection.anchor) || !this.focus.isEqual(otherSelection.focus)) {
            return false;
        }
        for (const thisRange of this._ranges) {
            let found = false;
            for (const otherRange of otherSelection._ranges) {
                if (thisRange.isEqual(otherRange)) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                return false;
            }
        }
        return true;
    }
    /**
     * Returns an iterable object that iterates over copies of selection ranges.
     *
     * @returns {Iterable.<module:engine/model/range~Range>}
     */
    *getRanges() {
        for (const range of this._ranges) {
            yield new Range(range.start, range.end);
        }
    }
    /**
     * Returns a copy of the first range in the selection.
     * First range is the one which {@link module:engine/model/range~Range#start start} position
     * {@link module:engine/model/position~Position#isBefore is before} start position of all other ranges
     * (not to confuse with the first range added to the selection).
     *
     * Returns `null` if there are no ranges in selection.
     *
     * @returns {module:engine/model/range~Range|null}
     */
    getFirstRange() {
        let first = null;
        for (const range of this._ranges) {
            if (!first || range.start.isBefore(first.start)) {
                first = range;
            }
        }
        return first ? new Range(first.start, first.end) : null;
    }
    /**
     * Returns a copy of the last range in the selection.
     * Last range is the one which {@link module:engine/model/range~Range#end end} position
     * {@link module:engine/model/position~Position#isAfter is after} end position of all other ranges (not to confuse with the range most
     * recently added to the selection).
     *
     * Returns `null` if there are no ranges in selection.
     *
     * @returns {module:engine/model/range~Range|null}
     */
    getLastRange() {
        let last = null;
        for (const range of this._ranges) {
            if (!last || range.end.isAfter(last.end)) {
                last = range;
            }
        }
        return last ? new Range(last.start, last.end) : null;
    }
    /**
     * Returns the first position in the selection.
     * First position is the position that {@link module:engine/model/position~Position#isBefore is before}
     * any other position in the selection.
     *
     * Returns `null` if there are no ranges in selection.
     *
     * @returns {module:engine/model/position~Position|null}
     */
    getFirstPosition() {
        const first = this.getFirstRange();
        return first ? first.start.clone() : null;
    }
    /**
     * Returns the last position in the selection.
     * Last position is the position that {@link module:engine/model/position~Position#isAfter is after}
     * any other position in the selection.
     *
     * Returns `null` if there are no ranges in selection.
     *
     * @returns {module:engine/model/position~Position|null}
     */
    getLastPosition() {
        const lastRange = this.getLastRange();
        return lastRange ? lastRange.end.clone() : null;
    }
    /**
     * Sets this selection's ranges and direction to the specified location based on the given
     * {@link module:engine/model/selection~Selectable selectable}.
     *
     *		// Removes all selection's ranges.
     *		selection.setTo( null );
     *
     *		// Sets selection to the given range.
     *		const range = writer.createRange( start, end );
     *		selection.setTo( range );
     *
     *		// Sets selection to given ranges.
     *		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
     *		selection.setTo( ranges );
     *
     *		// Sets selection to other selection.
     *		// Note: It doesn't copies selection attributes.
     *		const otherSelection = writer.createSelection();
     *		selection.setTo( otherSelection );
     *
     *		// Sets selection to the given document selection.
     *		// Note: It doesn't copies selection attributes.
     *		const documentSelection = new DocumentSelection( doc );
     *		selection.setTo( documentSelection );
     *
     *		// Sets collapsed selection at the given position.
     *		const position = writer.createPositionFromPath( root, path );
     *		selection.setTo( position );
     *
     *		// Sets collapsed selection at the position of the given node and an offset.
     *		selection.setTo( paragraph, offset );
     *
     * 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.
     *
     *		selection.setTo( paragraph, 'in' );
     *
     * Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends just after the item.
     *
     *		selection.setTo( paragraph, 'on' );
     *
     * `Selection#setTo()`' method allow passing additional options (`backward`) as the last argument.
     *
     *		// Sets backward selection.
     *		const selection = writer.createSelection( range, { backward: true } );
     *
     * @param {module:engine/model/selection~Selectable} selectable
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     */
    setTo(...args) {
        let [selectable, placeOrOffset, options] = args;
        if (typeof placeOrOffset == 'object') {
            options = placeOrOffset;
            placeOrOffset = undefined;
        }
        if (selectable === null) {
            this._setRanges([]);
        }
        else if (selectable instanceof Selection) {
            this._setRanges(selectable.getRanges(), selectable.isBackward);
        }
        else if (selectable && typeof selectable.getRanges == 'function') {
            // We assume that the selectable is a DocumentSelection.
            // It can't be imported here, because it would lead to circular imports.
            this._setRanges(selectable.getRanges(), selectable.isBackward);
        }
        else if (selectable instanceof Range) {
            this._setRanges([selectable], !!options && !!options.backward);
        }
        else if (selectable instanceof Position) {
            this._setRanges([new Range(selectable)]);
        }
        else if (selectable instanceof Node$1) {
            const backward = !!options && !!options.backward;
            let range;
            if (placeOrOffset == 'in') {
                range = Range._createIn(selectable);
            }
            else if (placeOrOffset == 'on') {
                range = Range._createOn(selectable);
            }
            else if (placeOrOffset !== undefined) {
                range = new Range(Position._createAt(selectable, placeOrOffset));
            }
            else {
                /**
                 * selection.setTo requires the second parameter when the first parameter is a node.
                 *
                 * @error model-selection-setto-required-second-parameter
                 */
                throw new CKEditorError('model-selection-setto-required-second-parameter', [this, selectable]);
            }
            this._setRanges([range], backward);
        }
        else if (isIterable(selectable)) {
            // We assume that the selectable is an iterable of ranges.
            this._setRanges(selectable, options && !!options.backward);
        }
        else {
            /**
             * Cannot set the selection to the given place.
             *
             * Invalid parameters were specified when setting the selection. Common issues:
             *
             * * A {@link module:engine/model/textproxy~TextProxy} instance was passed instead of
             * a real {@link module:engine/model/text~Text}.
             * * View nodes were passed instead of model nodes.
             * * `null`/`undefined` was passed.
             *
             * @error model-selection-setto-not-selectable
             */
            throw new CKEditorError('model-selection-setto-not-selectable', [this, selectable]);
        }
    }
    /**
     * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
     * is treated like the last added range and is used to set {@link module:engine/model/selection~Selection#anchor} and
     * {@link module:engine/model/selection~Selection#focus}. Accepts a flag describing in which direction the selection is made.
     *
     * @protected
     * @fires change:range
     * @param {Iterable.<module:engine/model/range~Range>} newRanges Ranges to set.
     * @param {Boolean} [isLastBackward=false] Flag describing if last added range was selected forward - from start to end (`false`)
     * or backward - from end to start (`true`).
     */
    _setRanges(newRanges, isLastBackward = false) {
        const ranges = Array.from(newRanges);
        // Check whether there is any range in new ranges set that is different than all already added ranges.
        const anyNewRange = ranges.some(newRange => {
            if (!(newRange instanceof Range)) {
                /**
                 * Selection range set to an object that is not an instance of {@link module:engine/model/range~Range}.
                 *
                 * Only {@link module:engine/model/range~Range} instances can be used to set a selection.
                 * Common mistakes leading to this error are:
                 *
                 * * using DOM `Range` object,
                 * * incorrect CKEditor 5 installation with multiple `ckeditor5-engine` packages having different versions.
                 *
                 * @error model-selection-set-ranges-not-range
                 */
                throw new CKEditorError('model-selection-set-ranges-not-range', [this, newRanges]);
            }
            return this._ranges.every(oldRange => {
                return !oldRange.isEqual(newRange);
            });
        });
        // Don't do anything if nothing changed.
        if (ranges.length === this._ranges.length && !anyNewRange) {
            return;
        }
        this._replaceAllRanges(ranges);
        this._lastRangeBackward = !!isLastBackward;
        this.fire('change:range', { directChange: true });
    }
    /**
     * Moves {@link module:engine/model/selection~Selection#focus} to the specified location.
     *
     * The location can be specified in the same form as
     * {@link module:engine/model/writer~Writer#createPositionAt writer.createPositionAt()} parameters.
     *
     * @fires change:range
     * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * first parameter is a {@link module:engine/model/item~Item model item}.
     */
    setFocus(itemOrPosition, offset) {
        if (this.anchor === null) {
            /**
             * Cannot set selection focus if there are no ranges in selection.
             *
             * @error model-selection-setfocus-no-ranges
             */
            throw new CKEditorError('model-selection-setfocus-no-ranges', [this, itemOrPosition]);
        }
        const newFocus = Position._createAt(itemOrPosition, offset);
        if (newFocus.compareWith(this.focus) == 'same') {
            return;
        }
        const anchor = this.anchor;
        if (this._ranges.length) {
            this._popRange();
        }
        if (newFocus.compareWith(anchor) == 'before') {
            this._pushRange(new Range(newFocus, anchor));
            this._lastRangeBackward = true;
        }
        else {
            this._pushRange(new Range(anchor, newFocus));
            this._lastRangeBackward = false;
        }
        this.fire('change:range', { directChange: true });
    }
    /**
     * Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
     *
     * @param {String} key Key of attribute to look for.
     * @returns {*} Attribute value or `undefined`.
     */
    getAttribute(key) {
        return this._attrs.get(key);
    }
    /**
     * Returns iterable that iterates over this selection'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 iterable that iterates over this selection's attribute keys.
     *
     * @returns {Iterable.<String>}
     */
    getAttributeKeys() {
        return this._attrs.keys();
    }
    /**
     * Checks if the selection 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 selection, `false` otherwise.
     */
    hasAttribute(key) {
        return this._attrs.has(key);
    }
    /**
     * Removes an attribute with given key from the selection.
     *
     * If given attribute was set on the selection, fires the {@link #event:change:range} event with
     * removed attribute key.
     *
     * @fires change:attribute
     * @param {String} key Key of attribute to remove.
     */
    removeAttribute(key) {
        if (this.hasAttribute(key)) {
            this._attrs.delete(key);
            this.fire('change:attribute', { attributeKeys: [key], directChange: true });
        }
    }
    /**
     * Sets attribute on the selection. If attribute with the same key already is set, it's value is overwritten.
     *
     * If the attribute value has changed, fires the {@link #event:change:range} event with
     * the attribute key.
     *
     * @fires change:attribute
     * @param {String} key Key of attribute to set.
     * @param {*} value Attribute value.
     */
    setAttribute(key, value) {
        if (this.getAttribute(key) !== value) {
            this._attrs.set(key, value);
            this.fire('change:attribute', { attributeKeys: [key], directChange: true });
        }
    }
    /**
     * Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
     * one range in the selection, and that range contains exactly one element.
     * Returns `null` if there is no selected element.
     *
     * @returns {module:engine/model/element~Element|null}
     */
    getSelectedElement() {
        if (this.rangeCount !== 1) {
            return null;
        }
        return this.getFirstRange().getContainedElement();
    }
    /**
     * Gets elements of type {@link module:engine/model/schema~Schema#isBlock "block"} touched by the selection.
     *
     * This method's result can be used for example to apply block styling to all blocks covered by this selection.
     *
     * **Note:** `getSelectedBlocks()` returns blocks that are nested in other non-block elements
     * but will not return blocks nested in other blocks.
     *
     * In this case the function will return exactly all 3 paragraphs (note: `<blockQuote>` is not a block itself):
     *
     *		<paragraph>[a</paragraph>
     *		<blockQuote>
     *			<paragraph>b</paragraph>
     *		</blockQuote>
     *		<paragraph>c]d</paragraph>
     *
     * In this case the paragraph will also be returned, despite the collapsed selection:
     *
     *		<paragraph>[]a</paragraph>
     *
     * In such a scenario, however, only blocks A, B & E will be returned as blocks C & D are nested in block B:
     *
     *		[<blockA></blockA>
     *		<blockB>
     *			<blockC></blockC>
     *			<blockD></blockD>
     *		</blockB>
     *		<blockE></blockE>]
     *
     * If the selection is inside a block all the inner blocks (A & B) are returned:
     *
     * 		<block>
     *			<blockA>[a</blockA>
     * 			<blockB>b]</blockB>
     * 		</block>
     *
     * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
     * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
     *
     *		<paragraph>[a</paragraph>
     *		<paragraph>b</paragraph>
     *		<paragraph>]c</paragraph> // this block will not be returned
     *
     * @returns {Iterable.<module:engine/model/element~Element>}
     */
    *getSelectedBlocks() {
        const visited = new WeakSet();
        for (const range of this.getRanges()) {
            // Get start block of range in case of a collapsed range.
            const startBlock = getParentBlock$1(range.start, visited);
            if (startBlock && isTopBlockInRange(startBlock, range)) {
                yield startBlock;
            }
            for (const value of range.getWalker()) {
                const block = value.item;
                if (value.type == 'elementEnd' && isUnvisitedTopBlock(block, visited, range)) {
                    yield block;
                }
            }
            const endBlock = getParentBlock$1(range.end, visited);
            // #984. Don't return the end block if the range ends right at its beginning.
            if (endBlock && !range.end.isTouching(Position._createAt(endBlock, 0)) && isTopBlockInRange(endBlock, range)) {
                yield endBlock;
            }
        }
    }
    /**
     * Checks whether the selection contains the entire content of the given element. This means that selection must start
     * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
     * touching the element's end.
     *
     * By default, this method will check whether the entire content of the selection's current root is selected.
     * Useful to check if e.g. the user has just pressed <kbd>Ctrl</kbd> + <kbd>A</kbd>.
     *
     * @param {module:engine/model/element~Element} [element=this.anchor.root]
     * @returns {Boolean}
     */
    containsEntireContent(element = this.anchor.root) {
        const limitStartPosition = Position._createAt(element, 0);
        const limitEndPosition = Position._createAt(element, 'end');
        return limitStartPosition.isTouching(this.getFirstPosition()) &&
            limitEndPosition.isTouching(this.getLastPosition());
    }
    /**
     * Adds given range to internal {@link #_ranges ranges array}. Throws an error
     * if given range is intersecting with any range that is already stored in this selection.
     *
     * @protected
     * @param {module:engine/model/range~Range} range Range to add.
     */
    _pushRange(range) {
        this._checkRange(range);
        this._ranges.push(new Range(range.start, range.end));
    }
    /**
     * Checks if given range intersects with ranges that are already in the selection. Throws an error if it does.
     *
     * @protected
     * @param {module:engine/model/range~Range} range Range to check.
     */
    _checkRange(range) {
        for (let i = 0; i < this._ranges.length; i++) {
            if (range.isIntersecting(this._ranges[i])) {
                /**
                 * Trying to add a range that intersects with another range in the selection.
                 *
                 * @error model-selection-range-intersects
                 * @param {module:engine/model/range~Range} addedRange Range that was added to the selection.
                 * @param {module:engine/model/range~Range} intersectingRange Range in the selection that intersects with `addedRange`.
                 */
                throw new CKEditorError('model-selection-range-intersects', [this, range], { addedRange: range, intersectingRange: this._ranges[i] });
            }
        }
    }
    /**
     * Replaces all the ranges by the given ones.
     * Uses {@link #_popRange _popRange} and {@link #_pushRange _pushRange} to ensure proper ranges removal and addition.
     *
     * @param {Array.<module:engine/model/range~Range>} ranges
     * @protected
     */
    _replaceAllRanges(ranges) {
        this._removeAllRanges();
        for (const range of ranges) {
            this._pushRange(range);
        }
    }
    /**
     * Deletes ranges from internal range array. Uses {@link #_popRange _popRange} to
     * ensure proper ranges removal.
     *
     * @protected
     */
    _removeAllRanges() {
        while (this._ranges.length > 0) {
            this._popRange();
        }
    }
    /**
     * Removes most recently added range from the selection.
     *
     * @protected
     */
    _popRange() {
        this._ranges.pop();
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		selection.is( 'selection' ); // -> true
 *		selection.is( 'model:selection' ); // -> true
 *
 *		selection.is( 'view:selection' ); // -> false
 *		selection.is( 'range' ); // -> false
 *
 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 *
 * @param {String} type
 * @returns {Boolean}
 */
Selection.prototype.is = function (type) {
    return type === 'selection' || type === 'model:selection';
};
// Checks whether the given element extends $block in the schema and has a parent (is not a root).
// Marks it as already visited.
function isUnvisitedBlock(element, visited) {
    if (visited.has(element)) {
        return false;
    }
    visited.add(element);
    return element.root.document.model.schema.isBlock(element) && element.parent;
}
// Checks if the given element is a $block was not previously visited and is a top block in a range.
function isUnvisitedTopBlock(element, visited, range) {
    return isUnvisitedBlock(element, visited) && isTopBlockInRange(element, range);
}
// Finds the lowest element in position's ancestors which is a block.
// It will search until first ancestor that is a limit element.
// Marks all ancestors as already visited to not include any of them later on.
function getParentBlock$1(position, visited) {
    const element = position.parent;
    const schema = element.root.document.model.schema;
    const ancestors = position.parent.getAncestors({ parentFirst: true, includeSelf: true });
    let hasParentLimit = false;
    const block = ancestors.find(element => {
        // Stop searching after first parent node that is limit element.
        if (hasParentLimit) {
            return false;
        }
        hasParentLimit = schema.isLimit(element);
        return !hasParentLimit && isUnvisitedBlock(element, visited);
    });
    // Mark all ancestors of this position's parent, because find() might've stopped early and
    // the found block may be a child of another block.
    ancestors.forEach(element => visited.add(element));
    return block;
}
// Checks if the blocks is not nested in other block inside a range.
//
// @param {module:engine/model/element~Element} block Block to check.
// @param {module:engine/model/range~Range} range Range to check.
function isTopBlockInRange(block, range) {
    const parentBlock = findAncestorBlock(block);
    if (!parentBlock) {
        return true;
    }
    // Add loose flag to check as parentRange can be equal to range.
    const isParentInRange = range.containsRange(Range._createOn(parentBlock), true);
    return !isParentInRange;
}
// Returns first ancestor block of a node.
//
// @param {module:engine/model/node~Node} node
// @returns {module:engine/model/node~Node|undefined}
function findAncestorBlock(node) {
    const schema = node.root.document.model.schema;
    let parent = node.parent;
    while (parent) {
        if (schema.isBlock(parent)) {
            return parent;
        }
        parent = parent.parent;
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * `LiveRange` is a type of {@link module:engine/model/range~Range Range}
 * that updates itself as {@link module:engine/model/document~Document document}
 * is changed through operations. It may be used as a bookmark.
 *
 * **Note:** Be very careful when dealing with `LiveRange`. Each `LiveRange` instance bind events that might
 * have to be unbound. Use {@link module:engine/model/liverange~LiveRange#detach detach} whenever you don't need `LiveRange` anymore.
 */
class LiveRange extends EmitterMixin(Range) {
    /**
     * Creates a live range.
     *
     * @see module:engine/model/range~Range
     */
    constructor(start, end) {
        super(start, end);
        bindWithDocument$1.call(this);
    }
    /**
     * Unbinds all events previously bound by `LiveRange`. Use it whenever you don't need `LiveRange` 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();
    }
    /**
     * Creates a {@link module:engine/model/range~Range range instance} that is equal to this live range.
     *
     * @returns {module:engine/model/range~Range}
     */
    toRange() {
        return new Range(this.start, this.end);
    }
    /**
     * Creates a `LiveRange` instance that is equal to the given range.
     *
     * @param {module:engine/model/range~Range} range
     * @returns {module:engine/model/liverange~LiveRange}
     */
    static fromRange(range) {
        return new LiveRange(range.start, range.end);
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		liveRange.is( 'range' ); // -> true
 *		liveRange.is( 'model:range' ); // -> true
 *		liveRange.is( 'liveRange' ); // -> true
 *		liveRange.is( 'model:liveRange' ); // -> true
 *
 *		liveRange.is( 'view:range' ); // -> false
 *		liveRange.is( 'documentSelection' ); // -> false
 *
 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 *
 * @param {String} type
 * @returns {Boolean}
 */
LiveRange.prototype.is = function (type) {
    return type === 'liveRange' || type === 'model:liveRange' ||
        // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
        type == 'range' || type === 'model:range';
};
// Binds this `LiveRange` to the {@link module:engine/model/document~Document document}
// that owns this range's {@link module:engine/model/range~Range#root root}.
//
// @private
function bindWithDocument$1() {
    this.listenTo(this.root.document.model, 'applyOperation', (event, args) => {
        const operation = args[0];
        if (!operation.isDocumentOperation) {
            return;
        }
        transform$2.call(this, operation);
    }, { priority: 'low' });
}
// Updates this range accordingly to the updates applied to the model. Bases on change events.
//
// @private
// @param {module:engine/model/operation/operation~Operation} operation Executed operation.
function transform$2(operation) {
    // Transform the range by the operation. Join the result ranges if needed.
    const ranges = this.getTransformedByOperation(operation);
    const result = Range._createFromRanges(ranges);
    const boundariesChanged = !result.isEqual(this);
    const contentChanged = doesOperationChangeRangeContent(this, operation);
    let deletionPosition = null;
    if (boundariesChanged) {
        // If range boundaries have changed, fire `change:range` event.
        //
        if (result.root.rootName == '$graveyard') {
            // If the range was moved to the graveyard root, set `deletionPosition`.
            if (operation.type == 'remove') {
                deletionPosition = operation.sourcePosition;
            }
            else {
                // Merge operation.
                deletionPosition = operation.deletionPosition;
            }
        }
        const oldRange = this.toRange();
        this.start = result.start;
        this.end = result.end;
        this.fire('change:range', oldRange, { deletionPosition });
    }
    else if (contentChanged) {
        // If range boundaries have not changed, but there was change inside the range, fire `change:content` event.
        this.fire('change:content', this.toRange(), { deletionPosition });
    }
}
// Checks whether given operation changes something inside the range (even if it does not change boundaries).
//
// @private
// @param {module:engine/model/range~Range} range Range to check.
// @param {module:engine/model/operation/operation~Operation} operation Executed operation.
// @returns {Boolean}
function doesOperationChangeRangeContent(range, operation) {
    switch (operation.type) {
        case 'insert':
            return range.containsPosition(operation.position);
        case 'move':
        case 'remove':
        case 'reinsert':
        case 'merge':
            return range.containsPosition(operation.sourcePosition) ||
                range.start.isEqual(operation.sourcePosition) ||
                range.containsPosition(operation.targetPosition);
        case 'split':
            return range.containsPosition(operation.splitPosition) || range.containsPosition(operation.insertionPosition);
    }
    return false;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const storePrefix = 'selection:';
/**
 * `DocumentSelection` is a special selection which is used as the
 * {@link module:engine/model/document~Document#selection document's selection}.
 * There can be only one instance of `DocumentSelection` per document.
 *
 * Document selection can only be changed by using the {@link module:engine/model/writer~Writer} instance
 * inside the {@link module:engine/model/model~Model#change `change()`} block, as it provides a secure way to modify model.
 *
 * `DocumentSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
 * to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
 *
 * Differences between {@link module:engine/model/selection~Selection} and `DocumentSelection` are:
 * * there is always a range in `DocumentSelection` - even if no ranges were added there is a "default range"
 * present in the selection,
 * * ranges added to this selection updates automatically when the document changes,
 * * attributes of `DocumentSelection` are updated automatically according to selection ranges.
 *
 * Since `DocumentSelection` uses {@link module:engine/model/liverange~LiveRange live ranges}
 * and is updated when {@link module:engine/model/document~Document document}
 * changes, it cannot be set on {@link module:engine/model/node~Node nodes}
 * that are inside {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
 * If you need to represent a selection in document fragment,
 * use {@link module:engine/model/selection~Selection Selection class} instead.
 *
 * @mixes module:utils/emittermixin~EmitterMixin
 */
class DocumentSelection extends EmitterMixin(TypeCheckable) {
    /**
     * Creates an empty live selection for given {@link module:engine/model/document~Document}.
     *
     * @param {module:engine/model/document~Document} doc Document which owns this selection.
     */
    constructor(doc) {
        super();
        /**
         * Selection used internally by that class (`DocumentSelection` is a proxy to that selection).
         *
         * @protected
         */
        this._selection = new LiveSelection(doc);
        this._selection.delegate('change:range').to(this);
        this._selection.delegate('change:attribute').to(this);
        this._selection.delegate('change:marker').to(this);
    }
    /**
     * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
     * collapsed.
     *
     * @readonly
     * @type {Boolean}
     */
    get isCollapsed() {
        return this._selection.isCollapsed;
    }
    /**
     * Selection anchor. Anchor may be described as a position where the most recent part of the selection starts.
     * Together with {@link #focus} they define the direction of selection, which is important
     * when expanding/shrinking selection. Anchor is always {@link module:engine/model/range~Range#start start} or
     * {@link module:engine/model/range~Range#end end} position of the most recently added range.
     *
     * Is set to `null` if there are no ranges in selection.
     *
     * @see #focus
     * @readonly
     * @type {module:engine/model/position~Position|null}
     */
    get anchor() {
        return this._selection.anchor;
    }
    /**
     * Selection focus. Focus is a position where the selection ends.
     *
     * Is set to `null` if there are no ranges in selection.
     *
     * @see #anchor
     * @readonly
     * @type {module:engine/model/position~Position|null}
     */
    get focus() {
        return this._selection.focus;
    }
    /**
     * Returns number of ranges in selection.
     *
     * @readonly
     * @type {Number}
     */
    get rangeCount() {
        return this._selection.rangeCount;
    }
    /**
     * Describes whether `Documentselection` has own range(s) set, or if it is defaulted to
     * {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
     *
     * @readonly
     * @type {Boolean}
     */
    get hasOwnRange() {
        return this._selection.hasOwnRange;
    }
    /**
     * Specifies whether the {@link #focus}
     * precedes {@link #anchor}.
     *
     * @readonly
     * @type {Boolean}
     */
    get isBackward() {
        return this._selection.isBackward;
    }
    /**
     * Describes whether the gravity is overridden (using {@link module:engine/model/writer~Writer#overrideSelectionGravity}) or not.
     *
     * Note that the gravity remains overridden as long as will not be restored the same number of times as it was overridden.
     *
     * @readonly
     * @returns {Boolean}
     */
    get isGravityOverridden() {
        return this._selection.isGravityOverridden;
    }
    /**
     * A collection of selection {@link module:engine/model/markercollection~Marker markers}.
     * Marker is a selection marker when selection range is inside the marker range.
     *
     * **Note**: Only markers from {@link ~DocumentSelection#observeMarkers observed markers groups} are collected.
     *
     * @readonly
     * @type {module:utils/collection~Collection}
     */
    get markers() {
        return this._selection.markers;
    }
    /**
     * Used for the compatibility with the {@link module:engine/model/selection~Selection#isEqual} method.
     *
     * @internal
     * @protected
     */
    get _ranges() {
        return this._selection._ranges;
    }
    /**
     * Returns an iterable that iterates over copies of selection ranges.
     *
     * @returns {Iterable.<module:engine/model/range~Range>}
     */
    getRanges() {
        return this._selection.getRanges();
    }
    /**
     * Returns the first position in the selection.
     * First position is the position that {@link module:engine/model/position~Position#isBefore is before}
     * any other position in the selection.
     *
     * Returns `null` if there are no ranges in selection.
     *
     * @returns {module:engine/model/position~Position|null}
     */
    getFirstPosition() {
        return this._selection.getFirstPosition();
    }
    /**
     * Returns the last position in the selection.
     * Last position is the position that {@link module:engine/model/position~Position#isAfter is after}
     * any other position in the selection.
     *
     * Returns `null` if there are no ranges in selection.
     *
     * @returns {module:engine/model/position~Position|null}
     */
    getLastPosition() {
        return this._selection.getLastPosition();
    }
    /**
     * Returns a copy of the first range in the selection.
     * First range is the one which {@link module:engine/model/range~Range#start start} position
     * {@link module:engine/model/position~Position#isBefore is before} start position of all other ranges
     * (not to confuse with the first range added to the selection).
     *
     * Returns `null` if there are no ranges in selection.
     *
     * @returns {module:engine/model/range~Range|null}
     */
    getFirstRange() {
        return this._selection.getFirstRange();
    }
    /**
     * Returns a copy of the last range in the selection.
     * Last range is the one which {@link module:engine/model/range~Range#end end} position
     * {@link module:engine/model/position~Position#isAfter is after} end position of all other ranges (not to confuse with the range most
     * recently added to the selection).
     *
     * Returns `null` if there are no ranges in selection.
     *
     * @returns {module:engine/model/range~Range|null}
     */
    getLastRange() {
        return this._selection.getLastRange();
    }
    /**
     * Gets elements of type {@link module:engine/model/schema~Schema#isBlock "block"} touched by the selection.
     *
     * This method's result can be used for example to apply block styling to all blocks covered by this selection.
     *
     * **Note:** `getSelectedBlocks()` returns blocks that are nested in other non-block elements
     * but will not return blocks nested in other blocks.
     *
     * In this case the function will return exactly all 3 paragraphs (note: `<blockQuote>` is not a block itself):
     *
     *		<paragraph>[a</paragraph>
     *		<blockQuote>
     *			<paragraph>b</paragraph>
     *		</blockQuote>
     *		<paragraph>c]d</paragraph>
     *
     * In this case the paragraph will also be returned, despite the collapsed selection:
     *
     *		<paragraph>[]a</paragraph>
     *
     * In such a scenario, however, only blocks A, B & E will be returned as blocks C & D are nested in block B:
     *
     *		[<blockA></blockA>
     *		<blockB>
     *			<blockC></blockC>
     *			<blockD></blockD>
     *		</blockB>
     *		<blockE></blockE>]
     *
     * If the selection is inside a block all the inner blocks (A & B) are returned:
     *
     * 		<block>
     *			<blockA>[a</blockA>
     * 			<blockB>b]</blockB>
     * 		</block>
     *
     * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
     * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
     *
     *		<paragraph>[a</paragraph>
     *		<paragraph>b</paragraph>
     *		<paragraph>]c</paragraph> // this block will not be returned
     *
     * @returns {Iterable.<module:engine/model/element~Element>}
     */
    getSelectedBlocks() {
        return this._selection.getSelectedBlocks();
    }
    /**
     * Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
     * one range in the selection, and that range contains exactly one element.
     * Returns `null` if there is no selected element.
     *
     * @returns {module:engine/model/element~Element|null}
     */
    getSelectedElement() {
        return this._selection.getSelectedElement();
    }
    /**
     * Checks whether the selection contains the entire content of the given element. This means that selection must start
     * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
     * touching the element's end.
     *
     * By default, this method will check whether the entire content of the selection's current root is selected.
     * Useful to check if e.g. the user has just pressed <kbd>Ctrl</kbd> + <kbd>A</kbd>.
     *
     * @param {module:engine/model/element~Element} [element=this.anchor.root]
     * @returns {Boolean}
     */
    containsEntireContent(element) {
        return this._selection.containsEntireContent(element);
    }
    /**
     * Unbinds all events previously bound by document selection.
     */
    destroy() {
        this._selection.destroy();
    }
    /**
     * Returns iterable that iterates over this selection's attribute keys.
     *
     * @returns {Iterable.<String>}
     */
    getAttributeKeys() {
        return this._selection.getAttributeKeys();
    }
    /**
     * Returns iterable that iterates over this selection'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._selection.getAttributes();
    }
    /**
     * Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
     *
     * @param {String} key Key of attribute to look for.
     * @returns {*} Attribute value or `undefined`.
     */
    getAttribute(key) {
        return this._selection.getAttribute(key);
    }
    /**
     * Checks if the selection 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 selection, `false` otherwise.
     */
    hasAttribute(key) {
        return this._selection.hasAttribute(key);
    }
    /**
     * Refreshes selection attributes and markers according to the current position in the model.
     */
    refresh() {
        this._selection.updateMarkers();
        this._selection._updateAttributes(false);
    }
    /**
     * Registers a marker group prefix or a marker name to be collected in the
     * {@link ~DocumentSelection#markers selection markers collection}.
     *
     * See also {@link module:engine/model/markercollection~MarkerCollection#getMarkersGroup `MarkerCollection#getMarkersGroup()`}.
     *
     * @param {String} prefixOrName The marker group prefix or marker name.
     */
    observeMarkers(prefixOrName) {
        this._selection.observeMarkers(prefixOrName);
    }
    /**
     * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
     * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionFocus} method.
     *
     * The location can be specified in the same form as
     * {@link module:engine/model/writer~Writer#createPositionAt writer.createPositionAt()} parameters.
     *
     * @see module:engine/model/writer~Writer#setSelectionFocus
     * @internal
     * @protected
     * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * first parameter is a {@link module:engine/model/item~Item model item}.
     */
    _setFocus(itemOrPosition, offset) {
        this._selection.setFocus(itemOrPosition, offset);
    }
    /**
     * Sets this selection's ranges and direction to the specified location based on the given
     * {@link module:engine/model/selection~Selectable selectable}.
     * Should be used only within the {@link module:engine/model/writer~Writer#setSelection} method.
     *
     * @see module:engine/model/writer~Writer#setSelection
     * @internal
     * @protected
     * @param {module:engine/model/selection~Selectable} selectable
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     */
    _setTo(...args) {
        this._selection.setTo(...args);
    }
    /**
     * Sets attribute on the selection. If attribute with the same key already is set, it's value is overwritten.
     * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionAttribute} method.
     *
     * @see module:engine/model/writer~Writer#setSelectionAttribute
     * @internal
     * @protected
     * @param {String} key Key of the attribute to set.
     * @param {*} value Attribute value.
     */
    _setAttribute(key, value) {
        this._selection.setAttribute(key, value);
    }
    /**
     * Removes an attribute with given key from the selection.
     * If the given attribute was set on the selection, fires the {@link module:engine/model/selection~Selection#event:change:range}
     * event with removed attribute key.
     * Should be used only within the {@link module:engine/model/writer~Writer#removeSelectionAttribute} method.
     *
     * @see module:engine/model/writer~Writer#removeSelectionAttribute
     * @internal
     * @protected
     * @param {String} key Key of the attribute to remove.
     */
    _removeAttribute(key) {
        this._selection.removeAttribute(key);
    }
    /**
     * Returns an iterable that iterates through all selection attributes stored in current selection's parent.
     *
     * @protected
     * @returns {Iterable.<*>}
     */
    _getStoredAttributes() {
        return this._selection.getStoredAttributes();
    }
    /**
     * Temporarily changes the gravity of the selection from the left to the right.
     *
     * The gravity defines from which direction the selection inherits its attributes. If it's the default left
     * gravity, the selection (after being moved by the the user) inherits attributes from its left hand side.
     * This method allows to temporarily override this behavior by forcing the gravity to the right.
     *
     * It returns an unique identifier which is required to restore the gravity. It guarantees the symmetry
     * of the process.
     *
     * @see module:engine/model/writer~Writer#overrideSelectionGravity
     * @internal
     * @protected
     * @returns {String} The unique id which allows restoring the gravity.
     */
    _overrideGravity() {
        return this._selection.overrideGravity();
    }
    /**
     * Restores the {@link ~DocumentSelection#_overrideGravity overridden gravity}.
     *
     * Restoring the gravity is only possible using the unique identifier returned by
     * {@link ~DocumentSelection#_overrideGravity}. Note that the gravity remains overridden as long as won't be restored
     * the same number of times it was overridden.
     *
     * @see module:engine/model/writer~Writer#restoreSelectionGravity
     * @internal
     * @protected
     * @param {String} uid The unique id returned by {@link #_overrideGravity}.
     */
    _restoreGravity(uid) {
        this._selection.restoreGravity(uid);
    }
    /**
     * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
     *
     * @internal
     * @protected
     * @param {String} key Attribute key to convert.
     * @returns {String} Converted attribute key, applicable for selection store.
     */
    static _getStoreAttributeKey(key) {
        return storePrefix + key;
    }
    /**
     * Checks whether the given attribute key is an attribute stored on an element.
     *
     * @protected
     * @param {String} key
     * @returns {Boolean}
     */
    static _isStoreAttributeKey(key) {
        return key.startsWith(storePrefix);
    }
}
/**
 * Checks whether this object is of the given type.
 *
 *		selection.is( 'selection' ); // -> true
 *		selection.is( 'documentSelection' ); // -> true
 *		selection.is( 'model:selection' ); // -> true
 *		selection.is( 'model:documentSelection' ); // -> true
 *
 *		selection.is( 'view:selection' ); // -> false
 *		selection.is( 'element' ); // -> false
 *		selection.is( 'node' ); // -> false
 *
 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 *
 * @param {String} type
 * @returns {Boolean}
 */
DocumentSelection.prototype.is = function (type) {
    return type === 'selection' ||
        type == 'model:selection' ||
        type == 'documentSelection' ||
        type == 'model:documentSelection';
};
// `LiveSelection` is used internally by {@link module:engine/model/documentselection~DocumentSelection} and shouldn't be used directly.
//
// LiveSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
// to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
//
// Differences between {@link module:engine/model/selection~Selection} and `LiveSelection` are:
// * there is always a range in `LiveSelection` - even if no ranges were added there is a "default range"
// present in the selection,
// * ranges added to this selection updates automatically when the document changes,
// * attributes of `LiveSelection` are updated automatically according to selection ranges.
//
// @extends module:engine/model/selection~Selection
//
class LiveSelection extends Selection {
    // Creates an empty live selection for given {@link module:engine/model/document~Document}.
    // @param {module:engine/model/document~Document} doc Document which owns this selection.
    constructor(doc) {
        super();
        // List of selection markers.
        // Marker is a selection marker when selection range is inside the marker range.
        //
        // @type {module:utils/collection~Collection}
        this.markers = new Collection({ idProperty: 'name' });
        // Document which owns this selection.
        //
        // @protected
        // @member {module:engine/model/model~Model}
        this._model = doc.model;
        // Document which owns this selection.
        //
        // @protected
        // @member {module:engine/model/document~Document}
        this._document = doc;
        // Keeps mapping of attribute name to priority with which the attribute got modified (added/changed/removed)
        // last time. Possible values of priority are: `'low'` and `'normal'`.
        //
        // Priorities are used by internal `LiveSelection` mechanisms. All attributes set using `LiveSelection`
        // attributes API are set with `'normal'` priority.
        //
        // @private
        // @member {Map} module:engine/model/liveselection~LiveSelection#_attributePriority
        this._attributePriority = new Map();
        // Position to which the selection should be set if the last selection range was moved to the graveyard.
        // @private
        // @member {module:engine/model/position~Position} module:engine/model/liveselection~LiveSelection#_selectionRestorePosition
        this._selectionRestorePosition = null;
        // Flag that informs whether the selection ranges have changed. It is changed on true when `LiveRange#change:range` event is fired.
        // @private
        // @member {Array} module:engine/model/liveselection~LiveSelection#_hasChangedRange
        this._hasChangedRange = false;
        // Each overriding gravity adds an UID to the set and each removal removes it.
        // Gravity is overridden when there's at least one UID in the set.
        // Gravity is restored when the set is empty.
        // This is to prevent conflicts when gravity is overridden by more than one feature at the same time.
        // @private
        // @type {Set}
        this._overriddenGravityRegister = new Set();
        // Prefixes of marker names that should affect `LiveSelection#markers` collection.
        // @private
        // @type {Set}
        this._observedMarkers = new Set();
        // Ensure selection is correct after each operation.
        this.listenTo(this._model, 'applyOperation', (evt, args) => {
            const operation = args[0];
            if (!operation.isDocumentOperation || operation.type == 'marker' || operation.type == 'rename' || operation.type == 'noop') {
                return;
            }
            // Fix selection if the last range was removed from it and we have a position to which we can restore the selection.
            if (this._ranges.length == 0 && this._selectionRestorePosition) {
                this._fixGraveyardSelection(this._selectionRestorePosition);
            }
            // "Forget" the restore position even if it was not "used".
            this._selectionRestorePosition = null;
            if (this._hasChangedRange) {
                this._hasChangedRange = false;
                this.fire('change:range', { directChange: false });
            }
        }, { priority: 'lowest' });
        // Ensure selection is correct and up to date after each range change.
        this.on('change:range', () => {
            this._validateSelectionRanges(this.getRanges());
        });
        // Update markers data stored by the selection after each marker change.
        // This handles only marker changes done through marker operations (not model tree changes).
        this.listenTo(this._model.markers, 'update', (evt, marker, oldRange, newRange) => {
            this._updateMarker(marker, newRange);
        });
        // Ensure selection is up to date after each change block.
        this.listenTo(this._document, 'change', (evt, batch) => {
            clearAttributesStoredInElement(this._model, batch);
        });
    }
    get isCollapsed() {
        const length = this._ranges.length;
        return length === 0 ? this._document._getDefaultRange().isCollapsed : super.isCollapsed;
    }
    get anchor() {
        return super.anchor || this._document._getDefaultRange().start;
    }
    get focus() {
        return super.focus || this._document._getDefaultRange().end;
    }
    get rangeCount() {
        return this._ranges.length ? this._ranges.length : 1;
    }
    // Describes whether `LiveSelection` has own range(s) set, or if it is defaulted to
    // {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
    //
    // @readonly
    // @type {Boolean}
    get hasOwnRange() {
        return this._ranges.length > 0;
    }
    // When set to `true` then selection attributes on node before the caret won't be taken
    // into consideration while updating selection attributes.
    //
    // @protected
    // @type {Boolean}
    get isGravityOverridden() {
        return !!this._overriddenGravityRegister.size;
    }
    // Unbinds all events previously bound by live selection.
    destroy() {
        for (let i = 0; i < this._ranges.length; i++) {
            this._ranges[i].detach();
        }
        this.stopListening();
    }
    *getRanges() {
        if (this._ranges.length) {
            yield* super.getRanges();
        }
        else {
            yield this._document._getDefaultRange();
        }
    }
    getFirstRange() {
        return super.getFirstRange() || this._document._getDefaultRange();
    }
    getLastRange() {
        return super.getLastRange() || this._document._getDefaultRange();
    }
    setTo(...args) {
        super.setTo(...args);
        this._updateAttributes(true);
        this.updateMarkers();
    }
    setFocus(itemOrPosition, offset) {
        super.setFocus(itemOrPosition, offset);
        this._updateAttributes(true);
        this.updateMarkers();
    }
    setAttribute(key, value) {
        if (this._setAttribute(key, value)) {
            // Fire event with exact data.
            const attributeKeys = [key];
            this.fire('change:attribute', { attributeKeys, directChange: true });
        }
    }
    removeAttribute(key) {
        if (this._removeAttribute(key)) {
            // Fire event with exact data.
            const attributeKeys = [key];
            this.fire('change:attribute', { attributeKeys, directChange: true });
        }
    }
    overrideGravity() {
        const overrideUid = uid();
        // Remember that another overriding has been requested. It will need to be removed
        // before the gravity is to be restored.
        this._overriddenGravityRegister.add(overrideUid);
        if (this._overriddenGravityRegister.size === 1) {
            this._updateAttributes(true);
        }
        return overrideUid;
    }
    restoreGravity(uid) {
        if (!this._overriddenGravityRegister.has(uid)) {
            /**
             * Restoring gravity for an unknown UID is not possible. Make sure you are using a correct
             * UID obtained from the {@link module:engine/model/writer~Writer#overrideSelectionGravity} to restore.
             *
             * @error document-selection-gravity-wrong-restore
             * @param {String} uid The unique identifier returned by
             * {@link module:engine/model/documentselection~DocumentSelection#_overrideGravity}.
             */
            throw new CKEditorError('document-selection-gravity-wrong-restore', this, { uid });
        }
        this._overriddenGravityRegister.delete(uid);
        // Restore gravity only when all overriding have been restored.
        if (!this.isGravityOverridden) {
            this._updateAttributes(true);
        }
    }
    observeMarkers(prefixOrName) {
        this._observedMarkers.add(prefixOrName);
        this.updateMarkers();
    }
    _replaceAllRanges(ranges) {
        this._validateSelectionRanges(ranges);
        super._replaceAllRanges(ranges);
    }
    _popRange() {
        this._ranges.pop().detach();
    }
    _pushRange(range) {
        const liveRange = this._prepareRange(range);
        // `undefined` is returned when given `range` is in graveyard root.
        if (liveRange) {
            this._ranges.push(liveRange);
        }
    }
    _validateSelectionRanges(ranges) {
        for (const range of ranges) {
            if (!this._document._validateSelectionRange(range)) {
                /**
                 * Range from {@link module:engine/model/documentselection~DocumentSelection document selection}
                 * starts or ends at incorrect position.
                 *
                 * @error document-selection-wrong-position
                 * @param {module:engine/model/range~Range} range
                 */
                throw new CKEditorError('document-selection-wrong-position', this, { range });
            }
        }
    }
    // Prepares given range to be added to selection. Checks if it is correct,
    // converts it to {@link module:engine/model/liverange~LiveRange LiveRange}
    // and sets listeners listening to the range's change event.
    //
    // @private
    // @param {module:engine/model/range~Range} range
    _prepareRange(range) {
        this._checkRange(range);
        if (range.root == this._document.graveyard) {
            // @if CK_DEBUG // console.warn( 'Trying to add a Range that is in the graveyard root. Range rejected.' );
            return;
        }
        const liveRange = LiveRange.fromRange(range);
        // If selection range is moved to the graveyard remove it from the selection object.
        // Also, save some data that can be used to restore selection later, on `Model#applyOperation` event.
        liveRange.on('change:range', (evt, oldRange, data) => {
            this._hasChangedRange = true;
            if (liveRange.root == this._document.graveyard) {
                this._selectionRestorePosition = data.deletionPosition;
                const index = this._ranges.indexOf(liveRange);
                this._ranges.splice(index, 1);
                liveRange.detach();
            }
        });
        return liveRange;
    }
    updateMarkers() {
        if (!this._observedMarkers.size) {
            return;
        }
        const markers = [];
        let changed = false;
        for (const marker of this._model.markers) {
            const markerGroup = marker.name.split(':', 1)[0];
            if (!this._observedMarkers.has(markerGroup)) {
                continue;
            }
            const markerRange = marker.getRange();
            for (const selectionRange of this.getRanges()) {
                if (markerRange.containsRange(selectionRange, !selectionRange.isCollapsed)) {
                    markers.push(marker);
                }
            }
        }
        const oldMarkers = Array.from(this.markers);
        for (const marker of markers) {
            if (!this.markers.has(marker)) {
                this.markers.add(marker);
                changed = true;
            }
        }
        for (const marker of Array.from(this.markers)) {
            if (!markers.includes(marker)) {
                this.markers.remove(marker);
                changed = true;
            }
        }
        if (changed) {
            this.fire('change:marker', { oldMarkers, directChange: false });
        }
    }
    _updateMarker(marker, markerRange) {
        const markerGroup = marker.name.split(':', 1)[0];
        if (!this._observedMarkers.has(markerGroup)) {
            return;
        }
        let changed = false;
        const oldMarkers = Array.from(this.markers);
        const hasMarker = this.markers.has(marker);
        if (!markerRange) {
            if (hasMarker) {
                this.markers.remove(marker);
                changed = true;
            }
        }
        else {
            let contained = false;
            for (const selectionRange of this.getRanges()) {
                if (markerRange.containsRange(selectionRange, !selectionRange.isCollapsed)) {
                    contained = true;
                    break;
                }
            }
            if (contained && !hasMarker) {
                this.markers.add(marker);
                changed = true;
            }
            else if (!contained && hasMarker) {
                this.markers.remove(marker);
                changed = true;
            }
        }
        if (changed) {
            this.fire('change:marker', { oldMarkers, directChange: false });
        }
    }
    // Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.
    //
    // @protected
    // @param {Boolean} clearAll
    // @fires change:attribute
    _updateAttributes(clearAll) {
        const newAttributes = toMap(this._getSurroundingAttributes());
        const oldAttributes = toMap(this.getAttributes());
        if (clearAll) {
            // If `clearAll` remove all attributes and reset priorities.
            this._attributePriority = new Map();
            this._attrs = new Map();
        }
        else {
            // If not, remove only attributes added with `low` priority.
            for (const [key, priority] of this._attributePriority) {
                if (priority == 'low') {
                    this._attrs.delete(key);
                    this._attributePriority.delete(key);
                }
            }
        }
        this._setAttributesTo(newAttributes);
        // Let's evaluate which attributes really changed.
        const changed = [];
        // First, loop through all attributes that are set on selection right now.
        // Check which of them are different than old attributes.
        for (const [newKey, newValue] of this.getAttributes()) {
            if (!oldAttributes.has(newKey) || oldAttributes.get(newKey) !== newValue) {
                changed.push(newKey);
            }
        }
        // Then, check which of old attributes got removed.
        for (const [oldKey] of oldAttributes) {
            if (!this.hasAttribute(oldKey)) {
                changed.push(oldKey);
            }
        }
        // Fire event with exact data (fire only if anything changed).
        if (changed.length > 0) {
            this.fire('change:attribute', { attributeKeys: changed, directChange: false });
        }
    }
    // Internal method for setting `LiveSelection` attribute. Supports attribute priorities (through `directChange`
    // parameter).
    //
    // @private
    // @param {String} key Attribute key.
    // @param {*} value Attribute value.
    // @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
    // is caused by `Batch` API.
    // @returns {Boolean} Whether value has changed.
    _setAttribute(key, value, directChange = true) {
        const priority = directChange ? 'normal' : 'low';
        if (priority == 'low' && this._attributePriority.get(key) == 'normal') {
            // Priority too low.
            return false;
        }
        const oldValue = super.getAttribute(key);
        // Don't do anything if value has not changed.
        if (oldValue === value) {
            return false;
        }
        this._attrs.set(key, value);
        // Update priorities map.
        this._attributePriority.set(key, priority);
        return true;
    }
    // Internal method for removing `LiveSelection` attribute. Supports attribute priorities (through `directChange`
    // parameter).
    //
    // NOTE: Even if attribute is not present in the selection but is provided to this method, it's priority will
    // be changed according to `directChange` parameter.
    //
    // @private
    // @param {String} key Attribute key.
    // @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
    // is caused by `Batch` API.
    // @returns {Boolean} Whether attribute was removed. May not be true if such attributes didn't exist or the
    // existing attribute had higher priority.
    _removeAttribute(key, directChange = true) {
        const priority = directChange ? 'normal' : 'low';
        if (priority == 'low' && this._attributePriority.get(key) == 'normal') {
            // Priority too low.
            return false;
        }
        // Update priorities map.
        this._attributePriority.set(key, priority);
        // Don't do anything if value has not changed.
        if (!super.hasAttribute(key)) {
            return false;
        }
        this._attrs.delete(key);
        return true;
    }
    // Internal method for setting multiple `LiveSelection` attributes. Supports attribute priorities (through
    // `directChange` parameter).
    //
    // @private
    // @param {Map.<String,*>} attrs Iterable object containing attributes to be set.
    // @returns {Set.<String>} Changed attribute keys.
    _setAttributesTo(attrs) {
        const changed = new Set();
        for (const [oldKey, oldValue] of this.getAttributes()) {
            // Do not remove attribute if attribute with same key and value is about to be set.
            if (attrs.get(oldKey) === oldValue) {
                continue;
            }
            // All rest attributes will be removed so changed attributes won't change .
            this._removeAttribute(oldKey, false);
        }
        for (const [key, value] of attrs) {
            // Attribute may not be set because of attributes or because same key/value is already added.
            const gotAdded = this._setAttribute(key, value, false);
            if (gotAdded) {
                changed.add(key);
            }
        }
        return changed;
    }
    // Returns an iterable that iterates through all selection attributes stored in current selection's parent.
    //
    // @public
    // @returns {Iterable.<*>}
    *getStoredAttributes() {
        const selectionParent = this.getFirstPosition().parent;
        if (this.isCollapsed && selectionParent.isEmpty) {
            for (const key of selectionParent.getAttributeKeys()) {
                if (key.startsWith(storePrefix)) {
                    const realKey = key.substr(storePrefix.length);
                    yield [realKey, selectionParent.getAttribute(key)];
                }
            }
        }
    }
    // Checks model text nodes that are closest to the selection's first position and returns attributes of first
    // found element. If there are no text nodes in selection's first position parent, it returns selection
    // attributes stored in that parent.
    //
    // @private
    // @returns {Iterable.<*>} Collection of attributes.
    _getSurroundingAttributes() {
        const position = this.getFirstPosition();
        const schema = this._model.schema;
        let attrs = null;
        if (!this.isCollapsed) {
            // 1. If selection is a range...
            const range = this.getFirstRange();
            // ...look for a first character node in that range and take attributes from it.
            for (const value of range) {
                // If the item is an object, we don't want to get attributes from its children.
                if (value.item.is('element') && schema.isObject(value.item)) {
                    break;
                }
                if (value.type == 'text') {
                    attrs = value.item.getAttributes();
                    break;
                }
            }
        }
        else {
            // 2. If the selection is a caret or the range does not contain a character node...
            const nodeBefore = position.textNode ? position.textNode : position.nodeBefore;
            const nodeAfter = position.textNode ? position.textNode : position.nodeAfter;
            // When gravity is overridden then don't take node before into consideration.
            if (!this.isGravityOverridden) {
                // ...look at the node before caret and take attributes from it if it is a character node.
                attrs = getAttrsIfCharacter(nodeBefore);
            }
            // 3. If not, look at the node after caret...
            if (!attrs) {
                attrs = getAttrsIfCharacter(nodeAfter);
            }
            // 4. If not, try to find the first character on the left, that is in the same node.
            // When gravity is overridden then don't take node before into consideration.
            if (!this.isGravityOverridden && !attrs) {
                let node = nodeBefore;
                while (node && !schema.isInline(node) && !attrs) {
                    node = node.previousSibling;
                    attrs = getAttrsIfCharacter(node);
                }
            }
            // 5. If not found, try to find the first character on the right, that is in the same node.
            if (!attrs) {
                let node = nodeAfter;
                while (node && !schema.isInline(node) && !attrs) {
                    node = node.nextSibling;
                    attrs = getAttrsIfCharacter(node);
                }
            }
            // 6. If not found, selection should retrieve attributes from parent.
            if (!attrs) {
                attrs = this.getStoredAttributes();
            }
        }
        return attrs;
    }
    // Fixes the selection after all its ranges got removed.
    //
    // @private
    // @param {module:engine/model/position~Position} deletionPosition Position where the deletion happened.
    _fixGraveyardSelection(deletionPosition) {
        // Find a range that is a correct selection range and is closest to the position where the deletion happened.
        const selectionRange = this._model.schema.getNearestSelectionRange(deletionPosition);
        // If nearest valid selection range has been found - add it in the place of old range.
        if (selectionRange) {
            // Check the range, convert it to live range, bind events, etc.
            this._pushRange(selectionRange);
        }
        // If nearest valid selection range cannot be found don't add any range. Selection will be set to the default range.
    }
}
// Helper function for {@link module:engine/model/liveselection~LiveSelection#_updateAttributes}.
//
// It takes model item, checks whether it is a text node (or text proxy) and, if so, returns it's attributes. If not, returns `null`.
//
// @param {module:engine/model/item~Item|null}  node
// @returns {Boolean}
function getAttrsIfCharacter(node) {
    if (node instanceof TextProxy || node instanceof Text$1) {
        return node.getAttributes();
    }
    return null;
}
// Removes selection attributes from element which is not empty anymore.
//
// @param {module:engine/model/model~Model} model
// @param {module:engine/model/batch~Batch} batch
function clearAttributesStoredInElement(model, batch) {
    const differ = model.document.differ;
    for (const entry of differ.getChanges()) {
        if (entry.type != 'insert') {
            continue;
        }
        const changeParent = entry.position.parent;
        const isNoLongerEmpty = entry.length === changeParent.maxOffset;
        if (isNoLongerEmpty) {
            model.enqueueChange(batch, writer => {
                const storedAttributes = Array.from(changeParent.getAttributeKeys())
                    .filter(key => key.startsWith(storePrefix));
                for (const key of storedAttributes) {
                    writer.removeAttribute(key, changeParent);
                }
            });
        }
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module engine/conversion/conversionhelpers
 */
/**
 * Base class for conversion helpers.
 */
class ConversionHelpers {
    /**
     * Creates a conversion helpers instance.
     *
     * @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
     * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers
     */
    constructor(dispatchers) {
        this._dispatchers = dispatchers;
    }
    /**
     * Registers a conversion helper.
     *
     * **Note**: See full usage example in the `{@link module:engine/conversion/conversion~Conversion#for conversion.for()}`
     * method description.
     *
     * @param {Function} conversionHelper The function to be called on event.
     * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers}
     */
    add(conversionHelper) {
        for (const dispatcher of this._dispatchers) {
            conversionHelper(dispatcher);
        }
        return this;
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Downcast conversion helper functions.
 *
 * Learn more about {@glink framework/guides/deep-dive/conversion/downcast downcast helpers}.
 *
 * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
 */
class DowncastHelpers extends ConversionHelpers {
    /**
     * Model element to view element conversion helper.
     *
     * This conversion results in creating a view element. For example, model `<paragraph>Foo</paragraph>` becomes `<p>Foo</p>` in the view.
     *
     *		editor.conversion.for( 'downcast' ).elementToElement( {
     *			model: 'paragraph',
     *			view: 'p'
     *		} );
     *
     *		editor.conversion.for( 'downcast' ).elementToElement( {
     *			model: 'paragraph',
     *			view: 'div',
     *			converterPriority: 'high'
     *		} );
     *
     *		editor.conversion.for( 'downcast' ).elementToElement( {
     *			model: 'fancyParagraph',
     *			view: {
     *				name: 'p',
     *				classes: 'fancy'
     *			}
     *		} );
     *
     *		editor.conversion.for( 'downcast' ).elementToElement( {
     *			model: 'heading',
     *			view: ( modelElement, conversionApi ) => {
     *				const { writer } = conversionApi;
     *
     *				return writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
     *			}
     *		} );
     *
     * The element-to-element conversion supports the reconversion mechanism. It can be enabled by using either the `attributes` or
     * the `children` props on a model description. You will find a couple examples below.
     *
     * In order to reconvert an element if any of its direct children have been added or removed, use the `children` property on a `model`
     * description. For example, this model:
     *
     *		<box>
     *			<paragraph>Some text.</paragraph>
     *		</box>
     *
     * will be converted into this structure in the view:
     *
     *		<div class="box" data-type="single">
     *			<p>Some text.</p>
     *		</div>
     *
     * But if more items were inserted in the model:
     *
     *		<box>
     *			<paragraph>Some text.</paragraph>
     *			<paragraph>Other item.</paragraph>
     *		</box>
     *
     * it will be converted into this structure in the view (note the element `data-type` change):
     *
     *		<div class="box" data-type="multiple">
     *			<p>Some text.</p>
     *			<p>Other item.</p>
     *		</div>
     *
     * Such a converter would look like this (note that the `paragraph` elements are converted separately):
     *
     *		editor.conversion.for( 'downcast' ).elementToElement( {
     *			model: {
     *	 			name: 'box',
     *	 			children: true
     *			},
     *			view: ( modelElement, conversionApi ) => {
     *				const { writer } = conversionApi;
     *
     *				return writer.createContainerElement( 'div', {
     *					class: 'box',
     *					'data-type': modelElement.childCount == 1 ? 'single' : 'multiple'
     *				} );
     *			}
     *		} );
     *
     * In order to reconvert element if any of its attributes have been updated, use the `attributes` property on a `model`
     * description. For example, this model:
     *
     *		<heading level="2">Some text.</heading>
     *
     * will be converted into this structure in the view:
     *
     *		<h2>Some text.</h2>
     *
     * But if the `heading` element's `level` attribute has been updated to `3` for example, then
     * it will be converted into this structure in the view:
     *
     *		<h3>Some text.</h3>
     *
     * Such a converter would look as follows:
     *
     *		editor.conversion.for( 'downcast' ).elementToElement( {
     *			model: {
     *	 			name: 'heading',
     *	 			attributes: 'level'
     *			},
     *			view: ( modelElement, conversionApi ) => {
     *				const { writer } = conversionApi;
     *
     *				return writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
     *			}
     *		} );
     *
     * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
     * to the conversion process.
     *
     * You can read more about the element-to-element conversion in the
     * {@glink framework/guides/deep-dive/conversion/downcast downcast conversion} guide.
     *
     * @method #elementToElement
     * @param {Object} config Conversion configuration.
     * @param {String|Object} config.model The description or a name of the model element to convert.
     * @param {String|Array.<String>} [config.model.attributes] The list of attribute names that should be consumed while creating
     * the view element. Note that the view will be reconverted if any of the listed attributes changes.
     * @param {Boolean} [config.model.children] Specifies whether the view element requires reconversion if the list
     * of the model child nodes changed.
     * @param {module:engine/view/elementdefinition~ElementDefinition|module:engine/conversion/downcasthelpers~ElementCreatorFunction}
     * config.view A view element definition or a function that takes the model element and
     * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
     * as parameters and returns a view container element.
     * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
     */
    elementToElement(config) {
        return this.add(downcastElementToElement(config));
    }
    /**
     * The model element to view structure (several elements) conversion helper.
     *
     * This conversion results in creating a view structure with one or more slots defined for the child nodes.
     * For example, a model `<table>` may become this structure in the view:
     *
     *		<figure class="table">
     *			<table>
     *				<tbody>${ slot for table rows }</tbody>
     *			</table>
     *		</figure>
     *
     * The children of the model's `<table>` element will be inserted into the `<tbody>` element.
     * If the `elementToElement()` helper was used, the children would be inserted into the `<figure>`.
     *
     * An example converter that converts the following model structure:
     *
     *		<wrappedParagraph>Some text.</wrappedParagraph>
     *
     * into this structure in the view:
     *
     *		<div class="wrapper">
     *			<p>Some text.</p>
     *		</div>
     *
     * would look like this:
     *
     *		editor.conversion.for( 'downcast' ).elementToStructure( {
     *			model: 'wrappedParagraph',
     *			view: ( modelElement, conversionApi ) => {
     *				const { writer } = conversionApi;
     *
     *				const wrapperViewElement = writer.createContainerElement( 'div', { class: 'wrapper' } );
     *				const paragraphViewElement = writer.createContainerElement( 'p' );
     *
     *				writer.insert( writer.createPositionAt( wrapperViewElement, 0 ), paragraphViewElement );
     *				writer.insert( writer.createPositionAt( paragraphViewElement, 0 ), writer.createSlot() );
     *
     *				return wrapperViewElement;
     *			}
     *		} );
     *
     * The `slorFor()` function can also take a callback that allows filtering which children of the model element
     * should be converted into this slot.
     *
     * Imagine a table feature where for this model structure:
     *
     *		<table headingRows="1">
     *			<tableRow> ... table cells 1 ... </tableRow>
     *			<tableRow> ... table cells 2 ... </tableRow>
     *			<tableRow> ... table cells 3 ... </tableRow>
     *			<caption>Caption text</caption>
     *		</table>
     *
     * we want to generate this view structure:
     *
     *		<figure class="table">
     *			<table>
     *				<thead>
     *					<tr> ... table cells 1 ... </tr>
     *				</thead>
     *				<tbody>
     *					<tr> ... table cells 2 ... </tr>
     *					<tr> ... table cells 3 ... </tr>
     *				</tbody>
     *			</table>
     *			<figcaption>Caption text</figcaption>
     *		</figure>
     *
     * The converter has to take the `headingRows` attribute into consideration when allocating the `<tableRow>` elements
     * into the `<tbody>` and `<thead>` elements. Hence, we need two slots and need to define proper filter callbacks for them.
     *
     * Additionally, all elements other than `<tableRow>` should be placed outside the `<table>` tag.
     * In the example above, this will handle the table caption.
     *
     * Such a converter would look like this:
     *
     *		editor.conversion.for( 'downcast' ).elementToStructure( {
     *			model: {
     *				name: 'table',
     *				attributes: [ 'headingRows' ]
     *			},
     *			view: ( modelElement, conversionApi ) => {
     *				const { writer } = conversionApi;
     *
     *				const figureElement = writer.createContainerElement( 'figure', { class: 'table' } );
     *				const tableElement = writer.createContainerElement( 'table' );
     *
     *				writer.insert( writer.createPositionAt( figureElement, 0 ), tableElement );
     *
     *				const headingRows = modelElement.getAttribute( 'headingRows' ) || 0;
     *
     *				if ( headingRows > 0 ) {
     *					const tableHead = writer.createContainerElement( 'thead' );
     *
     *					const headSlot = writer.createSlot( node => node.is( 'element', 'tableRow' ) && node.index < headingRows );
     *
     *					writer.insert( writer.createPositionAt( tableElement, 'end' ), tableHead );
     *					writer.insert( writer.createPositionAt( tableHead, 0 ), headSlot );
     *				}
     *
     *				if ( headingRows < tableUtils.getRows( table ) ) {
     *					const tableBody = writer.createContainerElement( 'tbody' );
     *
     *					const bodySlot = writer.createSlot( node => node.is( 'element', 'tableRow' ) && node.index >= headingRows );
     *
     *					writer.insert( writer.createPositionAt( tableElement, 'end' ), tableBody );
     *					writer.insert( writer.createPositionAt( tableBody, 0 ), bodySlot );
     *				}
     *
     *				const restSlot = writer.createSlot( node => !node.is( 'element', 'tableRow' ) );
     *
     *				writer.insert( writer.createPositionAt( figureElement, 'end' ), restSlot );
     *
     *				return figureElement;
     *			}
     *		} );
     *
     * Note: The children of a model element that's being converted must be allocated in the same order in the view
     * in which they are placed in the model.
     *
     * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
     * to the conversion process.
     *
     * @method #elementToStructure
     * @param {Object} config Conversion configuration.
     * @param {String|Object} config.model The description or a name of the model element to convert.
     * @param {String} [config.model.name] The name of the model element to convert.
     * @param {String|Array.<String>} [config.model.attributes] The list of attribute names that should be consumed while creating
     * the view structure. Note that the view will be reconverted if any of the listed attributes will change.
     * @param {module:engine/conversion/downcasthelpers~StructureCreatorFunction} config.view A function
     * that takes the model element and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast
     * conversion API} as parameters and returns a view container element with slots for model child nodes to be converted into.
     * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
     */
    elementToStructure(config) {
        return this.add(downcastElementToStructure(config));
    }
    /**
     * Model attribute to view element conversion helper.
     *
     * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with
     * `"Foo"` as data and the `bold` attribute becomes `<strong>Foo</strong>` in the view.
     *
     *		editor.conversion.for( 'downcast' ).attributeToElement( {
     *			model: 'bold',
     *			view: 'strong'
     *		} );
     *
     *		editor.conversion.for( 'downcast' ).attributeToElement( {
     *			model: 'bold',
     *			view: 'b',
     *			converterPriority: 'high'
     *		} );
     *
     *		editor.conversion.for( 'downcast' ).attributeToElement( {
     *			model: 'invert',
     *			view: {
     *				name: 'span',
     *				classes: [ 'font-light', 'bg-dark' ]
     *			}
     *		} );
     *
     *		editor.conversion.for( 'downcast' ).attributeToElement( {
     *			model: {
     *				key: 'fontSize',
     *				values: [ 'big', 'small' ]
     *			},
     *			view: {
     *				big: {
     *					name: 'span',
     *					styles: {
     *						'font-size': '1.2em'
     *					}
     *				},
     *				small: {
     *					name: 'span',
     *					styles: {
     *						'font-size': '0.8em'
     *					}
     *				}
     *			}
     *		} );
     *
     *		editor.conversion.for( 'downcast' ).attributeToElement( {
     *			model: 'bold',
     *			view: ( modelAttributeValue, conversionApi ) => {
     *				const { writer } = conversionApi;
     *
     *				return writer.createAttributeElement( 'span', {
     *					style: 'font-weight:' + modelAttributeValue
     *				} );
     *			}
     *		} );
     *
     *		editor.conversion.for( 'downcast' ).attributeToElement( {
     *			model: {
     *				key: 'color',
     *				name: '$text'
     *			},
     *			view: ( modelAttributeValue, conversionApi ) => {
     *				const { writer } = conversionApi;
     *
     *				return writer.createAttributeElement( 'span', {
     *					style: 'color:' + modelAttributeValue
     *				} );
     *			}
     *		} );
     *
     * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
     * to the conversion process.
     *
     * @method #attributeToElement
     * @param {Object} config Conversion configuration.
     * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
     * of `String`s with possible values if the model attribute is an enumerable.
     * @param {module:engine/view/elementdefinition~ElementDefinition|Object|
     * module:engine/conversion/downcasthelpers~AttributeElementCreatorFunction} config.view A view element definition or a function
     * that takes the model attribute value and
     * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as parameters and returns a view
     * attribute element. If `config.model.values` is given, `config.view` should be an object assigning values from `config.model.values`
     * to view element definitions or functions.
     * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
     * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
     */
    attributeToElement(config) {
        return this.add(downcastAttributeToElement(config));
    }
    /**
     * Model attribute to view attribute conversion helper.
     *
     * This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example,
     * `<imageInline src='foo.jpg'></imageInline>` is converted to `<img src='foo.jpg'></img>`.
     *
     *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
     *			model: 'source',
     *			view: 'src'
     *		} );
     *
     *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
     *			model: 'source',
     *			view: 'href',
     *			converterPriority: 'high'
     *		} );
     *
     *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
     *			model: {
     *				name: 'imageInline',
     *				key: 'source'
     *			},
     *			view: 'src'
     *		} );
     *
     *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
     *			model: {
     *				name: 'styled',
     *				values: [ 'dark', 'light' ]
     *			},
     *			view: {
     *				dark: {
     *					key: 'class',
     *					value: [ 'styled', 'styled-dark' ]
     *				},
     *				light: {
     *					key: 'class',
     *					value: [ 'styled', 'styled-light' ]
     *				}
     *			}
     *		} );
     *
     *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
     *			model: 'styled',
     *			view: modelAttributeValue => ( {
     *				key: 'class',
     *				value: 'styled-' + modelAttributeValue
     *			} )
     *		} );
     *
     * **Note**: Downcasting to a style property requires providing `value` as an object:
     *
     *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
     *			model: 'lineHeight',
     *			view: modelAttributeValue => ( {
     *				key: 'style',
     *				value: {
     *					'line-height': modelAttributeValue,
     *					'border-bottom': '1px dotted #ba2'
     *				}
     *			} )
     *		} );
     *
     * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
     * to the conversion process.
     *
     * @method #attributeToAttribute
     * @param {Object} config Conversion configuration.
     * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
     * the attribute key, possible values and, optionally, an element name to convert from.
     * @param {String|Object|module:engine/conversion/downcasthelpers~AttributeCreatorFunction} config.view A view attribute key,
     * or a `{ key, value }` object or a function that takes the model attribute value and
     * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
     * as parameters and returns a `{ key, value }` object. If the `key` is `'class'`, the `value` can be a `String` or an
     * array of `String`s. If the `key` is `'style'`, the `value` is an object with key-value pairs. In other cases, `value` is a `String`.
     * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
     * `{ key, value }` objects or a functions.
     * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
     * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
     */
    attributeToAttribute(config) {
        return this.add(downcastAttributeToAttribute(config));
    }
    /**
     * Model marker to view element conversion helper.
     *
     * **Note**: This method should be used mainly for editing the downcast and it is recommended
     * to use the {@link #markerToData `#markerToData()`} helper instead.
     *
     * This helper may produce invalid HTML code (e.g. a span between table cells).
     * It should only be used when you are sure that the produced HTML will be semantically correct.
     *
     * This conversion results in creating a view element on the boundaries of the converted marker. If the converted marker
     * is collapsed, only one element is created. For example, a model marker set like this: `<paragraph>F[oo b]ar</paragraph>`
     * becomes `<p>F<span data-marker="search"></span>oo b<span data-marker="search"></span>ar</p>` in the view.
     *
     *		editor.conversion.for( 'editingDowncast' ).markerToElement( {
     *			model: 'search',
     *			view: 'marker-search'
     *		} );
     *
     *		editor.conversion.for( 'editingDowncast' ).markerToElement( {
     *			model: 'search',
     *			view: 'search-result',
     *			converterPriority: 'high'
     *		} );
     *
     *		editor.conversion.for( 'editingDowncast' ).markerToElement( {
     *			model: 'search',
     *			view: {
     *				name: 'span',
     *				attributes: {
     *					'data-marker': 'search'
     *				}
     *			}
     *		} );
     *
     *		editor.conversion.for( 'editingDowncast' ).markerToElement( {
     *			model: 'search',
     *			view: ( markerData, conversionApi ) => {
     *				const { writer } = conversionApi;
     *
     *				return writer.createUIElement( 'span', {
     *					'data-marker': 'search',
     *					'data-start': markerData.isOpening
     *				} );
     *			}
     *		} );
     *
     * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function
     * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
     * as a parameters and should return an instance of the
     * {@link module:engine/view/uielement~UIElement view UI element}. The `data` object and
     * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi `conversionApi`} are passed from
     * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
     * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` for
     * the marker end boundary element.
     *
     * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
     * to the conversion process.
     *
     * @method #markerToElement
     * @param {Object} config Conversion configuration.
     * @param {String} config.model The name of the model marker (or model marker group) to convert.
     * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function that
     * takes the model marker data and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
     * as a parameters and returns a view UI element.
     * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
     * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
     */
    markerToElement(config) {
        return this.add(downcastMarkerToElement(config));
    }
    /**
     * Model marker to highlight conversion helper.
     *
     * This conversion results in creating a highlight on view nodes. For this kind of conversion,
     * the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor} should be provided.
     *
     * For text nodes, a `<span>` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes
     * in the converted marker range. For example, a model marker set like this: `<paragraph>F[oo b]ar</paragraph>` becomes
     * `<p>F<span class="comment">oo b</span>ar</p>` in the view.
     *
     * {@link module:engine/view/containerelement~ContainerElement} may provide a custom way of handling highlight. Most often,
     * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in `<span>`).
     * For example, a model marker set like this:
     * `[<imageInline src="foo.jpg"></imageInline>]` becomes `<img src="foo.jpg" class="comment"></img>` in the view.
     *
     * For container elements, the conversion is two-step. While the converter processes the highlight descriptor and passes it
     * to a container element, it is the container element instance itself that applies values from the highlight descriptor.
     * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that.
     *
     *		editor.conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
     *
     *		editor.conversion.for( 'downcast' ).markerToHighlight( {
     *			model: 'comment',
     *			view: { classes: 'comment' },
     *			converterPriority: 'high'
     *		} );
     *
     *		editor.conversion.for( 'downcast' ).markerToHighlight( {
     *			model: 'comment',
     *			view: ( data, conversionApi ) => {
     *				// Assuming that the marker name is in a form of comment:commentType:commentId.
     *				const [ , commentType, commentId ] = data.markerName.split( ':' );
     *
     *				return {
     *					classes: [ 'comment', 'comment-' + commentType ],
     *					attributes: { 'data-comment-id': commentId }
     *				};
     *			}
     *		} );
     *
     * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function
     * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
     * as the parameters and should return a
     * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor}.
     * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
     *
     * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
     * to the conversion process.
     *
     * @method #markerToHighlight
     * @param {Object} config Conversion configuration.
     * @param {String} config.model The name of the model marker (or model marker group) to convert.
     * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} config.view A highlight descriptor
     * that will be used for highlighting or a function that takes the model marker data and
     * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
     * and returns a highlight descriptor.
     * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
     * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
     */
    markerToHighlight(config) {
        return this.add(downcastMarkerToHighlight(config));
    }
    /**
     * Model marker converter for data downcast.
     *
     * This conversion creates a representation for model marker boundaries in the view:
     *
     * * If the marker boundary is before or after a model element, a view attribute is set on a corresponding view element.
     * * In other cases, a view element with the specified tag name is inserted at the corresponding view position.
     *
     * Typically, the marker names use the `group:uniqueId:otherData` convention. For example: `comment:e34zfk9k2n459df53sjl34:zx32c`.
     * The default configuration for this conversion is that the first part is the `group` part and the rest of
     * the marker name becomes the `name` part.
     *
     * Tag and attribute names and values are generated from the marker name:
     *
     * * The templates for attributes are `data-[group]-start-before="[name]"`, `data-[group]-start-after="[name]"`,
     * `data-[group]-end-before="[name]"` and `data-[group]-end-after="[name]"`.
     * * The templates for view elements are `<[group]-start name="[name]">` and `<[group]-end name="[name]">`.
     *
     * Attributes mark whether the given marker's start or end boundary is before or after the given element.
     * The `data-[group]-start-before` and `data-[group]-end-after` attributes are favored.
     * The other two are used when the former two cannot be used.
     *
     * The conversion configuration can take a function that will generate different group and name parts.
     * If such a function is set as the `config.view` parameter, it is passed a marker name and it is expected to return an object with two
     * properties: `group` and `name`. If the function returns a falsy value, the conversion will not take place.
     *
     * Basic usage:
     *
     *		// Using the default conversion.
     *		// In this case, all markers with names starting with 'comment:' will be converted.
     *		// The `group` parameter will be set to `comment`.
     *		// The `name` parameter will be the rest of the marker name (without the `:`).
     *		editor.conversion.for( 'dataDowncast' ).markerToData( {
     *			model: 'comment'
     *		} );
     *
     * An example of a view that may be generated by this conversion (assuming a marker with the name `comment:commentId:uid` marked
     * by `[]`):
     *
     *		// Model:
     *		<paragraph>Foo[bar</paragraph>
     *		<imageBlock src="abc.jpg"></imageBlock>]
     *
     *		// View:
     *		<p>Foo<comment-start name="commentId:uid"></comment-start>bar</p>
     *		<figure data-comment-end-after="commentId:uid" class="image"><img src="abc.jpg" /></figure>
     *
     * In the example above, the comment starts before "bar" and ends after the image.
     *
     * If the `name` part is empty, the following view may be generated:
     *
     *		<p>Foo <myMarker-start></myMarker-start>bar</p>
     *		<figure data-myMarker-end-after="" class="image"><img src="abc.jpg" /></figure>
     *
     * **Note:** A situation where some markers have the `name` part and some do not, is incorrect and should be avoided.
     *
     * Examples where `data-group-start-after` and `data-group-end-before` are used:
     *
     *		// Model:
     *		<blockQuote>[]<paragraph>Foo</paragraph></blockQuote>
     *
     *		// View:
     *		<blockquote><p data-group-end-before="name" data-group-start-before="name">Foo</p></blockquote>
     *
     * Similarly, when a marker is collapsed after the last element:
     *
     *		// Model:
     *		<blockQuote><paragraph>Foo</paragraph>[]</blockQuote>
     *
     *		// View:
     *		<blockquote><p data-group-end-after="name" data-group-start-after="name">Foo</p></blockquote>
     *
     * When there are multiple markers from the same group stored in the same attribute of the same element, their
     * name parts are put together in the attribute value, for example: `data-group-start-before="name1,name2,name3"`.
     *
     * Other examples of usage:
     *
     *		// Using a custom function which is the same as the default conversion:
     *		editor.conversion.for( 'dataDowncast' ).markerToData( {
     *			model: 'comment'
     *			view: markerName => ( {
     *				group: 'comment',
     *				name: markerName.substr( 8 ) // Removes 'comment:' part.
     *			} )
     *		} );
     *
     *		// Using the converter priority:
     *		editor.conversion.for( 'dataDowncast' ).markerToData( {
     *			model: 'comment'
     *			view: markerName => ( {
     *				group: 'comment',
     *				name: markerName.substr( 8 ) // Removes 'comment:' part.
     *			} ),
     *			converterPriority: 'high'
     *		} );
     *
     * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline.
     *
     * See the {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} API guide to learn how to
     * add a converter to the conversion process.
     *
     * @method #markerToData
     * @param {Object} config Conversion configuration.
     * @param {String} config.model The name of the model marker (or the model marker group) to convert.
     * @param {Function} [config.view] A function that takes the model marker name and
     * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as the parameters
     * and returns an object with the `group` and `name` properties.
     * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
     * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
     */
    markerToData(config) {
        return this.add(downcastMarkerToData(config));
    }
}
/**
 * Function factory that creates a default downcast converter for text insertion changes.
 *
 * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
 * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
 *
 *		modelDispatcher.on( 'insert:$text', insertText() );
 *
 * @returns {Function} Insert text event converter.
 */
function insertText() {
    return (evt, data, conversionApi) => {
        if (!conversionApi.consumable.consume(data.item, evt.name)) {
            return;
        }
        const viewWriter = conversionApi.writer;
        const viewPosition = conversionApi.mapper.toViewPosition(data.range.start);
        const viewText = viewWriter.createText(data.item.data);
        viewWriter.insert(viewPosition, viewText);
    };
}
/**
 * Function factory that creates a default downcast converter for triggering attributes and children conversion.
 *
 * @returns {Function} The converter.
 */
function insertAttributesAndChildren() {
    return (evt, data, conversionApi) => {
        conversionApi.convertAttributes(data.item);
        // Start converting children of the current item.
        // In case of reconversion children were already re-inserted or converted separately.
        if (!data.reconversion && data.item.is('element') && !data.item.isEmpty) {
            conversionApi.convertChildren(data.item);
        }
    };
}
/**
 * Function factory that creates a default downcast converter for node remove changes.
 *
 *		modelDispatcher.on( 'remove', remove() );
 *
 * @returns {Function} Remove event converter.
 */
function remove() {
    return (evt, data, conversionApi) => {
        // Find the view range start position by mapping the model position at which the remove happened.
        const viewStart = conversionApi.mapper.toViewPosition(data.position);
        const modelEnd = data.position.getShiftedBy(data.length);
        const viewEnd = conversionApi.mapper.toViewPosition(modelEnd, { isPhantom: true });
        const viewRange = conversionApi.writer.createRange(viewStart, viewEnd);
        // Trim the range to remove in case some UI elements are on the view range boundaries.
        const removed = conversionApi.writer.remove(viewRange.getTrimmed());
        // After the range is removed, unbind all view elements from the model.
        // Range inside view document fragment is used to unbind deeply.
        for (const child of conversionApi.writer.createRangeIn(removed).getItems()) {
            conversionApi.mapper.unbindViewElement(child, { defer: true });
        }
    };
}
/**
 * Creates a `<span>` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from the information
 * provided by the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor} object. If the priority
 * is not provided in the descriptor, the default priority will be used.
 *
 * @param {module:engine/view/downcastwriter~DowncastWriter} writer
 * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor
 * @returns {module:engine/view/attributeelement~AttributeElement}
 */
function createViewElementFromHighlightDescriptor(writer, descriptor) {
    const viewElement = writer.createAttributeElement('span', descriptor.attributes);
    if (descriptor.classes) {
        viewElement._addClass(descriptor.classes);
    }
    if (typeof descriptor.priority === 'number') {
        viewElement._priority = descriptor.priority;
    }
    viewElement._id = descriptor.id;
    return viewElement;
}
/**
 * Function factory that creates a converter which converts a non-collapsed {@link module:engine/model/selection~Selection model selection}
 * to a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
 * value from the `consumable` object and maps model positions from the selection to view positions.
 *
 *		modelDispatcher.on( 'selection', convertRangeSelection() );
 *
 * @returns {Function} Selection converter.
 */
function convertRangeSelection() {
    return (evt, data, conversionApi) => {
        const selection = data.selection;
        if (selection.isCollapsed) {
            return;
        }
        if (!conversionApi.consumable.consume(selection, 'selection')) {
            return;
        }
        const viewRanges = [];
        for (const range of selection.getRanges()) {
            viewRanges.push(conversionApi.mapper.toViewRange(range));
        }
        conversionApi.writer.setSelection(viewRanges, { backward: selection.isBackward });
    };
}
/**
 * Function factory that creates a converter which converts a collapsed {@link module:engine/model/selection~Selection model selection} to
 * a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
 * value from the `consumable` object, maps the model selection position to the view position and breaks
 * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position.
 *
 *		modelDispatcher.on( 'selection', convertCollapsedSelection() );
 *
 * An example of the view state before and after converting the collapsed selection:
 *
 *		   <p><strong>f^oo<strong>bar</p>
 *		-> <p><strong>f</strong>^<strong>oo</strong>bar</p>
 *
 * By breaking attribute elements like `<strong>`, the selection is in a correct element. Then, when the selection attribute is
 * converted, broken attributes might be merged again, or the position where the selection is may be wrapped
 * with different, appropriate attribute elements.
 *
 * See also {@link module:engine/conversion/downcasthelpers~clearAttributes} which does a clean-up
 * by merging attributes.
 *
 * @returns {Function} Selection converter.
 */
function convertCollapsedSelection() {
    return (evt, data, conversionApi) => {
        const selection = data.selection;
        if (!selection.isCollapsed) {
            return;
        }
        if (!conversionApi.consumable.consume(selection, 'selection')) {
            return;
        }
        const viewWriter = conversionApi.writer;
        const modelPosition = selection.getFirstPosition();
        const viewPosition = conversionApi.mapper.toViewPosition(modelPosition);
        const brokenPosition = viewWriter.breakAttributes(viewPosition);
        viewWriter.setSelection(brokenPosition);
    };
}
/**
 * Function factory that creates a converter which clears artifacts after the previous
 * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty
 * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merges sibling attributes at all start and end
 * positions of all ranges.
 *
 *		   <p><strong>^</strong></p>
 *		-> <p>^</p>
 *
 *		   <p><strong>foo</strong>^<strong>bar</strong>bar</p>
 *		-> <p><strong>foo^bar<strong>bar</p>
 *
 *		   <p><strong>foo</strong><em>^</em><strong>bar</strong>bar</p>
 *		-> <p><strong>foo^bar<strong>bar</p>
 *
 * This listener should be assigned before any converter for the new selection:
 *
 *		modelDispatcher.on( 'selection', clearAttributes() );
 *
 * See {@link module:engine/conversion/downcasthelpers~convertCollapsedSelection}
 * which does the opposite by breaking attributes in the selection position.
 *
 * @returns {Function} Selection converter.
 */
function clearAttributes() {
    return (evt, data, conversionApi) => {
        const viewWriter = conversionApi.writer;
        const viewSelection = viewWriter.document.selection;
        for (const range of viewSelection.getRanges()) {
            // Not collapsed selection should not have artifacts.
            if (range.isCollapsed) {
                // Position might be in the node removed by the view writer.
                if (range.end.parent.isAttached()) {
                    conversionApi.writer.mergeAttributes(range.start);
                }
            }
        }
        viewWriter.setSelection(null);
    };
}
/**
 * Function factory that creates a converter which converts the set/change/remove attribute changes from the model to the view.
 * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the
 * selection will be put inside it.
 *
 * Attributes from the model are converted to a view element that will be wrapping these view nodes that are bound to
 * model elements having the given attribute. This is useful for attributes like `bold` that may be set on text nodes in the model
 * but are represented as an element in the view:
 *
 *		[paragraph]              MODEL ====> VIEW        <p>
 *			|- a {bold: true}                             |- <b>
 *			|- b {bold: true}                             |   |- ab
 *			|- c                                          |- c
 *
 * Passed `Function` will be provided with the attribute value and then all the parameters of the
 * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute` event}.
 * It is expected that the function returns an {@link module:engine/view/element~Element}.
 * The result of the function will be the wrapping element.
 * When the provided `Function` does not return any element, no conversion will take place.
 *
 * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
 * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
 *
 *		modelDispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, { writer } ) => {
 *			return writer.createAttributeElement( 'strong' );
 *		} );
 *
 * @protected
 * @param {Function} elementCreator Function returning a view element that will be used for wrapping.
 * @returns {Function} Set/change attribute converter.
 */
function wrap(elementCreator) {
    return (evt, data, conversionApi) => {
        if (!conversionApi.consumable.test(data.item, evt.name)) {
            return;
        }
        // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
        // or the attribute was removed.
        const oldViewElement = elementCreator(data.attributeOldValue, conversionApi, data);
        // Create node to wrap with.
        const newViewElement = elementCreator(data.attributeNewValue, conversionApi, data);
        if (!oldViewElement && !newViewElement) {
            return;
        }
        conversionApi.consumable.consume(data.item, evt.name);
        const viewWriter = conversionApi.writer;
        const viewSelection = viewWriter.document.selection;
        if (data.item instanceof Selection || data.item instanceof DocumentSelection) {
            // Selection attribute conversion.
            viewWriter.wrap(viewSelection.getFirstRange(), newViewElement);
        }
        else {
            // Node attribute conversion.
            let viewRange = conversionApi.mapper.toViewRange(data.range);
            // First, unwrap the range from current wrapper.
            if (data.attributeOldValue !== null && oldViewElement) {
                viewRange = viewWriter.unwrap(viewRange, oldViewElement);
            }
            if (data.attributeNewValue !== null && newViewElement) {
                viewWriter.wrap(viewRange, newViewElement);
            }
        }
    };
}
/**
 * Function factory that creates a converter which converts node insertion changes from the model to the view.
 * The function passed will be provided with all the parameters of the dispatcher's
 * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}.
 * It is expected that the function returns an {@link module:engine/view/element~Element}.
 * The result of the function will be inserted into the view.
 *
 * The converter automatically consumes the corresponding value from the consumables list and binds the model and view elements.
 *
 *		downcastDispatcher.on(
 *			'insert:myElem',
 *			insertElement( ( modelItem, { writer } ) => {
 *				const text = writer.createText( 'myText' );
 *				const myElem = writer.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text );
 *
 *				// Do something fancy with `myElem` using `modelItem` or other parameters.
 *
 *				return myElem;
 *			}
 *		) );
 *
 * @protected
 * @param {Function} elementCreator Function returning a view element, which will be inserted.
 * @param {module:engine/conversion/downcasthelpers~ConsumerFunction} [consumer] Function defining element consumption process.
 * By default this function just consume passed item insertion.
 * @returns {Function} Insert element event converter.
 */
function insertElement(elementCreator, consumer = defaultConsumer) {
    return (evt, data, conversionApi) => {
        if (!consumer(data.item, conversionApi.consumable, { preflight: true })) {
            return;
        }
        const viewElement = elementCreator(data.item, conversionApi, data);
        if (!viewElement) {
            return;
        }
        // Consume an element insertion and all present attributes that are specified as a reconversion triggers.
        consumer(data.item, conversionApi.consumable);
        const viewPosition = conversionApi.mapper.toViewPosition(data.range.start);
        conversionApi.mapper.bindElements(data.item, viewElement);
        conversionApi.writer.insert(viewPosition, viewElement);
        // Convert attributes before converting children.
        conversionApi.convertAttributes(data.item);
        // Convert children or reinsert previous view elements.
        reinsertOrConvertNodes(viewElement, data.item.getChildren(), conversionApi, { reconversion: data.reconversion });
    };
}
/**
 * Function factory that creates a converter which converts a single model node insertion to a view structure.
 *
 * It is expected that the passed element creator function returns an {@link module:engine/view/element~Element} with attached slots
 * created with `writer.createSlot()` to indicate where child nodes should be converted.
 *
 * @see module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
 *
 * @protected
 * @param {module:engine/conversion/downcasthelpers~StructureCreatorFunction} elementCreator Function returning a view structure,
 * which will be inserted.
 * @param {module:engine/conversion/downcasthelpers~ConsumerFunction} consumer A callback that is expected to consume all the consumables
 * that were used by the element creator.
 * @returns {Function} Insert element event converter.
*/
function insertStructure(elementCreator, consumer) {
    return (evt, data, conversionApi) => {
        if (!consumer(data.item, conversionApi.consumable, { preflight: true })) {
            return;
        }
        const slotsMap = new Map();
        conversionApi.writer._registerSlotFactory(createSlotFactory(data.item, slotsMap, conversionApi));
        // View creation.
        const viewElement = elementCreator(data.item, conversionApi, data);
        conversionApi.writer._clearSlotFactory();
        if (!viewElement) {
            return;
        }
        // Check if all children are covered by slots and there is no child that landed in multiple slots.
        validateSlotsChildren(data.item, slotsMap, conversionApi);
        // Consume an element insertion and all present attributes that are specified as a reconversion triggers.
        consumer(data.item, conversionApi.consumable);
        const viewPosition = conversionApi.mapper.toViewPosition(data.range.start);
        conversionApi.mapper.bindElements(data.item, viewElement);
        conversionApi.writer.insert(viewPosition, viewElement);
        // Convert attributes before converting children.
        conversionApi.convertAttributes(data.item);
        // Fill view slots with previous view elements or create new ones.
        fillSlots(viewElement, slotsMap, conversionApi, { reconversion: data.reconversion });
    };
}
/**
 * Function factory that creates a converter which converts marker adding change to the
 * {@link module:engine/view/uielement~UIElement view UI element}.
 *
 * The view UI element that will be added to the view depends on the passed parameter. See {@link ~insertElement}.
 * In case of a non-collapsed range, the UI element will not wrap nodes but separate elements will be placed at the beginning
 * and at the end of the range.
 *
 * This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
 *
 * @protected
 * @param {module:engine/view/uielement~UIElement|Function} elementCreator A view UI element or a function returning the view element
 * that will be inserted.
 * @returns {Function} Insert element event converter.
 */
function insertUIElement(elementCreator) {
    return (evt, data, conversionApi) => {
        // Create two view elements. One will be inserted at the beginning of marker, one at the end.
        // If marker is collapsed, only "opening" element will be inserted.
        data.isOpening = true;
        const viewStartElement = elementCreator(data, conversionApi);
        data.isOpening = false;
        const viewEndElement = elementCreator(data, conversionApi);
        if (!viewStartElement || !viewEndElement) {
            return;
        }
        const markerRange = data.markerRange;
        // Marker that is collapsed has consumable build differently that non-collapsed one.
        // For more information see `addMarker` event description.
        // If marker's range is collapsed - check if it can be consumed.
        if (markerRange.isCollapsed && !conversionApi.consumable.consume(markerRange, evt.name)) {
            return;
        }
        // If marker's range is not collapsed - consume all items inside.
        for (const value of markerRange) {
            if (!conversionApi.consumable.consume(value.item, evt.name)) {
                return;
            }
        }
        const mapper = conversionApi.mapper;
        const viewWriter = conversionApi.writer;
        // Add "opening" element.
        viewWriter.insert(mapper.toViewPosition(markerRange.start), viewStartElement);
        conversionApi.mapper.bindElementToMarker(viewStartElement, data.markerName);
        // Add "closing" element only if range is not collapsed.
        if (!markerRange.isCollapsed) {
            viewWriter.insert(mapper.toViewPosition(markerRange.end), viewEndElement);
            conversionApi.mapper.bindElementToMarker(viewEndElement, data.markerName);
        }
        evt.stop();
    };
}
// Function factory that returns a default downcast converter for removing a {@link module:engine/view/uielement~UIElement UI element}
// based on marker remove change.
//
// This converter unbinds elements from the marker name.
//
// @returns {Function} Removed UI element converter.
function removeUIElement() {
    return (evt, data, conversionApi) => {
        const elements = conversionApi.mapper.markerNameToElements(data.markerName);
        if (!elements) {
            return;
        }
        for (const element of elements) {
            conversionApi.mapper.unbindElementFromMarkerName(element, data.markerName);
            conversionApi.writer.clear(conversionApi.writer.createRangeOn(element), element);
        }
        conversionApi.writer.clearClonedElementsGroup(data.markerName);
        evt.stop();
    };
}
// Function factory that creates a default converter for model markers.
//
// See {@link DowncastHelpers#markerToData} for more information what type of view is generated.
//
// This converter binds created UI elements and affected view elements with the marker name
// using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
//
// @returns {Function} Add marker converter.
function insertMarkerData(viewCreator) {
    return (evt, data, conversionApi) => {
        const viewMarkerData = viewCreator(data.markerName, conversionApi);
        if (!viewMarkerData) {
            return;
        }
        const markerRange = data.markerRange;
        if (!conversionApi.consumable.consume(markerRange, evt.name)) {
            return;
        }
        // Adding closing data first to keep the proper order in the view.
        handleMarkerBoundary(markerRange, false, conversionApi, data, viewMarkerData);
        handleMarkerBoundary(markerRange, true, conversionApi, data, viewMarkerData);
        evt.stop();
    };
}
// Helper function for `insertMarkerData()` that marks a marker boundary at the beginning or end of given `range`.
function handleMarkerBoundary(range, isStart, conversionApi, data, viewMarkerData) {
    const modelPosition = isStart ? range.start : range.end;
    const elementAfter = modelPosition.nodeAfter && modelPosition.nodeAfter.is('element') ? modelPosition.nodeAfter : null;
    const elementBefore = modelPosition.nodeBefore && modelPosition.nodeBefore.is('element') ? modelPosition.nodeBefore : null;
    if (elementAfter || elementBefore) {
        let modelElement;
        let isBefore;
        // If possible, we want to add `data-group-start-before` and `data-group-end-after` attributes.
        if (isStart && elementAfter || !isStart && !elementBefore) {
            // [<elementAfter>...</elementAfter> -> <elementAfter data-group-start-before="...">...</elementAfter>
            // <parent>]<elementAfter> -> <parent><elementAfter data-group-end-before="...">
            modelElement = elementAfter;
            isBefore = true;
        }
        else {
            // <elementBefore>...</elementBefore>] -> <elementBefore data-group-end-after="...">...</elementBefore>
            // </elementBefore>[</parent> -> </elementBefore data-group-start-after="..."></parent>
            modelElement = elementBefore;
            isBefore = false;
        }
        const viewElement = conversionApi.mapper.toViewElement(modelElement);
        // In rare circumstances, the model element may be not mapped to any view element and that would cause an error.
        // One of those situations is a soft break inside code block.
        if (viewElement) {
            insertMarkerAsAttribute(viewElement, isStart, isBefore, conversionApi, data, viewMarkerData);
            return;
        }
    }
    const viewPosition = conversionApi.mapper.toViewPosition(modelPosition);
    insertMarkerAsElement(viewPosition, isStart, conversionApi, data, viewMarkerData);
}
// Helper function for `insertMarkerData()` that marks a marker boundary in the view as an attribute on a view element.
function insertMarkerAsAttribute(viewElement, isStart, isBefore, conversionApi, data, viewMarkerData) {
    const attributeName = `data-${viewMarkerData.group}-${isStart ? 'start' : 'end'}-${isBefore ? 'before' : 'after'}`;
    const markerNames = viewElement.hasAttribute(attributeName) ? viewElement.getAttribute(attributeName).split(',') : [];
    // Adding marker name at the beginning to have the same order in the attribute as there is with marker elements.
    markerNames.unshift(viewMarkerData.name);
    conversionApi.writer.setAttribute(attributeName, markerNames.join(','), viewElement);
    conversionApi.mapper.bindElementToMarker(viewElement, data.markerName);
}
// Helper function for `insertMarkerData()` that marks a marker boundary in the view as a separate view ui element.
function insertMarkerAsElement(position, isStart, conversionApi, data, viewMarkerData) {
    const viewElementName = `${viewMarkerData.group}-${isStart ? 'start' : 'end'}`;
    const attrs = viewMarkerData.name ? { 'name': viewMarkerData.name } : null;
    const viewElement = conversionApi.writer.createUIElement(viewElementName, attrs);
    conversionApi.writer.insert(position, viewElement);
    conversionApi.mapper.bindElementToMarker(viewElement, data.markerName);
}
// Function factory that creates a converter for removing a model marker data added by the {@link #insertMarkerData} converter.
//
// @returns {Function} Remove marker converter.
function removeMarkerData(viewCreator) {
    return (evt, data, conversionApi) => {
        const viewData = viewCreator(data.markerName, conversionApi);
        if (!viewData) {
            return;
        }
        const elements = conversionApi.mapper.markerNameToElements(data.markerName);
        if (!elements) {
            return;
        }
        for (const element of elements) {
            conversionApi.mapper.unbindElementFromMarkerName(element, data.markerName);
            if (element.is('containerElement')) {
                removeMarkerFromAttribute(`data-${viewData.group}-start-before`, element);
                removeMarkerFromAttribute(`data-${viewData.group}-start-after`, element);
                removeMarkerFromAttribute(`data-${viewData.group}-end-before`, element);
                removeMarkerFromAttribute(`data-${viewData.group}-end-after`, element);
            }
            else {
                conversionApi.writer.clear(conversionApi.writer.createRangeOn(element), element);
            }
        }
        conversionApi.writer.clearClonedElementsGroup(data.markerName);
        evt.stop();
        function removeMarkerFromAttribute(attributeName, element) {
            if (element.hasAttribute(attributeName)) {
                const markerNames = new Set(element.getAttribute(attributeName).split(','));
                markerNames.delete(viewData.name);
                if (markerNames.size == 0) {
                    conversionApi.writer.removeAttribute(attributeName, element);
                }
                else {
                    conversionApi.writer.setAttribute(attributeName, Array.from(markerNames).join(','), element);
                }
            }
        }
    };
}
// Function factory that creates a converter which converts the set/change/remove attribute changes from the model to the view.
//
// Attributes from the 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/remove. If not provided, model attributes will be converted to view element
// attributes on a one-to-one basis.
//
// *Note:** The provided attribute creator should always return the same `key` for a given attribute from the model.
//
// The converter automatically consumes the corresponding value from the consumables list and stops the event (see
// {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
//
//		modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( value, data ) => {
//			// Change attribute key from `customAttr` to `class` in the 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 the key-value pair.
//			return { key, value };
//		} ) );
//
// @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
// represent the attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
// The function is passed the model attribute value as the first parameter and additional data about the change as the second parameter.
// @returns {Function} Set/change attribute converter.
function changeAttribute(attributeCreator) {
    return (evt, data, conversionApi) => {
        if (!conversionApi.consumable.test(data.item, evt.name)) {
            return;
        }
        const oldAttribute = attributeCreator(data.attributeOldValue, conversionApi, data);
        const newAttribute = attributeCreator(data.attributeNewValue, conversionApi, data);
        if (!oldAttribute && !newAttribute) {
            return;
        }
        conversionApi.consumable.consume(data.item, evt.name);
        const viewElement = conversionApi.mapper.toViewElement(data.item);
        const viewWriter = conversionApi.writer;
        // If model item cannot be mapped to a view element, it means item is not an `Element` instance but a `TextProxy` node.
        // Only elements can have attributes in a view so do not proceed for anything else (#1587).
        if (!viewElement) {
            /**
             * This error occurs when a {@link module:engine/model/textproxy~TextProxy text node's} attribute is to be downcasted
             * by an {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `Attribute to Attribute converter`}.
             * In most cases it is caused by converters misconfiguration when only "generic" converter is defined:
             *
             *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
             *			model: 'attribute-name',
             *			view: 'attribute-name'
             *		} ) );
             *
             * and given attribute is used on text node, for example:
             *
             *		model.change( writer => {
             *			writer.insertText( 'Foo', { 'attribute-name': 'bar' }, parent, 0 );
             *		} );
             *
             * In such cases, to convert the same attribute for both {@link module:engine/model/element~Element}
             * and {@link module:engine/model/textproxy~TextProxy `Text`} nodes, text specific
             * {@link module:engine/conversion/conversion~Conversion#attributeToElement `Attribute to Element converter`}
             * with higher {@link module:utils/priorities~PriorityString priority} must also be defined:
             *
             *		editor.conversion.for( 'downcast' ).attributeToElement( {
             *			model: {
             *				key: 'attribute-name',
             *				name: '$text'
             *			},
             *			view: ( value, { writer } ) => {
             *				return writer.createAttributeElement( 'span', { 'attribute-name': value } );
             *			},
             *			converterPriority: 'high'
             *		} ) );
             *
             * @error conversion-attribute-to-attribute-on-text
             */
            throw new CKEditorError('conversion-attribute-to-attribute-on-text', conversionApi.dispatcher, data);
        }
        // First remove the old attribute if there was one.
        if (data.attributeOldValue !== null && oldAttribute) {
            if (oldAttribute.key == 'class') {
                const classes = toArray(oldAttribute.value);
                for (const className of classes) {
                    viewWriter.removeClass(className, viewElement);
                }
            }
            else if (oldAttribute.key == 'style') {
                const keys = Object.keys(oldAttribute.value);
                for (const key of keys) {
                    viewWriter.removeStyle(key, viewElement);
                }
            }
            else {
                viewWriter.removeAttribute(oldAttribute.key, viewElement);
            }
        }
        // Then set the new attribute.
        if (data.attributeNewValue !== null && newAttribute) {
            if (newAttribute.key == 'class') {
                const classes = toArray(newAttribute.value);
                for (const className of classes) {
                    viewWriter.addClass(className, viewElement);
                }
            }
            else if (newAttribute.key == 'style') {
                const keys = Object.keys(newAttribute.value);
                for (const key of keys) {
                    viewWriter.setStyle(key, newAttribute.value[key], viewElement);
                }
            }
            else {
                viewWriter.setAttribute(newAttribute.key, newAttribute.value, viewElement);
            }
        }
    };
}
// Function factory that creates a converter which converts the text inside marker's range. The converter wraps the text with
// {@link module:engine/view/attributeelement~AttributeElement} created from the provided descriptor.
// See {link module:engine/conversion/downcasthelpers~createViewElementFromHighlightDescriptor}.
//
// It can also be used to convert the selection that is inside a marker. In that case, an empty attribute element will be
// created and the selection will be put inside it.
//
// If the highlight descriptor does not provide the `priority` property, `10` will be used.
//
// If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
//
// This converter binds the created {@link module:engine/view/attributeelement~AttributeElement attribute elemens} with the marker name
// using the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
//
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
// @returns {Function}
function highlightText(highlightDescriptor) {
    return (evt, data, conversionApi) => {
        if (!data.item) {
            return;
        }
        if (!(data.item instanceof Selection || data.item instanceof DocumentSelection) && !data.item.is('$textProxy')) {
            return;
        }
        const descriptor = prepareDescriptor(highlightDescriptor, data, conversionApi);
        if (!descriptor) {
            return;
        }
        if (!conversionApi.consumable.consume(data.item, evt.name)) {
            return;
        }
        const viewWriter = conversionApi.writer;
        const viewElement = createViewElementFromHighlightDescriptor(viewWriter, descriptor);
        const viewSelection = viewWriter.document.selection;
        if (data.item instanceof Selection || data.item instanceof DocumentSelection) {
            viewWriter.wrap(viewSelection.getFirstRange(), viewElement);
        }
        else {
            const viewRange = conversionApi.mapper.toViewRange(data.range);
            const rangeAfterWrap = viewWriter.wrap(viewRange, viewElement);
            for (const element of rangeAfterWrap.getItems()) {
                if (element.is('attributeElement') && element.isSimilar(viewElement)) {
                    conversionApi.mapper.bindElementToMarker(element, data.markerName);
                    // One attribute element is enough, because all of them are bound together by the view writer.
                    // Mapper uses this binding to get all the elements no matter how many of them are registered in the mapper.
                    break;
                }
            }
        }
    };
}
// Converter function factory. It creates a function which applies the marker's highlight to an element inside the marker's range.
//
// The converter checks if an element has the `addHighlight` function stored as a
// {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight.
// In such case the converter will consume all element's children, assuming that they were handled by the element itself.
//
// When the `addHighlight` custom property is not present, the element is not converted in any special way.
// This means that converters will proceed to convert the element's child nodes.
//
// If the highlight descriptor does not provide the `priority` property, `10` will be used.
//
// If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
//
// This converter binds altered {@link module:engine/view/containerelement~ContainerElement container elements} with the marker name using
// the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
//
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
// @returns {Function}
function highlightElement(highlightDescriptor) {
    return (evt, data, conversionApi) => {
        if (!data.item) {
            return;
        }
        if (!(data.item instanceof Element)) {
            return;
        }
        const descriptor = prepareDescriptor(highlightDescriptor, data, conversionApi);
        if (!descriptor) {
            return;
        }
        if (!conversionApi.consumable.test(data.item, evt.name)) {
            return;
        }
        const viewElement = conversionApi.mapper.toViewElement(data.item);
        if (viewElement && viewElement.getCustomProperty('addHighlight')) {
            // Consume element itself.
            conversionApi.consumable.consume(data.item, evt.name);
            // Consume all children nodes.
            for (const value of Range._createIn(data.item)) {
                conversionApi.consumable.consume(value.item, evt.name);
            }
            const addHighlightCallback = viewElement.getCustomProperty('addHighlight');
            addHighlightCallback(viewElement, descriptor, conversionApi.writer);
            conversionApi.mapper.bindElementToMarker(viewElement, data.markerName);
        }
    };
}
// Function factory that creates a converter which converts the removing model marker to the view.
//
// Both text nodes and elements are handled by this converter but they are handled a bit differently.
//
// Text nodes are unwrapped using the {@link module:engine/view/attributeelement~AttributeElement attribute element} created from the
// provided highlight descriptor. See {link module:engine/conversion/downcasthelpers~HighlightDescriptor}.
//
// For elements, the converter checks if an element has the `removeHighlight` function stored as a
// {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight.
// In such case, the children of that element will not be converted.
//
// When `removeHighlight` is not present, the element is not converted in any special way.
// The converter will proceed to convert the element's child nodes instead.
//
// If the highlight descriptor does not provide the `priority` property, `10` will be used.
//
// If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
//
// This converter unbinds elements from the marker name.
//
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
// @returns {Function}
function removeHighlight$1(highlightDescriptor) {
    return (evt, data, conversionApi) => {
        // This conversion makes sense only for non-collapsed range.
        if (data.markerRange.isCollapsed) {
            return;
        }
        const descriptor = prepareDescriptor(highlightDescriptor, data, conversionApi);
        if (!descriptor) {
            return;
        }
        // View element that will be used to unwrap `AttributeElement`s.
        const viewHighlightElement = createViewElementFromHighlightDescriptor(conversionApi.writer, descriptor);
        // Get all elements bound with given marker name.
        const elements = conversionApi.mapper.markerNameToElements(data.markerName);
        if (!elements) {
            return;
        }
        for (const element of elements) {
            conversionApi.mapper.unbindElementFromMarkerName(element, data.markerName);
            if (element.is('attributeElement')) {
                conversionApi.writer.unwrap(conversionApi.writer.createRangeOn(element), viewHighlightElement);
            }
            else {
                // if element.is( 'containerElement' ).
                const removeHighlightCallback = element.getCustomProperty('removeHighlight');
                removeHighlightCallback(element, descriptor.id, conversionApi.writer);
            }
        }
        conversionApi.writer.clearClonedElementsGroup(data.markerName);
        evt.stop();
    };
}
// Model element to view element conversion helper.
//
// See {@link ~DowncastHelpers#elementToElement `.elementToElement()` downcast helper} for examples and config params description.
//
// @param {Object} config Conversion configuration.
// @param {String|Object} config.model The description or a name of the model element to convert.
// @param {String|Array.<String>} [config.model.attributes] List of attributes triggering element reconversion.
// @param {Boolean} [config.model.children] Should reconvert element if the list of model child nodes changed.
// @param {module:engine/view/elementdefinition~ElementDefinition|module:engine/conversion/downcasthelpers~ElementCreatorFunction}
// config.view
// @returns {Function} Conversion helper.
function downcastElementToElement(config) {
    const model = normalizeModelElementConfig(config.model);
    const view = normalizeToElementConfig(config.view, 'container');
    // Trigger reconversion on children list change if element is a subject to any reconversion.
    // This is required to be able to trigger Differ#refreshItem() on a direct child of the reconverted element.
    if (model.attributes.length) {
        model.children = true;
    }
    return (dispatcher) => {
        dispatcher.on(`insert:${model.name}`, insertElement(view, createConsumer(model)), { priority: config.converterPriority || 'normal' });
        if (model.children || model.attributes.length) {
            dispatcher.on('reduceChanges', createChangeReducer(model), { priority: 'low' });
        }
    };
}
// Model element to view structure conversion helper.
//
// See {@link ~DowncastHelpers#elementToStructure `.elementToStructure()` downcast helper} for examples and config params description.
//
// @param {Object} config Conversion configuration.
// @param {String|Object} config.model
// @param {String} [config.model.name]
// @param {Array.<String>} [config.model.attributes]
// @param {module:engine/conversion/downcasthelpers~StructureCreatorFunction} config.view
// @returns {Function} Conversion helper.
function downcastElementToStructure(config) {
    const model = normalizeModelElementConfig(config.model);
    const view = normalizeToElementConfig(config.view, 'container');
    // Trigger reconversion on children list change because it always needs to use slots to put children in proper places.
    // This is required to be able to trigger Differ#refreshItem() on a direct child of the reconverted element.
    model.children = true;
    return (dispatcher) => {
        if (dispatcher._conversionApi.schema.checkChild(model.name, '$text')) {
            /**
             * This error occurs when a {@link module:engine/model/element~Element model element} is downcasted
             * via {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure} helper but the element was
             * allowed to host `$text` by the {@link module:engine/model/schema~Schema model schema}.
             *
             * For instance, this may be the result of `myElement` allowing the content of
             * {@glink framework/guides/deep-dive/schema#generic-items `$block`} in its schema definition:
             *
             *		// Element definition in schema.
             *		schema.register( 'myElement', {
             *			allowContentOf: '$block',
             *
             *			// ...
             *		} );
             *
             *		// ...
             *
             *		// Conversion of myElement with the use of elementToStructure().
             *		editor.conversion.for( 'downcast' ).elementToStructure( {
             *			model: 'myElement',
             *			view: ( modelElement, { writer } ) => {
             *				// ...
             *			}
             *		} );
             *
             * In such case, {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} helper
             * can be used instead to get around this problem:
             *
             *		editor.conversion.for( 'downcast' ).elementToElement( {
             *			model: 'myElement',
             *			view: ( modelElement, { writer } ) => {
             *				// ...
             *			}
             *		} );
             *
             * @error conversion-element-to-structure-disallowed-text
             * @param {String} elementName The name of the element the structure is to be created for.
             */
            throw new CKEditorError('conversion-element-to-structure-disallowed-text', dispatcher, { elementName: model.name });
        }
        dispatcher.on(`insert:${model.name}`, insertStructure(view, createConsumer(model)), { priority: config.converterPriority || 'normal' });
        dispatcher.on('reduceChanges', createChangeReducer(model), { priority: 'low' });
    };
}
// Model attribute to view element conversion helper.
//
// See {@link ~DowncastHelpers#attributeToElement `.attributeToElement()` downcast helper} for examples.
//
// @param {Object} config Conversion configuration.
// @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
// of `String`s with possible values if the model attribute is an enumerable.
// @param {module:engine/view/elementdefinition~ElementDefinition|module:engine/conversion/downcasthelpers~AttributeElementCreatorFunction|
// Object} config.view A view element definition or a function that takes the model attribute value and
// {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} as parameters and returns a view attribute element.
// If `config.model.values` is given, `config.view` should be an object assigning values from `config.model.values` to view element
// definitions or functions.
// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
// @returns {Function} Conversion helper.
function downcastAttributeToElement(config) {
    config = cloneDeep(config);
    let model = config.model;
    if (typeof model == 'string') {
        model = { key: model };
    }
    let eventName = `attribute:${model.key}`;
    if (model.name) {
        eventName += ':' + model.name;
    }
    if (model.values) {
        for (const modelValue of model.values) {
            config.view[modelValue] = normalizeToElementConfig(config.view[modelValue], 'attribute');
        }
    }
    else {
        config.view = normalizeToElementConfig(config.view, 'attribute');
    }
    const elementCreator = getFromAttributeCreator(config);
    return (dispatcher) => {
        dispatcher.on(eventName, wrap(elementCreator), { priority: config.converterPriority || 'normal' });
    };
}
// Model attribute to view attribute conversion helper.
//
// See {@link ~DowncastHelpers#attributeToAttribute `.attributeToAttribute()` downcast helper} for examples.
//
// @param {Object} config Conversion configuration.
// @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
// the attribute key, possible values and, optionally, an element name to convert from.
// @param {String|Object|module:engine/conversion/downcasthelpers~AttributeCreatorFunction} config.view A view attribute key,
// or a `{ key, value }` object or a function that takes the model attribute value and returns a `{ key, value }` object.
// If `key` is `'class'`, `value` can be a `String` or an array of `String`s. If `key` is `'style'`, `value` is an object with
// key-value pairs. In other cases, `value` is a `String`.
// If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
// `{ key, value }` objects or a functions.
// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
// @returns {Function} Conversion helper.
function downcastAttributeToAttribute(config) {
    config = cloneDeep(config);
    let model = config.model;
    if (typeof model == 'string') {
        model = { key: model };
    }
    let eventName = `attribute:${model.key}`;
    if (model.name) {
        eventName += ':' + model.name;
    }
    if (model.values) {
        for (const modelValue of model.values) {
            config.view[modelValue] = normalizeToAttributeConfig(config.view[modelValue]);
        }
    }
    else {
        config.view = normalizeToAttributeConfig(config.view);
    }
    const elementCreator = getFromAttributeCreator(config);
    return (dispatcher) => {
        dispatcher.on(eventName, changeAttribute(elementCreator), { priority: config.converterPriority || 'normal' });
    };
}
// Model marker to view element conversion helper.
//
// See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
//
// @param {Object} config Conversion configuration.
// @param {String} config.model The name of the model marker (or model marker group) to convert.
// @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
// that takes the model marker data as a parameter and returns a view UI element.
// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
// @returns {Function} Conversion helper.
function downcastMarkerToElement(config) {
    const view = normalizeToElementConfig(config.view, 'ui');
    return (dispatcher) => {
        dispatcher.on(`addMarker:${config.model}`, insertUIElement(view), { priority: config.converterPriority || 'normal' });
        dispatcher.on(`removeMarker:${config.model}`, removeUIElement(), { priority: config.converterPriority || 'normal' });
    };
}
// Model marker to view data conversion helper.
//
// See {@link ~DowncastHelpers#markerToData `markerToData()` downcast helper} to learn more.
//
// @param {Object} config
// @param {String} config.model
// @param {Function} [config.view]
// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal']
// @returns {Function} Conversion helper.
function downcastMarkerToData(config) {
    config = cloneDeep(config);
    const group = config.model;
    let view = config.view;
    // Default conversion.
    if (!view) {
        view = markerName => ({
            group,
            name: markerName.substr(config.model.length + 1)
        });
    }
    return (dispatcher) => {
        dispatcher.on(`addMarker:${group}`, insertMarkerData(view), { priority: config.converterPriority || 'normal' });
        dispatcher.on(`removeMarker:${group}`, removeMarkerData(view), { priority: config.converterPriority || 'normal' });
    };
}
// Model marker to highlight conversion helper.
//
// See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
//
// @param {Object} config Conversion configuration.
// @param {String} config.model The name of the model marker (or model marker group) to convert.
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} config.view A highlight descriptor
// that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor.
// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
// @returns {Function} Conversion helper.
function downcastMarkerToHighlight(config) {
    return (dispatcher) => {
        dispatcher.on(`addMarker:${config.model}`, highlightText(config.view), { priority: config.converterPriority || 'normal' });
        dispatcher.on(`addMarker:${config.model}`, highlightElement(config.view), { priority: config.converterPriority || 'normal' });
        dispatcher.on(`removeMarker:${config.model}`, removeHighlight$1(config.view), { priority: config.converterPriority || 'normal' });
    };
}
// Takes `config.model`, and converts it to an object with normalized structure.
//
// @param {String|Object} model Model configuration or element name.
// @param {String} model.name
// @param {Array.<String>} [model.attributes]
// @param {Boolean} [model.children]
// @returns {Object}
function normalizeModelElementConfig(model) {
    if (typeof model == 'string') {
        model = { name: model };
    }
    // List of attributes that should trigger reconversion.
    if (!model.attributes) {
        model.attributes = [];
    }
    else if (!Array.isArray(model.attributes)) {
        model.attributes = [model.attributes];
    }
    // Whether a children insertion/deletion should trigger reconversion.
    model.children = !!model.children;
    return model;
}
// Takes `config.view`, and if it is an {@link module:engine/view/elementdefinition~ElementDefinition}, converts it
// to a function (because lower level converters accept only element creator functions).
//
// @param {module:engine/view/elementdefinition~ElementDefinition|Function} view View configuration.
// @param {'container'|'attribute'|'ui'} viewElementType View element type to create.
// @returns {Function} Element creator function to use in lower level converters.
function normalizeToElementConfig(view, viewElementType) {
    if (typeof view == 'function') {
        // If `view` is already a function, don't do anything.
        return view;
    }
    return ((modelData, conversionApi) => createViewElementFromDefinition(view, conversionApi, viewElementType));
}
// Creates a view element instance from the provided {@link module:engine/view/elementdefinition~ElementDefinition} and class.
//
// @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition
// @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
// @param {'container'|'attribute'|'ui'} viewElementType
// @returns {module:engine/view/element~Element}
function createViewElementFromDefinition(viewElementDefinition, conversionApi, viewElementType) {
    if (typeof viewElementDefinition == 'string') {
        // If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property.
        viewElementDefinition = { name: viewElementDefinition };
    }
    let element;
    const viewWriter = conversionApi.writer;
    const attributes = Object.assign({}, viewElementDefinition.attributes);
    if (viewElementType == 'container') {
        element = viewWriter.createContainerElement(viewElementDefinition.name, attributes);
    }
    else if (viewElementType == 'attribute') {
        const options = {
            priority: viewElementDefinition.priority || AttributeElement.DEFAULT_PRIORITY
        };
        element = viewWriter.createAttributeElement(viewElementDefinition.name, attributes, options);
    }
    else {
        // 'ui'.
        element = viewWriter.createUIElement(viewElementDefinition.name, attributes);
    }
    if (viewElementDefinition.styles) {
        const keys = Object.keys(viewElementDefinition.styles);
        for (const key of keys) {
            viewWriter.setStyle(key, viewElementDefinition.styles[key], element);
        }
    }
    if (viewElementDefinition.classes) {
        const classes = viewElementDefinition.classes;
        if (typeof classes == 'string') {
            viewWriter.addClass(classes, element);
        }
        else {
            for (const className of classes) {
                viewWriter.addClass(className, element);
            }
        }
    }
    return element;
}
function getFromAttributeCreator(config) {
    if (config.model.values) {
        return ((modelAttributeValue, conversionApi, data) => {
            const view = config.view[modelAttributeValue];
            if (view) {
                return view(modelAttributeValue, conversionApi, data);
            }
            return null;
        });
    }
    else {
        return config.view;
    }
}
// Takes the configuration, adds default parameters if they do not exist and normalizes other parameters to be used in downcast converters
// for generating a view attribute.
//
// @param {Object} view View configuration.
function normalizeToAttributeConfig(view) {
    if (typeof view == 'string') {
        return modelAttributeValue => ({ key: view, value: modelAttributeValue });
    }
    else if (typeof view == 'object') {
        // { key, value, ... }
        if (view.value) {
            return () => view;
        }
        // { key, ... }
        else {
            return modelAttributeValue => ({ key: view.key, value: modelAttributeValue });
        }
    }
    else {
        // function.
        return view;
    }
}
// Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter.
function prepareDescriptor(highlightDescriptor, data, conversionApi) {
    // If passed descriptor is a creator function, call it. If not, just use passed value.
    const descriptor = typeof highlightDescriptor == 'function' ?
        highlightDescriptor(data, conversionApi) :
        highlightDescriptor;
    if (!descriptor) {
        return null;
    }
    // Apply default descriptor priority.
    if (!descriptor.priority) {
        descriptor.priority = 10;
    }
    // Default descriptor id is marker name.
    if (!descriptor.id) {
        descriptor.id = data.markerName;
    }
    return descriptor;
}
// Creates a function that checks a single differ diff item whether it should trigger reconversion.
//
// @param {Object} model A normalized `config.model` converter configuration.
// @param {String} model.name The name of element.
// @param {Array.<String>} model.attributes The list of attribute names that should trigger reconversion.
// @param {Boolean} [model.children] Whether the child list change should trigger reconversion.
// @returns {Function}
function createChangeReducerCallback(model) {
    return (node, change) => {
        if (!node.is('element', model.name)) {
            return false;
        }
        if (change.type == 'attribute') {
            if (model.attributes.includes(change.attributeKey)) {
                return true;
            }
        }
        else {
            /* istanbul ignore else: This is always true because otherwise it would not register a reducer callback. */
            if (model.children) {
                return true;
            }
        }
        return false;
    };
}
// Creates a `reduceChanges` event handler for reconversion.
//
// @param {Object} model A normalized `config.model` converter configuration.
// @param {String} model.name The name of element.
// @param {Array.<String>} model.attributes The list of attribute names that should trigger reconversion.
// @param {Boolean} [model.children] Whether the child list change should trigger reconversion.
// @returns {Function}
function createChangeReducer(model) {
    const shouldReplace = createChangeReducerCallback(model);
    return (evt, data) => {
        const reducedChanges = [];
        if (!data.reconvertedElements) {
            data.reconvertedElements = new Set();
        }
        for (const change of data.changes) {
            // For attribute use node affected by the change.
            // For insert or remove use parent element because we need to check if it's added/removed child.
            const node = change.type == 'attribute' ? change.range.start.nodeAfter : change.position.parent;
            if (!node || !shouldReplace(node, change)) {
                reducedChanges.push(change);
                continue;
            }
            // If it's already marked for reconversion, so skip this change, otherwise add the diff items.
            if (!data.reconvertedElements.has(node)) {
                data.reconvertedElements.add(node);
                const position = Position._createBefore(node);
                reducedChanges.push({
                    type: 'remove',
                    name: node.name,
                    position,
                    length: 1
                }, {
                    type: 'reinsert',
                    name: node.name,
                    position,
                    length: 1
                });
            }
        }
        data.changes = reducedChanges;
    };
}
// Creates a function that checks if an element and its watched attributes can be consumed and consumes them.
//
// @param {Object} model A normalized `config.model` converter configuration.
// @param {String} model.name The name of element.
// @param {Array.<String>} model.attributes The list of attribute names that should trigger reconversion.
// @param {Boolean} [model.children] Whether the child list change should trigger reconversion.
// @returns {module:engine/conversion/downcasthelpers~ConsumerFunction}
function createConsumer(model) {
    return (node, consumable, options = {}) => {
        const events = ['insert'];
        // Collect all set attributes that are triggering conversion.
        for (const attributeName of model.attributes) {
            if (node.hasAttribute(attributeName)) {
                events.push(`attribute:${attributeName}`);
            }
        }
        if (!events.every(event => consumable.test(node, event))) {
            return false;
        }
        if (!options.preflight) {
            events.forEach(event => consumable.consume(node, event));
        }
        return true;
    };
}
// Creates a function that create view slots.
//
// @param {module:engine/model/element~Element} element
// @param {Map.<module:engine/view/element~Element,Array.<module:engine/model/node~Node>>} slotsMap
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
// @returns {Function} Exposed by writer as createSlot().
function createSlotFactory(element, slotsMap, conversionApi) {
    return (writer, modeOrFilter = 'children') => {
        const slot = writer.createContainerElement('$slot');
        let children = null;
        if (modeOrFilter === 'children') {
            children = Array.from(element.getChildren());
        }
        else if (typeof modeOrFilter == 'function') {
            children = Array.from(element.getChildren()).filter(element => modeOrFilter(element));
        }
        else {
            /**
             * Unknown slot mode was provided to `writer.createSlot()` in downcast converter.
             *
             * @error conversion-slot-mode-unknown
             */
            throw new CKEditorError('conversion-slot-mode-unknown', conversionApi.dispatcher, { modeOrFilter });
        }
        slotsMap.set(slot, children);
        return slot;
    };
}
// Checks if all children are covered by slots and there is no child that landed in multiple slots.
//
// @param {module:engine/model/element~Element}
// @param {Map.<module:engine/view/element~Element,Array.<module:engine/model/node~Node>>} slotsMap
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
function validateSlotsChildren(element, slotsMap, conversionApi) {
    const childrenInSlots = Array.from(slotsMap.values()).flat();
    const uniqueChildrenInSlots = new Set(childrenInSlots);
    if (uniqueChildrenInSlots.size != childrenInSlots.length) {
        /**
         * Filters provided to `writer.createSlot()` overlap (at least two filters accept the same child element).
         *
         * @error conversion-slot-filter-overlap
         * @param {module:engine/model/element~Element} element The element of which children would not be properly
         * allocated to multiple slots.
         */
        throw new CKEditorError('conversion-slot-filter-overlap', conversionApi.dispatcher, { element });
    }
    if (uniqueChildrenInSlots.size != element.childCount) {
        /**
         * Filters provided to `writer.createSlot()` are incomplete and exclude at least one children element (one of
         * the children elements would not be assigned to any of the slots).
         *
         * @error conversion-slot-filter-incomplete
         * @param {module:engine/model/element~Element} element The element of which children would not be properly
         * allocated to multiple slots.
         */
        throw new CKEditorError('conversion-slot-filter-incomplete', conversionApi.dispatcher, { element });
    }
}
// Fill slots with appropriate view elements.
//
// @param {module:engine/view/element~Element} viewElement
// @param {Map.<module:engine/view/element~Element,Array.<module:engine/model/node~Node>>} slotsMap
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
// @param {Object} options
// @param {Boolean} [options.reconversion]
function fillSlots(viewElement, slotsMap, conversionApi, options) {
    // Set temporary position mapping to redirect child view elements into a proper slots.
    conversionApi.mapper.on('modelToViewPosition', toViewPositionMapping, { priority: 'highest' });
    let currentSlot = null;
    let currentSlotNodes = null;
    // Fill slots with nested view nodes.
    for ([currentSlot, currentSlotNodes] of slotsMap) {
        reinsertOrConvertNodes(viewElement, currentSlotNodes, conversionApi, options);
        conversionApi.writer.move(conversionApi.writer.createRangeIn(currentSlot), conversionApi.writer.createPositionBefore(currentSlot));
        conversionApi.writer.remove(currentSlot);
    }
    conversionApi.mapper.off('modelToViewPosition', toViewPositionMapping);
    function toViewPositionMapping(evt, data) {
        const element = data.modelPosition.nodeAfter;
        // Find the proper offset within the slot.
        const index = currentSlotNodes.indexOf(element);
        if (index < 0) {
            return;
        }
        data.viewPosition = data.mapper.findPositionIn(currentSlot, index);
    }
}
// Inserts view representation of `nodes` into the `viewElement` either by bringing back just removed view nodes
// or by triggering conversion for them.
//
// @param {module:engine/view/element~Element} viewElement
// @param {Iterable.<module:engine/model/element~Element>} modelNodes
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
// @param {Object} options
// @param {Boolean} [options.reconversion]
function reinsertOrConvertNodes(viewElement, modelNodes, conversionApi, options) {
    // Fill with nested view nodes.
    for (const modelChildNode of modelNodes) {
        // Try reinserting the view node for the specified model node...
        if (!reinsertNode(viewElement.root, modelChildNode, conversionApi, options)) {
            // ...or else convert the model element to the view.
            conversionApi.convertItem(modelChildNode);
        }
    }
}
// Checks if the view for the given model element could be reused and reinserts it to the view.
//
// @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} viewRoot
// @param {module:engine/model/element~Element} modelElement
// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
// @param {Object} options
// @param {Boolean} [options.reconversion]
// @returns {Boolean} `false` if view element can't be reused.
function reinsertNode(viewRoot, modelNode, conversionApi, options) {
    const { writer, mapper } = conversionApi;
    // Don't reinsert if this is not a reconversion...
    if (!options.reconversion) {
        return false;
    }
    const viewChildNode = mapper.toViewElement(modelNode);
    // ...or there is no view to reinsert or it was already inserted to the view structure...
    if (!viewChildNode || viewChildNode.root == viewRoot) {
        return false;
    }
    // ...or it was strictly marked as not to be reused.
    if (!conversionApi.canReuseView(viewChildNode)) {
        return false;
    }
    // Otherwise reinsert the view node.
    writer.move(writer.createRangeOn(viewChildNode), mapper.toViewPosition(Position._createBefore(modelNode)));
    return true;
}
// The default consumer for insert events.
// @param {module:engine/model/item~Item} item Model item.
// @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable The model consumable.
// @param {Object} [options]
// @param {Boolean} [options.preflight=false] Whether should consume or just check if can be consumed.
// @returns {Boolean}
function defaultConsumer(item, consumable, { preflight } = {}) {
    if (preflight) {
        return consumable.test(item, 'insert');
    }
    else {
        return consumable.consume(item, 'insert');
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module engine/model/utils/autoparagraphing
 */
/**
 * Fixes all empty roots.
 *
 * @protected
 * @param {module:engine/model/writer~Writer} writer The model writer.
 * @returns {Boolean} `true` if any change has been applied, `false` otherwise.
 */
function autoParagraphEmptyRoots(writer) {
    const { schema, document } = writer.model;
    for (const rootName of document.getRootNames()) {
        const root = document.getRoot(rootName);
        if (root.isEmpty && !schema.checkChild(root, '$text')) {
            // If paragraph element is allowed in the root, create paragraph element.
            if (schema.checkChild(root, 'paragraph')) {
                writer.insertElement('paragraph', root);
                // Other roots will get fixed in the next post-fixer round. Those will be triggered
                // in the same batch no matter if this method was triggered by the post-fixing or not
                // (the above insertElement call will trigger the post-fixers).
                return true;
            }
        }
    }
    return false;
}
/**
 * Checks if the given node wrapped with a paragraph would be accepted by the schema in the given position.
 *
 * @internal
 * @protected
 * @param {module:engine/model/position~Position} position The position at which to check.
 * @param {module:engine/model/node~Node|String} nodeOrType The child node or child type to check.
 * @param {module:engine/model/schema~Schema} schema A schema instance used for element validation.
 */
function isParagraphable(position, nodeOrType, schema) {
    const context = schema.createContext(position);
    // When paragraph is allowed in this context...
    if (!schema.checkChild(context, 'paragraph')) {
        return false;
    }
    // And a node would be allowed in this paragraph...
    if (!schema.checkChild(context.push('paragraph'), nodeOrType)) {
        return false;
    }
    return true;
}
/**
 * Inserts a new paragraph at the given position and returns a position inside that paragraph.
 *
 * @protected
 * @param {module:engine/model/position~Position} position The position where a paragraph should be inserted.
 * @param {module:engine/model/writer~Writer} writer The model writer.
 * @returns {module:engine/model/position~Position} Position inside the created paragraph.
 */
function wrapInParagraph(position, writer) {
    const paragraph = writer.createElement('paragraph');
    writer.insert(paragraph, position);
    return writer.createPositionAt(paragraph, 0);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Contains the {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
 * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}.
 *
 * @module engine/conversion/upcasthelpers
 */
/**
 * Upcast conversion helper functions.
 *
 * Learn more about {@glink framework/guides/deep-dive/conversion/upcast upcast helpers}.
 *
 * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
 */
class UpcastHelpers extends ConversionHelpers {
    /**
     * View element to model element conversion helper.
     *
     * This conversion results in creating a model element. For example,
     * view `<p>Foo</p>` becomes `<paragraph>Foo</paragraph>` in the model.
     *
     * Keep in mind that the element will be inserted only if it is allowed
     * by {@link module:engine/model/schema~Schema schema} configuration.
     *
     *		editor.conversion.for( 'upcast' ).elementToElement( {
     *			view: 'p',
     *			model: 'paragraph'
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).elementToElement( {
     *			view: 'p',
     *			model: 'paragraph',
     *			converterPriority: 'high'
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).elementToElement( {
     *			view: {
     *				name: 'p',
     *				classes: 'fancy'
     *			},
     *			model: 'fancyParagraph'
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).elementToElement( {
     * 			view: {
     *				name: 'p',
     *				classes: 'heading'
     * 			},
     * 			model: ( viewElement, conversionApi ) => {
     * 				const modelWriter = conversionApi.writer;
     *
     * 				return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } );
     * 			}
     * 		} );
     *
     * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
     * to the conversion process.
     *
     * @method #elementToElement
     * @param {Object} config Conversion configuration.
     * @param {module:engine/view/matcher~MatcherPattern} [config.view] Pattern matching all view elements which should be converted. If not
     * set, the converter will fire for every view element.
     * @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element instance or a
     * function that takes a view element and {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi upcast conversion API}
     * and returns a model element. The model element will be inserted in the model.
     * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
     * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
     */
    elementToElement(config) {
        return this.add(upcastElementToElement(config));
    }
    /**
     * View element to model attribute conversion helper.
     *
     * This conversion results in setting an attribute on a model node. For example, view `<strong>Foo</strong>` becomes
     * `Foo` {@link module:engine/model/text~Text model text node} with `bold` attribute set to `true`.
     *
     * This helper is meant to set a model attribute on all the elements that are inside the converted element:
     *
     *		<strong>Foo</strong>   -->   <strong><p>Foo</p></strong>   -->   <paragraph><$text bold="true">Foo</$text></paragraph>
     *
     * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
     * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text. See
     * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#attributeToAttribute} for comparison.
     *
     * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
     *
     *		editor.conversion.for( 'upcast' ).elementToAttribute( {
     *			view: 'strong',
     *			model: 'bold'
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).elementToAttribute( {
     *			view: 'strong',
     *			model: 'bold',
     *			converterPriority: 'high'
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).elementToAttribute( {
     *			view: {
     *				name: 'span',
     *				classes: 'bold'
     *			},
     *			model: 'bold'
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).elementToAttribute( {
     *			view: {
     *				name: 'span',
     *				classes: [ 'styled', 'styled-dark' ]
     *			},
     *			model: {
     *				key: 'styled',
     *				value: 'dark'
     *			}
     *		} );
     *
     * 		editor.conversion.for( 'upcast' ).elementToAttribute( {
     *			view: {
     *				name: 'span',
     *				styles: {
     *					'font-size': /[\s\S]+/
     *				}
     *			},
     *			model: {
     *				key: 'fontSize',
     *				value: ( viewElement, conversionApi ) => {
     *					const fontSize = viewElement.getStyle( 'font-size' );
     *					const value = fontSize.substr( 0, fontSize.length - 2 );
     *
     *					if ( value <= 10 ) {
     *						return 'small';
     *					} else if ( value > 12 ) {
     *						return 'big';
     *					}
     *
     *					return null;
     *				}
     *			}
     *		} );
     *
     * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
     * to the conversion process.
     *
     * @method #elementToAttribute
     * @param {Object} config Conversion configuration.
     * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
     * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
     * the model attribute. `value` property may be set as a function that takes a view element and
     * {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi upcast conversion API} and returns the value.
     * If `String` is given, the model attribute value will be set to `true`.
     * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
     * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
     */
    elementToAttribute(config) {
        return this.add(upcastElementToAttribute(config));
    }
    /**
     * View attribute to model attribute conversion helper.
     *
     * This conversion results in setting an attribute on a model node. For example, view `<img src="foo.jpg"></img>` becomes
     * `<imageBlock source="foo.jpg"></imageBlock>` in the model.
     *
     * This helper is meant to convert view attributes from view elements which got converted to the model, so the view attribute
     * is set only on the corresponding model node:
     *
     *		<div class="dark"><div>foo</div></div>    -->    <div dark="true"><div>foo</div></div>
     *
     * Above, `class="dark"` attribute is added only to the `<div>` elements that has it. This is in contrary to
     * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToAttribute} which sets attributes for
     * all the children in the model:
     *
     *		<strong>Foo</strong>   -->   <strong><p>Foo</p></strong>   -->   <paragraph><$text bold="true">Foo</$text></paragraph>
     *
     * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
     * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text.
     *
     * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
     *
     *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
     *			view: 'src',
     *			model: 'source'
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
     *			view: { key: 'src' },
     *			model: 'source'
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
     *			view: { key: 'src' },
     *			model: 'source',
     *			converterPriority: 'normal'
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
     *			view: {
     *				key: 'data-style',
     *				value: /[\s\S]+/
     *			},
     *			model: 'styled'
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
     *			view: {
     *				name: 'img',
     *				key: 'class',
     *				value: 'styled-dark'
     *			},
     *			model: {
     *				key: 'styled',
     *				value: 'dark'
     *			}
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
     *			view: {
     *				key: 'class',
     *				value: /styled-[\S]+/
     *			},
     *			model: {
     *				key: 'styled'
     *				value: ( viewElement, conversionApi ) => {
     *					const regexp = /styled-([\S]+)/;
     *					const match = viewElement.getAttribute( 'class' ).match( regexp );
     *
     *					return match[ 1 ];
     *				}
     *			}
     *		} );
     *
     * Converting styles works a bit differently as it requires `view.styles` to be an object and by default
     * a model attribute will be set to `true` by such a converter. You can set the model attribute to any value by providing the `value`
     * callback that returns the desired value.
     *
     *		// Default conversion of font-weight style will result in setting bold attribute to true.
     *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
     *			view: {
     *				styles: {
     *					'font-weight': 'bold'
     *				}
     *			},
     *			model: 'bold'
     *		} );
     *
     *		// This converter will pass any style value to the `lineHeight` model attribute.
     *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
     *			view: {
     *				styles: {
     *					'line-height': /[\s\S]+/
     *				}
     *			},
     *			model: {
     *				key: 'lineHeight',
     *				value: ( viewElement, conversionApi ) => viewElement.getStyle( 'line-height' )
     *			}
     *		} );
     *
     * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
     * to the conversion process.
     *
     * @method #attributeToAttribute
     * @param {Object} config Conversion configuration.
     * @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed,
     * attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property,
     * specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name`
     * property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`,
     * a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`.
     * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
     * the model attribute. `value` property may be set as a function that takes a view element and
     * {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi upcast conversion API} and returns the value.
     * If `String` is given, the model attribute value will be same as view attribute value.
     * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
     * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
     */
    attributeToAttribute(config) {
        return this.add(upcastAttributeToAttribute(config));
    }
    /**
     * View element to model marker conversion helper.
     *
     * This conversion results in creating a model marker. For example, if the marker was stored in a view as an element:
     * `<p>Fo<span data-marker="comment" data-comment-id="7"></span>o</p><p>B<span data-marker="comment" data-comment-id="7"></span>ar</p>`,
     * after the conversion is done, the marker will be available in
     * {@link module:engine/model/model~Model#markers model document markers}.
     *
     * **Note**: When this helper is used in the data upcast in combination with
     * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToData `#markerToData()`} in the data downcast,
     * then invalid HTML code (e.g. a span between table cells) may be produced by the latter converter.
     *
     * In most of the cases, the {@link #dataToMarker} should be used instead.
     *
     *		editor.conversion.for( 'upcast' ).elementToMarker( {
     *			view: 'marker-search',
     *			model: 'search'
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).elementToMarker( {
     *			view: 'marker-search',
     *			model: 'search',
     *			converterPriority: 'high'
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).elementToMarker( {
     *			view: 'marker-search',
     *			model: ( viewElement, conversionApi ) => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
     *		} );
     *
     *		editor.conversion.for( 'upcast' ).elementToMarker( {
     *			view: {
     *				name: 'span',
     *				attributes: {
     *					'data-marker': 'search'
     *				}
     *			},
     *			model: 'search'
     *		} );
     *
     * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
     * to the conversion process.
     *
     * @method #elementToMarker
     * @param {Object} config Conversion configuration.
     * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
     * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns
     * a model marker name.
     * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
     * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
     */
    elementToMarker(config) {
        return this.add(upcastElementToMarker(config));
    }
    /**
     * View-to-model marker conversion helper.
     *
     * Converts view data created by {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToData `#markerToData()`}
     * back to a model marker.
     *
     * This converter looks for specific view elements and view attributes that mark marker boundaries. See
     * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToData `#markerToData()`} to learn what view data
     * is expected by this converter.
     *
     * The `config.view` property is equal to the marker group name to convert.
     *
     * By default, this converter creates markers with the `group:name` name convention (to match the default `markerToData` conversion).
     *
     * The conversion configuration can take a function that will generate a marker name.
     * If such function is set as the `config.model` parameter, it is passed the `name` part from the view element or attribute and it is
     * expected to return a string with the marker name.
     *
     * Basic usage:
     *
     *		// Using the default conversion.
     *		// In this case, all markers from the `comment` group will be converted.
     *		// The conversion will look for `<comment-start>` and `<comment-end>` tags and
     *		// `data-comment-start-before`, `data-comment-start-after`,
     *		// `data-comment-end-before` and `data-comment-end-after` attributes.
     *		editor.conversion.for( 'upcast' ).dataToMarker( {
     *			view: 'comment'
     *		} );
     *
     * An example of a model that may be generated by this conversion:
     *
     *		// View:
     *		<p>Foo<comment-start name="commentId:uid"></comment-start>bar</p>
     *		<figure data-comment-end-after="commentId:uid" class="image"><img src="abc.jpg" /></figure>
     *
     *		// Model:
     *		<paragraph>Foo[bar</paragraph>
     *		<imageBlock src="abc.jpg"></imageBlock>]
     *
     * Where `[]` are boundaries of a marker that will receive the `comment:commentId:uid` name.
     *
     * Other examples of usage:
     *
     *		// Using a custom function which is the same as the default conversion:
     *		editor.conversion.for( 'upcast' ).dataToMarker( {
     *			view: 'comment',
     *			model: ( name, conversionApi ) => 'comment:' + name,
     *		} );
     *
     *		// Using the converter priority:
     *		editor.conversion.for( 'upcast' ).dataToMarker( {
     *			view: 'comment',
     *			model: ( name, conversionApi ) => 'comment:' + name,
     *			converterPriority: 'high'
     *		} );
     *
     * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
     * to the conversion process.
     *
     * @method #dataToMarker
     * @param {Object} config Conversion configuration.
     * @param {String} config.view The marker group name to convert.
     * @param {Function} [config.model] A function that takes the `name` part from the view element or attribute and
     * {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi upcast conversion API} and returns the marker name.
     * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
     * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
     */
    dataToMarker(config) {
        return this.add(upcastDataToMarker(config));
    }
}
/**
 * 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 upcast (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, conversionApi) => {
        // Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement.
        if (!data.modelRange && conversionApi.consumable.consume(data.viewItem, { name: true })) {
            const { modelRange, modelCursor } = conversionApi.convertChildren(data.viewItem, data.modelCursor);
            data.modelRange = modelRange;
            data.modelCursor = modelCursor;
        }
    };
}
/**
 * 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, { schema, consumable, writer }) => {
        let position = data.modelCursor;
        // When node is already converted then do nothing.
        if (!consumable.test(data.viewItem)) {
            return;
        }
        if (!schema.checkChild(position, '$text')) {
            if (!isParagraphable(position, '$text', schema)) {
                return;
            }
            // Do not auto-paragraph whitespaces.
            if (data.viewItem.data.trim().length == 0) {
                return;
            }
            position = wrapInParagraph(position, writer);
        }
        consumable.consume(data.viewItem);
        const text = writer.createText(data.viewItem.data);
        writer.insert(text, position);
        data.modelRange = writer.createRange(position, position.getShiftedBy(text.offsetSize));
        data.modelCursor = data.modelRange.end;
    };
}
/**
 * Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection
 * view selection} taken from the {@link module:engine/view/document~Document#event:selectionChange} event
 * and sets in on the {@link module:engine/model/document~Document#selection model}.
 *
 * **Note**: because there is no view selection change dispatcher nor any other advanced view selection to model
 * conversion mechanism, the callback should be set directly on view document.
 *
 *		view.document.on( 'selectionChange', convertSelectionChange( modelDocument, mapper ) );
 *
 * @param {module:engine/model/model~Model} model Data model.
 * @param {module:engine/conversion/mapper~Mapper} mapper Conversion mapper.
 * @returns {Function} {@link module:engine/view/document~Document#event:selectionChange} callback function.
 */
function convertSelectionChange(model, mapper) {
    return (evt, data) => {
        const viewSelection = data.newSelection;
        const ranges = [];
        for (const viewRange of viewSelection.getRanges()) {
            ranges.push(mapper.toModelRange(viewRange));
        }
        const modelSelection = model.createSelection(ranges, { backward: viewSelection.isBackward });
        if (!modelSelection.isEqual(model.document.selection)) {
            model.change(writer => {
                writer.setSelection(modelSelection);
            });
        }
    };
}
// View element to model element conversion helper.
//
// See {@link ~UpcastHelpers#elementToElement `.elementToElement()` upcast helper} for examples.
//
// @param {Object} config Conversion configuration.
// @param {module:engine/view/matcher~MatcherPattern} [config.view] Pattern matching all view elements which should be converted. If not
// set, the converter will fire for every view element.
// @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element
// instance or a function that takes a view element and returns a model element. The model element will be inserted in the model.
// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
// @returns {Function} Conversion helper.
function upcastElementToElement(config) {
    config = cloneDeep(config);
    const converter = prepareToElementConverter(config);
    const elementName = getViewElementNameFromConfig(config.view);
    const eventName = elementName ? `element:${elementName}` : 'element';
    return (dispatcher) => {
        dispatcher.on(eventName, converter, { priority: config.converterPriority || 'normal' });
    };
}
// View element to model attribute conversion helper.
//
// See {@link ~UpcastHelpers#elementToAttribute `.elementToAttribute()` upcast helper} for examples.
//
// @param {Object} config Conversion configuration.
// @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
// @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
// the model attribute. `value` property may be set as a function that takes a view element and returns the value.
// If `String` is given, the model attribute value will be set to `true`.
// @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
// @returns {Function} Conversion helper.
function upcastElementToAttribute(config) {
    config = cloneDeep(config);
    normalizeModelAttributeConfig(config);
    const converter = prepareToAttributeConverter(config, false);
    const elementName = getViewElementNameFromConfig(config.view);
    const eventName = elementName ? `element:${elementName}` : 'element';
    return (dispatcher) => {
        dispatcher.on(eventName, converter, { priority: config.converterPriority || 'low' });
    };
}
// View attribute to model attribute conversion helper.
//
// See {@link ~UpcastHelpers#attributeToAttribute `.attributeToAttribute()` upcast helper} for examples.
//
// @param {Object} config Conversion configuration.
// @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed,
// attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property,
// specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name`
// property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`,
// a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`.
// @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
// the model attribute. `value` property may be set as a function that takes a view element and returns the value.
// If `String` is given, the model attribute value will be same as view attribute value.
// @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
// @returns {Function} Conversion helper.
function upcastAttributeToAttribute(config) {
    config = cloneDeep(config);
    let viewKey = null;
    if (typeof config.view == 'string' || config.view.key) {
        viewKey = normalizeViewAttributeKeyValueConfig(config);
    }
    normalizeModelAttributeConfig(config, viewKey);
    const converter = prepareToAttributeConverter(config, true);
    return (dispatcher) => {
        dispatcher.on('element', converter, { priority: config.converterPriority || 'low' });
    };
}
// View element to model marker conversion helper.
//
// See {@link ~UpcastHelpers#elementToMarker `.elementToMarker()` upcast helper} for examples.
//
// @param {Object} config Conversion configuration.
// @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
// @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns
// a model marker name.
// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
// @returns {Function} Conversion helper.
function upcastElementToMarker(config) {
    const model = normalizeElementToMarkerModelConfig(config.model);
    return upcastElementToElement({ ...config, model });
}
// View data to model marker conversion helper.
//
// See {@link ~UpcastHelpers#dataToMarker} to learn more.
//
// @param {Object} config
// @param {String} config.view
// @param {Function} [config.model]
// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal']
// @returns {Function} Conversion helper.
function upcastDataToMarker(config) {
    config = cloneDeep(config);
    // Default conversion.
    if (!config.model) {
        config.model = name => {
            return name ? config.view + ':' + name : config.view;
        };
    }
    const normalizedConfig = {
        view: config.view,
        model: config.model
    };
    const converterStart = prepareToElementConverter(normalizeDataToMarkerConfig(normalizedConfig, 'start'));
    const converterEnd = prepareToElementConverter(normalizeDataToMarkerConfig(normalizedConfig, 'end'));
    return (dispatcher) => {
        dispatcher.on(`element:${config.view}-start`, converterStart, { priority: config.converterPriority || 'normal' });
        dispatcher.on(`element:${config.view}-end`, converterEnd, { priority: config.converterPriority || 'normal' });
        // Below is a hack that is needed to properly handle `converterPriority` for both elements and attributes.
        // Attribute conversion needs to be performed *after* element conversion.
        // This converter handles both element conversion and attribute conversion, which means that if a single
        // `config.converterPriority` is used, it will lead to problems. For example, if the `'high'` priority is used,
        // the attribute conversion will be performed before a lot of element upcast converters.
        // On the other hand, we want to support `config.converterPriority` and converter overwriting.
        //
        // To make it work, we need to do some extra processing for priority for attribute converter.
        // Priority `'low'` value should be the base value and then we will change it depending on `config.converterPriority` value.
        //
        // This hack probably would not be needed if attributes are upcasted separately.
        //
        const basePriority = priorities.get('low');
        const maxPriority = priorities.get('highest');
        const priorityFactor = priorities.get(config.converterPriority) / maxPriority; // Number in range [ -1, 1 ].
        dispatcher.on('element', upcastAttributeToMarker(normalizedConfig), { priority: basePriority + priorityFactor });
    };
}
// Function factory, returns a callback function which converts view attributes to a model marker.
//
// The converter looks for elements with `data-group-start-before`, `data-group-start-after`, `data-group-end-before`
// and `data-group-end-after` attributes and inserts `$marker` model elements before/after those elements.
// `group` part is specified in `config.view`.
//
// @param {Object} config
// @param {String} config.view
// @param {Function} [config.model]
// @returns {Function} Marker converter.
function upcastAttributeToMarker(config) {
    return (evt, data, conversionApi) => {
        const attrName = `data-${config.view}`;
        // Check if any attribute for the given view item can be consumed before changing the conversion data
        // and consuming view items with these attributes.
        if (!conversionApi.consumable.test(data.viewItem, { attributes: attrName + '-end-after' }) &&
            !conversionApi.consumable.test(data.viewItem, { attributes: attrName + '-start-after' }) &&
            !conversionApi.consumable.test(data.viewItem, { attributes: attrName + '-end-before' }) &&
            !conversionApi.consumable.test(data.viewItem, { attributes: attrName + '-start-before' })) {
            return;
        }
        // This converter wants to add a model element, marking a marker, before/after an element (or maybe even group of elements).
        // To do that, we can use `data.modelRange` which is set on an element (or a group of elements) that has been upcasted.
        // But, if the processed view element has not been upcasted yet (it does not have been converted), we need to
        // fire conversion for its children first, then we will have `data.modelRange` available.
        if (!data.modelRange) {
            Object.assign(data, conversionApi.convertChildren(data.viewItem, data.modelCursor));
        }
        if (conversionApi.consumable.consume(data.viewItem, { attributes: attrName + '-end-after' })) {
            addMarkerElements(data.modelRange.end, data.viewItem.getAttribute(attrName + '-end-after').split(','));
        }
        if (conversionApi.consumable.consume(data.viewItem, { attributes: attrName + '-start-after' })) {
            addMarkerElements(data.modelRange.end, data.viewItem.getAttribute(attrName + '-start-after').split(','));
        }
        if (conversionApi.consumable.consume(data.viewItem, { attributes: attrName + '-end-before' })) {
            addMarkerElements(data.modelRange.start, data.viewItem.getAttribute(attrName + '-end-before').split(','));
        }
        if (conversionApi.consumable.consume(data.viewItem, { attributes: attrName + '-start-before' })) {
            addMarkerElements(data.modelRange.start, data.viewItem.getAttribute(attrName + '-start-before').split(','));
        }
        function addMarkerElements(position, markerViewNames) {
            for (const markerViewName of markerViewNames) {
                const markerName = config.model(markerViewName, conversionApi);
                const element = conversionApi.writer.createElement('$marker', { 'data-name': markerName });
                conversionApi.writer.insert(element, position);
                if (data.modelCursor.isEqual(position)) {
                    data.modelCursor = data.modelCursor.getShiftedBy(1);
                }
                else {
                    data.modelCursor = data.modelCursor._getTransformedByInsertion(position, 1);
                }
                data.modelRange = data.modelRange._getTransformedByInsertion(position, 1)[0];
            }
        }
    };
}
// Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name
// and if so, returns it.
//
// @param {Object} config Conversion view config.
// @returns {String|null} View element name or `null` if name is not directly set.
function getViewElementNameFromConfig(viewConfig) {
    if (typeof viewConfig == 'string') {
        return viewConfig;
    }
    if (typeof viewConfig == 'object' && typeof viewConfig.name == 'string') {
        return viewConfig.name;
    }
    return null;
}
// Helper for to-model-element conversion. Takes a config object and returns a proper converter function.
//
// @param {Object} config Conversion configuration.
// @returns {Function} View to model converter.
function prepareToElementConverter(config) {
    const matcher = new Matcher(config.view);
    return (evt, data, conversionApi) => {
        const matcherResult = matcher.match(data.viewItem);
        if (!matcherResult) {
            return;
        }
        const match = matcherResult.match;
        // Force consuming element's name.
        match.name = true;
        if (!conversionApi.consumable.test(data.viewItem, match)) {
            return;
        }
        const modelElement = getModelElement(config.model, data.viewItem, conversionApi);
        if (!modelElement) {
            return;
        }
        if (!conversionApi.safeInsert(modelElement, data.modelCursor)) {
            return;
        }
        conversionApi.consumable.consume(data.viewItem, match);
        conversionApi.convertChildren(data.viewItem, modelElement);
        conversionApi.updateConversionResult(modelElement, data);
    };
}
// Helper function for upcasting-to-element converter. Takes the model configuration, the converted view element
// and a writer instance and returns a model element instance to be inserted in the model.
//
// @param {String|Function|module:engine/model/element~Element} model Model conversion configuration.
// @param {module:engine/view/node~Node} input The converted view node.
// @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi The upcast conversion API.
function getModelElement(model, input, conversionApi) {
    if (model instanceof Function) {
        return model(input, conversionApi);
    }
    else {
        return conversionApi.writer.createElement(model);
    }
}
// Helper function view-attribute-to-model-attribute helper. Normalizes `config.view` which was set as `String` or
// as an `Object` with `key`, `value` and `name` properties. Normalized `config.view` has is compatible with
// {@link module:engine/view/matcher~MatcherPattern}.
//
// @param {Object} config Conversion config.
// @returns {String} Key of the converted view attribute.
function normalizeViewAttributeKeyValueConfig(config) {
    if (typeof config.view == 'string') {
        config.view = { key: config.view };
    }
    const key = config.view.key;
    let normalized;
    if (key == 'class' || key == 'style') {
        const keyName = key == 'class' ? 'classes' : 'styles';
        normalized = {
            [keyName]: config.view.value
        };
    }
    else {
        const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value;
        normalized = {
            attributes: {
                [key]: value
            }
        };
    }
    if (config.view.name) {
        normalized.name = config.view.name;
    }
    config.view = normalized;
    return key;
}
// Helper function that normalizes `config.model` in from-model-attribute conversion. `config.model` can be set
// as a `String`, an `Object` with only `key` property or an `Object` with `key` and `value` properties. Normalized
// `config.model` is an `Object` with `key` and `value` properties.
//
// @param {Object} config Conversion config.
// @param {String} viewAttributeKeyToCopy Key of the converted view attribute. If it is set, model attribute value
// will be equal to view attribute value.
function normalizeModelAttributeConfig(config, viewAttributeKeyToCopy = null) {
    const defaultModelValue = viewAttributeKeyToCopy === null ? true :
        (viewElement) => viewElement.getAttribute(viewAttributeKeyToCopy);
    const key = typeof config.model != 'object' ? config.model : config.model.key;
    const value = typeof config.model != 'object' || typeof config.model.value == 'undefined' ? defaultModelValue : config.model.value;
    config.model = { key, value };
}
// Helper for to-model-attribute conversion. Takes the model attribute name and conversion configuration and returns
// a proper converter function.
//
// @param {String} modelAttributeKey The key of the model attribute to set on a model node.
// @param {Object|Array.<Object>} config Conversion configuration. It is possible to provide multiple configurations in an array.
// @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
// on all elements in the range.
function prepareToAttributeConverter(config, shallow) {
    const matcher = new Matcher(config.view);
    return (evt, data, conversionApi) => {
        // Converting an attribute of an element that has not been converted to anything does not make sense
        // because there will be nowhere to set that attribute on. At this stage, the element should've already
        // been converted (https://github.com/ckeditor/ckeditor5/issues/11000).
        if (!data.modelRange && shallow) {
            return;
        }
        const match = matcher.match(data.viewItem);
        // If there is no match, this callback should not do anything.
        if (!match) {
            return;
        }
        if (onlyViewNameIsDefined(config.view, data.viewItem)) {
            match.match.name = true;
        }
        else {
            // Do not test `name` consumable because it could get consumed already while upcasting some other attribute
            // on the same element (for example <span class="big" style="color: red">foo</span>).
            delete match.match.name;
        }
        // Try to consume appropriate values from consumable values list.
        if (!conversionApi.consumable.test(data.viewItem, match.match)) {
            return;
        }
        const modelKey = config.model.key;
        const modelValue = typeof config.model.value == 'function' ?
            config.model.value(data.viewItem, conversionApi) : config.model.value;
        // Do not convert if attribute building function returned falsy value.
        if (modelValue === null) {
            return;
        }
        // Since we are converting to attribute we need a range on which we will set the attribute.
        // If the range is not created yet, let's create it by converting children of the current node first.
        if (!data.modelRange) {
            // Convert children and set conversion result as a current data.
            Object.assign(data, conversionApi.convertChildren(data.viewItem, data.modelCursor));
        }
        // Set attribute on current `output`. `Schema` is checked inside this helper function.
        const attributeWasSet = setAttributeOn(data.modelRange, { key: modelKey, value: modelValue }, shallow, conversionApi);
        // It may happen that a converter will try to set an attribute that is not allowed in the given context.
        // In such a situation we cannot consume the attribute. See: https://github.com/ckeditor/ckeditor5/pull/9249#issuecomment-815658459.
        if (attributeWasSet) {
            // Verify if the element itself wasn't consumed yet. It could be consumed already while upcasting some other attribute
            // on the same element (for example <span class="big" style="color: red">foo</span>).
            // We need to consume it so other features (especially GHS) won't try to convert it.
            // Note that it's not tested by the other element-to-attribute converters whether an element was consumed before
            // (in case of converters that the element itself is just a context and not the primary information to convert).
            if (conversionApi.consumable.test(data.viewItem, { name: true })) {
                match.match.name = true;
            }
            conversionApi.consumable.consume(data.viewItem, match.match);
        }
    };
}
// Helper function that checks if element name should be consumed in attribute converters.
//
// @param {Object} config Conversion view config.
// @returns {Boolean}
function onlyViewNameIsDefined(viewConfig, viewItem) {
    // https://github.com/ckeditor/ckeditor5-engine/issues/1786
    const configToTest = typeof viewConfig == 'function' ? viewConfig(viewItem) : viewConfig;
    if (typeof configToTest == 'object' && !getViewElementNameFromConfig(configToTest)) {
        return false;
    }
    return !configToTest.classes && !configToTest.attributes && !configToTest.styles;
}
// Helper function for to-model-attribute converter. Sets model attribute on given range. Checks {@link module:engine/model/schema~Schema}
// to ensure proper model structure.
//
// If any node on the given range has already defined an attribute with the same name, its value will not be updated.
//
// @param {module:engine/model/range~Range} modelRange Model range on which attribute should be set.
// @param {Object} modelAttribute Model attribute to set.
// @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion API.
// @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
// on all elements in the range.
// @returns {Boolean} `true` if attribute was set on at least one node from given `modelRange`.
function setAttributeOn(modelRange, modelAttribute, shallow, conversionApi) {
    let result = false;
    // Set attribute on each item in range according to Schema.
    for (const node of Array.from(modelRange.getItems({ shallow }))) {
        // Skip if not allowed.
        if (!conversionApi.schema.checkAttribute(node, modelAttribute.key)) {
            continue;
        }
        // Mark the node as consumed even if the attribute will not be updated because it's in a valid context (schema)
        // and would be converted if the attribute wouldn't be present. See #8921.
        result = true;
        // Do not override the attribute if it's already present.
        if (node.hasAttribute(modelAttribute.key)) {
            continue;
        }
        conversionApi.writer.setAttribute(modelAttribute.key, modelAttribute.value, node);
    }
    return result;
}
// Helper function for upcasting-to-marker conversion. Takes the config in a format requested by `upcastElementToMarker()`
// function and converts it to a format that is supported by `upcastElementToElement()` function.
//
// @param {Object} config Conversion configuration.
function normalizeElementToMarkerModelConfig(model) {
    return (viewElement, conversionApi) => {
        const markerName = typeof model == 'string' ? model : model(viewElement, conversionApi);
        return conversionApi.writer.createElement('$marker', { 'data-name': markerName });
    };
}
// Helper function for upcasting-to-marker conversion. Takes the config in a format requested by `upcastDataToMarker()`
// function and converts it to a format that is supported by `upcastElementToElement()` function.
//
// @param {Object} config Conversion configuration.
function normalizeDataToMarkerConfig(config, type) {
    const elementCreatorFunction = (viewElement, conversionApi) => {
        const viewName = viewElement.getAttribute('name');
        const markerName = config.model(viewName, conversionApi);
        return conversionApi.writer.createElement('$marker', { 'data-name': markerName });
    };
    return {
        // Upcast <markerGroup-start> and <markerGroup-end> elements.
        view: `${config.view}-${type}`,
        model: elementCreatorFunction
    };
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// @if CK_DEBUG_ENGINE // const { dumpTrees, initDocumentDumping } = require( '../dev-utils/utils' );
/**
 * A controller for the editing pipeline. The editing pipeline controls the {@link ~EditingController#model model} rendering,
 * including selection handling. It also creates the {@link ~EditingController#view view} which builds a
 * browser-independent virtualization over the DOM elements. The editing controller also attaches default converters.
 *
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class EditingController extends Observable {
    /**
     * Creates an editing controller instance.
     *
     * @param {module:engine/model/model~Model} model Editing model.
     * @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor The styles processor instance.
     */
    constructor(model, stylesProcessor) {
        super();
        /**
         * Editor model.
         *
         * @readonly
         * @member {module:engine/model/model~Model}
         */
        this.model = model;
        /**
         * Editing view controller.
         *
         * @readonly
         * @member {module:engine/view/view~View}
         */
        this.view = new View$1(stylesProcessor);
        /**
         * A mapper that describes the model-view binding.
         *
         * @readonly
         * @member {module:engine/conversion/mapper~Mapper}
         */
        this.mapper = new Mapper();
        /**
         * Downcast dispatcher that converts changes from the model to the {@link #view editing view}.
         *
         * @readonly
         * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #downcastDispatcher
         */
        this.downcastDispatcher = new DowncastDispatcher({
            mapper: this.mapper,
            schema: model.schema
        });
        const doc = this.model.document;
        const selection = doc.selection;
        const markers = this.model.markers;
        // When plugins listen on model changes (on selection change, post fixers, etc.) and change the view as a result of
        // the model's change, they might trigger view rendering before the conversion is completed (e.g. before the selection
        // is converted). We disable rendering for the length of the outermost model change() block to prevent that.
        //
        // See https://github.com/ckeditor/ckeditor5-engine/issues/1528
        this.listenTo(this.model, '_beforeChanges', () => {
            this.view._disableRendering(true);
        }, { priority: 'highest' });
        this.listenTo(this.model, '_afterChanges', () => {
            this.view._disableRendering(false);
        }, { priority: 'lowest' });
        // Whenever model document is changed, convert those changes to the view (using model.Document#differ).
        // Do it on 'low' priority, so changes are converted after other listeners did their job.
        // Also convert model selection.
        this.listenTo(doc, 'change', () => {
            this.view.change(writer => {
                this.downcastDispatcher.convertChanges(doc.differ, markers, writer);
                this.downcastDispatcher.convertSelection(selection, markers, writer);
            });
        }, { priority: 'low' });
        // Convert selection from the view to the model when it changes in the view.
        this.listenTo(this.view.document, 'selectionChange', convertSelectionChange(this.model, this.mapper));
        // Attach default model converters.
        this.downcastDispatcher.on('insert:$text', insertText(), { priority: 'lowest' });
        this.downcastDispatcher.on('insert', insertAttributesAndChildren(), { priority: 'lowest' });
        this.downcastDispatcher.on('remove', remove(), { priority: 'low' });
        // Attach default model selection converters.
        this.downcastDispatcher.on('selection', clearAttributes(), { priority: 'high' });
        this.downcastDispatcher.on('selection', convertRangeSelection(), { priority: 'low' });
        this.downcastDispatcher.on('selection', convertCollapsedSelection(), { priority: 'low' });
        // Binds {@link module:engine/view/document~Document#roots view roots collection} to
        // {@link module:engine/model/document~Document#roots model roots collection} so creating
        // model root automatically creates corresponding view root.
        this.view.document.roots.bindTo(this.model.document.roots).using(root => {
            // $graveyard is a special root that has no reflection in the view.
            if (root.rootName == '$graveyard') {
                return null;
            }
            const viewRoot = new RootEditableElement(this.view.document, root.name);
            viewRoot.rootName = root.rootName;
            this.mapper.bindElements(root, viewRoot);
            return viewRoot;
        });
        // @if CK_DEBUG_ENGINE // initDocumentDumping( this.model.document );
        // @if CK_DEBUG_ENGINE // initDocumentDumping( this.view.document );
        // @if CK_DEBUG_ENGINE // dumpTrees( this.model.document, this.model.document.version );
        // @if CK_DEBUG_ENGINE // dumpTrees( this.view.document, this.model.document.version );
        // @if CK_DEBUG_ENGINE // this.model.document.on( 'change', () => {
        // @if CK_DEBUG_ENGINE //	dumpTrees( this.view.document, this.model.document.version );
        // @if CK_DEBUG_ENGINE // }, { priority: 'lowest' } );
    }
    /**
     * Removes all event listeners attached to the `EditingController`. Destroys all objects created
     * by `EditingController` that need to be destroyed.
     */
    destroy() {
        this.view.destroy();
        this.stopListening();
    }
    /**
     * Calling this method will refresh the marker by triggering the downcast conversion for it.
     *
     * Reconverting the marker is useful when you want to change its {@link module:engine/view/element~Element view element}
     * without changing any marker data. For instance:
     *
     *		let isCommentActive = false;
     *
     *		model.conversion.markerToHighlight( {
     *			model: 'comment',
     *			view: data => {
     *				const classes = [ 'comment-marker' ];
     *
     *				if ( isCommentActive ) {
     *					classes.push( 'comment-marker--active' );
     *				}
     *
     *				return { classes };
     *			}
     *		} );
     *
     *		// ...
     *
     *		// Change the property that indicates if marker is displayed as active or not.
     *		isCommentActive = true;
     *
     *		// Reconverting will downcast and synchronize the marker with the new isCommentActive state value.
     *		editor.editing.reconvertMarker( 'comment' );
     *
     * **Note**: If you want to reconvert a model item, use {@link #reconvertItem} instead.
     *
     * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of a marker to update, or a marker instance.
     */
    reconvertMarker(markerOrName) {
        const markerName = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
        const currentMarker = this.model.markers.get(markerName);
        if (!currentMarker) {
            /**
             * The marker with the provided name does not exist and cannot be reconverted.
             *
             * @error editingcontroller-reconvertmarker-marker-not-exist
             * @param {String} markerName The name of the reconverted marker.
             */
            throw new CKEditorError('editingcontroller-reconvertmarker-marker-not-exist', this, { markerName });
        }
        this.model.change(() => {
            this.model.markers._refresh(currentMarker);
        });
    }
    /**
     * Calling this method will downcast a model item on demand (by requesting a refresh in the {@link module:engine/model/differ~Differ}).
     *
     * You can use it if you want the view representation of a specific item updated as a response to external modifications. For instance,
     * when the view structure depends not only on the associated model data but also on some external state.
     *
     * **Note**: If you want to reconvert a model marker, use {@link #reconvertMarker} instead.
     *
     * @param {module:engine/model/item~Item} item Item to refresh.
     */
    reconvertItem(item) {
        this.model.change(() => {
            this.model.document.differ._refreshItem(item);
        });
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Collection of commands. Its instance is available in {@link module:core/editor/editor~Editor#commands `editor.commands`}.
 */
class 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.
	 * @param {*} [...commandParams] Command parameters.
	 * @returns {*} The value returned by the {@link module:core/command~Command#execute `command.execute()`}.
	 */
	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', this, { commandName } );
		}

		return command.execute( ...args );
	}

	/**
	 * Returns iterator of command names.
	 *
	 * @returns {Iterable.<String>}
	 */
	* names() {
		yield* this._commands.keys();
	}

	/**
	 * Returns iterator of command instances.
	 *
	 * @returns {Iterable.<module:core/command~Command>}
	 */
	* commands() {
		yield* this._commands.values();
	}

	/**
	 * Iterable interface.
	 *
	 * Returns `[ commandName, commandInstance ]` pairs.
	 *
	 * @returns {Iterable.<Array>}
	 */
	[ Symbol.iterator ]() {
		return this._commands[ Symbol.iterator ]();
	}

	/**
	 * Destroys all collection commands.
	 */
	destroy() {
		for ( const command of this.commands() ) {
			command.destroy();
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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, { attributes: 'name' } ); // Adds element's attribute.
     *		viewConsumable.add( p, { classes: 'foobar' } ); // Adds element's class.
     *		viewConsumable.add( p, { styles: 'color' } ); // Adds element's style
     *		viewConsumable.add( p, { attributes: 'name', styles: 'color' } ); // Adds attribute and style.
     *		viewConsumable.add( p, { classes: [ '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, { attributes: 'style' } ); // This call will throw an exception.
     *		viewConsumable.add( p, { styles: '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.attributes Attribute name or array of attribute names.
     * @param {String|Array.<String>} consumables.classes Class name or array of class names.
     * @param {String|Array.<String>} consumables.styles 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 ViewElementConsumables(element);
            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, { attributes: 'name' } ); // Tests attribute.
     *		viewConsumable.test( p, { classes: 'foobar' } ); // Tests class.
     *		viewConsumable.test( p, { styles: 'color' } ); // Tests style.
     *		viewConsumable.test( p, { attributes: 'name', styles: 'color' } ); // Tests attribute and style.
     *		viewConsumable.test( p, { classes: [ '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, { attributes: 'class' } ); // Tests if all added classes can be consumed.
     *		viewConsumable.test( p, { attributes: '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.attributes Attribute name or array of attribute names.
     * @param {String|Array.<String>} consumables.classes Class name or array of class names.
     * @param {String|Array.<String>} consumables.styles 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, { attributes: 'name' } ); // Consumes element's attribute.
     *		viewConsumable.consume( p, { classes: 'foobar' } ); // Consumes element's class.
     *		viewConsumable.consume( p, { styles: 'color' } ); // Consumes element's style.
     *		viewConsumable.consume( p, { attributes: 'name', styles: 'color' } ); // Consumes attribute and style.
     *		viewConsumable.consume( p, { classes: [ '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, { attributes: 'class' } ); // Consume only if all added classes can be consumed.
     *		viewConsumable.consume( p, { attributes: '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.attributes Attribute name or array of attribute names.
     * @param {String|Array.<String>} consumables.classes Class name or array of class names.
     * @param {String|Array.<String>} consumables.styles 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, { attributes: 'name' } ); // Reverts element's attribute.
     *		viewConsumable.revert( p, { classes: 'foobar' } ); // Reverts element's class.
     *		viewConsumable.revert( p, { styles: 'color' } ); // Reverts element's style.
     *		viewConsumable.revert( p, { attributes: 'name', styles: 'color' } ); // Reverts attribute and style.
     *		viewConsumable.revert( p, { classes: [ '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, { attributes: 'class' } ); // Reverts all classes added for consumption.
     *		viewConsumable.revert( p, { attributes: '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.attributes Attribute name or array of attribute names.
     * @param {String|Array.<String>} consumables.classes Class name or array of class names.
     * @param {String|Array.<String>} consumables.styles 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 = {
            element,
            name: true,
            attributes: [],
            classes: [],
            styles: []
        };
        const attributes = element.getAttributeKeys();
        for (const attribute of attributes) {
            // Skip classes and styles - will be added separately.
            if (attribute == 'style' || attribute == 'class') {
                continue;
            }
            consumables.attributes.push(attribute);
        }
        const classes = element.getClassNames();
        for (const className of classes) {
            consumables.classes.push(className);
        }
        const styles = element.getStyleNames();
        for (const style of styles) {
            consumables.styles.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;
    }
}
const CONSUMABLE_TYPES = ['attributes', 'classes', 'styles'];
/**
 * 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 ViewElementConsumables {
    /**
     * Creates ViewElementConsumables instance.
     *
     * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} from View node or document fragment
     * from which `ViewElementConsumables` is being created.
     */
    constructor(from) {
        /**
         * @readonly
         * @member {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment}
         */
        this.element = from;
        /**
         * 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 = {
            attributes: new Map(),
            styles: new Map(),
            classes: 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( { attributes: 'title', classes: 'foo', styles: 'color' } );
     *		consumables.add( { attributes: [ 'title', 'name' ], classes: [ '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.attributes Attribute name or array of attribute names to add as consumable.
     * @param {String|Array.<String>} consumables.classes Class name or array of class names to add as consumable.
     * @param {String|Array.<String>} consumables.styles Style name or array of style names to add as consumable.
     */
    add(consumables) {
        if (consumables.name) {
            this._canConsumeName = true;
        }
        for (const type of CONSUMABLE_TYPES) {
            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( { attributes: 'title', classes: 'foo', styles: 'color' } );
     *		consumables.test( { attributes: [ 'title', 'name' ], classes: [ '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.attributes Attribute name or array of attribute names to test.
     * @param {String|Array.<String>} consumables.classes Class name or array of class names to test.
     * @param {String|Array.<String>} consumables.styles 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 of CONSUMABLE_TYPES) {
            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( { attributes: 'title', classes: 'foo', styles: 'color' } );
     *		consumables.consume( { attributes: [ 'title', 'name' ], classes: [ '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.attributes Attribute name or array of attribute names to consume.
     * @param {String|Array.<String>} consumables.classes Class name or array of class names to consume.
     * @param {String|Array.<String>} consumables.styles Style name or array of style names to consume.
     */
    consume(consumables) {
        if (consumables.name) {
            this._canConsumeName = false;
        }
        for (const type of CONSUMABLE_TYPES) {
            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( { attributes: 'title', classes: 'foo', styles: 'color' } );
     *		consumables.revert( { attributes: [ 'title', 'name' ], classes: [ '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.attributes Attribute name or array of attribute names to revert.
     * @param {String|Array.<String>} consumables.classes Class name or array of class names to revert.
     * @param {String|Array.<String>} consumables.styles Style name or array of style names to revert.
     */
    revert(consumables) {
        if (consumables.name) {
            this._canConsumeName = true;
        }
        for (const type of CONSUMABLE_TYPES) {
            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: `attributes`, `classes` or `styles`.
     * @param {String|Array.<String>} item Consumable item or array of items.
     */
    _add(type, item) {
        const items = isArray$1(item) ? item : [item];
        const consumables = this._consumables[type];
        for (const name of items) {
            if (type === 'attributes' && (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( { attributes: [ 'class', 'style' ] } );
                 *
                 * While each class and style should be registered separately:
                 *
                 *		consumables.add( { classes: 'some-class', styles: 'font-weight' } );
                 *
                 * @error viewconsumable-invalid-attribute
                 */
                throw new CKEditorError('viewconsumable-invalid-attribute', this);
            }
            consumables.set(name, true);
            if (type === 'styles') {
                for (const alsoName of this.element.document.stylesProcessor.getRelatedStyles(name)) {
                    consumables.set(alsoName, true);
                }
            }
        }
    }
    /**
     * Helper method that tests consumables of a given type: attribute, class or style.
     *
     * @private
     * @param {String} type Type of the consumable item: `attributes`, `classes` or `styles`.
     * @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 = isArray$1(item) ? item : [item];
        const consumables = this._consumables[type];
        for (const name of items) {
            if (type === 'attributes' && (name === 'class' || name === 'style')) {
                const consumableName = name == 'class' ? 'classes' : 'styles';
                // Check all classes/styles if class/style attribute is tested.
                const value = this._test(consumableName, [...this._consumables[consumableName].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: `attributes`, `classes` or `styles`.
     * @param {String|Array.<String>} item Consumable item or array of items.
     */
    _consume(type, item) {
        const items = isArray$1(item) ? item : [item];
        const consumables = this._consumables[type];
        for (const name of items) {
            if (type === 'attributes' && (name === 'class' || name === 'style')) {
                const consumableName = name == 'class' ? 'classes' : 'styles';
                // If class or style is provided for consumption - consume them all.
                this._consume(consumableName, [...this._consumables[consumableName].keys()]);
            }
            else {
                consumables.set(name, false);
                if (type == 'styles') {
                    for (const toConsume of this.element.document.stylesProcessor.getRelatedStyles(name)) {
                        consumables.set(toConsume, false);
                    }
                }
            }
        }
    }
    /**
     * Helper method that reverts items of a given type: attribute, class or style.
     *
     * @private
     * @param {String} type Type of the consumable item: `attributes`, `classes` or , `styles`.
     * @param {String|Array.<String>} item Consumable item or array of items.
     */
    _revert(type, item) {
        const items = isArray$1(item) ? item : [item];
        const consumables = this._consumables[type];
        for (const name of items) {
            if (type === 'attributes' && (name === 'class' || name === 'style')) {
                const consumableName = name == 'class' ? 'classes' : 'styles';
                // If class or style is provided for reverting - revert them all.
                this._revert(consumableName, [...this._consumables[consumableName].keys()]);
            }
            else {
                const value = consumables.get(name);
                if (value === false) {
                    consumables.set(name, true);
                }
            }
        }
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * The model's schema. It defines the allowed and disallowed structures of nodes as well as nodes' attributes.
 * The schema is usually defined by the features and based on them, the editing framework and features
 * make decisions on how to change and process the model.
 *
 * The instance of schema is available in {@link module:engine/model/model~Model#schema `editor.model.schema`}.
 *
 * Read more about the schema in:
 *
 * * The {@glink framework/guides/architecture/editing-engine#schema schema section} of the
 * {@glink framework/guides/architecture/editing-engine Introduction to the Editing engine architecture} guide.
 * * The {@glink framework/guides/deep-dive/schema Schema deep dive} guide.
 *
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class Schema extends Observable {
    /**
     * Creates a schema instance.
     */
    constructor() {
        super();
        this._sourceDefinitions = {};
        /**
         * A dictionary containing attribute properties.
         *
         * @private
         * @member {Object.<String,AttributeProperties>}
         */
        this._attributeProperties = {};
        this.decorate('checkChild');
        this.decorate('checkAttribute');
        this.on('checkAttribute', (evt, args) => {
            args[0] = new SchemaContext(args[0]);
        }, { priority: 'highest' });
        this.on('checkChild', (evt, args) => {
            args[0] = new SchemaContext(args[0]);
            args[1] = this.getDefinition(args[1]);
        }, { priority: 'highest' });
    }
    /**
     * Registers a schema item. Can only be called once for every item name.
     *
     *		schema.register( 'paragraph', {
     *			inheritAllFrom: '$block'
     *		} );
     *
     * @param {String} itemName
     * @param {module:engine/model/schema~SchemaItemDefinition} definition
     */
    register(itemName, definition) {
        if (this._sourceDefinitions[itemName]) {
            /**
             * A single item cannot be registered twice in the schema.
             *
             * This situation may happen when:
             *
             * * Two or more plugins called {@link #register `register()`} with the same name. This will usually mean that
             * there is a collision between plugins which try to use the same element in the model. Unfortunately,
             * the only way to solve this is by modifying one of these plugins to use a unique model element name.
             * * A single plugin was loaded twice. This happens when it is installed by npm/yarn in two versions
             * and usually means one or more of the following issues:
             *     * a version mismatch (two of your dependencies require two different versions of this plugin),
             *     * incorrect imports (this plugin is somehow imported twice in a way which confuses webpack),
             *     * mess in `node_modules/` (`rm -rf node_modules/` may help).
             *
             * **Note:** Check the logged `itemName` to better understand which plugin was duplicated/conflicting.
             *
             * @param itemName The name of the model element that is being registered twice.
             * @error schema-cannot-register-item-twice
             */
            throw new CKEditorError('schema-cannot-register-item-twice', this, {
                itemName
            });
        }
        this._sourceDefinitions[itemName] = [
            Object.assign({}, definition)
        ];
        this._clearCache();
    }
    /**
     * Extends a {@link #register registered} item's definition.
     *
     * Extending properties such as `allowIn` will add more items to the existing properties,
     * while redefining properties such as `isBlock` will override the previously defined ones.
     *
     *		schema.register( 'foo', {
     *			allowIn: '$root',
     *			isBlock: true;
     *		} );
     *		schema.extend( 'foo', {
     *			allowIn: 'blockQuote',
     *			isBlock: false
     *		} );
     *
     *		schema.getDefinition( 'foo' );
     *		//	{
     *		//		allowIn: [ '$root', 'blockQuote' ],
     *		// 		isBlock: false
     *		//	}
     *
     * @param {String} itemName
     * @param {module:engine/model/schema~SchemaItemDefinition} definition
     */
    extend(itemName, definition) {
        if (!this._sourceDefinitions[itemName]) {
            /**
             * Cannot extend an item which was not registered yet.
             *
             * This error happens when a plugin tries to extend the schema definition of an item which was not
             * {@link #register registered} yet.
             *
             * @param itemName The name of the model element which is being extended.
             * @error schema-cannot-extend-missing-item
             */
            throw new CKEditorError('schema-cannot-extend-missing-item', this, {
                itemName
            });
        }
        this._sourceDefinitions[itemName].push(Object.assign({}, definition));
        this._clearCache();
    }
    /**
     * Returns data of all registered items.
     *
     * This method should normally be used for reflection purposes (e.g. defining a clone of a certain element,
     * checking a list of all block elements, etc).
     * Use specific methods (such as {@link #checkChild `checkChild()`} or {@link #isLimit `isLimit()`})
     * in other cases.
     *
     * @returns {Object.<String,module:engine/model/schema~SchemaCompiledItemDefinition>}
     */
    getDefinitions() {
        if (!this._compiledDefinitions) {
            this._compile();
        }
        return this._compiledDefinitions;
    }
    /**
     * Returns a definition of the given item or `undefined` if an item is not registered.
     *
     * This method should normally be used for reflection purposes (e.g. defining a clone of a certain element,
     * checking a list of all block elements, etc).
     * Use specific methods (such as {@link #checkChild `checkChild()`} or {@link #isLimit `isLimit()`})
     * in other cases.
     *
     * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
     * @returns {module:engine/model/schema~SchemaCompiledItemDefinition}
     */
    getDefinition(item) {
        let itemName;
        if (typeof item == 'string') {
            itemName = item;
        }
        else if ('is' in item && (item.is('$text') || item.is('$textProxy'))) {
            itemName = '$text';
        }
        // Element or module:engine/model/schema~SchemaContextItem.
        else {
            itemName = item.name;
        }
        return this.getDefinitions()[itemName];
    }
    /**
     * Returns `true` if the given item is registered in the schema.
     *
     *		schema.isRegistered( 'paragraph' ); // -> true
     *		schema.isRegistered( editor.model.document.getRoot() ); // -> true
     *		schema.isRegistered( 'foo' ); // -> false
     *
     * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
     * @returns {Boolean}
     */
    isRegistered(item) {
        return !!this.getDefinition(item);
    }
    /**
     * Returns `true` if the given item is defined to be
     * a block by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isBlock` property.
     *
     *		schema.isBlock( 'paragraph' ); // -> true
     *		schema.isBlock( '$root' ); // -> false
     *
     *		const paragraphElement = writer.createElement( 'paragraph' );
     *		schema.isBlock( paragraphElement ); // -> true
     *
     * See the {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of
     * the {@glink framework/guides/deep-dive/schema Schema deep dive} guide for more details.
     *
     * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
     * @returns {Boolean}
     */
    isBlock(item) {
        const def = this.getDefinition(item);
        return !!(def && def.isBlock);
    }
    /**
     * Returns `true` if the given item should be treated as a limit element.
     *
     * It considers an item to be a limit element if its
     * {@link module:engine/model/schema~SchemaItemDefinition}'s
     * {@link module:engine/model/schema~SchemaItemDefinition#isLimit `isLimit`} or
     * {@link module:engine/model/schema~SchemaItemDefinition#isObject `isObject`} property
     * was set to `true`.
     *
     *		schema.isLimit( 'paragraph' ); // -> false
     *		schema.isLimit( '$root' ); // -> true
     *		schema.isLimit( editor.model.document.getRoot() ); // -> true
     *		schema.isLimit( 'imageBlock' ); // -> true
     *
     * See the {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of
     * the {@glink framework/guides/deep-dive/schema Schema deep dive} guide for more details.
     *
     * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
     * @returns {Boolean}
     */
    isLimit(item) {
        const def = this.getDefinition(item);
        if (!def) {
            return false;
        }
        return !!(def.isLimit || def.isObject);
    }
    /**
     * Returns `true` if the given item should be treated as an object element.
     *
     * It considers an item to be an object element if its
     * {@link module:engine/model/schema~SchemaItemDefinition}'s
     * {@link module:engine/model/schema~SchemaItemDefinition#isObject `isObject`} property
     * was set to `true`.
     *
     *		schema.isObject( 'paragraph' ); // -> false
     *		schema.isObject( 'imageBlock' ); // -> true
     *
     *		const imageElement = writer.createElement( 'imageBlock' );
     *		schema.isObject( imageElement ); // -> true
     *
     * See the {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of
     * the {@glink framework/guides/deep-dive/schema Schema deep dive} guide for more details.
     *
     * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
     * @returns {Boolean}
     */
    isObject(item) {
        const def = this.getDefinition(item);
        if (!def) {
            return false;
        }
        // Note: Check out the implementation of #isLimit(), #isSelectable(), and #isContent()
        // to understand why these three constitute an object.
        return !!(def.isObject || (def.isLimit && def.isSelectable && def.isContent));
    }
    /**
     * Returns `true` if the given item is defined to be
     * an inline element by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isInline` property.
     *
     *		schema.isInline( 'paragraph' ); // -> false
     *		schema.isInline( 'softBreak' ); // -> true
     *
     *		const text = writer.createText( 'foo' );
     *		schema.isInline( text ); // -> true
     *
     * See the {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of
     * the {@glink framework/guides/deep-dive/schema Schema deep dive} guide for more details.
     *
     * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
     * @returns {Boolean}
     */
    isInline(item) {
        const def = this.getDefinition(item);
        return !!(def && def.isInline);
    }
    /**
     * Returns `true` if the given item is defined to be
     * a selectable element by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isSelectable` property.
     *
     *		schema.isSelectable( 'paragraph' ); // -> false
     *		schema.isSelectable( 'heading1' ); // -> false
     *		schema.isSelectable( 'imageBlock' ); // -> true
     *		schema.isSelectable( 'tableCell' ); // -> true
     *
     *		const text = writer.createText( 'foo' );
     *		schema.isSelectable( text ); // -> false
     *
     * See the {@glink framework/guides/deep-dive/schema#selectable-elements Selectable elements section} of
     * the {@glink framework/guides/deep-dive/schema Schema deep dive} guide for more details.
     *
     * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
     * @returns {Boolean}
     */
    isSelectable(item) {
        const def = this.getDefinition(item);
        if (!def) {
            return false;
        }
        return !!(def.isSelectable || def.isObject);
    }
    /**
     * Returns `true` if the given item is defined to be
     * a content by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isContent` property.
     *
     *		schema.isContent( 'paragraph' ); // -> false
     *		schema.isContent( 'heading1' ); // -> false
     *		schema.isContent( 'imageBlock' ); // -> true
     *		schema.isContent( 'horizontalLine' ); // -> true
     *
     *		const text = writer.createText( 'foo' );
     *		schema.isContent( text ); // -> true
     *
     * See the {@glink framework/guides/deep-dive/schema#content-elements Content elements section} of
     * the {@glink framework/guides/deep-dive/schema Schema deep dive} guide for more details.
     *
     * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
     * @returns {Boolean}
     */
    isContent(item) {
        const def = this.getDefinition(item);
        if (!def) {
            return false;
        }
        return !!(def.isContent || def.isObject);
    }
    /**
     * Checks whether the given node (`child`) can be a child of the given context.
     *
     *		schema.checkChild( model.document.getRoot(), paragraph ); // -> false
     *
     *		schema.register( 'paragraph', {
     *			allowIn: '$root'
     *		} );
     *		schema.checkChild( model.document.getRoot(), paragraph ); // -> true
     *
     * Note: When verifying whether the given node can be a child of the given context, the
     * schema also verifies the entire context &mdash; from its root to its last element. Therefore, it is possible
     * for `checkChild()` to return `false` even though the context's last element can contain the checked child.
     * It happens if one of the context's elements does not allow its child.
     *
     * @fires checkChild
     * @param {module:engine/model/schema~SchemaContextDefinition} context The context in which the child will be checked.
     * @param {module:engine/model/node~Node|String} def The child to check.
     * @returns {Boolean}
     */
    checkChild(context, def) {
        // Note: context and child are already normalized here to a SchemaContext and SchemaCompiledItemDefinition.
        if (!def) {
            return false;
        }
        return this._checkContextMatch(def, context);
    }
    /**
     * Checks whether the given attribute can be applied in the given context (on the last
     * item of the context).
     *
     *		schema.checkAttribute( textNode, 'bold' ); // -> false
     *
     *		schema.extend( '$text', {
     *			allowAttributes: 'bold'
     *		} );
     *		schema.checkAttribute( textNode, 'bold' ); // -> true
     *
     * @fires checkAttribute
     * @param {module:engine/model/schema~SchemaContextDefinition} context The context in which the attribute will be checked.
     * @param {String} attributeName
     * @returns {Boolean}
     */
    checkAttribute(context, attributeName) {
        const def = this.getDefinition(context.last);
        if (!def) {
            return false;
        }
        return def.allowAttributes.includes(attributeName);
    }
    /**
     * Checks whether the given element (`elementToMerge`) can be merged with the specified base element (`positionOrBaseElement`).
     *
     * In other words &mdash; whether `elementToMerge`'s children {@link #checkChild are allowed} in the `positionOrBaseElement`.
     *
     * This check ensures that elements merged with {@link module:engine/model/writer~Writer#merge `Writer#merge()`}
     * will be valid.
     *
     * Instead of elements, you can pass the instance of the {@link module:engine/model/position~Position} class as the
     * `positionOrBaseElement`. It means that the elements before and after the position will be checked whether they can be merged.
     *
     * @param {module:engine/model/position~Position|module:engine/model/element~Element} positionOrBaseElement The position or base
     * element to which the `elementToMerge` will be merged.
     * @param {module:engine/model/element~Element} elementToMerge The element to merge. Required if `positionOrBaseElement` is an element.
     * @returns {Boolean}
     */
    checkMerge(positionOrBaseElement, elementToMerge) {
        if (positionOrBaseElement instanceof Position) {
            const nodeBefore = positionOrBaseElement.nodeBefore;
            const nodeAfter = positionOrBaseElement.nodeAfter;
            if (!(nodeBefore instanceof Element)) {
                /**
                 * The node before the merge position must be an element.
                 *
                 * @error schema-check-merge-no-element-before
                 */
                throw new CKEditorError('schema-check-merge-no-element-before', this);
            }
            if (!(nodeAfter instanceof Element)) {
                /**
                 * The node after the merge position must be an element.
                 *
                 * @error schema-check-merge-no-element-after
                 */
                throw new CKEditorError('schema-check-merge-no-element-after', this);
            }
            return this.checkMerge(nodeBefore, nodeAfter);
        }
        for (const child of elementToMerge.getChildren()) {
            if (!this.checkChild(positionOrBaseElement, child)) {
                return false;
            }
        }
        return true;
    }
    /**
     * Allows registering a callback to the {@link #checkChild} method calls.
     *
     * Callbacks allow you to implement rules which are not otherwise possible to achieve
     * by using the declarative API of {@link module:engine/model/schema~SchemaItemDefinition}.
     * For example, by using this method you can disallow elements in specific contexts.
     *
     * This method is a shorthand for using the {@link #event:checkChild} event. For even better control,
     * you can use that event instead.
     *
     * Example:
     *
     *		// Disallow heading1 directly inside a blockQuote.
     *		schema.addChildCheck( ( context, childDefinition ) => {
     *			if ( context.endsWith( 'blockQuote' ) && childDefinition.name == 'heading1' ) {
     *				return false;
     *			}
     *		} );
     *
     * Which translates to:
     *
     *		schema.on( 'checkChild', ( evt, args ) => {
     *			const context = args[ 0 ];
     *			const childDefinition = args[ 1 ];
     *
     *			if ( context.endsWith( 'blockQuote' ) && childDefinition && childDefinition.name == 'heading1' ) {
     *				// Prevent next listeners from being called.
     *				evt.stop();
     *				// Set the checkChild()'s return value.
     *				evt.return = false;
     *			}
     *		}, { priority: 'high' } );
     *
     * @param {Function} callback The callback to be called. It is called with two parameters:
     * {@link module:engine/model/schema~SchemaContext} (context) instance and
     * {@link module:engine/model/schema~SchemaCompiledItemDefinition} (child-to-check definition).
     * The callback may return `true/false` to override `checkChild()`'s return value. If it does not return
     * a boolean value, the default algorithm (or other callbacks) will define `checkChild()`'s return value.
     */
    addChildCheck(callback) {
        this.on('checkChild', (evt, [ctx, childDef]) => {
            // checkChild() was called with a non-registered child.
            // In 99% cases such check should return false, so not to overcomplicate all callbacks
            // don't even execute them.
            if (!childDef) {
                return;
            }
            const retValue = callback(ctx, childDef);
            if (typeof retValue == 'boolean') {
                evt.stop();
                evt.return = retValue;
            }
        }, { priority: 'high' });
    }
    /**
     * Allows registering a callback to the {@link #checkAttribute} method calls.
     *
     * Callbacks allow you to implement rules which are not otherwise possible to achieve
     * by using the declarative API of {@link module:engine/model/schema~SchemaItemDefinition}.
     * For example, by using this method you can disallow attribute if node to which it is applied
     * is contained within some other element (e.g. you want to disallow `bold` on `$text` within `heading1`).
     *
     * This method is a shorthand for using the {@link #event:checkAttribute} event. For even better control,
     * you can use that event instead.
     *
     * Example:
     *
     *		// Disallow bold on $text inside heading1.
     *		schema.addAttributeCheck( ( context, attributeName ) => {
     *			if ( context.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
     *				return false;
     *			}
     *		} );
     *
     * Which translates to:
     *
     *		schema.on( 'checkAttribute', ( evt, args ) => {
     *			const context = args[ 0 ];
     *			const attributeName = args[ 1 ];
     *
     *			if ( context.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
     *				// Prevent next listeners from being called.
     *				evt.stop();
     *				// Set the checkAttribute()'s return value.
     *				evt.return = false;
     *			}
     *		}, { priority: 'high' } );
     *
     * @param {Function} callback The callback to be called. It is called with two parameters:
     * {@link module:engine/model/schema~SchemaContext} (context) instance and attribute name.
     * The callback may return `true/false` to override `checkAttribute()`'s return value. If it does not return
     * a boolean value, the default algorithm (or other callbacks) will define `checkAttribute()`'s return value.
     */
    addAttributeCheck(callback) {
        this.on('checkAttribute', (evt, [ctx, attributeName]) => {
            const retValue = callback(ctx, attributeName);
            if (typeof retValue == 'boolean') {
                evt.stop();
                evt.return = retValue;
            }
        }, { priority: 'high' });
    }
    /**
     * This method allows assigning additional metadata to the model attributes. For example,
     * {@link module:engine/model/schema~AttributeProperties `AttributeProperties#isFormatting` property} is
     * used to mark formatting attributes (like `bold` or `italic`).
     *
     *		// Mark bold as a formatting attribute.
     *		schema.setAttributeProperties( 'bold', {
     *			isFormatting: true
     *		} );
     *
     *		// Override code not to be considered a formatting markup.
     *		schema.setAttributeProperties( 'code', {
     *			isFormatting: false
     *		} );
     *
     * Properties are not limited to members defined in the
     * {@link module:engine/model/schema~AttributeProperties `AttributeProperties` type} and you can also use custom properties:
     *
     *		schema.setAttributeProperties( 'blockQuote', {
     *			customProperty: 'value'
     *		} );
     *
     * Subsequent calls with the same attribute will extend its custom properties:
     *
     *		schema.setAttributeProperties( 'blockQuote', {
     *			one: 1
     *		} );
     *
     *		schema.setAttributeProperties( 'blockQuote', {
     *			two: 2
     *		} );
     *
     *		console.log( schema.getAttributeProperties( 'blockQuote' ) );
     *		// Logs: { one: 1, two: 2 }
     *
     * @param {String} attributeName A name of the attribute to receive the properties.
     * @param {module:engine/model/schema~AttributeProperties} properties A dictionary of properties.
     */
    setAttributeProperties(attributeName, properties) {
        this._attributeProperties[attributeName] = Object.assign(this.getAttributeProperties(attributeName), properties);
    }
    /**
     * Returns properties associated with a given model attribute. See {@link #setAttributeProperties `setAttributeProperties()`}.
     *
     * @param {String} attributeName A name of the attribute.
     * @returns {module:engine/model/schema~AttributeProperties}
     */
    getAttributeProperties(attributeName) {
        return this._attributeProperties[attributeName] || {};
    }
    /**
     * Returns the lowest {@link module:engine/model/schema~Schema#isLimit limit element} containing the entire
     * selection/range/position or the root otherwise.
     *
     * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
     * module:engine/model/range~Range|module:engine/model/position~Position} selectionOrRangeOrPosition
     * The selection/range/position to check.
     * @returns {module:engine/model/element~Element} The lowest limit element containing
     * the entire `selectionOrRangeOrPosition`.
     */
    getLimitElement(selectionOrRangeOrPosition) {
        let element;
        if (selectionOrRangeOrPosition instanceof Position) {
            element = selectionOrRangeOrPosition.parent;
        }
        else {
            const ranges = selectionOrRangeOrPosition instanceof Range ?
                [selectionOrRangeOrPosition] :
                Array.from(selectionOrRangeOrPosition.getRanges());
            // Find the common ancestor for all selection's ranges.
            element = ranges
                .reduce((element, range) => {
                const rangeCommonAncestor = range.getCommonAncestor();
                if (!element) {
                    return rangeCommonAncestor;
                }
                return element.getCommonAncestor(rangeCommonAncestor, { includeSelf: true });
            }, null);
        }
        while (!this.isLimit(element)) {
            if (element.parent) {
                element = element.parent;
            }
            else {
                break;
            }
        }
        return element;
    }
    /**
     * Checks whether the attribute is allowed in selection:
     *
     * * if the selection is not collapsed, then checks if the attribute is allowed on any of nodes in that range,
     * * if the selection is collapsed, then checks if on the selection position there's a text with the
     * specified attribute allowed.
     *
     * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
     * Selection which will be checked.
     * @param {String} attribute The name of the attribute to check.
     * @returns {Boolean}
     */
    checkAttributeInSelection(selection, attribute) {
        if (selection.isCollapsed) {
            const firstPosition = selection.getFirstPosition();
            const context = [
                ...firstPosition.getAncestors(),
                new Text$1('', selection.getAttributes())
            ];
            // Check whether schema allows for a text with the attribute in the selection.
            return this.checkAttribute(context, attribute);
        }
        else {
            const ranges = selection.getRanges();
            // For all ranges, check nodes in them until you find a node that is allowed to have the attribute.
            for (const range of ranges) {
                for (const value of range) {
                    if (this.checkAttribute(value.item, attribute)) {
                        // If we found a node that is allowed to have the attribute, return true.
                        return true;
                    }
                }
            }
        }
        // If we haven't found such node, return false.
        return false;
    }
    /**
     * Transforms the given set of ranges into a set of ranges where the given attribute is allowed (and can be applied).
     *
     * @param {Iterable.<module:engine/model/range~Range>} ranges Ranges to be validated.
     * @param {String} attribute The name of the attribute to check.
     * @returns {Iterable.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
     */
    *getValidRanges(ranges, attribute) {
        ranges = convertToMinimalFlatRanges(ranges);
        for (const range of ranges) {
            yield* this._getValidRangesForRange(range, attribute);
        }
    }
    /**
     * Basing on given `position`, finds and returns a {@link module:engine/model/range~Range range} which is
     * nearest to that `position` and is a correct range for selection.
     *
     * The correct selection range might be collapsed when it is located in a position where the text node can be placed.
     * Non-collapsed range is returned when selection can be placed around element marked as an "object" in
     * the {@link module:engine/model/schema~Schema schema}.
     *
     * Direction of searching for the nearest correct selection range can be specified as:
     *
     * * `both` - searching will be performed in both ways,
     * * `forward` - searching will be performed only forward,
     * * `backward` - searching will be performed only backward.
     *
     * When valid selection range cannot be found, `null` is returned.
     *
     * @param {module:engine/model/position~Position} position Reference position where new selection range should be looked for.
     * @param {'both'|'forward'|'backward'} [direction='both'] Search direction.
     * @returns {module:engine/model/range~Range|null} Nearest selection range or `null` if one cannot be found.
     */
    getNearestSelectionRange(position, direction = 'both') {
        // Return collapsed range if provided position is valid.
        if (this.checkChild(position, '$text')) {
            return new Range(position);
        }
        let backwardWalker, forwardWalker;
        // Never leave a limit element.
        const limitElement = position.getAncestors().reverse().find(item => this.isLimit(item)) ||
            position.root;
        if (direction == 'both' || direction == 'backward') {
            backwardWalker = new TreeWalker({
                boundaries: Range._createIn(limitElement),
                startPosition: position,
                direction: 'backward'
            });
        }
        if (direction == 'both' || direction == 'forward') {
            forwardWalker = new TreeWalker({
                boundaries: Range._createIn(limitElement),
                startPosition: position
            });
        }
        for (const data of combineWalkers(backwardWalker, forwardWalker)) {
            const type = (data.walker == backwardWalker ? 'elementEnd' : 'elementStart');
            const value = data.value;
            if (value.type == type && this.isObject(value.item)) {
                return Range._createOn(value.item);
            }
            if (this.checkChild(value.nextPosition, '$text')) {
                return new Range(value.nextPosition);
            }
        }
        return null;
    }
    /**
     * Tries to find position ancestors that allow to insert a given node.
     * It starts searching from the given position and goes node by node to the top of the model tree
     * as long as a {@link module:engine/model/schema~Schema#isLimit limit element}, an
     * {@link module:engine/model/schema~Schema#isObject object element} or a topmost ancestor is not reached.
     *
     * @param {module:engine/model/position~Position} position The position that the search will start from.
     * @param {module:engine/model/node~Node|String} node The node for which an allowed parent should be found or its name.
     * @returns {module:engine/model/element~Element|null} element Allowed parent or null if nothing was found.
     */
    findAllowedParent(position, node) {
        let parent = position.parent;
        while (parent) {
            if (this.checkChild(parent, node)) {
                return parent;
            }
            // Do not split limit elements.
            if (this.isLimit(parent)) {
                return null;
            }
            parent = parent.parent;
        }
        return null;
    }
    /**
     * Sets attributes allowed by the schema on a given node.
     *
     * @param {module:engine/model/node~Node} node A node to set attributes on.
     * @param {Object} attributes Attributes keys and values.
     * @param {module:engine/model/writer~Writer} writer An instance of the model writer.
     */
    setAllowedAttributes(node, attributes, writer) {
        const model = writer.model;
        for (const [attributeName, attributeValue] of Object.entries(attributes)) {
            if (model.schema.checkAttribute(node, attributeName)) {
                writer.setAttribute(attributeName, attributeValue, node);
            }
        }
    }
    /**
     * Removes attributes disallowed by the schema.
     *
     * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes that will be filtered.
     * @param {module:engine/model/writer~Writer} writer
     */
    removeDisallowedAttributes(nodes, writer) {
        for (const node of nodes) {
            // When node is a `Text` it has no children, so just filter it out.
            if (node.is('$text')) {
                removeDisallowedAttributeFromNode(this, node, writer);
            }
            // In a case of `Element` iterates through positions between nodes inside this element
            // and filter out node before the current position, or position parent when position
            // is at start of an element. Using positions prevent from omitting merged nodes
            // see https://github.com/ckeditor/ckeditor5-engine/issues/1789.
            else {
                const rangeInNode = Range._createIn(node);
                const positionsInRange = rangeInNode.getPositions();
                for (const position of positionsInRange) {
                    const item = position.nodeBefore || position.parent;
                    removeDisallowedAttributeFromNode(this, item, writer);
                }
            }
        }
    }
    /**
     * Gets attributes of a node that have a given property.
     *
     * @param {module:engine/model/node~Node} node Node to get attributes from.
     * @param {String} propertyName Name of the property that attribute must have to return it.
     * @param {Boolean|Symbol|String|Number|Object|null|undefined} propertyValue Desired value of the property that we want to check.
     * When `undefined` attributes will be returned if they have set a given property no matter what the value is. If specified it will
     * return attributes which given property's value is equal to this parameter.
     * @returns {Object} Object with attributes' names as key and attributes' values as value.
     */
    getAttributesWithProperty(node, propertyName, propertyValue) {
        const attributes = {};
        for (const [attributeName, attributeValue] of node.getAttributes()) {
            const attributeProperties = this.getAttributeProperties(attributeName);
            if (attributeProperties[propertyName] === undefined) {
                continue;
            }
            if (propertyValue === undefined || propertyValue === attributeProperties[propertyName]) {
                attributes[attributeName] = attributeValue;
            }
        }
        return attributes;
    }
    /**
     * Creates an instance of the schema context.
     *
     * @param {module:engine/model/schema~SchemaContextDefinition} context
     * @returns {module:engine/model/schema~SchemaContext}
     */
    createContext(context) {
        return new SchemaContext(context);
    }
    /**
     * @private
     */
    _clearCache() {
        this._compiledDefinitions = null;
    }
    /**
     * @private
     */
    _compile() {
        const compiledDefinitions = {};
        const sourceRules = this._sourceDefinitions;
        const itemNames = Object.keys(sourceRules);
        for (const itemName of itemNames) {
            compiledDefinitions[itemName] = compileBaseItemRule(sourceRules[itemName], itemName);
        }
        for (const itemName of itemNames) {
            compileAllowChildren(compiledDefinitions, itemName);
        }
        for (const itemName of itemNames) {
            compileAllowContentOf(compiledDefinitions, itemName);
        }
        for (const itemName of itemNames) {
            compileAllowWhere(compiledDefinitions, itemName);
        }
        for (const itemName of itemNames) {
            compileAllowAttributesOf(compiledDefinitions, itemName);
            compileInheritPropertiesFrom(compiledDefinitions, itemName);
        }
        for (const itemName of itemNames) {
            cleanUpAllowIn(compiledDefinitions, itemName);
            setupAllowChildren(compiledDefinitions, itemName);
            cleanUpAllowAttributes(compiledDefinitions, itemName);
        }
        this._compiledDefinitions = compiledDefinitions;
    }
    /**
     * @private
     * @param {module:engine/model/schema~SchemaCompiledItemDefinition} def
     * @param {module:engine/model/schema~SchemaContext} context
     * @param {Number} contextItemIndex
     */
    _checkContextMatch(def, context, contextItemIndex = context.length - 1) {
        const contextItem = context.getItem(contextItemIndex);
        if (def.allowIn.includes(contextItem.name)) {
            if (contextItemIndex == 0) {
                return true;
            }
            else {
                const parentRule = this.getDefinition(contextItem);
                return this._checkContextMatch(parentRule, context, contextItemIndex - 1);
            }
        }
        else {
            return false;
        }
    }
    /**
     * Takes a flat range and an attribute name. Traverses the range recursively and deeply to find and return all ranges
     * inside the given range on which the attribute can be applied.
     *
     * This is a helper function for {@link ~Schema#getValidRanges}.
     *
     * @private
     * @param {module:engine/model/range~Range} range The range to process.
     * @param {String} attribute The name of the attribute to check.
     * @returns {Iterable.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
     */
    *_getValidRangesForRange(range, attribute) {
        let start = range.start;
        let end = range.start;
        for (const item of range.getItems({ shallow: true })) {
            if (item.is('element')) {
                yield* this._getValidRangesForRange(Range._createIn(item), attribute);
            }
            if (!this.checkAttribute(item, attribute)) {
                if (!start.isEqual(end)) {
                    yield new Range(start, end);
                }
                start = Position._createAfter(item);
            }
            end = Position._createAfter(item);
        }
        if (!start.isEqual(end)) {
            yield new Range(start, end);
        }
    }
}
/**
 * A schema context &mdash; a list of ancestors of a given position in the document.
 *
 * Considering such position:
 *
 *		<$root>
 *			<blockQuote>
 *				<paragraph>
 *					^
 *				</paragraph>
 *			</blockQuote>
 *		</$root>
 *
 * The context of this position is its {@link module:engine/model/position~Position#getAncestors lists of ancestors}:
 *
 *		[ rootElement, blockQuoteElement, paragraphElement ]
 *
 * Contexts are used in the {@link module:engine/model/schema~Schema#event:checkChild `Schema#checkChild`} and
 * {@link module:engine/model/schema~Schema#event:checkAttribute `Schema#checkAttribute`} events as a definition
 * of a place in the document where the check occurs. The context instances are created based on the first arguments
 * of the {@link module:engine/model/schema~Schema#checkChild `Schema#checkChild()`} and
 * {@link module:engine/model/schema~Schema#checkAttribute `Schema#checkAttribute()`} methods so when
 * using these methods you need to use {@link module:engine/model/schema~SchemaContextDefinition}s.
 */
class SchemaContext {
    /**
     * Creates an instance of the context.
     *
     * @param {module:engine/model/schema~SchemaContextDefinition} context
     */
    constructor(context) {
        if (context instanceof SchemaContext) {
            return context;
        }
        let items;
        if (typeof context == 'string') {
            items = [context];
        }
        else if (!Array.isArray(context)) {
            // `context` is item or position.
            // Position#getAncestors() doesn't accept any parameters but it works just fine here.
            items = context.getAncestors({ includeSelf: true });
        }
        else {
            items = context;
        }
        this._items = items.map(mapContextItem);
    }
    /**
     * The number of items.
     *
     * @type {Number}
     */
    get length() {
        return this._items.length;
    }
    /**
     * The last item (the lowest node).
     *
     * @type {module:engine/model/schema~SchemaContextItem}
     */
    get last() {
        return this._items[this._items.length - 1];
    }
    /**
     * Iterable interface.
     *
     * Iterates over all context items.
     *
     * @returns {Iterable.<module:engine/model/schema~SchemaContextItem>}
     */
    [Symbol.iterator]() {
        return this._items[Symbol.iterator]();
    }
    /**
     * Returns a new schema context instance with an additional item.
     *
     * Item can be added as:
     *
     * 		const context = new SchemaContext( [ '$root' ] );
     *
     * 		// An element.
     * 		const fooElement = writer.createElement( 'fooElement' );
     * 		const newContext = context.push( fooElement ); // [ '$root', 'fooElement' ]
     *
     * 		// A text node.
     * 		const text = writer.createText( 'foobar' );
     * 		const newContext = context.push( text ); // [ '$root', '$text' ]
     *
     * 		// A string (element name).
     * 		const newContext = context.push( 'barElement' ); // [ '$root', 'barElement' ]
     *
     * **Note** {@link module:engine/model/node~Node} that is already in the model tree will be added as the only item
     * (without ancestors).
     *
     * @param {String|module:engine/model/node~Node} item An item that will be added
     * to the current context.
     * @returns {module:engine/model/schema~SchemaContext} A new schema context instance with an additional item.
     */
    push(item) {
        const ctx = new SchemaContext([item]);
        ctx._items = [...this._items, ...ctx._items];
        return ctx;
    }
    /**
     * Gets an item on the given index.
     *
     * @returns {module:engine/model/schema~SchemaContextItem}
     */
    getItem(index) {
        return this._items[index];
    }
    /**
     * Returns the names of items.
     *
     * @returns {Iterable.<String>}
     */
    *getNames() {
        yield* this._items.map(item => item.name);
    }
    /**
     * Checks whether the context ends with the given nodes.
     *
     *		const ctx = new SchemaContext( [ rootElement, paragraphElement, textNode ] );
     *
     *		ctx.endsWith( '$text' ); // -> true
     *		ctx.endsWith( 'paragraph $text' ); // -> true
     *		ctx.endsWith( '$root' ); // -> false
     *		ctx.endsWith( 'paragraph' ); // -> false
     *
     * @param {String} query
     * @returns {Boolean}
     */
    endsWith(query) {
        return Array.from(this.getNames()).join(' ').endsWith(query);
    }
    /**
     * Checks whether the context starts with the given nodes.
     *
     *		const ctx = new SchemaContext( [ rootElement, paragraphElement, textNode ] );
     *
     *		ctx.endsWith( '$root' ); // -> true
     *		ctx.endsWith( '$root paragraph' ); // -> true
     *		ctx.endsWith( '$text' ); // -> false
     *		ctx.endsWith( 'paragraph' ); // -> false
     *
     * @param {String} query
     * @returns {Boolean}
     */
    startsWith(query) {
        return Array.from(this.getNames()).join(' ').startsWith(query);
    }
}
function compileBaseItemRule(sourceItemRules, itemName) {
    const itemRule = {
        name: itemName,
        allowIn: [],
        allowContentOf: [],
        allowWhere: [],
        allowAttributes: [],
        allowAttributesOf: [],
        allowChildren: [],
        inheritTypesFrom: []
    };
    copyTypes(sourceItemRules, itemRule);
    copyProperty(sourceItemRules, itemRule, 'allowIn');
    copyProperty(sourceItemRules, itemRule, 'allowContentOf');
    copyProperty(sourceItemRules, itemRule, 'allowWhere');
    copyProperty(sourceItemRules, itemRule, 'allowAttributes');
    copyProperty(sourceItemRules, itemRule, 'allowAttributesOf');
    copyProperty(sourceItemRules, itemRule, 'allowChildren');
    copyProperty(sourceItemRules, itemRule, 'inheritTypesFrom');
    makeInheritAllWork(sourceItemRules, itemRule);
    return itemRule;
}
function compileAllowChildren(compiledDefinitions, itemName) {
    const item = compiledDefinitions[itemName];
    for (const allowChildrenItem of item.allowChildren) {
        const allowedChildren = compiledDefinitions[allowChildrenItem];
        // The allowChildren property may point to an unregistered element.
        if (!allowedChildren) {
            continue;
        }
        allowedChildren.allowIn.push(itemName);
    }
    // The allowIn property already includes correct items, reset the allowChildren property
    // to avoid duplicates later when setting up compilation results.
    item.allowChildren.length = 0;
}
function compileAllowContentOf(compiledDefinitions, itemName) {
    for (const allowContentOfItemName of compiledDefinitions[itemName].allowContentOf) {
        // The allowContentOf property may point to an unregistered element.
        if (compiledDefinitions[allowContentOfItemName]) {
            const allowedChildren = getAllowedChildren(compiledDefinitions, allowContentOfItemName);
            allowedChildren.forEach(allowedItem => {
                allowedItem.allowIn.push(itemName);
            });
        }
    }
    delete compiledDefinitions[itemName].allowContentOf;
}
function compileAllowWhere(compiledDefinitions, itemName) {
    for (const allowWhereItemName of compiledDefinitions[itemName].allowWhere) {
        const inheritFrom = compiledDefinitions[allowWhereItemName];
        // The allowWhere property may point to an unregistered element.
        if (inheritFrom) {
            const allowedIn = inheritFrom.allowIn;
            compiledDefinitions[itemName].allowIn.push(...allowedIn);
        }
    }
    delete compiledDefinitions[itemName].allowWhere;
}
function compileAllowAttributesOf(compiledDefinitions, itemName) {
    for (const allowAttributeOfItem of compiledDefinitions[itemName].allowAttributesOf) {
        const inheritFrom = compiledDefinitions[allowAttributeOfItem];
        if (inheritFrom) {
            const inheritAttributes = inheritFrom.allowAttributes;
            compiledDefinitions[itemName].allowAttributes.push(...inheritAttributes);
        }
    }
    delete compiledDefinitions[itemName].allowAttributesOf;
}
function compileInheritPropertiesFrom(compiledDefinitions, itemName) {
    const item = compiledDefinitions[itemName];
    for (const inheritPropertiesOfItem of item.inheritTypesFrom) {
        const inheritFrom = compiledDefinitions[inheritPropertiesOfItem];
        if (inheritFrom) {
            const typeNames = Object.keys(inheritFrom).filter(name => name.startsWith('is'));
            for (const name of typeNames) {
                if (!(name in item)) {
                    item[name] = inheritFrom[name];
                }
            }
        }
    }
    delete item.inheritTypesFrom;
}
// Remove items which weren't registered (because it may break some checks or we'd need to complicate them).
// Make sure allowIn doesn't contain repeated values.
function cleanUpAllowIn(compiledDefinitions, itemName) {
    const itemRule = compiledDefinitions[itemName];
    const existingItems = itemRule.allowIn.filter(itemToCheck => compiledDefinitions[itemToCheck]);
    itemRule.allowIn = Array.from(new Set(existingItems));
}
// Setup allowChildren items based on allowIn.
function setupAllowChildren(compiledDefinitions, itemName) {
    const itemRule = compiledDefinitions[itemName];
    for (const allowedParentItemName of itemRule.allowIn) {
        const allowedParentItem = compiledDefinitions[allowedParentItemName];
        allowedParentItem.allowChildren.push(itemName);
    }
}
function cleanUpAllowAttributes(compiledDefinitions, itemName) {
    const itemRule = compiledDefinitions[itemName];
    itemRule.allowAttributes = Array.from(new Set(itemRule.allowAttributes));
}
function copyTypes(sourceItemRules, itemRule) {
    for (const sourceItemRule of sourceItemRules) {
        const typeNames = Object.keys(sourceItemRule).filter(name => name.startsWith('is'));
        for (const name of typeNames) {
            itemRule[name] = !!sourceItemRule[name];
        }
    }
}
function copyProperty(sourceItemRules, itemRule, propertyName) {
    for (const sourceItemRule of sourceItemRules) {
        const value = sourceItemRule[propertyName];
        if (typeof value == 'string') {
            itemRule[propertyName].push(value);
        }
        else if (Array.isArray(value)) {
            itemRule[propertyName].push(...value);
        }
    }
}
function makeInheritAllWork(sourceItemRules, itemRule) {
    for (const sourceItemRule of sourceItemRules) {
        const inheritFrom = sourceItemRule.inheritAllFrom;
        if (inheritFrom) {
            itemRule.allowContentOf.push(inheritFrom);
            itemRule.allowWhere.push(inheritFrom);
            itemRule.allowAttributesOf.push(inheritFrom);
            itemRule.inheritTypesFrom.push(inheritFrom);
        }
    }
}
function getAllowedChildren(compiledDefinitions, itemName) {
    const itemRule = compiledDefinitions[itemName];
    return getValues(compiledDefinitions).filter(def => def.allowIn.includes(itemRule.name));
}
function getValues(obj) {
    return Object.keys(obj).map(key => obj[key]);
}
function mapContextItem(ctxItem) {
    if (typeof ctxItem == 'string' || ctxItem.is('documentFragment')) {
        return {
            name: typeof ctxItem == 'string' ? ctxItem : '$documentFragment',
            *getAttributeKeys() { },
            getAttribute() { }
        };
    }
    else {
        return {
            // '$text' means text nodes and text proxies.
            name: ctxItem.is('element') ? ctxItem.name : '$text',
            *getAttributeKeys() {
                yield* ctxItem.getAttributeKeys();
            },
            getAttribute(key) {
                return ctxItem.getAttribute(key);
            }
        };
    }
}
// Generator function returning values from provided walkers, switching between them at each iteration. If only one walker
// is provided it will return data only from that walker.
//
// @param {module:engine/module/treewalker~TreeWalker} [backward] Walker iterating in backward direction.
// @param {module:engine/module/treewalker~TreeWalker} [forward] Walker iterating in forward direction.
// @returns {Iterable.<Object>} Object returned at each iteration contains `value` and `walker` (informing which walker returned
// given value) fields.
function* combineWalkers(backward, forward) {
    let done = false;
    while (!done) {
        done = true;
        if (backward) {
            const step = backward.next();
            if (!step.done) {
                done = false;
                yield {
                    walker: backward,
                    value: step.value
                };
            }
        }
        if (forward) {
            const step = forward.next();
            if (!step.done) {
                done = false;
                yield {
                    walker: forward,
                    value: step.value
                };
            }
        }
    }
}
// Takes an array of non-intersecting ranges. For each of them gets minimal flat ranges covering that range and returns
// all those minimal flat ranges.
//
// @param {Array.<module:engine/model/range~Range>} ranges Ranges to process.
// @returns {Iterable.<module:engine/model/range~Range>} Minimal flat ranges of given `ranges`.
function* convertToMinimalFlatRanges(ranges) {
    for (const range of ranges) {
        yield* range.getMinimalFlatRanges();
    }
}
function removeDisallowedAttributeFromNode(schema, node, writer) {
    for (const attribute of node.getAttributeKeys()) {
        if (!schema.checkAttribute(node, attribute)) {
            writer.removeAttribute(attribute, node);
        }
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Upcast dispatcher is a central point of the view-to-model conversion, which is a process of
 * converting a given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
 * {@link module:engine/view/element~Element view element} into a correct model structure.
 *
 * During the conversion process, the dispatcher fires events for all {@link module:engine/view/node~Node view nodes}
 * from the converted view document fragment.
 * Special callbacks called "converters" should listen to these events in order to convert the view nodes.
 *
 * The second parameter of the callback is the `data` object with the following properties:
 *
 * * `data.viewItem` contains a {@link module:engine/view/node~Node view node} or a
 * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
 * that is converted at the moment and might be handled by the callback.
 * * `data.modelRange` is used to point to the result
 * of the current conversion (e.g. the element that is being inserted)
 * and is always a {@link module:engine/model/range~Range} when the conversion succeeds.
 * * `data.modelCursor` is a {@link module:engine/model/position~Position position} on which the converter should insert
 * the newly created items.
 *
 * The third parameter of the callback is an instance of {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi}
 * which provides additional tools for converters.
 *
 * You can read more about conversion in the {@glink framework/guides/deep-dive/conversion/upcast Upcast conversion} guide.
 *
 * Examples of event-based converters:
 *
 *		// A converter for links (<a>).
 *		editor.data.upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
 *			if ( conversionApi.consumable.consume( data.viewItem, { name: true, attributes: [ 'href' ] } ) ) {
 *				// The <a> element is inline and is represented by an attribute in the model.
 *				// This is why you need to convert only children.
 *				const { modelRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
 *
 *				for ( let item of modelRange.getItems() ) {
 *					if ( conversionApi.schema.checkAttribute( item, 'linkHref' ) ) {
 *						conversionApi.writer.setAttribute( 'linkHref', data.viewItem.getAttribute( 'href' ), item );
 *					}
 *				}
 *			}
 *		} );
 *
 *		// Convert <p> element's font-size style.
 *		// Note: You should use a low-priority observer in order to ensure that
 *		// it is executed after the element-to-element converter.
 *		editor.data.upcastDispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
 *			const { consumable, schema, writer } = conversionApi;
 *
 *			if ( !consumable.consume( data.viewItem, { style: 'font-size' } ) ) {
 *				return;
 *			}
 *
 *			const fontSize = data.viewItem.getStyle( 'font-size' );
 *
 *			// Do not go for the model element after data.modelCursor because it might happen
 *			// that a single view element was converted to multiple model elements. Get all of them.
 *			for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
 *				if ( schema.checkAttribute( item, 'fontSize' ) ) {
 *					writer.setAttribute( 'fontSize', fontSize, item );
 *				}
 *			}
 *		}, { priority: 'low' } );
 *
 *		// Convert all elements which have no custom converter into a paragraph (autoparagraphing).
 *		editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
 *			// Check if an element can be converted.
 *			if ( !conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
 *				// When an element is already consumed by higher priority converters, do nothing.
 *				return;
 *			}
 *
 *			const paragraph = conversionApi.writer.createElement( 'paragraph' );
 *
 *			// Try to safely insert a paragraph at the model cursor - it will find an allowed parent for the current element.
 *			if ( !conversionApi.safeInsert( paragraph, data.modelCursor ) ) {
 *				// When an element was not inserted, it means that you cannot insert a paragraph at this position.
 *				return;
 *			}
 *
 *			// Consume the inserted element.
 *			conversionApi.consumable.consume( data.viewItem, { name: data.viewItem.name } ) );
 *
 *			// Convert the children to a paragraph.
 *			const { modelRange } = conversionApi.convertChildren( data.viewItem,  paragraph ) );
 *
 *			// Update `modelRange` and `modelCursor` in the `data` as a conversion result.
 *			conversionApi.updateConversionResult( paragraph, data );
 *		}, { priority: 'low' } );
 *
 * @mixes module:utils/emittermixin~EmitterMixin
 * @fires viewCleanup
 * @fires element
 * @fires text
 * @fires documentFragment
 */
class UpcastDispatcher extends Emitter$1 {
    /**
     * Creates an upcast dispatcher that operates using the passed API.
     *
     * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi
     * @param {Object} [conversionApi] Additional properties for an interface that will be passed to events fired
     * by the upcast dispatcher.
     */
    constructor(conversionApi) {
        super();
        /**
         * The list of elements that were created during splitting.
         *
         * After the conversion process, the list is cleared.
         *
         * @private
         * @type {Map.<module:engine/model/element~Element,Array.<module:engine/model/element~Element>>}
         */
        this._splitParts = new Map();
        /**
         * The list of cursor parent elements that were created during splitting.
         *
         * After the conversion process the list is cleared.
         *
         * @private
         * @type {Map.<module:engine/model/element~Element,Array.<module:engine/model/element~Element>>}
         */
        this._cursorParents = new Map();
        /**
         * The position in the temporary structure where the converted content is inserted. The structure reflects the context of
         * the target position where the content will be inserted. This property is built based on the context parameter of the
         * convert method.
         *
         * @private
         * @type {module:engine/model/position~Position|null}
         */
        this._modelCursor = null;
        /**
         * The list of elements that were created during the splitting but should not get removed on conversion end even if they are empty.
         *
         * The list is cleared after the conversion process.
         *
         * @private
         * @type {Set.<module:engine/model/element~Element>}
         */
        this._emptyElementsToKeep = new Set();
        /**
         * An interface passed by the dispatcher to the event callbacks.
         *
         * @member {module:engine/conversion/upcastdispatcher~UpcastConversionApi}
         */
        this.conversionApi = {
            ...conversionApi,
            consumable: null,
            writer: null,
            store: null,
            convertItem: (viewItem, modelCursor) => this._convertItem(viewItem, modelCursor),
            convertChildren: (viewElement, positionOrElement) => this._convertChildren(viewElement, positionOrElement),
            safeInsert: (modelElement, position) => this._safeInsert(modelElement, position),
            updateConversionResult: (modelElement, data) => this._updateConversionResult(modelElement, data),
            // Advanced API - use only if custom position handling is needed.
            splitToAllowedParent: (modelElement, modelCursor) => this._splitToAllowedParent(modelElement, modelCursor),
            getSplitParts: modelElement => this._getSplitParts(modelElement),
            keepEmptyElement: modelElement => this._keepEmptyElement(modelElement)
        };
    }
    /**
     * 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} viewElement
     * The part of the view to be converted.
     * @param {module:engine/model/writer~Writer} writer An instance of the model writer.
     * @param {module:engine/model/schema~SchemaContextDefinition} [context=['$root']] Elements will be converted according to this context.
     * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is the result of the conversion process
     * wrapped in `DocumentFragment`. Converted marker elements will be set as the document fragment's
     * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
     */
    convert(viewElement, writer, context = ['$root']) {
        this.fire('viewCleanup', viewElement);
        // Create context tree and set position in the top element.
        // Items will be converted according to this position.
        this._modelCursor = createContextTree(context, writer);
        // Store writer in conversion as a conversion API
        // to be sure that conversion process will use the same batch.
        this.conversionApi.writer = writer;
        // Create consumable values list for conversion process.
        this.conversionApi.consumable = ViewConsumable.createFrom(viewElement);
        // Custom data stored by converter for conversion process.
        this.conversionApi.store = {};
        // Do the conversion.
        const { modelRange } = this._convertItem(viewElement, this._modelCursor);
        // Conversion result is always a document fragment so let's create it.
        const documentFragment = writer.createDocumentFragment();
        // When there is a conversion result.
        if (modelRange) {
            // Remove all empty elements that were create while splitting.
            this._removeEmptyElements();
            // Move all items that were converted in context tree to the document fragment.
            for (const item of Array.from(this._modelCursor.parent.getChildren())) {
                writer.append(item, documentFragment);
            }
            // Extract temporary markers elements from model and set as static markers collection.
            documentFragment.markers = extractMarkersFromModelFragment(documentFragment, writer);
        }
        // Clear context position.
        this._modelCursor = null;
        // Clear split elements & parents lists.
        this._splitParts.clear();
        this._cursorParents.clear();
        this._emptyElementsToKeep.clear();
        // Clear conversion API.
        this.conversionApi.writer = null;
        this.conversionApi.store = null;
        // Return fragment as conversion result.
        return documentFragment;
    }
    /**
     * @private
     * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertItem
     */
    _convertItem(viewItem, modelCursor) {
        const data = { viewItem, modelCursor, modelRange: null };
        if (viewItem.is('element')) {
            this.fire(`element:${viewItem.name}`, data, this.conversionApi);
        }
        else if (viewItem.is('$text')) {
            this.fire('text', data, this.conversionApi);
        }
        else {
            this.fire('documentFragment', data, this.conversionApi);
        }
        // Handle incorrect conversion result.
        if (data.modelRange && !(data.modelRange instanceof Range)) {
            /**
             * Incorrect conversion result was dropped.
             *
             * {@link module:engine/model/range~Range Model range} should be a conversion result.
             *
             * @error view-conversion-dispatcher-incorrect-result
             */
            throw new CKEditorError('view-conversion-dispatcher-incorrect-result', this);
        }
        return { modelRange: data.modelRange, modelCursor: data.modelCursor };
    }
    /**
     * @private
     * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertChildren
     */
    _convertChildren(viewItem, elementOrModelCursor) {
        let nextModelCursor = elementOrModelCursor.is('position') ?
            elementOrModelCursor : Position._createAt(elementOrModelCursor, 0);
        const modelRange = new Range(nextModelCursor);
        for (const viewChild of Array.from(viewItem.getChildren())) {
            const result = this._convertItem(viewChild, nextModelCursor);
            if (result.modelRange instanceof Range) {
                modelRange.end = result.modelRange.end;
                nextModelCursor = result.modelCursor;
            }
        }
        return { modelRange, modelCursor: nextModelCursor };
    }
    /**
     * @private
     * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#safeInsert
     */
    _safeInsert(modelElement, position) {
        // Find allowed parent for element that we are going to insert.
        // If current parent does not allow to insert element but one of the ancestors does
        // then split nodes to allowed parent.
        const splitResult = this._splitToAllowedParent(modelElement, position);
        // When there is no split result it means that we can't insert element to model tree, so let's skip it.
        if (!splitResult) {
            return false;
        }
        // Insert element on allowed position.
        this.conversionApi.writer.insert(modelElement, splitResult.position);
        return true;
    }
    /**
     * @private
     * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#updateConversionResult
     */
    _updateConversionResult(modelElement, data) {
        const parts = this._getSplitParts(modelElement);
        const writer = this.conversionApi.writer;
        // Set conversion result range - only if not set already.
        if (!data.modelRange) {
            data.modelRange = writer.createRange(writer.createPositionBefore(modelElement), writer.createPositionAfter(parts[parts.length - 1]));
        }
        const savedCursorParent = this._cursorParents.get(modelElement);
        // Now we need to check where the `modelCursor` should be.
        if (savedCursorParent) {
            // If we split parent to insert our element then we want to continue conversion in the new part of the split parent.
            //
            // before: <allowed><notAllowed>foo[]</notAllowed></allowed>
            // after:  <allowed><notAllowed>foo</notAllowed> <converted></converted> <notAllowed>[]</notAllowed></allowed>
            data.modelCursor = writer.createPositionAt(savedCursorParent, 0);
        }
        else {
            // Otherwise just continue after inserted element.
            data.modelCursor = data.modelRange.end;
        }
    }
    /**
     * @private
     * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#splitToAllowedParent
     */
    _splitToAllowedParent(node, modelCursor) {
        const { schema, writer } = this.conversionApi;
        // Try to find allowed parent.
        let allowedParent = schema.findAllowedParent(modelCursor, node);
        if (allowedParent) {
            // When current position parent allows to insert node then return this position.
            if (allowedParent === modelCursor.parent) {
                return { position: modelCursor };
            }
            // When allowed parent is in context tree (it's outside the converted tree).
            if (this._modelCursor.parent.getAncestors().includes(allowedParent)) {
                allowedParent = null;
            }
        }
        if (!allowedParent) {
            // Check if the node wrapped with a paragraph would be accepted by the schema.
            if (!isParagraphable(modelCursor, node, schema)) {
                return null;
            }
            return {
                position: wrapInParagraph(modelCursor, writer)
            };
        }
        // Split element to allowed parent.
        const splitResult = this.conversionApi.writer.split(modelCursor, allowedParent);
        // Using the range returned by `model.Writer#split`, we will pair original elements with their split parts.
        //
        // The range returned from the writer spans "over the split" or, precisely saying, from the end of the original element (the one
        // that got split) to the beginning of the other part of that element:
        //
        // <limit><a><b><c>X[]Y</c></b><a></limit> ->
        // <limit><a><b><c>X[</c></b></a><a><b><c>]Y</c></b></a>
        //
        // After the split there cannot be any full node between the positions in `splitRange`. The positions are touching.
        // Also, because of how splitting works, it is easy to notice, that "closing tags" are in the reverse order than "opening tags".
        // Also, since we split all those elements, each of them has to have the other part.
        //
        // With those observations in mind, we will pair the original elements with their split parts by saving "closing tags" and matching
        // them with "opening tags" in the reverse order. For that we can use a stack.
        const stack = [];
        for (const treeWalkerValue of splitResult.range.getWalker()) {
            if (treeWalkerValue.type == 'elementEnd') {
                stack.push(treeWalkerValue.item);
            }
            else {
                // There should not be any text nodes after the element is split, so the only other value is `elementStart`.
                const originalPart = stack.pop();
                const splitPart = treeWalkerValue.item;
                this._registerSplitPair(originalPart, splitPart);
            }
        }
        const cursorParent = splitResult.range.end.parent;
        this._cursorParents.set(node, cursorParent);
        return {
            position: splitResult.position,
            cursorParent
        };
    }
    /**
     * Registers that a `splitPart` element is a split part of the `originalPart` element.
     *
     * The data set by this method is used by {@link #_getSplitParts} and {@link #_removeEmptyElements}.
     *
     * @private
     * @param {module:engine/model/element~Element} originalPart
     * @param {module:engine/model/element~Element} splitPart
     */
    _registerSplitPair(originalPart, splitPart) {
        if (!this._splitParts.has(originalPart)) {
            this._splitParts.set(originalPart, [originalPart]);
        }
        const list = this._splitParts.get(originalPart);
        this._splitParts.set(splitPart, list);
        list.push(splitPart);
    }
    /**
     * @private
     * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#getSplitParts
     */
    _getSplitParts(element) {
        let parts;
        if (!this._splitParts.has(element)) {
            parts = [element];
        }
        else {
            parts = this._splitParts.get(element);
        }
        return parts;
    }
    /**
     * Mark an element that were created during the splitting to not get removed on conversion end even if it is empty.
     *
     * @private
     */
    _keepEmptyElement(element) {
        this._emptyElementsToKeep.add(element);
    }
    /**
     * Checks if there are any empty elements created while splitting and removes them.
     *
     * This method works recursively to re-check empty elements again after at least one element was removed in the initial call,
     * as some elements might have become empty after other empty elements were removed from them.
     *
     * @private
     */
    _removeEmptyElements() {
        let anyRemoved = false;
        for (const element of this._splitParts.keys()) {
            if (element.isEmpty && !this._emptyElementsToKeep.has(element)) {
                this.conversionApi.writer.remove(element);
                this._splitParts.delete(element);
                anyRemoved = true;
            }
        }
        if (anyRemoved) {
            this._removeEmptyElements();
        }
    }
}
// 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, writer) {
    const markerElements = new Set();
    const markers = new Map();
    // Create ModelTreeWalker.
    const range = Range._createIn(modelItem).getItems();
    // Walk through DocumentFragment and collect marker elements.
    for (const item of range) {
        // Check if current element is a marker.
        if (item.is('element', '$marker')) {
            markerElements.add(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 = writer.createPositionBefore(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(currentPosition.clone()));
            // Otherwise is means that we have found end of the marker range.
        }
        else {
            markers.get(markerName).end = currentPosition.clone();
        }
        // Remove marker element from DocumentFragment.
        writer.remove(markerElement);
    }
    return markers;
}
// Creates model fragment according to given context and returns position in the bottom (the deepest) element.
function createContextTree(contextDefinition, writer) {
    let position;
    for (const item of new SchemaContext(contextDefinition)) {
        const attributes = {};
        for (const key of item.getAttributeKeys()) {
            attributes[key] = item.getAttribute(key);
        }
        const current = writer.createElement(item.name, attributes);
        if (position) {
            writer.insert(current, position);
        }
        position = Position._createAt(current, 0);
    }
    return position;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module engine/dataprocessor/basichtmlwriter
 */
/* globals document */
/**
 * Basic HTML writer. It uses the native `innerHTML` property for basic conversion
 * from a document fragment to an HTML string.
 *
 * @implements module:engine/dataprocessor/htmlwriter~HtmlWriter
 */
class BasicHtmlWriter {
    /**
     * Returns an HTML string created from the document fragment.
     *
     * @param {DocumentFragment} fragment
     * @returns {String}
     */
    getHtml(fragment) {
        const doc = document.implementation.createHTMLDocument('');
        const container = doc.createElement('div');
        container.appendChild(fragment);
        return container.innerHTML;
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * The HTML data processor class.
 * This data processor implementation uses HTML as input and output data.
 *
 * @implements module:engine/dataprocessor/dataprocessor~DataProcessor
 */
class HtmlDataProcessor {
    /**
     * Creates a new instance of the HTML data processor class.
     *
     * @param {module:engine/view/document~Document} document The view document instance.
     */
    constructor(document) {
        /**
         * A DOM parser instance used to parse an HTML string to an HTML document.
         *
         * @member {DOMParser}
         */
        this.domParser = new DOMParser();
        /**
         * A DOM converter used to convert DOM elements to view elements.
         *
         * @member {module:engine/view/domconverter~DomConverter}
         */
        this.domConverter = new DomConverter(document, { renderingMode: 'data' });
        /**
         * A basic HTML writer instance used to convert DOM elements to an HTML string.
         *
         * @member {module:engine/dataprocessor/htmlwriter~HtmlWriter}
         */
        this.htmlWriter = new BasicHtmlWriter();
    }
    /**
     * Converts a provided {@link module:engine/view/documentfragment~DocumentFragment document fragment}
     * to data format &mdash; in this case to an HTML string.
     *
     * @param {module:engine/view/documentfragment~DocumentFragment} viewFragment
     * @returns {String} HTML string.
     */
    toData(viewFragment) {
        // Convert view DocumentFragment to DOM DocumentFragment.
        const domFragment = this.domConverter.viewToDom(viewFragment);
        // Convert DOM DocumentFragment to HTML output.
        return this.htmlWriter.getHtml(domFragment);
    }
    /**
     * Converts the provided HTML string to a view tree.
     *
     * @param {String} data An HTML string.
     * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} A converted view element.
     */
    toView(data) {
        // Convert input HTML data to DOM DocumentFragment.
        const domFragment = this._toDom(data);
        // Convert DOM DocumentFragment to view DocumentFragment.
        return this.domConverter.domToView(domFragment);
    }
    /**
     * Registers a {@link module:engine/view/matcher~MatcherPattern} for view elements whose content should be treated as raw data
     * and not processed during the conversion from the DOM to the view elements.
     *
     * The raw data can be later accessed by a
     * {@link module:engine/view/element~Element#getCustomProperty custom property of a view element} called `"$rawContent"`.
     *
     * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements whose content should
     * be treated as raw data.
     */
    registerRawContentMatcher(pattern) {
        this.domConverter.registerRawContentMatcher(pattern);
    }
    /**
     * If the processor is set to use marked fillers, it will insert `&nbsp;` fillers wrapped in `<span>` elements
     * (`<span data-cke-filler="true">&nbsp;</span>`) instead of regular `&nbsp;` characters.
     *
     * This mode allows for a more precise handling of the block fillers (so they do not leak into the editor content) but
     * bloats the editor data with additional markup.
     *
     * This mode may be required by some features and will be turned on by them automatically.
     *
     * @param {'default'|'marked'} type Whether to use the default or the marked `&nbsp;` block fillers.
     */
    useFillerType(type) {
        this.domConverter.blockFillerMode = type == 'marked' ? 'markedNbsp' : 'nbsp';
    }
    /**
     * Converts an HTML string to its DOM representation. Returns a document fragment containing nodes parsed from
     * the provided data.
     *
     * @private
     * @param {String} data
     * @returns {DocumentFragment}
     */
    _toDom(data) {
        // Wrap data with a <body> tag so leading non-layout nodes (like <script>, <style>, HTML comment)
        // will be preserved in the body collection.
        // Do it only for data that is not a full HTML document.
        if (!data.match(/<(?:html|body|head|meta)(?:\s[^>]*)?>/i)) {
            data = `<body>${data}</body>`;
        }
        const document = this.domParser.parseFromString(data, 'text/html');
        const fragment = document.createDocumentFragment();
        const bodyChildNodes = document.body.childNodes;
        while (bodyChildNodes.length > 0) {
            fragment.appendChild(bodyChildNodes[0]);
        }
        return fragment;
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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 the given:
 *
 * * {@link module:engine/dataprocessor/dataprocessor~DataProcessor data processor},
 * * downcast converters,
 * * upcast converters.
 *
 * An instance of the data controller is always available in the {@link module:core/editor/editor~Editor#data `editor.data`}
 * property:
 *
 *		editor.data.get( { rootName: 'customRoot' } ); // -> '<p>Hello!</p>'
 *
 * @mixes module:utils/emittermixin~EmitterMixin
 */
class DataController extends Emitter$1 {
    /**
     * Creates a data controller instance.
     *
     * @param {module:engine/model/model~Model} model Data model.
     * @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor The styles processor instance.
     */
    constructor(model, stylesProcessor) {
        super();
        /**
         * Data model.
         *
         * @readonly
         * @member {module:engine/model/model~Model}
         */
        this.model = model;
        /**
         * Mapper used for the conversion. It has no permanent bindings, because these are created while getting data and
         * ae cleared directly after the data are converted. However, the mapper is defined as a class property, because
         * it needs to be passed to the `DowncastDispatcher` as a conversion API.
         *
         * @readonly
         * @member {module:engine/conversion/mapper~Mapper}
         */
        this.mapper = new Mapper();
        /**
         * Downcast dispatcher used by the {@link #get get method}. Downcast converters should be attached to it.
         *
         * @readonly
         * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher}
         */
        this.downcastDispatcher = new DowncastDispatcher({
            mapper: this.mapper,
            schema: model.schema
        });
        this.downcastDispatcher.on('insert:$text', insertText(), { priority: 'lowest' });
        this.downcastDispatcher.on('insert', insertAttributesAndChildren(), { priority: 'lowest' });
        /**
         * Upcast dispatcher used by the {@link #set set method}. Upcast converters should be attached to it.
         *
         * @readonly
         * @member {module:engine/conversion/upcastdispatcher~UpcastDispatcher}
         */
        this.upcastDispatcher = new UpcastDispatcher({
            schema: model.schema
        });
        /**
         * The view document used by the data controller.
         *
         * @readonly
         * @member {module:engine/view/document~Document}
         */
        this.viewDocument = new Document$1(stylesProcessor);
        /**
         * Styles processor used during the conversion.
         *
         * @readonly
         * @member {module:engine/view/stylesmap~StylesProcessor}
         */
        this.stylesProcessor = stylesProcessor;
        /**
         * Data processor used specifically for HTML conversion.
         *
         * @readonly
         * @member {module:engine/dataprocessor/htmldataprocessor~HtmlDataProcessor} #htmlProcessor
         */
        this.htmlProcessor = new HtmlDataProcessor(this.viewDocument);
        /**
         * Data processor used during the conversion.
         * Same instance as {@link #htmlProcessor} by default. Can be replaced at run time to handle different format, e.g. XML or Markdown.
         *
         * @member {module:engine/dataprocessor/dataprocessor~DataProcessor} #processor
         */
        this.processor = this.htmlProcessor;
        /**
         * The view downcast writer just for data conversion purposes, i.e. to modify
         * the {@link #viewDocument}.
         *
         * @private
         * @readonly
         * @member {module:engine/view/downcastwriter~DowncastWriter}
         */
        this._viewWriter = new DowncastWriter(this.viewDocument);
        // 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 therefore add `convertToModelFragment` as a last converter so it converts children of that
        // element to the document fragment so `<b>foo</b>` will still be converted to `foo` even if there is no converter for `<b>`.
        this.upcastDispatcher.on('text', convertText(), { priority: 'lowest' });
        this.upcastDispatcher.on('element', convertToModelFragment(), { priority: 'lowest' });
        this.upcastDispatcher.on('documentFragment', convertToModelFragment(), { priority: 'lowest' });
        Observable.prototype.decorate.call(this, 'init');
        Observable.prototype.decorate.call(this, 'set');
        Observable.prototype.decorate.call(this, 'get');
        // Fire the `ready` event when the initialization has completed. Such low-level listener offers the possibility
        // to plug into the initialization pipeline without interrupting the initialization flow.
        this.on('init', () => {
            this.fire('ready');
        }, { priority: 'lowest' });
        // Fix empty roots after DataController is 'ready' (note that the init method could be decorated and stopped).
        // We need to handle this event because initial data could be empty and the post-fixer would not get triggered.
        this.on('ready', () => {
            this.model.enqueueChange({ isUndoable: false }, autoParagraphEmptyRoots);
        }, { priority: 'lowest' });
    }
    /**
     * Returns the model's data converted by downcast dispatchers attached to {@link #downcastDispatcher} and
     * formatted by the {@link #processor data processor}.
     *
     * @fires get
     * @param {Object} [options] Additional configuration for the retrieved data. `DataController` provides two optional
     * properties: `rootName` and `trim`. Other properties of this object are specified by various editor features.
     * @param {String} [options.rootName='main'] Root name.
     * @param {String} [options.trim='empty'] Whether returned data should be trimmed. This option is set to `empty` by default,
     * which means whenever editor content is considered empty, an empty string will be returned. To turn off trimming completely
     * use `'none'`. In such cases the exact content will be returned (for example a `<p>&nbsp;</p>` for an empty editor).
     * @returns {String} Output data.
     */
    get(options = {}) {
        const { rootName = 'main', trim = 'empty' } = options;
        if (!this._checkIfRootsExists([rootName])) {
            /**
             * Cannot get data from a non-existing root. This error is thrown when {@link #get DataController#get() method}
             * is called with a non-existent root name. For example, if there is an editor instance with only `main` root,
             * calling {@link #get} like:
             *
             *		data.get( { rootName: 'root2' } );
             *
             * will throw this error.
             *
             * @error datacontroller-get-non-existent-root
             */
            throw new CKEditorError('datacontroller-get-non-existent-root', this);
        }
        const root = this.model.document.getRoot(rootName);
        if (trim === 'empty' && !this.model.hasContent(root, { ignoreWhitespaces: true })) {
            return '';
        }
        return this.stringify(root, options);
    }
    /**
     * 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 downcast converters
     * attached to the {@link #downcastDispatcher} and formatted by the {@link #processor data processor}.
     *
     * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
     * The element whose content will be stringified.
     * @param {Object} [options] Additional configuration passed to the conversion process.
     * @returns {String} Output data.
     */
    stringify(modelElementOrFragment, options = {}) {
        // Model -> view.
        const viewDocumentFragment = this.toView(modelElementOrFragment, options);
        // 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 downcast
     * converters attached to {@link #downcastDispatcher} into 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 whose content will be converted.
     * @param {Object} [options={}] Additional configuration that will be available through the
     * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi#options} during the conversion process.
     * @returns {module:engine/view/documentfragment~DocumentFragment} Output view DocumentFragment.
     */
    toView(modelElementOrFragment, options = {}) {
        const viewDocument = this.viewDocument;
        const viewWriter = this._viewWriter;
        // Clear bindings so the call to this method returns correct results.
        this.mapper.clearBindings();
        // First, convert elements.
        const modelRange = Range._createIn(modelElementOrFragment);
        const viewDocumentFragment = new DocumentFragment$1(viewDocument);
        this.mapper.bindElements(modelElementOrFragment, viewDocumentFragment);
        // Prepare list of markers.
        // For document fragment, simply take the markers assigned to this document fragment.
        // For model root, all markers in that root will be taken.
        // For model element, we need to check which markers are intersecting with this element and relatively modify the markers' ranges.
        // Collapsed markers at element boundary, although considered as not intersecting with the element, will also be returned.
        const markers = modelElementOrFragment.is('documentFragment') ?
            modelElementOrFragment.markers :
            _getMarkersRelativeToElement(modelElementOrFragment);
        this.downcastDispatcher.convert(modelRange, markers, viewWriter, options);
        return viewDocumentFragment;
    }
    /**
     * Sets the initial input data parsed by the {@link #processor data processor} and
     * converted by the {@link #upcastDispatcher view-to-model converters}.
     * Initial data can be only set to a document whose {@link module:engine/model/document~Document#version} is equal 0.
     *
     * **Note** This method is {@link module:utils/observablemixin~ObservableMixin#decorate decorated} which is
     * used by e.g. collaborative editing plugin that syncs remote data on init.
     *
     * When data is passed as a string, it is initialized on the default `main` root:
     *
     *		dataController.init( '<p>Foo</p>' ); // Initializes data on the `main` root only, as no other is specified.
     *
     * To initialize data on a different root or multiple roots at once, an object containing `rootName` - `data` pairs should be passed:
     *
     *		dataController.init( { main: '<p>Foo</p>', title: '<h1>Bar</h1>' } ); // Initializes data on both the `main` and `title` roots.
     *
     * @fires init
     * @param {String|Object.<String,String>} data Input data as a string or an object containing the `rootName` - `data`
     * pairs to initialize data on multiple roots at once.
     * @returns {Promise} Promise that is resolved after the data is set on the editor.
     */
    init(data) {
        if (this.model.document.version) {
            /**
             * Cannot set initial data to a non-empty {@link module:engine/model/document~Document}.
             * Initial data should be set once, during the {@link module:core/editor/editor~Editor} initialization,
             * when the {@link module:engine/model/document~Document#version} is equal 0.
             *
             * @error datacontroller-init-document-not-empty
             */
            throw new CKEditorError('datacontroller-init-document-not-empty', this);
        }
        let initialData = {};
        if (typeof data === 'string') {
            initialData.main = data; // Default root is 'main'. To initiate data on a different root, object should be passed.
        }
        else {
            initialData = data;
        }
        if (!this._checkIfRootsExists(Object.keys(initialData))) {
            /**
             * Cannot init data on a non-existent root. This error is thrown when {@link #init DataController#init() method}
             * is called with non-existent root name. For example, if there is an editor instance with only `main` root,
             * calling {@link #init} like:
             *
             * 		data.init( { main: '<p>Foo</p>', root2: '<p>Bar</p>' } );
             *
             * will throw this error.
             *
             * @error datacontroller-init-non-existent-root
             */
            throw new CKEditorError('datacontroller-init-non-existent-root', this);
        }
        this.model.enqueueChange({ isUndoable: false }, writer => {
            for (const rootName of Object.keys(initialData)) {
                const modelRoot = this.model.document.getRoot(rootName);
                writer.insert(this.parse(initialData[rootName], modelRoot), modelRoot, 0);
            }
        });
        return Promise.resolve();
    }
    /**
     * Sets the input data parsed by the {@link #processor data processor} and
     * converted by the {@link #upcastDispatcher view-to-model converters}.
     * This method can be used any time to replace existing editor data with the new one without clearing the
     * {@link module:engine/model/document~Document#history document history}.
     *
     * This method also creates a batch with all the changes applied. If all you need is to parse data, use
     * the {@link #parse} method.
     *
     * When data is passed as a string it is set on the default `main` root:
     *
     *		dataController.set( '<p>Foo</p>' ); // Sets data on the `main` root, as no other is specified.
     *
     * To set data on a different root or multiple roots at once, an object containing `rootName` - `data` pairs should be passed:
     *
     *		dataController.set( { main: '<p>Foo</p>', title: '<h1>Bar</h1>' } ); // Sets data on the `main` and `title` roots as specified.
     *
     * To set the data with a preserved undo stack and add the change to the undo stack, set `{ isUndoable: true }` as a `batchType` option.
     *
     *		dataController.set( '<p>Foo</p>', { batchType: { isUndoable: true } } );
     *
     * @fires set
     * @param {String|Object.<String,String>} data Input data as a string or an object containing the `rootName` - `data`
     * pairs to set data on multiple roots at once.
     * @param {Object} [options={}] Options for setting data.
     * @param {Object} [options.batchType] The batch type that will be used to create a batch for the changes applied by this method.
     * By default, the batch will be set as {@link module:engine/model/batch~Batch#isUndoable not undoable} and the undo stack will be
     * cleared after the new data is applied (all undo steps will be removed). If the batch type `isUndoable` flag is be set to `true`,
     * the undo stack will be preserved instead and not cleared when new data is applied.
     */
    set(data, options = {}) {
        let newData = {};
        if (typeof data === 'string') {
            newData.main = data; // The default root is 'main'. To set data on a different root, an object should be passed.
        }
        else {
            newData = data;
        }
        if (!this._checkIfRootsExists(Object.keys(newData))) {
            /**
             * Cannot set data on a non-existent root. This error is thrown when the {@link #set DataController#set() method}
             * is called with non-existent root name. For example, if there is an editor instance with only the default `main` root,
             * calling {@link #set} like:
             *
             * 		data.set( { main: '<p>Foo</p>', root2: '<p>Bar</p>' } );
             *
             * will throw this error.
             *
             * @error datacontroller-set-non-existent-root
             */
            throw new CKEditorError('datacontroller-set-non-existent-root', this);
        }
        this.model.enqueueChange(options.batchType || {}, writer => {
            writer.setSelection(null);
            writer.removeSelectionAttribute(this.model.document.selection.getAttributeKeys());
            for (const rootName of Object.keys(newData)) {
                // Save to model.
                const modelRoot = this.model.document.getRoot(rootName);
                writer.remove(writer.createRangeIn(modelRoot));
                writer.insert(this.parse(newData[rootName], modelRoot), modelRoot, 0);
            }
        });
    }
    /**
     * Returns the data parsed by the {@link #processor data processor} and then converted by upcast converters
     * attached to the {@link #upcastDispatcher}.
     *
     * @see #set
     * @param {String} data Data to parse.
     * @param {module:engine/model/schema~SchemaContextDefinition} [context='$root'] Base context in which the view will
     * be converted to the model. See: {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#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 the 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 #upcastDispatcher view-to-model converters}, wrapped by {@link module:engine/model/documentfragment~DocumentFragment}.
     *
     * When marker elements were converted during the conversion process, it will be set as a document fragment's
     * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
     *
     * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment
     * The element or document fragment whose content will be converted.
     * @param {module:engine/model/schema~SchemaContextDefinition} [context='$root'] Base context in which the view will
     * be converted to the model. See: {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#convert}.
     * @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment.
     */
    toModel(viewElementOrFragment, context = '$root') {
        return this.model.change(writer => {
            return this.upcastDispatcher.convert(viewElementOrFragment, writer, context);
        });
    }
    /**
     * Adds the style processor normalization rules.
     *
     * You can implement your own rules as well as use one of the available processor rules:
     *
     * * background: {@link module:engine/view/styles/background~addBackgroundRules}
     * * border: {@link module:engine/view/styles/border~addBorderRules}
     * * margin: {@link module:engine/view/styles/margin~addMarginRules}
     * * padding: {@link module:engine/view/styles/padding~addPaddingRules}
     *
     * @param {Function} callback
     */
    addStyleProcessorRules(callback) {
        callback(this.stylesProcessor);
    }
    /**
     * Registers a {@link module:engine/view/matcher~MatcherPattern} on an {@link #htmlProcessor htmlProcessor}
     * and a {@link #processor processor} for view elements whose content should be treated as raw data
     * and not processed during the conversion from DOM to view elements.
     *
     * The raw data can be later accessed by the {@link module:engine/view/element~Element#getCustomProperty view element custom property}
     * `"$rawContent"`.
     *
     * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements whose content should
     * be treated as a raw data.
     */
    registerRawContentMatcher(pattern) {
        // No need to register the pattern if both the `htmlProcessor` and `processor` are the same instances.
        if (this.processor && this.processor !== this.htmlProcessor) {
            this.processor.registerRawContentMatcher(pattern);
        }
        this.htmlProcessor.registerRawContentMatcher(pattern);
    }
    /**
     * Removes all event listeners set by the DataController.
     */
    destroy() {
        this.stopListening();
    }
    /**
     * Checks whether all provided root names are actually existing editor roots.
     *
     * @private
     * @param {Array.<String>} rootNames Root names to check.
     * @returns {Boolean} Whether all provided root names are existing editor roots.
     */
    _checkIfRootsExists(rootNames) {
        for (const rootName of rootNames) {
            if (!this.model.document.getRootNames().includes(rootName)) {
                return false;
            }
        }
        return true;
    }
}
// Helper function for downcast conversion.
//
// Takes a document element (element that is added to a model document) and checks which markers are inside it. If the marker is collapsed
// at element boundary, it is considered as contained inside the element and marker range is returned. Otherwise, if the marker is
// intersecting with the element, the intersection is returned.
function _getMarkersRelativeToElement(element) {
    const result = [];
    const doc = element.root.document;
    if (!doc) {
        return new Map();
    }
    const elementRange = Range._createIn(element);
    for (const marker of doc.model.markers) {
        const markerRange = marker.getRange();
        const isMarkerCollapsed = markerRange.isCollapsed;
        const isMarkerAtElementBoundary = markerRange.start.isEqual(elementRange.start) || markerRange.end.isEqual(elementRange.end);
        if (isMarkerCollapsed && isMarkerAtElementBoundary) {
            result.push([marker.name, markerRange]);
        }
        else {
            const updatedMarkerRange = elementRange.getIntersection(markerRange);
            if (updatedMarkerRange) {
                result.push([marker.name, updatedMarkerRange]);
            }
        }
    }
    // Sort the markers in a stable fashion to ensure that the order in which they are
    // added to the model's marker collection does not affect how they are
    // downcast. One particular use case that we are targeting here, is one where
    // two markers are adjacent but not overlapping, such as an insertion/deletion
    // suggestion pair representing the replacement of a range of text. In this
    // case, putting the markers in DOM order causes the first marker's end to be
    // serialized right after the second marker's start, while putting the markers
    // in reverse DOM order causes it to be right before the second marker's
    // start. So, we sort these in a way that ensures non-intersecting ranges are in
    // reverse DOM order, and intersecting ranges are in something approximating
    // reverse DOM order (since reverse DOM order doesn't have a precise meaning
    // when working with intersecting ranges).
    result.sort(([n1, r1], [n2, r2]) => {
        if (r1.end.compareWith(r2.start) !== 'after') {
            // m1.end <= m2.start -- m1 is entirely <= m2
            return 1;
        }
        else if (r1.start.compareWith(r2.end) !== 'before') {
            // m1.start >= m2.end -- m1 is entirely >= m2
            return -1;
        }
        else {
            // they overlap, so use their start positions as the primary sort key and
            // end positions as the secondary sort key
            switch (r1.start.compareWith(r2.start)) {
                case 'before':
                    return 1;
                case 'after':
                    return -1;
                default:
                    switch (r1.end.compareWith(r2.end)) {
                        case 'before':
                            return 1;
                        case 'after':
                            return -1;
                        default:
                            return n2.localeCompare(n1);
                    }
            }
        }
    });
    return new Map(result);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * A utility class that helps add converters to upcast and downcast dispatchers.
 *
 * We recommend reading the {@glink framework/guides/deep-dive/conversion/intro editor conversion} guide first to
 * understand the core concepts of the conversion mechanisms.
 *
 * An instance of the conversion manager is available in the
 * {@link module:core/editor/editor~Editor#conversion `editor.conversion`} property
 * and by default has the following groups of dispatchers (i.e. directions of conversion):
 *
 * * `downcast` (editing and data downcasts)
 * * `editingDowncast`
 * * `dataDowncast`
 * * `upcast`
 *
 * # One-way converters
 *
 * To add a converter to a specific group, use the {@link module:engine/conversion/conversion~Conversion#for `for()`}
 * method:
 *
 *		// Add a converter to editing downcast and data downcast.
 *		editor.conversion.for( 'downcast' ).elementToElement( config ) );
 *
 *		// Add a converter to the data pipepline only:
 *		editor.conversion.for( 'dataDowncast' ).elementToElement( dataConversionConfig ) );
 *
 *		// And a slightly different one for the editing pipeline:
 *		editor.conversion.for( 'editingDowncast' ).elementToElement( editingConversionConfig ) );
 *
 * See {@link module:engine/conversion/conversion~Conversion#for `for()`} method documentation to learn more about
 * available conversion helpers and how to use your custom ones.
 *
 * # Two-way converters
 *
 * Besides using one-way converters via the `for()` method, you can also use other methods available in this
 * class to add two-way converters (upcast and downcast):
 *
 * * {@link module:engine/conversion/conversion~Conversion#elementToElement `elementToElement()`} &ndash;
 * Model element to view element and vice versa.
 * * {@link module:engine/conversion/conversion~Conversion#attributeToElement `attributeToElement()`} &ndash;
 * Model attribute to view element and vice versa.
 * * {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `attributeToAttribute()`} &ndash;
 * Model attribute to view attribute and vice versa.
 */
class Conversion {
    /**
     * Creates a new conversion instance.
     *
     * @param {module:engine/conversion/downcastdispatcher~DowncastDispatcher|
     * Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher>} downcastDispatchers
     * @param {module:engine/conversion/upcastdispatcher~UpcastDispatcher|
     * Array.<module:engine/conversion/upcastdispatcher~UpcastDispatcher>} upcastDispatchers
     */
    constructor(downcastDispatchers, upcastDispatchers) {
        /**
         * Maps dispatchers group name to ConversionHelpers instances.
         *
         * @private
         * @member {Map.<String,module:engine/conversion/conversionhelpers~ConversionHelpers>}
         */
        this._helpers = new Map();
        // Define default 'downcast' & 'upcast' dispatchers groups. Those groups are always available as two-way converters needs them.
        this._downcast = toArray(downcastDispatchers);
        this._createConversionHelpers({ name: 'downcast', dispatchers: this._downcast, isDowncast: true });
        this._upcast = toArray(upcastDispatchers);
        this._createConversionHelpers({ name: 'upcast', dispatchers: this._upcast, isDowncast: false });
    }
    /**
     * Define an alias for registered dispatcher.
     *
     *		const conversion = new Conversion(
     *			[ dataDowncastDispatcher, editingDowncastDispatcher ],
     *			upcastDispatcher
     *		);
     *
     *		conversion.addAlias( 'dataDowncast', dataDowncastDispatcher );
     *
     * @param {String} alias An alias of a dispatcher.
     * @param {module:engine/conversion/downcastdispatcher~DowncastDispatcher|
     * module:engine/conversion/upcastdispatcher~UpcastDispatcher} dispatcher Dispatcher which should have an alias.
     */
    addAlias(alias, dispatcher) {
        const isDowncast = this._downcast.includes(dispatcher);
        const isUpcast = this._upcast.includes(dispatcher);
        if (!isUpcast && !isDowncast) {
            /**
             * Trying to register an alias for a dispatcher that nas not been registered.
             *
             * @error conversion-add-alias-dispatcher-not-registered
             */
            throw new CKEditorError('conversion-add-alias-dispatcher-not-registered', this);
        }
        this._createConversionHelpers({ name: alias, dispatchers: [dispatcher], isDowncast });
    }
    /**
     * Provides a chainable API to assign converters to a conversion dispatchers group.
     *
     * If the given group name has not been registered, the
     * {@link module:utils/ckeditorerror~CKEditorError `conversion-for-unknown-group` error} is thrown.
     *
     * You can use conversion helpers available directly in the `for()` chain or your custom ones via
     * the {@link module:engine/conversion/conversionhelpers~ConversionHelpers#add `add()`} method.
     *
     * # Using built-in conversion helpers
     *
     * The `for()` chain comes with a set of conversion helpers which you can use like this:
     *
     *		editor.conversion.for( 'downcast' )
     *			.elementToElement( config1 )        // Adds an element-to-element downcast converter.
     *			.attributeToElement( config2 );     // Adds an attribute-to-element downcast converter.
     *
     *		editor.conversion.for( 'upcast' )
     *			.elementToAttribute( config3 );     // Adds an element-to-attribute upcast converter.
     *
     * Refer to the documentation of built-in conversion helpers to learn about their configuration options.
     *
     * * downcast (model-to-view) conversion helpers:
     *
     *	* {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`},
     *	* {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement `attributeToElement()`},
     *	* {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToAttribute `attributeToAttribute()`}.
     *	* {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToElement `markerToElement()`}.
     *	* {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToHighlight `markerToHighlight()`}.
     *
     * * upcast (view-to-model) conversion helpers:
     *
     *	* {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToElement `elementToElement()`},
     *	* {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToAttribute `elementToAttribute()`},
     *	* {@link module:engine/conversion/upcasthelpers~UpcastHelpers#attributeToAttribute `attributeToAttribute()`}.
     *	* {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToMarker `elementToMarker()`}.
     *
     * # Using custom conversion helpers
     *
     * If you need to implement an atypical converter, you can do so by calling:
     *
     *		editor.conversion.for( direction ).add( customHelper );
     *
     * The `.add()` method takes exactly one parameter, which is a function. This function should accept one parameter that
     * is a dispatcher instance. The function should add an actual converter to the passed dispatcher instance.
     *
     * Example:
     *
     *		editor.conversion.for( 'upcast' ).add( dispatcher => {
     *			dispatcher.on( 'element:a',  ( evt, data, conversionApi ) => {
     *				// Do something with a view <a> element.
     *			} );
     *		} );
     *
     * Refer to the documentation of {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}
     * and {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} to learn how to write
     * custom converters.
     *
     * @param {String} groupName The name of dispatchers group to add the converters to.
     * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers}
     */
    for(groupName) {
        if (!this._helpers.has(groupName)) {
            /**
             * Trying to add a converter to an unknown dispatchers group.
             *
             * @error conversion-for-unknown-group
             */
            throw new CKEditorError('conversion-for-unknown-group', this);
        }
        return this._helpers.get(groupName);
    }
    /**
     * Sets up converters between the model and the view that convert a model element to a view element (and vice versa).
     * For example, the model `<paragraph>Foo</paragraph>` is turned into `<p>Foo</p>` in the view.
     *
     *		// A simple conversion from the `paragraph` model element to the `<p>` view element (and vice versa).
     *		editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
     *
     *		// Override other converters by specifying a converter definition with a higher priority.
     *		editor.conversion.elementToElement( { model: 'paragraph', view: 'div', converterPriority: 'high' } );
     *
     *		// View specified as an object instead of a string.
     *		editor.conversion.elementToElement( {
     *			model: 'fancyParagraph',
     *			view: {
     *				name: 'p',
     *				classes: 'fancy'
     *			}
     *		} );
     *
     *		// Use `upcastAlso` to define other view elements that should also be converted to a `paragraph` element.
     *		editor.conversion.elementToElement( {
     *			model: 'paragraph',
     *			view: 'p',
     *			upcastAlso: [
     *				'div',
     *				{
     *					// Any element with the `display: block` style.
     *					styles: {
     *						display: 'block'
     *					}
     *				}
     *			]
     *		} );
     *
     *		// `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
     *		editor.conversion.elementToElement( {
     *			model: 'heading',
     *			view: 'h2',
     *			// Convert "heading-like" paragraphs to headings.
     *			upcastAlso: viewElement => {
     *				const fontSize = viewElement.getStyle( 'font-size' );
     *
     *				if ( !fontSize ) {
     *					return null;
     *				}
     *
     *				const match = fontSize.match( /(\d+)\s*px/ );
     *
     *				if ( !match ) {
     *					return null;
     *				}
     *
     *				const size = Number( match[ 1 ] );
     *
     *				if ( size > 26 ) {
     *					// Returned value can be an object with the matched properties.
     *					// These properties will be "consumed" during the conversion.
     *					// See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more details.
     *
     *					return { name: true, styles: [ 'font-size' ] };
     *				}
     *
     *				return null;
     *			}
     *		} );
     *
     * `definition.model` is a `String` with a model element name to convert from or to.
     * See {@link module:engine/conversion/conversion~ConverterDefinition} to learn about other parameters.
     *
     * @param {module:engine/conversion/conversion~ConverterDefinition} definition The converter definition.
     */
    elementToElement(definition) {
        // Set up downcast converter.
        this.for('downcast').elementToElement(definition);
        // Set up upcast converter.
        for (const { model, view } of _getAllUpcastDefinitions(definition)) {
            this.for('upcast')
                .elementToElement({
                model,
                view,
                converterPriority: definition.converterPriority
            });
        }
    }
    /**
     * Sets up converters between the model and the view that convert a model attribute to a view element (and vice versa).
     * For example, a model text node with `"Foo"` as data and the `bold` attribute will be turned to `<strong>Foo</strong>` in the view.
     *
     *		// A simple conversion from the `bold=true` attribute to the `<strong>` view element (and vice versa).
     *		editor.conversion.attributeToElement( { model: 'bold', view: 'strong' } );
     *
     *		// Override other converters by specifying a converter definition with a higher priority.
     *		editor.conversion.attributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } );
     *
     *		// View specified as an object instead of a string.
     *		editor.conversion.attributeToElement( {
     *			model: 'bold',
     *			view: {
     *				name: 'span',
     *				classes: 'bold'
     *			}
     *		} );
     *
     *		// Use `config.model.name` to define the conversion only from a given node type, `$text` in this case.
     *		// The same attribute on different elements may then be handled by a different converter.
     *		editor.conversion.attributeToElement( {
     *			model: {
     *				key: 'textDecoration',
     *				values: [ 'underline', 'lineThrough' ],
     *				name: '$text'
     *			},
     *			view: {
     *				underline: {
     *					name: 'span',
     *					styles: {
     *						'text-decoration': 'underline'
     *					}
     *				},
     *				lineThrough: {
     *					name: 'span',
     *					styles: {
     *						'text-decoration': 'line-through'
     *					}
     *				}
     *			}
     *		} );
     *
     *		// Use `upcastAlso` to define other view elements that should also be converted to the `bold` attribute.
     *		editor.conversion.attributeToElement( {
     *			model: 'bold',
     *			view: 'strong',
     *			upcastAlso: [
     *				'b',
     *				{
     *					name: 'span',
     *					classes: 'bold'
     *				},
     *				{
     *					name: 'span',
     *					styles: {
     *						'font-weight': 'bold'
     *					}
     *				},
     *				viewElement => {
     *					const fontWeight = viewElement.getStyle( 'font-weight' );
     *
     *					if ( viewElement.is( 'element', 'span' ) && fontWeight && /\d+/.test() && Number( fontWeight ) > 500 ) {
     *						// Returned value can be an object with the matched properties.
     *						// These properties will be "consumed" during the conversion.
     *						// See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more details.
     *
     *						return {
     *							name: true,
     *							styles: [ 'font-weight' ]
     *						};
     *					}
     *				}
     *			]
     *		} );
     *
     *		// Conversion from and to a model attribute key whose value is an enum (`fontSize=big|small`).
     *		// `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
     *		editor.conversion.attributeToElement( {
     *			model: {
     *				key: 'fontSize',
     *				values: [ 'big', 'small' ]
     *			},
     *			view: {
     *				big: {
     *					name: 'span',
     *					styles: {
     *						'font-size': '1.2em'
     *					}
     *				},
     *				small: {
     *					name: 'span',
     *					styles: {
     *						'font-size': '0.8em'
     *					}
     *				}
     *			},
     *			upcastAlso: {
     *				big: viewElement => {
     *					const fontSize = viewElement.getStyle( 'font-size' );
     *
     *					if ( !fontSize ) {
     *						return null;
     *					}
     *
     *					const match = fontSize.match( /(\d+)\s*px/ );
     *
     *					if ( !match ) {
     *						return null;
     *					}
     *
     *					const size = Number( match[ 1 ] );
     *
     *					if ( viewElement.is( 'element', 'span' ) && size > 10 ) {
     *						// Returned value can be an object with the matched properties.
     *						// These properties will be "consumed" during the conversion.
     *						// See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more details.
     *
     *						return { name: true, styles: [ 'font-size' ] };
     *					}
     *
     *					return null;
     *				},
     *				small: viewElement => {
     *					const fontSize = viewElement.getStyle( 'font-size' );
     *
     *					if ( !fontSize ) {
     *						return null;
     *					}
     *
     *					const match = fontSize.match( /(\d+)\s*px/ );
     *
     *					if ( !match ) {
     *						return null;
     *					}
     *
     *					const size = Number( match[ 1 ] );
     *
     *					if ( viewElement.is( 'element', 'span' ) && size < 10 ) {
     *						// Returned value can be an object with the matched properties.
     *						// These properties will be "consumed" during the conversion.
     *						// See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more details.
     *
     *						return { name: true, styles: [ 'font-size' ] };
     *					}
     *
     *					return null;
     *				}
     *			}
     *		} );
     *
     * The `definition.model` parameter specifies which model attribute should be converted from or to. It can be a `{ key, value }` object
     * describing the attribute key and value to convert or a `String` specifying just the attribute key (in such a case
     * `value` is set to `true`).
     * See {@link module:engine/conversion/conversion~ConverterDefinition} to learn about other parameters.
     *
     * @param {module:engine/conversion/conversion~ConverterDefinition} definition The converter definition.
     */
    attributeToElement(definition) {
        // Set up downcast converter.
        this.for('downcast').attributeToElement(definition);
        // Set up upcast converter.
        for (const { model, view } of _getAllUpcastDefinitions(definition)) {
            this.for('upcast')
                .elementToAttribute({
                view,
                model,
                converterPriority: definition.converterPriority
            });
        }
    }
    /**
     * Sets up converters between the model and the view that convert a model attribute to a view attribute (and vice versa). For example,
     * `<imageBlock src='foo.jpg'></imageBlock>` is converted to `<img src='foo.jpg'></img>` (the same attribute key and value).
     * This type of converters is intended to be used with {@link module:engine/model/element~Element model element} nodes.
     * To convert the text attributes,
     * the {@link module:engine/conversion/conversion~Conversion#attributeToElement `attributeToElement converter`}should be set up.
     *
     *		// A simple conversion from the `source` model attribute to the `src` view attribute (and vice versa).
     *		editor.conversion.attributeToAttribute( { model: 'source', view: 'src' } );
     *
     *		// Attribute values are strictly specified.
     *		editor.conversion.attributeToAttribute( {
     *			model: {
     *				name: 'imageInline',
     *				key: 'aside',
     *				values: [ 'aside' ]
     *			},
     *			view: {
     *				aside: {
     *					name: 'img',
     *					key: 'class',
     *					value: [ 'aside', 'half-size' ]
     *				}
     *			}
     *		} );
     *
     *		// Set the style attribute.
     *		editor.conversion.attributeToAttribute( {
     *			model: {
     *				name: 'imageInline',
     *				key: 'aside',
     *				values: [ 'aside' ]
     *			},
     *			view: {
     *				aside: {
     *					name: 'img',
     *					key: 'style',
     *					value: {
     *						float: 'right',
     *						width: '50%',
     *						margin: '5px'
     *					}
     *				}
     *			}
     *		} );
     *
     *		// Conversion from and to a model attribute key whose value is an enum (`align=right|center`).
     *		// Use `upcastAlso` to define other view elements that should also be converted to the `align=right` attribute.
     *		editor.conversion.attributeToAttribute( {
     *			model: {
     *				key: 'align',
     *				values: [ 'right', 'center' ]
     *			},
     *			view: {
     *				right: {
     *					key: 'class',
     *					value: 'align-right'
     *				},
     *				center: {
     *					key: 'class',
     *					value: 'align-center'
     *				}
     *			},
     *			upcastAlso: {
     *				right: {
     *					styles: {
     *						'text-align': 'right'
     *					}
     *				},
     *				center: {
     *					styles: {
     *						'text-align': 'center'
     *					}
     *				}
     *			}
     *		} );
     *
     * The `definition.model` parameter specifies which model attribute should be converted from and to.
     * It can be a `{ key, [ values ], [ name ] }` object or a `String`, which will be treated like `{ key: definition.model }`.
     * The `key` property is the model attribute key to convert from and to.
     * The `values` are the possible model attribute values. If the `values` parameter is not set, the model attribute value
     * will be the same as the view attribute value.
     * If `name` is set, the conversion will be set up only for model elements with the given name.
     *
     * The `definition.view` parameter specifies which view attribute should be converted from and to.
     * It can be a `{ key, value, [ name ] }` object or a `String`, which will be treated like `{ key: definition.view }`.
     * The `key` property is the view attribute key to convert from and to.
     * The `value` is the view attribute value to convert from and to. If `definition.value` is not set, the view attribute value will be
     * the same as the model attribute value.
     * If `key` is `'class'`, `value` can be a `String` or an array of `String`s.
     * If `key` is `'style'`, `value` is an object with key-value pairs.
     * In other cases, `value` is a `String`.
     * If `name` is set, the conversion will be set up only for model elements with the given name.
     * If `definition.model.values` is set, `definition.view` is an object that assigns values from `definition.model.values`
     * to `{ key, value, [ name ] }` objects.
     *
     * `definition.upcastAlso` specifies which other matching view elements should also be upcast to the given model configuration.
     * If `definition.model.values` is set, `definition.upcastAlso` should be an object assigning values from `definition.model.values`
     * to {@link module:engine/view/matcher~MatcherPattern}s or arrays of {@link module:engine/view/matcher~MatcherPattern}s.
     *
     * **Note:** `definition.model` and `definition.view` form should be mirrored, so the same types of parameters should
     * be given in both parameters.
     *
     * @param {Object} definition The converter definition.
     * @param {String|Object} definition.model The model attribute to convert from and to.
     * @param {String|Object} definition.view The view attribute to convert from and to.
     * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
     * Any view element matching `definition.upcastAlso` will also be converted to the given model attribute. `definition.upcastAlso`
     * is used only if `config.model.values` is specified.
     */
    attributeToAttribute(definition) {
        // Set up downcast converter.
        this.for('downcast').attributeToAttribute(definition);
        // Set up upcast converter.
        for (const { model, view } of _getAllUpcastDefinitions(definition)) {
            this.for('upcast')
                .attributeToAttribute({
                view,
                model
            });
        }
    }
    /**
     * Creates and caches conversion helpers for given dispatchers group.
     *
     * @private
     * @param {Object} options
     * @param {String} options.name Group name.
     * @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
     * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} options.dispatchers
     * @param {Boolean} options.isDowncast
     */
    _createConversionHelpers({ name, dispatchers, isDowncast }) {
        if (this._helpers.has(name)) {
            /**
             * Trying to register a group name that has already been registered.
             *
             * @error conversion-group-exists
             */
            throw new CKEditorError('conversion-group-exists', this);
        }
        const helpers = isDowncast ?
            new DowncastHelpers(dispatchers) :
            new UpcastHelpers(dispatchers);
        this._helpers.set(name, helpers);
    }
}
/**
 * Defines how the model should be converted from and to the view.
 *
 * @typedef {Object} module:engine/conversion/conversion~ConverterDefinition
 *
 * @property {*} [model] The model conversion definition. Describes the model element or model attribute to convert. This parameter differs
 * for different functions that accept `ConverterDefinition`. See the description of the function to learn how to set it.
 * @property {module:engine/view/elementdefinition~ElementDefinition|Object} view The definition of the view element to convert from and
 * to. If `model` describes multiple values, `view` is an object that assigns these values (`view` object keys) to view element definitions
 * (`view` object values).
 * @property {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [upcastAlso]
 * Any view element matching `upcastAlso` will also be converted to the model. If `model` describes multiple values, `upcastAlso`
 * is an object that assigns these values (`upcastAlso` object keys) to {@link module:engine/view/matcher~MatcherPattern}s
 * (`upcastAlso` object values).
 * @property {module:utils/priorities~PriorityString} [converterPriority] The converter priority.
 */
// Helper function that creates a joint array out of an item passed in `definition.view` and items passed in
// `definition.upcastAlso`.
//
// @param {module:engine/conversion/conversion~ConverterDefinition} definition
// @returns {Array} Array containing view definitions.
function* _getAllUpcastDefinitions(definition) {
    if (definition.model.values) {
        for (const value of definition.model.values) {
            const model = { key: definition.model.key, value };
            const view = definition.view[value];
            const upcastAlso = definition.upcastAlso ? definition.upcastAlso[value] : undefined;
            yield* _getUpcastDefinition(model, view, upcastAlso);
        }
    }
    else {
        yield* _getUpcastDefinition(definition.model, definition.view, definition.upcastAlso);
    }
}
function* _getUpcastDefinition(model, view, upcastAlso) {
    yield { model, view };
    if (upcastAlso) {
        for (const upcastAlsoItem of toArray(upcastAlso)) {
            yield { model, view: upcastAlsoItem };
        }
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * A batch instance groups model changes ({@link module:engine/model/operation/operation~Operation operations}). All operations
 * grouped in a single batch can be reverted together, so you can also think about a batch as of a single undo step. If you want
 * to extend a given undo step, you can add more changes to the batch using {@link module:engine/model/model~Model#enqueueChange}:
 *
 *		model.enqueueChange( batch, writer => {
 *			writer.insertText( 'foo', paragraph, 'end' );
 *		} );
 *
 * @see module:engine/model/model~Model#enqueueChange
 * @see module:engine/model/model~Model#change
 */
class Batch {
    /**
     * Creates a batch instance.
     *
     * @see module:engine/model/model~Model#enqueueChange
     * @see module:engine/model/model~Model#change
     * @param {Object} [type] A set of flags that specify the type of the batch. Batch type can alter how some of the features work
     * when encountering a given `Batch` instance (for example, when a feature listens to applied operations).
     * @param {Boolean} [type.isUndoable=true] Whether a batch can be undone through undo feature.
     * @param {Boolean} [type.isLocal=true] Whether a batch includes operations created locally (`true`) or operations created on
     * other, remote editors (`false`).
     * @param {Boolean} [type.isUndo=false] Whether a batch was created by the undo feature and undoes other operations.
     * @param {Boolean} [type.isTyping=false] Whether a batch includes operations connected with a typing action.
     */
    constructor(type = {}) {
        if (typeof type === 'string') {
            type = type === 'transparent' ? { isUndoable: false } : {};
            /**
             * The string value for a `type` property of the `Batch` constructor has been deprecated and will be removed in the near future.
             * Please refer to the {@link module:engine/model/batch~Batch#constructor `Batch` constructor API documentation} for more
             * information.
             *
             * @error batch-constructor-deprecated-string-type
             */
            logWarning('batch-constructor-deprecated-string-type');
        }
        const { isUndoable = true, isLocal = true, isUndo = false, isTyping = false } = type;
        /**
         * An array of operations that compose this batch.
         *
         * @readonly
         * @type {Array.<module:engine/model/operation/operation~Operation>}
         */
        this.operations = [];
        /**
         * Whether the batch can be undone through the undo feature.
         *
         * @readonly
         * @type {Boolean}
         */
        this.isUndoable = isUndoable;
        /**
         * Whether the batch includes operations created locally (`true`) or operations created on other, remote editors (`false`).
         *
         * @readonly
         * @type {Boolean}
         */
        this.isLocal = isLocal;
        /**
         * Whether the batch was created by the undo feature and undoes other operations.
         *
         * @readonly
         * @type {Boolean}
         */
        this.isUndo = isUndo;
        /**
         * Whether the batch includes operations connected with typing.
         *
         * @readonly
         * @type {Boolean}
         */
        this.isTyping = isTyping;
    }
    /**
     * The type of the batch.
     *
     * **This property has been deprecated and is always set to the `'default'` value.**
     *
     * It can be one of the following values:
     * * `'default'` &ndash; All "normal" batches. This is the most commonly used type.
     * * `'transparent'` &ndash; A batch that should be ignored by other features, i.e. an initial batch or collaborative editing
     * changes.
     *
     * @deprecated
     * @type {'default'}
     */
    get type() {
        /**
         * The {@link module:engine/model/batch~Batch#type `Batch#type` } property has been deprecated and will be removed in the near
         * future. Use `Batch#isLocal`, `Batch#isUndoable`, `Batch#isUndo` and `Batch#isTyping` instead.
         *
         * @error batch-type-deprecated
         */
        logWarning('batch-type-deprecated');
        return 'default';
    }
    /**
     * Returns the base version of this batch, which is equal to the base version of the first operation in the batch.
     * If there are no operations in the batch or neither operation has the base version set, it returns `null`.
     *
     * @readonly
     * @type {Number|null}
     */
    get baseVersion() {
        for (const op of this.operations) {
            if (op.baseVersion !== null) {
                return op.baseVersion;
            }
        }
        return null;
    }
    /**
     * Adds an operation to the batch instance.
     *
     * @param {module:engine/model/operation/operation~Operation} operation An operation to add.
     * @returns {module:engine/model/operation/operation~Operation} The added operation.
     */
    addOperation(operation) {
        operation.batch = this;
        this.operations.push(operation);
        return operation;
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Calculates the difference between two model states.
 *
 * Receives operations that are to be applied on the model document. Marks parts of the model document tree which
 * are changed and saves the state of these elements before the change. Then, it compares saved elements with the
 * changed elements, after all changes are applied on the model document. Calculates the diff between saved
 * elements and new ones and returns a change set.
 */
class Differ {
    /**
     * Creates a `Differ` instance.
     *
     * @param {module:engine/model/markercollection~MarkerCollection} markerCollection Model's marker collection.
     */
    constructor(markerCollection) {
        /**
         * Reference to the model's marker collection.
         *
         * @private
         * @type {module:engine/model/markercollection~MarkerCollection}
         */
        this._markerCollection = markerCollection;
        /**
         * A map that stores changes that happened in a given element.
         *
         * The keys of the map are references to the model elements.
         * The values of the map are arrays with changes that were done on this element.
         *
         * @private
         * @type {Map}
         */
        this._changesInElement = new Map();
        /**
         * A map that stores "element's children snapshots". A snapshot is representing children of a given element before
         * the first change was applied on that element. Snapshot items are objects with two properties: `name`,
         * containing the element name (or `'$text'` for a text node) and `attributes` which is a map of the node's attributes.
         *
         * @private
         * @type {Map}
         */
        this._elementSnapshots = new Map();
        /**
         * A map that stores all changed markers.
         *
         * The keys of the map are marker names.
         * The values of the map are objects with the following properties:
         * - `oldMarkerData`,
         * - `newMarkerData`.
         *
         * @private
         * @type {Map.<String, Object>}
         */
        this._changedMarkers = new Map();
        /**
         * Stores the number of changes that were processed. Used to order the changes chronologically. It is important
         * when changes are sorted.
         *
         * @private
         * @type {Number}
         */
        this._changeCount = 0;
        /**
         * For efficiency purposes, `Differ` stores the change set returned by the differ after {@link #getChanges} call.
         * Cache is reset each time a new operation is buffered. If the cache has not been reset, {@link #getChanges} will
         * return the cached value instead of calculating it again.
         *
         * This property stores those changes that did not take place in graveyard root.
         *
         * @private
         * @type {Array.<Object>|null}
         */
        this._cachedChanges = null;
        /**
         * For efficiency purposes, `Differ` stores the change set returned by the differ after the {@link #getChanges} call.
         * The cache is reset each time a new operation is buffered. If the cache has not been reset, {@link #getChanges} will
         * return the cached value instead of calculating it again.
         *
         * This property stores all changes evaluated by `Differ`, including those that took place in the graveyard.
         *
         * @private
         * @type {Array.<Object>|null}
         */
        this._cachedChangesWithGraveyard = null;
        /**
         * Set of model items that were marked to get refreshed in {@link #_refreshItem}.
         *
         * @private
         * @type {Set.<module:engine/model/item~Item>}
         */
        this._refreshedItems = new Set();
    }
    /**
     * Informs whether there are any changes buffered in `Differ`.
     *
     * @readonly
     * @type {Boolean}
     */
    get isEmpty() {
        return this._changesInElement.size == 0 && this._changedMarkers.size == 0;
    }
    /**
     * Buffers the given operation. An operation has to be buffered before it is executed.
     *
     * Operation type is checked and it is checked which nodes it will affect. These nodes are then stored in `Differ`
     * in the state before the operation is executed.
     *
     * @param {module:engine/model/operation/operation~Operation} operationToBuffer An operation to buffer.
     */
    bufferOperation(operationToBuffer) {
        // Below we take an operation, check its type, then use its parameters in marking (private) methods.
        // The general rule is to not mark elements inside inserted element. All inserted elements are re-rendered.
        // Marking changes in them would cause a "double" changing then.
        //
        const operation = operationToBuffer;
        switch (operation.type) {
            case 'insert': {
                if (this._isInInsertedElement(operation.position.parent)) {
                    return;
                }
                this._markInsert(operation.position.parent, operation.position.offset, operation.nodes.maxOffset);
                break;
            }
            case 'addAttribute':
            case 'removeAttribute':
            case 'changeAttribute': {
                for (const item of operation.range.getItems({ shallow: true })) {
                    if (this._isInInsertedElement(item.parent)) {
                        continue;
                    }
                    this._markAttribute(item);
                }
                break;
            }
            case 'remove':
            case 'move':
            case 'reinsert': {
                // When range is moved to the same position then not mark it as a change.
                // See: https://github.com/ckeditor/ckeditor5-engine/issues/1664.
                if (operation.sourcePosition.isEqual(operation.targetPosition) ||
                    operation.sourcePosition.getShiftedBy(operation.howMany).isEqual(operation.targetPosition)) {
                    return;
                }
                const sourceParentInserted = this._isInInsertedElement(operation.sourcePosition.parent);
                const targetParentInserted = this._isInInsertedElement(operation.targetPosition.parent);
                if (!sourceParentInserted) {
                    this._markRemove(operation.sourcePosition.parent, operation.sourcePosition.offset, operation.howMany);
                }
                if (!targetParentInserted) {
                    this._markInsert(operation.targetPosition.parent, operation.getMovedRangeStart().offset, operation.howMany);
                }
                break;
            }
            case 'rename': {
                if (this._isInInsertedElement(operation.position.parent)) {
                    return;
                }
                this._markRemove(operation.position.parent, operation.position.offset, 1);
                this._markInsert(operation.position.parent, operation.position.offset, 1);
                const range = Range._createFromPositionAndShift(operation.position, 1);
                for (const marker of this._markerCollection.getMarkersIntersectingRange(range)) {
                    const markerData = marker.getData();
                    this.bufferMarkerChange(marker.name, markerData, markerData);
                }
                break;
            }
            case 'split': {
                const splitElement = operation.splitPosition.parent;
                // Mark that children of the split element were removed.
                if (!this._isInInsertedElement(splitElement)) {
                    this._markRemove(splitElement, operation.splitPosition.offset, operation.howMany);
                }
                // Mark that the new element (split copy) was inserted.
                if (!this._isInInsertedElement(operation.insertionPosition.parent)) {
                    this._markInsert(operation.insertionPosition.parent, operation.insertionPosition.offset, 1);
                }
                // If the split took the element from the graveyard, mark that the element from the graveyard was removed.
                if (operation.graveyardPosition) {
                    this._markRemove(operation.graveyardPosition.parent, operation.graveyardPosition.offset, 1);
                }
                break;
            }
            case 'merge': {
                // Mark that the merged element was removed.
                const mergedElement = operation.sourcePosition.parent;
                if (!this._isInInsertedElement(mergedElement.parent)) {
                    this._markRemove(mergedElement.parent, mergedElement.startOffset, 1);
                }
                // Mark that the merged element was inserted into graveyard.
                const graveyardParent = operation.graveyardPosition.parent;
                this._markInsert(graveyardParent, operation.graveyardPosition.offset, 1);
                // Mark that children of merged element were inserted at new parent.
                const mergedIntoElement = operation.targetPosition.parent;
                if (!this._isInInsertedElement(mergedIntoElement)) {
                    this._markInsert(mergedIntoElement, operation.targetPosition.offset, mergedElement.maxOffset);
                }
                break;
            }
        }
        // Clear cache after each buffered operation as it is no longer valid.
        this._cachedChanges = null;
    }
    /**
     * Buffers a marker change.
     *
     * @param {String} markerName The name of the marker that changed.
     * @param {module:engine/model/markercollection~MarkerData} oldMarkerData Marker data before the change.
     * @param {module:engine/model/markercollection~MarkerData} newMarkerData Marker data after the change.
     */
    bufferMarkerChange(markerName, oldMarkerData, newMarkerData) {
        const buffered = this._changedMarkers.get(markerName);
        if (!buffered) {
            this._changedMarkers.set(markerName, {
                newMarkerData,
                oldMarkerData
            });
        }
        else {
            buffered.newMarkerData = newMarkerData;
            if (buffered.oldMarkerData.range == null && newMarkerData.range == null) {
                // The marker is going to be removed (`newMarkerData.range == null`) but it did not exist before the first buffered change
                // (`buffered.oldMarkerData.range == null`). In this case, do not keep the marker in buffer at all.
                this._changedMarkers.delete(markerName);
            }
        }
    }
    /**
     * Returns all markers that should be removed as a result of buffered changes.
     *
     * @returns {Array.<Object>} Markers to remove. Each array item is an object containing the `name` and `range` properties.
     */
    getMarkersToRemove() {
        const result = [];
        for (const [name, change] of this._changedMarkers) {
            if (change.oldMarkerData.range != null) {
                result.push({ name, range: change.oldMarkerData.range });
            }
        }
        return result;
    }
    /**
     * Returns all markers which should be added as a result of buffered changes.
     *
     * @returns {Array.<Object>} Markers to add. Each array item is an object containing the `name` and `range` properties.
     */
    getMarkersToAdd() {
        const result = [];
        for (const [name, change] of this._changedMarkers) {
            if (change.newMarkerData.range != null) {
                result.push({ name, range: change.newMarkerData.range });
            }
        }
        return result;
    }
    /**
     * Returns all markers which changed.
     *
     * @returns {Array.<Object>}
     */
    getChangedMarkers() {
        return Array.from(this._changedMarkers).map(([name, change]) => ({
            name,
            data: {
                oldRange: change.oldMarkerData.range,
                newRange: change.newMarkerData.range
            }
        }));
    }
    /**
     * Checks whether some of the buffered changes affect the editor data.
     *
     * Types of changes which affect the editor data:
     *
     * * model structure changes,
     * * attribute changes,
     * * changes of markers which were defined as `affectsData`,
     * * changes of markers' `affectsData` property.
     *
     * @returns {Boolean}
     */
    hasDataChanges() {
        if (this._changesInElement.size > 0) {
            return true;
        }
        for (const { newMarkerData, oldMarkerData } of this._changedMarkers.values()) {
            if (newMarkerData.affectsData !== oldMarkerData.affectsData) {
                return true;
            }
            if (newMarkerData.affectsData) {
                const markerAdded = newMarkerData.range && !oldMarkerData.range;
                const markerRemoved = !newMarkerData.range && oldMarkerData.range;
                const markerChanged = newMarkerData.range && oldMarkerData.range && !newMarkerData.range.isEqual(oldMarkerData.range);
                if (markerAdded || markerRemoved || markerChanged) {
                    return true;
                }
            }
        }
        return false;
    }
    /**
     * Calculates the diff between the old model tree state (the state before the first buffered operations since the last {@link #reset}
     * call) and the new model tree state (actual one). It should be called after all buffered operations are executed.
     *
     * The diff set is returned as an array of {@link module:engine/model/differ~DiffItem diff items}, each describing a change done
     * on the model. The items are sorted by the position on which the change happened. If a position
     * {@link module:engine/model/position~Position#isBefore is before} another one, it will be on an earlier index in the diff set.
     *
     * **Note**: Elements inside inserted element will not have a separate diff item, only the top most element change will be reported.
     *
     * Because calculating the diff is a costly operation, the result is cached. If no new operation was buffered since the
     * previous {@link #getChanges} call, the next call will return the cached value.
     *
     * @param {Object} options Additional options.
     * @param {Boolean} [options.includeChangesInGraveyard=false] If set to `true`, also changes that happened
     * in the graveyard root will be returned. By default, changes in the graveyard root are not returned.
     * @returns {Array.<module:engine/model/differ~DiffItem>} Diff between the old and the new model tree state.
     */
    getChanges(options = {}) {
        // If there are cached changes, just return them instead of calculating changes again.
        if (this._cachedChanges) {
            if (options.includeChangesInGraveyard) {
                return this._cachedChangesWithGraveyard.slice();
            }
            else {
                return this._cachedChanges.slice();
            }
        }
        // Will contain returned results.
        let diffSet = [];
        // Check all changed elements.
        for (const element of this._changesInElement.keys()) {
            // Get changes for this element and sort them.
            const changes = this._changesInElement.get(element).sort((a, b) => {
                if (a.offset === b.offset) {
                    if (a.type != b.type) {
                        // If there are multiple changes at the same position, "remove" change should be first.
                        // If the order is different, for example, we would first add some nodes and then removed them
                        // (instead of the nodes that we should remove).
                        return a.type == 'remove' ? -1 : 1;
                    }
                    return 0;
                }
                return a.offset < b.offset ? -1 : 1;
            });
            // Get children of this element before any change was applied on it.
            const snapshotChildren = this._elementSnapshots.get(element);
            // Get snapshot of current element's children.
            const elementChildren = _getChildrenSnapshot(element.getChildren());
            // Generate actions basing on changes done on element.
            const actions = _generateActionsFromChanges(snapshotChildren.length, changes);
            let i = 0; // Iterator in `elementChildren` array -- iterates through current children of element.
            let j = 0; // Iterator in `snapshotChildren` array -- iterates through old children of element.
            // Process every action.
            for (const action of actions) {
                if (action === 'i') {
                    // Generate diff item for this element and insert it into the diff set.
                    diffSet.push(this._getInsertDiff(element, i, elementChildren[i]));
                    i++;
                }
                else if (action === 'r') {
                    // Generate diff item for this element and insert it into the diff set.
                    diffSet.push(this._getRemoveDiff(element, i, snapshotChildren[j]));
                    j++;
                }
                else if (action === 'a') {
                    // Take attributes from saved and current children.
                    const elementAttributes = elementChildren[i].attributes;
                    const snapshotAttributes = snapshotChildren[j].attributes;
                    let range;
                    if (elementChildren[i].name == '$text') {
                        range = new Range(Position._createAt(element, i), Position._createAt(element, i + 1));
                    }
                    else {
                        const index = element.offsetToIndex(i);
                        range = new Range(Position._createAt(element, i), Position._createAt(element.getChild(index), 0));
                    }
                    // Generate diff items for this change (there might be multiple attributes changed and
                    // there is a single diff for each of them) and insert them into the diff set.
                    diffSet.push(...this._getAttributesDiff(range, snapshotAttributes, elementAttributes));
                    i++;
                    j++;
                }
                else {
                    // `action` is 'equal'. Child not changed.
                    i++;
                    j++;
                }
            }
        }
        // Then, sort the changes by the position (change at position before other changes is first).
        diffSet.sort((a, b) => {
            // If the change is in different root, we don't care much, but we'd like to have all changes in given
            // root "together" in the array. So let's just sort them by the root name. It does not matter which root
            // will be processed first.
            if (a.position.root != b.position.root) {
                return a.position.root.rootName < b.position.root.rootName ? -1 : 1;
            }
            // If change happens at the same position...
            if (a.position.isEqual(b.position)) {
                // Keep chronological order of operations.
                return a.changeCount - b.changeCount;
            }
            // If positions differ, position "on the left" should be earlier in the result.
            return a.position.isBefore(b.position) ? -1 : 1;
        });
        // Glue together multiple changes (mostly on text nodes).
        for (let i = 1, prevIndex = 0; i < diffSet.length; i++) {
            const prevDiff = diffSet[prevIndex];
            const thisDiff = diffSet[i];
            // Glue remove changes if they happen on text on same position.
            const isConsecutiveTextRemove = prevDiff.type == 'remove' && thisDiff.type == 'remove' &&
                prevDiff.name == '$text' && thisDiff.name == '$text' &&
                prevDiff.position.isEqual(thisDiff.position);
            // Glue insert changes if they happen on text on consecutive fragments.
            const isConsecutiveTextAdd = prevDiff.type == 'insert' && thisDiff.type == 'insert' &&
                prevDiff.name == '$text' && thisDiff.name == '$text' &&
                prevDiff.position.parent == thisDiff.position.parent &&
                prevDiff.position.offset + prevDiff.length == thisDiff.position.offset;
            // Glue attribute changes if they happen on consecutive fragments and have same key, old value and new value.
            const isConsecutiveAttributeChange = prevDiff.type == 'attribute' && thisDiff.type == 'attribute' &&
                prevDiff.position.parent == thisDiff.position.parent &&
                prevDiff.range.isFlat && thisDiff.range.isFlat &&
                (prevDiff.position.offset + prevDiff.length) == thisDiff.position.offset &&
                prevDiff.attributeKey == thisDiff.attributeKey &&
                prevDiff.attributeOldValue == thisDiff.attributeOldValue &&
                prevDiff.attributeNewValue == thisDiff.attributeNewValue;
            if (isConsecutiveTextRemove || isConsecutiveTextAdd || isConsecutiveAttributeChange) {
                prevDiff.length++;
                if (isConsecutiveAttributeChange) {
                    prevDiff.range.end = prevDiff.range.end.getShiftedBy(1);
                }
                diffSet[i] = null;
            }
            else {
                prevIndex = i;
            }
        }
        diffSet = diffSet.filter(v => v);
        // Remove `changeCount` property from diff items. It is used only for sorting and is internal thing.
        for (const item of diffSet) {
            delete item.changeCount;
            if (item.type == 'attribute') {
                delete item.position;
                delete item.length;
            }
        }
        this._changeCount = 0;
        // Cache changes.
        this._cachedChangesWithGraveyard = diffSet;
        this._cachedChanges = diffSet.filter(_changesInGraveyardFilter);
        if (options.includeChangesInGraveyard) {
            return this._cachedChangesWithGraveyard.slice();
        }
        else {
            return this._cachedChanges.slice();
        }
    }
    /**
     * Returns a set of model items that were marked to get refreshed.
     *
     * @return {Set.<module:engine/model/item~Item>}
     */
    getRefreshedItems() {
        return new Set(this._refreshedItems);
    }
    /**
     * Resets `Differ`. Removes all buffered changes.
     */
    reset() {
        this._changesInElement.clear();
        this._elementSnapshots.clear();
        this._changedMarkers.clear();
        this._refreshedItems = new Set();
        this._cachedChanges = null;
    }
    /**
     * Marks the given `item` in differ to be "refreshed". It means that the item will be marked as removed and inserted
     * in the differ changes set, so it will be effectively re-converted when the differ changes are handled by a dispatcher.
     *
     * @protected
     * @internal
     * @param {module:engine/model/item~Item} item Item to refresh.
     */
    _refreshItem(item) {
        if (this._isInInsertedElement(item.parent)) {
            return;
        }
        this._markRemove(item.parent, item.startOffset, item.offsetSize);
        this._markInsert(item.parent, item.startOffset, item.offsetSize);
        this._refreshedItems.add(item);
        const range = Range._createOn(item);
        for (const marker of this._markerCollection.getMarkersIntersectingRange(range)) {
            const markerData = marker.getData();
            this.bufferMarkerChange(marker.name, markerData, markerData);
        }
        // Clear cache after each buffered operation as it is no longer valid.
        this._cachedChanges = null;
    }
    /**
     * Saves and handles an insert change.
     *
     * @private
     * @param {module:engine/model/element~Element} parent
     * @param {Number} offset
     * @param {Number} howMany
     */
    _markInsert(parent, offset, howMany) {
        const changeItem = { type: 'insert', offset, howMany, count: this._changeCount++ };
        this._markChange(parent, changeItem);
    }
    /**
     * Saves and handles a remove change.
     *
     * @private
     * @param {module:engine/model/element~Element} parent
     * @param {Number} offset
     * @param {Number} howMany
     */
    _markRemove(parent, offset, howMany) {
        const changeItem = { type: 'remove', offset, howMany, count: this._changeCount++ };
        this._markChange(parent, changeItem);
        this._removeAllNestedChanges(parent, offset, howMany);
    }
    /**
     * Saves and handles an attribute change.
     *
     * @private
     * @param {module:engine/model/item~Item} item
     */
    _markAttribute(item) {
        const changeItem = { type: 'attribute', offset: item.startOffset, howMany: item.offsetSize, count: this._changeCount++ };
        this._markChange(item.parent, changeItem);
    }
    /**
     * Saves and handles a model change.
     *
     * @private
     * @param {module:engine/model/element~Element} parent
     * @param {Object} changeItem
     */
    _markChange(parent, changeItem) {
        // First, make a snapshot of this parent's children (it will be made only if it was not made before).
        this._makeSnapshot(parent);
        // Then, get all changes that already were done on the element (empty array if this is the first change).
        const changes = this._getChangesForElement(parent);
        // Then, look through all the changes, and transform them or the new change.
        this._handleChange(changeItem, changes);
        // Add the new change.
        changes.push(changeItem);
        // Remove incorrect changes. During transformation some change might be, for example, included in another.
        // In that case, the change will have `howMany` property set to `0` or less. We need to remove those changes.
        for (let i = 0; i < changes.length; i++) {
            if (changes[i].howMany < 1) {
                changes.splice(i, 1);
                i--;
            }
        }
    }
    /**
     * Gets an array of changes that have already been saved for a given element.
     *
     * @private
     * @param {module:engine/model/element~Element} element
     * @returns {Array.<Object>}
     */
    _getChangesForElement(element) {
        let changes;
        if (this._changesInElement.has(element)) {
            changes = this._changesInElement.get(element);
        }
        else {
            changes = [];
            this._changesInElement.set(element, changes);
        }
        return changes;
    }
    /**
     * Saves a children snapshot for a given element.
     *
     * @private
     * @param {module:engine/model/element~Element} element
     */
    _makeSnapshot(element) {
        if (!this._elementSnapshots.has(element)) {
            this._elementSnapshots.set(element, _getChildrenSnapshot(element.getChildren()));
        }
    }
    /**
     * For a given newly saved change, compares it with a change already done on the element and modifies the incoming
     * change and/or the old change.
     *
     * @private
     * @param {Object} inc Incoming (new) change.
     * @param {Array.<Object>} changes An array containing all the changes done on that element.
     */
    _handleChange(inc, changes) {
        // We need a helper variable that will store how many nodes are to be still handled for this change item.
        // `nodesToHandle` (how many nodes still need to be handled) and `howMany` (how many nodes were affected)
        // needs to be differentiated.
        //
        // This comes up when there are multiple changes that are affected by `inc` change item.
        //
        // For example: assume two insert changes: `{ offset: 2, howMany: 1 }` and `{ offset: 5, howMany: 1 }`.
        // Assume that `inc` change is remove `{ offset: 2, howMany: 2, nodesToHandle: 2 }`.
        //
        // Then, we:
        // - "forget" about first insert change (it is "eaten" by remove),
        // - because of that, at the end we will want to remove only one node (`nodesToHandle = 1`),
        // - but still we have to change offset of the second insert change from `5` to `3`!
        //
        // So, `howMany` does not change throughout items transformation and keeps information about how many nodes were affected,
        // while `nodesToHandle` means how many nodes need to be handled after the change item is transformed by other changes.
        inc.nodesToHandle = inc.howMany;
        for (const old of changes) {
            const incEnd = inc.offset + inc.howMany;
            const oldEnd = old.offset + old.howMany;
            if (inc.type == 'insert') {
                if (old.type == 'insert') {
                    if (inc.offset <= old.offset) {
                        old.offset += inc.howMany;
                    }
                    else if (inc.offset < oldEnd) {
                        old.howMany += inc.nodesToHandle;
                        inc.nodesToHandle = 0;
                    }
                }
                if (old.type == 'remove') {
                    if (inc.offset < old.offset) {
                        old.offset += inc.howMany;
                    }
                }
                if (old.type == 'attribute') {
                    if (inc.offset <= old.offset) {
                        old.offset += inc.howMany;
                    }
                    else if (inc.offset < oldEnd) {
                        // This case is more complicated, because attribute change has to be split into two.
                        // Example (assume that uppercase and lowercase letters mean different attributes):
                        //
                        // initial state:		abcxyz
                        // attribute change:	aBCXYz
                        // incoming insert:		aBCfooXYz
                        //
                        // Change ranges cannot intersect because each item has to be described exactly (it was either
                        // not changed, inserted, removed, or its attribute was changed). That's why old attribute
                        // change has to be split and both parts has to be handled separately from now on.
                        const howMany = old.howMany;
                        old.howMany = inc.offset - old.offset;
                        // Add the second part of attribute change to the beginning of processed array so it won't
                        // be processed again in this loop.
                        changes.unshift({
                            type: 'attribute',
                            offset: incEnd,
                            howMany: howMany - old.howMany,
                            count: this._changeCount++
                        });
                    }
                }
            }
            if (inc.type == 'remove') {
                if (old.type == 'insert') {
                    if (incEnd <= old.offset) {
                        old.offset -= inc.howMany;
                    }
                    else if (incEnd <= oldEnd) {
                        if (inc.offset < old.offset) {
                            const intersectionLength = incEnd - old.offset;
                            old.offset = inc.offset;
                            old.howMany -= intersectionLength;
                            inc.nodesToHandle -= intersectionLength;
                        }
                        else {
                            old.howMany -= inc.nodesToHandle;
                            inc.nodesToHandle = 0;
                        }
                    }
                    else {
                        if (inc.offset <= old.offset) {
                            inc.nodesToHandle -= old.howMany;
                            old.howMany = 0;
                        }
                        else if (inc.offset < oldEnd) {
                            const intersectionLength = oldEnd - inc.offset;
                            old.howMany -= intersectionLength;
                            inc.nodesToHandle -= intersectionLength;
                        }
                    }
                }
                if (old.type == 'remove') {
                    if (incEnd <= old.offset) {
                        old.offset -= inc.howMany;
                    }
                    else if (inc.offset < old.offset) {
                        inc.nodesToHandle += old.howMany;
                        old.howMany = 0;
                    }
                }
                if (old.type == 'attribute') {
                    if (incEnd <= old.offset) {
                        old.offset -= inc.howMany;
                    }
                    else if (inc.offset < old.offset) {
                        const intersectionLength = incEnd - old.offset;
                        old.offset = inc.offset;
                        old.howMany -= intersectionLength;
                    }
                    else if (inc.offset < oldEnd) {
                        if (incEnd <= oldEnd) {
                            // On first sight in this case we don't need to split attribute operation into two.
                            // However the changes set is later converted to actions (see `_generateActionsFromChanges`).
                            // For that reason, no two changes may intersect.
                            // So we cannot have an attribute change that "contains" remove change.
                            // Attribute change needs to be split.
                            const howMany = old.howMany;
                            old.howMany = inc.offset - old.offset;
                            const howManyAfter = howMany - old.howMany - inc.nodesToHandle;
                            // Add the second part of attribute change to the beginning of processed array so it won't
                            // be processed again in this loop.
                            changes.unshift({
                                type: 'attribute',
                                offset: inc.offset,
                                howMany: howManyAfter,
                                count: this._changeCount++
                            });
                        }
                        else {
                            old.howMany -= oldEnd - inc.offset;
                        }
                    }
                }
            }
            if (inc.type == 'attribute') {
                // In case of attribute change, `howMany` should be kept same as `nodesToHandle`. It's not an error.
                if (old.type == 'insert') {
                    if (inc.offset < old.offset && incEnd > old.offset) {
                        if (incEnd > oldEnd) {
                            // This case is similar to a case described when incoming change was insert and old change was attribute.
                            // See comment above.
                            //
                            // This time incoming change is attribute. We need to split incoming change in this case too.
                            // However this time, the second part of the attribute change needs to be processed further
                            // because there might be other changes that it collides with.
                            const attributePart = {
                                type: 'attribute',
                                offset: oldEnd,
                                howMany: incEnd - oldEnd,
                                count: this._changeCount++
                            };
                            this._handleChange(attributePart, changes);
                            changes.push(attributePart);
                        }
                        inc.nodesToHandle = old.offset - inc.offset;
                        inc.howMany = inc.nodesToHandle;
                    }
                    else if (inc.offset >= old.offset && inc.offset < oldEnd) {
                        if (incEnd > oldEnd) {
                            inc.nodesToHandle = incEnd - oldEnd;
                            inc.offset = oldEnd;
                        }
                        else {
                            inc.nodesToHandle = 0;
                        }
                    }
                }
                if (old.type == 'remove') {
                    // This is a case when attribute change "contains" remove change.
                    // The attribute change needs to be split into two because changes cannot intersect.
                    if (inc.offset < old.offset && incEnd > old.offset) {
                        const attributePart = {
                            type: 'attribute',
                            offset: old.offset,
                            howMany: incEnd - old.offset,
                            count: this._changeCount++
                        };
                        this._handleChange(attributePart, changes);
                        changes.push(attributePart);
                        inc.nodesToHandle = old.offset - inc.offset;
                        inc.howMany = inc.nodesToHandle;
                    }
                }
                if (old.type == 'attribute') {
                    // There are only two conflicting scenarios possible here:
                    if (inc.offset >= old.offset && incEnd <= oldEnd) {
                        // `old` change includes `inc` change, or they are the same.
                        inc.nodesToHandle = 0;
                        inc.howMany = 0;
                        inc.offset = 0;
                    }
                    else if (inc.offset <= old.offset && incEnd >= oldEnd) {
                        // `inc` change includes `old` change.
                        old.howMany = 0;
                    }
                }
            }
        }
        inc.howMany = inc.nodesToHandle;
        delete inc.nodesToHandle;
    }
    /**
     * Returns an object with a single insert change description.
     *
     * @private
     * @param {module:engine/model/element~Element} parent The element in which the change happened.
     * @param {Number} offset The offset at which change happened.
     * @param {Object} elementSnapshot The snapshot of the removed element a character.
     * @returns {Object} The diff item.
     */
    _getInsertDiff(parent, offset, elementSnapshot) {
        return {
            type: 'insert',
            position: Position._createAt(parent, offset),
            name: elementSnapshot.name,
            attributes: new Map(elementSnapshot.attributes),
            length: 1,
            changeCount: this._changeCount++
        };
    }
    /**
     * Returns an object with a single remove change description.
     *
     * @private
     * @param {module:engine/model/element~Element} parent The element in which change happened.
     * @param {Number} offset The offset at which change happened.
     * @param {Object} elementSnapshot The snapshot of the removed element a character.
     * @returns {Object} The diff item.
     */
    _getRemoveDiff(parent, offset, elementSnapshot) {
        return {
            type: 'remove',
            position: Position._createAt(parent, offset),
            name: elementSnapshot.name,
            attributes: new Map(elementSnapshot.attributes),
            length: 1,
            changeCount: this._changeCount++
        };
    }
    /**
     * Returns an array of objects where each one is a single attribute change description.
     *
     * @private
     * @param {module:engine/model/range~Range} range The range where the change happened.
     * @param {Map} oldAttributes A map, map iterator or compatible object that contains attributes before the change.
     * @param {Map} newAttributes A map, map iterator or compatible object that contains attributes after the change.
     * @returns {Array.<Object>} An array containing one or more diff items.
     */
    _getAttributesDiff(range, oldAttributes, newAttributes) {
        // Results holder.
        const diffs = [];
        // Clone new attributes as we will be performing changes on this object.
        newAttributes = new Map(newAttributes);
        // Look through old attributes.
        for (const [key, oldValue] of oldAttributes) {
            // Check what is the new value of the attribute (or if it was removed).
            const newValue = newAttributes.has(key) ? newAttributes.get(key) : null;
            // If values are different (or attribute was removed)...
            if (newValue !== oldValue) {
                // Add diff item.
                diffs.push({
                    type: 'attribute',
                    position: range.start,
                    range: range.clone(),
                    length: 1,
                    attributeKey: key,
                    attributeOldValue: oldValue,
                    attributeNewValue: newValue,
                    changeCount: this._changeCount++
                });
            }
            // Prevent returning two diff items for the same change.
            newAttributes.delete(key);
        }
        // Look through new attributes that weren't handled above.
        for (const [key, newValue] of newAttributes) {
            // Each of them is a new attribute. Add diff item.
            diffs.push({
                type: 'attribute',
                position: range.start,
                range: range.clone(),
                length: 1,
                attributeKey: key,
                attributeOldValue: null,
                attributeNewValue: newValue,
                changeCount: this._changeCount++
            });
        }
        return diffs;
    }
    /**
     * Checks whether given element or any of its parents is an element that is buffered as an inserted element.
     *
     * @private
     * @param {module:engine/model/element~Element} element Element to check.
     * @returns {Boolean}
     */
    _isInInsertedElement(element) {
        const parent = element.parent;
        if (!parent) {
            return false;
        }
        const changes = this._changesInElement.get(parent);
        const offset = element.startOffset;
        if (changes) {
            for (const change of changes) {
                if (change.type == 'insert' && offset >= change.offset && offset < change.offset + change.howMany) {
                    return true;
                }
            }
        }
        return this._isInInsertedElement(parent);
    }
    /**
     * Removes deeply all buffered changes that are registered in elements from range specified by `parent`, `offset`
     * and `howMany`.
     *
     * @private
     * @param {module:engine/model/element~Element} parent
     * @param {Number} offset
     * @param {Number} howMany
     */
    _removeAllNestedChanges(parent, offset, howMany) {
        const range = new Range(Position._createAt(parent, offset), Position._createAt(parent, offset + howMany));
        for (const item of range.getItems({ shallow: true })) {
            if (item.is('element')) {
                this._elementSnapshots.delete(item);
                this._changesInElement.delete(item);
                this._removeAllNestedChanges(item, 0, item.maxOffset);
            }
        }
    }
}
// Returns an array that is a copy of passed child list with the exception that text nodes are split to one or more
// objects, each representing one character and attributes set on that character.
function _getChildrenSnapshot(children) {
    const snapshot = [];
    for (const child of children) {
        if (child.is('$text')) {
            for (let i = 0; i < child.data.length; i++) {
                snapshot.push({
                    name: '$text',
                    attributes: new Map(child.getAttributes())
                });
            }
        }
        else {
            snapshot.push({
                name: child.name,
                attributes: new Map(child.getAttributes())
            });
        }
    }
    return snapshot;
}
// Generates array of actions for given changes set.
// It simulates what `diff` function does.
// Generated actions are:
// - 'e' for 'equal' - when item at that position did not change,
// - 'i' for 'insert' - when item at that position was inserted,
// - 'r' for 'remove' - when item at that position was removed,
// - 'a' for 'attribute' - when item at that position has it attributes changed.
//
// Example (assume that uppercase letters have bold attribute, compare with function code):
//
// children before:	fooBAR
// children after:	foxybAR
//
// changes: type: remove, offset: 1, howMany: 1
//			type: insert, offset: 2, howMany: 2
//			type: attribute, offset: 4, howMany: 1
//
// expected actions: equal (f), remove (o), equal (o), insert (x), insert (y), attribute (b), equal (A), equal (R)
//
// steps taken by th script:
//
// 1. change = "type: remove, offset: 1, howMany: 1"; offset = 0; oldChildrenHandled = 0
//    1.1 between this change and the beginning is one not-changed node, fill with one equal action, one old child has been handled
//    1.2 this change removes one node, add one remove action
//    1.3 change last visited `offset` to 1
//    1.4 since an old child has been removed, one more old child has been handled
//    1.5 actions at this point are: equal, remove
//
// 2. change = "type: insert, offset: 2, howMany: 2"; offset = 1; oldChildrenHandled = 2
//    2.1 between this change and previous change is one not-changed node, add equal action, another one old children has been handled
//    2.2 this change inserts two nodes, add two insert actions
//    2.3 change last visited offset to the end of the inserted range, that is 4
//    2.4 actions at this point are: equal, remove, equal, insert, insert
//
// 3. change = "type: attribute, offset: 4, howMany: 1"; offset = 4, oldChildrenHandled = 3
//    3.1 between this change and previous change are no not-changed nodes
//    3.2 this change changes one node, add one attribute action
//    3.3 change last visited `offset` to the end of change range, that is 5
//    3.4 since an old child has been changed, one more old child has been handled
//    3.5 actions at this point are: equal, remove, equal, insert, insert, attribute
//
// 4. after loop oldChildrenHandled = 4, oldChildrenLength = 6 (fooBAR is 6 characters)
//    4.1 fill up with two equal actions
//
// The result actions are: equal, remove, equal, insert, insert, attribute, equal, equal.
function _generateActionsFromChanges(oldChildrenLength, changes) {
    const actions = [];
    let offset = 0;
    let oldChildrenHandled = 0;
    // Go through all buffered changes.
    for (const change of changes) {
        // First, fill "holes" between changes with "equal" actions.
        if (change.offset > offset) {
            for (let i = 0; i < change.offset - offset; i++) {
                actions.push('e');
            }
            oldChildrenHandled += change.offset - offset;
        }
        // Then, fill up actions accordingly to change type.
        if (change.type == 'insert') {
            for (let i = 0; i < change.howMany; i++) {
                actions.push('i');
            }
            // The last handled offset is after inserted range.
            offset = change.offset + change.howMany;
        }
        else if (change.type == 'remove') {
            for (let i = 0; i < change.howMany; i++) {
                actions.push('r');
            }
            // The last handled offset is at the position where the nodes were removed.
            offset = change.offset;
            // We removed `howMany` old nodes, update `oldChildrenHandled`.
            oldChildrenHandled += change.howMany;
        }
        else {
            actions.push(...'a'.repeat(change.howMany).split(''));
            // The last handled offset is at the position after the changed range.
            offset = change.offset + change.howMany;
            // We changed `howMany` old nodes, update `oldChildrenHandled`.
            oldChildrenHandled += change.howMany;
        }
    }
    // Fill "equal" actions at the end of actions set. Use `oldChildrenHandled` to see how many children
    // has not been changed / removed at the end of their parent.
    if (oldChildrenHandled < oldChildrenLength) {
        for (let i = 0; i < oldChildrenLength - oldChildrenHandled - offset; i++) {
            actions.push('e');
        }
    }
    return actions;
}
// Filter callback for Array.filter that filters out change entries that are in graveyard.
function _changesInGraveyardFilter(entry) {
    const posInGy = 'position' in entry && entry.position.root.rootName == '$graveyard';
    const rangeInGy = 'range' in entry && entry.range.root.rootName == '$graveyard';
    return !posInGy && !rangeInGy;
}
/**
 * The type of diff item.
 *
 * @member {'attribute'} module:engine/model/differ~DiffItemAttribute#type
 */
/**
 * The name of the changed attribute.
 *
 * @member {String} module:engine/model/differ~DiffItemAttribute#attributeKey
 */
/**
 * An attribute previous value (before change).
 *
 * @member {String} module:engine/model/differ~DiffItemAttribute#attributeOldValue
 */
/**
 * An attribute new value (after change).
 *
 * @member {String} module:engine/model/differ~DiffItemAttribute#attributeNewValue
 */
/**
 * The range where the change happened.
 *
 * @member {module:engine/model/range~Range} module:engine/model/differ~DiffItemAttribute#range
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module engine/model/history
 */
/**
 * `History` keeps the track of all the operations applied to the {@link module:engine/model/document~Document document}.
 */
class History {
    /**
     * Creates an empty History instance.
     */
    constructor() {
        /**
         * Operations added to the history.
         *
         * @private
         * @readonly
         * @type {Array.<module:engine/model/operation/operation~Operation>}
         */
        this._operations = [];
        /**
         * Holds an information which {@link module:engine/model/operation/operation~Operation operation} undoes which
         * {@link module:engine/model/operation/operation~Operation operation}.
         *
         * Keys of the map are "undoing operations", that is operations that undone some other operations. For each key, the
         * value is an operation that has been undone by the "undoing operation".
         *
         * @private
         * @member {Map} module:engine/model/history~History#_undoPairs
         */
        this._undoPairs = new Map();
        /**
         * Holds all undone operations.
         *
         * @private
         * @type {Set.<module:engine/model/operation/operation~Operation>}
         */
        this._undoneOperations = new Set();
        /**
         * A map that allows retrieving the operations fast based on the given base version.
         *
         * @private
         * @type Map.<Number,Number>
         */
        this._baseVersionToOperationIndex = new Map();
        /**
         * The history version.
         *
         * @private
         * @type {Number}
         */
        this._version = 0;
        /**
         * The gap pairs kept in the <from,to> format.
         *
         * Anytime the `history.version` is set to a version larger than `history.version + 1`,
         * a new <lastHistoryVersion, newHistoryVersion> entry is added to the map.
         *
         * @private
         * @type Map.<number,number>
         */
        this._gaps = new Map();
    }
    /**
     * The version of the last operation in the history.
     *
     * The history version is incremented automatically when a new operation is added to the history.
     * Setting the version manually should be done only in rare circumstances when a gap is planned
     * between history versions. When doing so, a gap will be created and the history will accept adding
     * an operation with base version equal to the new history version.
     *
     * @type {Number}
     */
    get version() {
        return this._version;
    }
    set version(version) {
        // Store a gap if there are some operations already in the history and the
        // new version does not increment the latest one.
        if (this._operations.length && version > this._version + 1) {
            this._gaps.set(this._version, version);
        }
        this._version = version;
    }
    /**
     * The last history operation.
     *
     * @readonly
     * @type {module:engine/model/operation/operation~Operation|undefined}
     */
    get lastOperation() {
        return this._operations[this._operations.length - 1];
    }
    /**
     * Adds an operation to the history and increments the history version.
     *
     * The operation's base version should be equal to the history version. Otherwise an error is thrown.
     *
     * @param {module:engine/model/operation/operation~Operation} operation Operation to add.
     */
    addOperation(operation) {
        if (operation.baseVersion !== this.version) {
            /**
             * Only operations with matching versions can be added to the history.
             *
             * @error model-document-history-addoperation-incorrect-version
             * @param {Object} errorData The operation and the current document history version.
             */
            throw new CKEditorError('model-document-history-addoperation-incorrect-version', this, {
                operation,
                historyVersion: this.version
            });
        }
        this._operations.push(operation);
        this._version++;
        this._baseVersionToOperationIndex.set(operation.baseVersion, this._operations.length - 1);
    }
    /**
     * Returns operations from the given range of operation base versions that were added to the history.
     *
     * Note that there may be gaps in operations base versions.
     *
     * @param {Number} [fromBaseVersion] Base version from which operations should be returned (inclusive).
     * @param {Number} [toBaseVersion] Base version up to which operations should be returned (exclusive).
     * @returns {Array.<module:engine/model/operation/operation~Operation>} History operations for the given range, in chronological order.
     */
    getOperations(fromBaseVersion, toBaseVersion = this.version) {
        // When there is no operation in the history, return an empty array.
        // After that we can be sure that `firstOperation`, `lastOperation` are not nullish.
        if (!this._operations.length) {
            return [];
        }
        const firstOperation = this._operations[0];
        if (fromBaseVersion === undefined) {
            fromBaseVersion = firstOperation.baseVersion;
        }
        // Change exclusive `toBaseVersion` to inclusive, so it will refer to the actual index.
        // Thanks to that mapping from base versions to operation indexes are possible.
        let inclusiveTo = toBaseVersion - 1;
        // Check if "from" or "to" point to a gap between versions.
        // If yes, then change the incorrect position to the proper side of the gap.
        // Thanks to it, it will be possible to get index of the operation.
        for (const [gapFrom, gapTo] of this._gaps) {
            if (fromBaseVersion > gapFrom && fromBaseVersion < gapTo) {
                fromBaseVersion = gapTo;
            }
            if (inclusiveTo > gapFrom && inclusiveTo < gapTo) {
                inclusiveTo = gapFrom - 1;
            }
        }
        // If the whole range is outside of the operation versions, then return an empty array.
        if (inclusiveTo < firstOperation.baseVersion || fromBaseVersion > this.lastOperation.baseVersion) {
            return [];
        }
        let fromIndex = this._baseVersionToOperationIndex.get(fromBaseVersion);
        // If the range starts before the first operation, then use the first operation as the range's start.
        if (fromIndex === undefined) {
            fromIndex = 0;
        }
        let toIndex = this._baseVersionToOperationIndex.get(inclusiveTo);
        // If the range ends after the last operation, then use the last operation as the range's end.
        if (toIndex === undefined) {
            toIndex = this._operations.length - 1;
        }
        // Return the part of the history operations based on the calculated start index and end index.
        return this._operations.slice(fromIndex, 
        // The `toIndex` should be included in the returned operations, so add `1`.
        toIndex + 1);
    }
    /**
     * Returns operation from the history that bases on given `baseVersion`.
     *
     * @param {Number} baseVersion Base version of the operation to get.
     * @returns {module:engine/model/operation/operation~Operation|undefined} Operation with given base version or `undefined` if
     * there is no such operation in history.
     */
    getOperation(baseVersion) {
        const operationIndex = this._baseVersionToOperationIndex.get(baseVersion);
        if (operationIndex === undefined) {
            return;
        }
        return this._operations[operationIndex];
    }
    /**
     * Marks in history that one operation is an operation that is undoing the other operation. By marking operation this way,
     * history is keeping more context information about operations, which helps in operational transformation.
     *
     * @param {module:engine/model/operation/operation~Operation} undoneOperation Operation which is undone by `undoingOperation`.
     * @param {module:engine/model/operation/operation~Operation} undoingOperation Operation which undoes `undoneOperation`.
     */
    setOperationAsUndone(undoneOperation, undoingOperation) {
        this._undoPairs.set(undoingOperation, undoneOperation);
        this._undoneOperations.add(undoneOperation);
    }
    /**
     * Checks whether given `operation` is undoing any other operation.
     *
     * @param {module:engine/model/operation/operation~Operation} operation Operation to check.
     * @returns {Boolean} `true` if given `operation` is undoing any other operation, `false` otherwise.
     */
    isUndoingOperation(operation) {
        return this._undoPairs.has(operation);
    }
    /**
     * Checks whether given `operation` has been undone by any other operation.
     *
     * @param {module:engine/model/operation/operation~Operation} operation Operation to check.
     * @returns {Boolean} `true` if given `operation` has been undone any other operation, `false` otherwise.
     */
    isUndoneOperation(operation) {
        return this._undoneOperations.has(operation);
    }
    /**
     * For given `undoingOperation`, returns the operation which has been undone by it.
     *
     * @param {module:engine/model/operation/operation~Operation} undoingOperation
     * @returns {module:engine/model/operation/operation~Operation|undefined} Operation that has been undone by given
     * `undoingOperation` or `undefined` if given `undoingOperation` is not undoing any other operation.
     */
    getUndoneOperation(undoingOperation) {
        return this._undoPairs.get(undoingOperation);
    }
    /**
     * Resets the history of operations.
     */
    reset() {
        this._version = 0;
        this._undoPairs = new Map();
        this._operations = [];
        this._undoneOperations = new Set();
        this._gaps = new Map();
        this._baseVersionToOperationIndex = new Map();
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Type of {@link module:engine/model/element~Element} that is a root of a model tree.
 * @extends module:engine/model/element~Element
 */
class RootElement extends Element {
    /**
     * Creates root element.
     *
     * @param {module:engine/model/document~Document} document Document that is an owner of this root.
     * @param {String} name Node name.
     * @param {String} [rootName='main'] Unique root name used to identify this root
     * element by {@link module:engine/model/document~Document}.
     */
    constructor(document, name, rootName = 'main') {
        super(name);
        /**
         * Document that is an owner of this root.
         *
         * @private
         * @member {module:engine/model/document~Document}
         */
        this._document = document;
        /**
         * Unique root name used to identify this root element by {@link module:engine/model/document~Document}.
         *
         * @readonly
         * @member {String}
         */
        this.rootName = rootName;
    }
    /**
     * {@link module:engine/model/document~Document Document} that owns this root element.
     *
     * @readonly
     * @type {module:engine/model/document~Document|null}
     */
    get document() {
        return this._document;
    }
    /**
     * Converts `RootElement` instance to `String` containing it's name.
     *
     * @returns {String} `RootElement` instance converted to `String`.
     */
    toJSON() {
        return this.rootName;
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		rootElement.is( 'rootElement' ); // -> true
 *		rootElement.is( 'element' ); // -> true
 *		rootElement.is( 'node' ); // -> true
 *		rootElement.is( 'model:rootElement' ); // -> true
 *		rootElement.is( 'model:element' ); // -> true
 *		rootElement.is( 'model:node' ); // -> true
 *
 *		rootElement.is( 'view:element' ); // -> false
 *		rootElement.is( 'documentFragment' ); // -> false
 *
 * Assuming that the object being checked is an element, you can also check its
 * {@link module:engine/model/element~Element#name name}:
 *
 *		rootElement.is( 'rootElement', '$root' ); // -> same as above
 *
 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 *
 * @param {String} type Type to check.
 * @param {String} [name] Element name.
 * @returns {Boolean}
 */
RootElement.prototype.is = function (type, name) {
    if (!name) {
        return type === 'rootElement' || type === 'model:rootElement' ||
            // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
            type === 'element' || type === 'model:element' ||
            type === 'node' || type === 'model:node';
    }
    return name === this.name && (type === 'rootElement' || type === 'model:rootElement' ||
        // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
        type === 'element' || type === 'model:element');
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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) {
    // eslint-disable-next-line no-misleading-character-class
    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));
}
const EMOJI_PATTERN = buildEmojiRegexp();
/**
 * Checks whether given offset in a string is inside multi-character emoji sequence.
 *
 * @param {String} string String to check.
 * @param {Number} offset Offset to check.
 * @returns {Boolean}
 */
function isInsideEmojiSequence(string, offset) {
    const matches = String(string).matchAll(EMOJI_PATTERN);
    return Array.from(matches).some(match => match.index < offset && offset < match.index + match[0].length);
}
function buildEmojiRegexp() {
    const parts = [
        // Emoji Tag Sequence (ETS)
        /\p{Emoji}[\u{E0020}-\u{E007E}]+\u{E007F}/u,
        // Emoji Keycap Sequence
        /\p{Emoji}\u{FE0F}?\u{20E3}/u,
        // Emoji Presentation Sequence
        /\p{Emoji}\u{FE0F}/u,
        // Single-Character Emoji / Emoji Modifier Sequence
        /(?=\p{General_Category=Other_Symbol})\p{Emoji}\p{Emoji_Modifier}*/u
    ];
    const flagSequence = /\p{Regional_Indicator}{2}/u.source;
    const emoji = '(?:' + parts.map(part => part.source).join('|') + ')';
    const sequence = `${flagSequence}|${emoji}(?:\u{200D}${emoji})*`;
    return new RegExp(sequence, 'ug');
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// @if CK_DEBUG_ENGINE // const { logDocument } = require( '../dev-utils/utils' );
const graveyardName = '$graveyard';
/**
 * Data model's document. It contains the model's structure, its selection and the history of changes.
 *
 * Read more about working with the model in
 * {@glink framework/guides/architecture/editing-engine#model introduction to the the editing engine's architecture}.
 *
 * Usually, the document contains just one {@link module:engine/model/document~Document#roots root element}, so
 * you can retrieve it by just calling {@link module:engine/model/document~Document#getRoot} without specifying its name:
 *
 *		model.document.getRoot(); // -> returns the main root
 *
 * However, the document may contain multiple roots – e.g. when the editor has multiple editable areas
 * (e.g. a title and a body of a message).
 *
 * @mixes module:utils/emittermixin~EmitterMixin
 */
class Document extends Emitter$1 {
    /**
     * Creates an empty document instance with no {@link #roots} (other than
     * the {@link #graveyard graveyard root}).
     */
    constructor(model) {
        super();
        /**
         * The {@link module:engine/model/model~Model model} that the document is a part of.
         *
         * @readonly
         * @type {module:engine/model/model~Model}
         */
        this.model = model;
        /**
         * The document's history.
         *
         * @readonly
         * @type {module:engine/model/history~History}
         */
        this.history = new History();
        /**
         * The selection in this document.
         *
         * @readonly
         * @type {module:engine/model/documentselection~DocumentSelection}
         */
        this.selection = new DocumentSelection(this);
        /**
         * A list of roots that are owned and managed by this document. Use {@link #createRoot} and
         * {@link #getRoot} to manipulate it.
         *
         * @readonly
         * @type {module:utils/collection~Collection}
         */
        this.roots = new Collection({ idProperty: 'rootName' });
        /**
         * The model differ object. Its role is to buffer changes done on the model document and then calculate a diff of those changes.
         *
         * @readonly
         * @type {module:engine/model/differ~Differ}
         */
        this.differ = new Differ(model.markers);
        /**
         * Post-fixer callbacks registered to the model document.
         *
         * @private
         * @type {Set.<Function>}
         */
        this._postFixers = new Set();
        /**
         * A boolean indicates whether the selection has changed until
         *
         * @private
         * @type {Boolean}
         */
        this._hasSelectionChangedFromTheLastChangeBlock = false;
        // Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
        this.createRoot('$root', graveyardName);
        // Then, still before an operation is applied on model, buffer the change in differ.
        this.listenTo(model, 'applyOperation', (evt, args) => {
            const operation = args[0];
            if (operation.isDocumentOperation) {
                this.differ.bufferOperation(operation);
            }
        }, { priority: 'high' });
        // After the operation is applied, bump document's version and add the operation to the history.
        this.listenTo(model, 'applyOperation', (evt, args) => {
            const operation = args[0];
            if (operation.isDocumentOperation) {
                this.history.addOperation(operation);
            }
        }, { priority: 'low' });
        // Listen to selection changes. If selection changed, mark it.
        this.listenTo(this.selection, 'change', () => {
            this._hasSelectionChangedFromTheLastChangeBlock = true;
        });
        // Buffer marker changes.
        // This is not covered in buffering operations because markers may change outside of them (when they
        // are modified using `model.markers` collection, not through `MarkerOperation`).
        this.listenTo(model.markers, 'update', (evt, marker, oldRange, newRange, oldMarkerData) => {
            // Copy the `newRange` to the new marker data as during the marker removal the range is not updated.
            const newMarkerData = { ...marker.getData(), range: newRange };
            // Whenever marker is updated, buffer that change.
            this.differ.bufferMarkerChange(marker.name, oldMarkerData, newMarkerData);
            if (oldRange === null) {
                // If this is a new marker, add a listener that will buffer change whenever marker changes.
                marker.on('change', (evt, oldRange) => {
                    const markerData = marker.getData();
                    this.differ.bufferMarkerChange(marker.name, { ...markerData, range: oldRange }, markerData);
                });
            }
        });
    }
    /**
     * The document version. Every applied operation increases the version number. It is used to
     * ensure that operations are applied on a proper document version.
     *
     * This property is equal to {@link module:engine/model/history~History#version `model.Document#history#version`}.
     *
     * If the {@link module:engine/model/operation/operation~Operation#baseVersion base version} does not match the document version,
     * a {@link module:utils/ckeditorerror~CKEditorError model-document-applyoperation-wrong-version} error is thrown.
     *
     * @type {Number}
     */
    get version() {
        return this.history.version;
    }
    set version(version) {
        this.history.version = version;
    }
    /**
     * The graveyard tree root. A document always has a graveyard root that stores removed nodes.
     *
     * @readonly
     * @member {module:engine/model/rootelement~RootElement}
     */
    get graveyard() {
        return this.getRoot(graveyardName);
    }
    /**
     * Creates a new root.
     *
     * @param {String} [elementName='$root'] The element name. Defaults to `'$root'` which also has some basic schema defined
     * (`$block`s are allowed inside the `$root`). Make sure to define a proper schema if you use a different name.
     * @param {String} [rootName='main'] A unique root name.
     * @returns {module:engine/model/rootelement~RootElement} The created root.
     */
    createRoot(elementName = '$root', rootName = 'main') {
        if (this.roots.get(rootName)) {
            /**
             * A root with the specified name already exists.
             *
             * @error model-document-createroot-name-exists
             * @param {module:engine/model/document~Document} doc
             * @param {String} name
             */
            throw new CKEditorError('model-document-createroot-name-exists', this, { name: rootName });
        }
        const root = new RootElement(this, elementName, rootName);
        this.roots.add(root);
        return root;
    }
    /**
     * Removes all event listeners set by the document instance.
     */
    destroy() {
        this.selection.destroy();
        this.stopListening();
    }
    /**
     * Returns a root by its name.
     *
     * @param {String} [name='main'] A unique root name.
     * @returns {module:engine/model/rootelement~RootElement|null} The root registered under a given name or `null` when
     * there is no root with the given name.
     */
    getRoot(name = 'main') {
        return this.roots.get(name);
    }
    /**
     * Returns an array with names of all roots (without the {@link #graveyard}) added to the document.
     *
     * @returns {Array.<String>} Roots names.
     */
    getRootNames() {
        return Array.from(this.roots, root => root.rootName).filter(name => name != graveyardName);
    }
    /**
     * Used to register a post-fixer callback. A post-fixer mechanism guarantees that the features
     * will operate on a correct model state.
     *
     * An execution of a feature may lead to an incorrect document tree state. The callbacks are used to fix the document tree after
     * it has changed. Post-fixers are fired just after all changes from the outermost change block were applied but
     * before the {@link module:engine/model/document~Document#event:change change event} is fired. If a post-fixer callback made
     * a change, it should return `true`. When this happens, all post-fixers are fired again to check if something else should
     * not be fixed in the new document tree state.
     *
     * As a parameter, a post-fixer callback receives a {@link module:engine/model/writer~Writer writer} instance connected with the
     * executed changes block. Thanks to that, all changes done by the callback will be added to the same
     * {@link module:engine/model/batch~Batch batch} (and undo step) as the original changes. This makes post-fixer changes transparent
     * for the user.
     *
     * An example of a post-fixer is a callback that checks if all the data were removed from the editor. If so, the
     * callback should add an empty paragraph so that the editor is never empty:
     *
     *		document.registerPostFixer( writer => {
     *			const changes = document.differ.getChanges();
     *
     *			// Check if the changes lead to an empty root in the editor.
     *			for ( const entry of changes ) {
     *				if ( entry.type == 'remove' && entry.position.root.isEmpty ) {
     *					writer.insertElement( 'paragraph', entry.position.root, 0 );
     *
     *					// It is fine to return early, even if multiple roots would need to be fixed.
     *					// All post-fixers will be fired again, so if there are more empty roots, those will be fixed, too.
     *					return true;
     *				}
     *			}
     *
     *			return false;
     *		} );
     *
     * @param {Function} postFixer
     */
    registerPostFixer(postFixer) {
        this._postFixers.add(postFixer);
    }
    /**
     * A custom `toJSON()` method to solve child-parent circular dependencies.
     *
     * @returns {Object} A clone of this object with the document property changed to a string.
     */
    toJSON() {
        const json = clone$1(this);
        // Due to circular references we need to remove parent reference.
        json.selection = '[engine.model.DocumentSelection]';
        json.model = '[engine.model.Model]';
        return json;
    }
    /**
     * Check if there were any changes done on document, and if so, call post-fixers,
     * fire `change` event for features and conversion and then reset the differ.
     * Fire `change:data` event when at least one operation or buffered marker changes the data.
     *
     * @internal
     * @protected
     * @fires change
     * @fires change:data
     * @param {module:engine/model/writer~Writer} writer The writer on which post-fixers will be called.
     */
    _handleChangeBlock(writer) {
        if (this._hasDocumentChangedFromTheLastChangeBlock()) {
            this._callPostFixers(writer);
            // Refresh selection attributes according to the final position in the model after the change.
            this.selection.refresh();
            if (this.differ.hasDataChanges()) {
                this.fire('change:data', writer.batch);
            }
            else {
                this.fire('change', writer.batch);
            }
            // Theoretically, it is not necessary to refresh selection after change event because
            // post-fixers are the last who should change the model, but just in case...
            this.selection.refresh();
            this.differ.reset();
        }
        this._hasSelectionChangedFromTheLastChangeBlock = false;
    }
    /**
     * Returns whether there is a buffered change or if the selection has changed from the last
     * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()` block}
     * or {@link module:engine/model/model~Model#change `change()` block}.
     *
     * @protected
     * @returns {Boolean} Returns `true` if document has changed from the last `change()` or `enqueueChange()` block.
     */
    _hasDocumentChangedFromTheLastChangeBlock() {
        return !this.differ.isEmpty || this._hasSelectionChangedFromTheLastChangeBlock;
    }
    /**
     * Returns the default root for this document which is either the first root that was added to the document using
     * {@link #createRoot} or the {@link #graveyard graveyard root} if no other roots were created.
     *
     * @protected
     * @returns {module:engine/model/rootelement~RootElement} The default root for this document.
     */
    _getDefaultRoot() {
        for (const root of this.roots) {
            if (root !== this.graveyard) {
                return root;
            }
        }
        return this.graveyard;
    }
    /**
     * Returns the default range for this selection. The default range is a collapsed range that starts and ends
     * at the beginning of this selection's document {@link #_getDefaultRoot default root}.
     *
     * @internal
     * @protected
     * @returns {module:engine/model/range~Range}
     */
    _getDefaultRange() {
        const defaultRoot = this._getDefaultRoot();
        const model = this.model;
        const schema = model.schema;
        // Find the first position where the selection can be put.
        const position = model.createPositionFromPath(defaultRoot, [0]);
        const nearestRange = schema.getNearestSelectionRange(position);
        // If valid selection range is not found - return range collapsed at the beginning of the root.
        return nearestRange || model.createRange(position);
    }
    /**
     * Checks whether a given {@link module:engine/model/range~Range range} is a valid range for
     * the {@link #selection document's selection}.
     *
     * @internal
     * @protected
     * @param {module:engine/model/range~Range} range A range to check.
     * @returns {Boolean} `true` if `range` is valid, `false` otherwise.
     */
    _validateSelectionRange(range) {
        return validateTextNodePosition(range.start) && validateTextNodePosition(range.end);
    }
    /**
     * Performs post-fixer loops. Executes post-fixer callbacks as long as none of them has done any changes to the model.
     *
     * @private
     * @param {module:engine/model/writer~Writer} writer The writer on which post-fixer callbacks will be called.
     */
    _callPostFixers(writer) {
        let wasFixed = false;
        do {
            for (const callback of this._postFixers) {
                // Ensure selection attributes are up to date before each post-fixer.
                // https://github.com/ckeditor/ckeditor5-engine/issues/1673.
                //
                // It might be good to refresh the selection after each operation but at the moment it leads
                // to losing attributes for composition or and spell checking
                // https://github.com/ckeditor/ckeditor5-typing/issues/188
                this.selection.refresh();
                wasFixed = callback(writer);
                if (wasFixed) {
                    break;
                }
            }
        } while (wasFixed);
    }
}
// Checks whether given range boundary position is valid for document selection, meaning that is not between
// unicode surrogate pairs or base character and combining marks.
function validateTextNodePosition(rangeBoundary) {
    const textNode = rangeBoundary.textNode;
    if (textNode) {
        const data = textNode.data;
        const offset = rangeBoundary.offset - textNode.startOffset;
        return !isInsideSurrogatePair(data, offset) && !isInsideCombinedSymbol(data, offset);
    }
    return true;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * The collection of all {@link module:engine/model/markercollection~Marker markers} attached to the document.
 * It lets you {@link module:engine/model/markercollection~MarkerCollection#get get} markers or track them using
 * {@link module:engine/model/markercollection~MarkerCollection#event:update} event.
 *
 * To create, change or remove makers use {@link module:engine/model/writer~Writer model writers'} methods:
 * {@link module:engine/model/writer~Writer#addMarker} or {@link module:engine/model/writer~Writer#removeMarker}. Since
 * the writer is the only proper way to change the data model it is not possible to change markers directly using this
 * collection. All markers created by the writer will be automatically added to this collection.
 *
 * By default there is one marker collection available as {@link module:engine/model/model~Model#markers model property}.
 *
 * @see module:engine/model/markercollection~Marker
 */
class MarkerCollection extends Emitter$1 {
    /**
     * Creates a markers collection.
     */
    constructor() {
        super();
        /**
         * Stores {@link ~Marker markers} added to the collection.
         *
         * @private
         * @member {Map} #_markers
         */
        this._markers = new Map();
    }
    /**
     * Iterable interface.
     *
     * Iterates over all {@link ~Marker markers} added to the collection.
     *
     * @returns {Iterator}
     */
    [Symbol.iterator]() {
        return this._markers.values();
    }
    /**
     * Checks if given {@link ~Marker marker} or marker name is in the collection.
     *
     * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of marker or marker instance to check.
     * @returns {Boolean} `true` if marker is in the collection, `false` otherwise.
     */
    has(markerOrName) {
        const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
        return this._markers.has(markerName);
    }
    /**
     * Returns {@link ~Marker marker} with given `markerName`.
     *
     * @param {String} markerName Name of marker to get.
     * @returns {module:engine/model/markercollection~Marker|null} Marker with given name or `null` if such marker was
     * not added to the collection.
     */
    get(markerName) {
        return this._markers.get(markerName) || null;
    }
    /**
     * Creates and adds a {@link ~Marker marker} to the `MarkerCollection` with given name on given
     * {@link module:engine/model/range~Range range}.
     *
     * If `MarkerCollection` already had a marker with given name (or {@link ~Marker marker} was passed), the marker in
     * collection is updated and {@link module:engine/model/markercollection~MarkerCollection#event:update} event is fired
     * but only if there was a change (marker range or {@link module:engine/model/markercollection~Marker#managedUsingOperations}
     * flag has changed.
     *
     * @internal
     * @protected
     * @fires module:engine/model/markercollection~MarkerCollection#event:update
     * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of marker to set or marker instance to update.
     * @param {module:engine/model/range~Range} range Marker range.
     * @param {Boolean} [managedUsingOperations=false] Specifies whether the marker is managed using operations.
     * @param {Boolean} [affectsData=false] Specifies whether the marker affects the data produced by the data pipeline
     * (is persisted in the editor's data).
     * @returns {module:engine/model/markercollection~Marker} `Marker` instance which was added or updated.
     */
    _set(markerOrName, range, managedUsingOperations = false, affectsData = false) {
        const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
        if (markerName.includes(',')) {
            /**
             * Marker name cannot contain the "," character.
             *
             * @error markercollection-incorrect-marker-name
             */
            throw new CKEditorError('markercollection-incorrect-marker-name', this);
        }
        const oldMarker = this._markers.get(markerName);
        if (oldMarker) {
            const oldMarkerData = oldMarker.getData();
            const oldRange = oldMarker.getRange();
            let hasChanged = false;
            if (!oldRange.isEqual(range)) {
                oldMarker._attachLiveRange(LiveRange.fromRange(range));
                hasChanged = true;
            }
            if (managedUsingOperations != oldMarker.managedUsingOperations) {
                oldMarker._managedUsingOperations = managedUsingOperations;
                hasChanged = true;
            }
            if (typeof affectsData === 'boolean' && affectsData != oldMarker.affectsData) {
                oldMarker._affectsData = affectsData;
                hasChanged = true;
            }
            if (hasChanged) {
                this.fire(`update:${markerName}`, oldMarker, oldRange, range, oldMarkerData);
            }
            return oldMarker;
        }
        const liveRange = LiveRange.fromRange(range);
        const marker = new Marker(markerName, liveRange, managedUsingOperations, affectsData);
        this._markers.set(markerName, marker);
        this.fire(`update:${markerName}`, marker, null, range, { ...marker.getData(), range: null });
        return marker;
    }
    /**
     * Removes given {@link ~Marker marker} or a marker with given name from the `MarkerCollection`.
     *
     * @internal
     * @protected
     * @fires module:engine/model/markercollection~MarkerCollection#event:update
     * @param {String|module:engine/model/markercollection~Marker} markerOrName Marker or name of a marker to remove.
     * @returns {Boolean} `true` if marker was found and removed, `false` otherwise.
     */
    _remove(markerOrName) {
        const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
        const oldMarker = this._markers.get(markerName);
        if (oldMarker) {
            this._markers.delete(markerName);
            this.fire(`update:${markerName}`, oldMarker, oldMarker.getRange(), null, oldMarker.getData());
            this._destroyMarker(oldMarker);
            return true;
        }
        return false;
    }
    /**
     * Fires an {@link module:engine/model/markercollection~MarkerCollection#event:update} event for the given {@link ~Marker marker}
     * but does not change the marker. Useful to force {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast
     * conversion} for the marker.
     *
     * @internal
     * @protected
     * @fires module:engine/model/markercollection~MarkerCollection#event:update
     * @param {String|module:engine/model/markercollection~Marker} markerOrName Marker or name of a marker to refresh.
     */
    _refresh(markerOrName) {
        const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
        const marker = this._markers.get(markerName);
        if (!marker) {
            /**
             * Marker with provided name does not exists.
             *
             * @error markercollection-refresh-marker-not-exists
             */
            throw new CKEditorError('markercollection-refresh-marker-not-exists', this);
        }
        const range = marker.getRange();
        this.fire(`update:${markerName}`, marker, range, range, marker.getData());
    }
    /**
     * Returns iterator that iterates over all markers, which ranges contain given {@link module:engine/model/position~Position position}.
     *
     * @param {module:engine/model/position~Position} position
     * @returns {Iterable.<module:engine/model/markercollection~Marker>}
     */
    *getMarkersAtPosition(position) {
        for (const marker of this) {
            if (marker.getRange().containsPosition(position)) {
                yield marker;
            }
        }
    }
    /**
     * Returns iterator that iterates over all markers, which intersects with given {@link module:engine/model/range~Range range}.
     *
     * @param {module:engine/model/range~Range} range
     * @returns {Iterable.<module:engine/model/markercollection~Marker>}
     */
    *getMarkersIntersectingRange(range) {
        for (const marker of this) {
            if (marker.getRange().getIntersection(range) !== null) {
                yield marker;
            }
        }
    }
    /**
     * Destroys marker collection and all markers inside it.
     */
    destroy() {
        for (const marker of this._markers.values()) {
            this._destroyMarker(marker);
        }
        this._markers = null;
        this.stopListening();
    }
    /**
     * Iterates over all markers that starts with given `prefix`.
     *
     *		const markerFooA = markersCollection.set( 'foo:a', rangeFooA );
     *		const markerFooB = markersCollection.set( 'foo:b', rangeFooB );
     *		const markerBarA = markersCollection.set( 'bar:a', rangeBarA );
     *		const markerFooBarA = markersCollection.set( 'foobar:a', rangeFooBarA );
     *		Array.from( markersCollection.getMarkersGroup( 'foo' ) ); // [ markerFooA, markerFooB ]
     *		Array.from( markersCollection.getMarkersGroup( 'a' ) ); // []
     *
     * @param prefix
     * @returns {Iterable.<module:engine/model/markercollection~Marker>}
     */
    *getMarkersGroup(prefix) {
        for (const marker of this._markers.values()) {
            if (marker.name.startsWith(prefix + ':')) {
                yield marker;
            }
        }
    }
    /**
     * Destroys the marker.
     *
     * @private
     * @param {module:engine/model/markercollection~Marker} marker Marker to destroy.
     */
    _destroyMarker(marker) {
        marker.stopListening();
        marker._detachLiveRange();
    }
}
/**
 * `Marker` is a continuous parts of model (like a range), is named and represent some kind of information about marked
 * part of model document. In contrary to {@link module:engine/model/node~Node nodes}, which are building blocks of
 * model document tree, markers are not stored directly in document tree but in
 * {@link module:engine/model/model~Model#markers model markers' collection}. Still, they are document data, by giving
 * additional meaning to the part of a model document between marker start and marker end.
 *
 * In this sense, markers are similar to adding and converting attributes on nodes. The difference is that attribute is
 * connected with a given node (e.g. a character is bold no matter if it gets moved or content around it changes).
 * Markers on the other hand are continuous ranges and are characterized by their start and end position. This means that
 * any character in the marker is marked by the marker. For example, if a character is moved outside of marker it stops being
 * "special" and the marker is shrunk. Similarly, when a character is moved into the marker from other place in document
 * model, it starts being "special" and the marker is enlarged.
 *
 * Another upside of markers is that finding marked part of document is fast and easy. Using attributes to mark some nodes
 * and then trying to find that part of document would require traversing whole document tree. Marker gives instant access
 * to the range which it is marking at the moment.
 *
 * Markers are built from a name and a range.
 *
 * Range of the marker is updated automatically when document changes, using
 * {@link module:engine/model/liverange~LiveRange live range} mechanism.
 *
 * Name is used to group and identify markers. Names have to be unique, but markers can be grouped by
 * using common prefixes, separated with `:`, for example: `user:john` or `search:3`. That's useful in term of creating
 * namespaces for custom elements (e.g. comments, highlights). You can use this prefixes in
 * {@link module:engine/model/markercollection~MarkerCollection#event:update} listeners to listen on changes in a group of markers.
 * For instance: `model.markers.on( 'update:user', callback );` will be called whenever any `user:*` markers changes.
 *
 * There are two types of markers.
 *
 * 1. Markers managed directly, without using operations. They are added directly by {@link module:engine/model/writer~Writer}
 * to the {@link module:engine/model/markercollection~MarkerCollection} without any additional mechanism. They can be used
 * as bookmarks or visual markers. They are great for showing results of the find, or select link when the focus is in the input.
 *
 * 1. Markers managed using operations. These markers are also stored in {@link module:engine/model/markercollection~MarkerCollection}
 * but changes in these markers is managed the same way all other changes in the model structure - using operations.
 * Therefore, they are handled in the undo stack and synchronized between clients if the collaboration plugin is enabled.
 * This type of markers is useful for solutions like spell checking or comments.
 *
 * Both type of them should be added / updated by {@link module:engine/model/writer~Writer#addMarker}
 * and removed by {@link module:engine/model/writer~Writer#removeMarker} methods.
 *
 *		model.change( ( writer ) => {
 * 			const marker = writer.addMarker( name, { range, usingOperation: true } );
 *
 * 			// ...
 *
 * 			writer.removeMarker( marker );
 *		} );
 *
 * See {@link module:engine/model/writer~Writer} to find more examples.
 *
 * Since markers need to track change in the document, for efficiency reasons, it is best to create and keep as little
 * markers as possible and remove them as soon as they are not needed anymore.
 *
 * Markers can be downcasted and upcasted.
 *
 * Markers downcast happens on {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} and
 * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} events.
 * Use {@link module:engine/conversion/downcasthelpers downcast converters} or attach a custom converter to mentioned events.
 * For {@link module:engine/controller/datacontroller~DataController data pipeline}, marker should be downcasted to an element.
 * Then, it can be upcasted back to a marker. Again, use {@link module:engine/conversion/upcasthelpers upcast converters} or
 * attach a custom converter to {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element}.
 *
 * `Marker` instances are created and destroyed only by {@link ~MarkerCollection MarkerCollection}.
 */
class Marker extends EmitterMixin(TypeCheckable) {
    /**
     * Creates a marker instance.
     *
     * @param {String} name Marker name.
     * @param {module:engine/model/liverange~LiveRange} liveRange Range marked by the marker.
     * @param {Boolean} managedUsingOperations Specifies whether the marker is managed using operations.
     * @param {Boolean} affectsData Specifies whether the marker affects the data produced by the data pipeline
     * (is persisted in the editor's data).
     */
    constructor(name, liveRange, managedUsingOperations, affectsData) {
        super();
        /**
         * Marker's name.
         *
         * @readonly
         * @type {String}
         */
        this.name = name;
        /**
         * Range marked by the marker.
         *
         * @protected
         * @member {module:engine/model/liverange~LiveRange}
         */
        this._liveRange = this._attachLiveRange(liveRange);
        /**
         * Flag indicates if the marker is managed using operations or not.
         *
         * @private
         * @member {Boolean}
         */
        this._managedUsingOperations = managedUsingOperations;
        /**
         * Specifies whether the marker affects the data produced by the data pipeline
         * (is persisted in the editor's data).
         *
         * @private
         * @member {Boolean}
         */
        this._affectsData = affectsData;
    }
    /**
     * A value indicating if the marker is managed using operations.
     * See {@link ~Marker marker class description} to learn more about marker types.
     * See {@link module:engine/model/writer~Writer#addMarker}.
     *
     * @returns {Boolean}
     */
    get managedUsingOperations() {
        if (!this._liveRange) {
            throw new CKEditorError('marker-destroyed', this);
        }
        return this._managedUsingOperations;
    }
    /**
     * A value indicating if the marker changes the data.
     *
     * @returns {Boolean}
     */
    get affectsData() {
        if (!this._liveRange) {
            throw new CKEditorError('marker-destroyed', this);
        }
        return this._affectsData;
    }
    /**
     * Returns the marker data (properties defining the marker).
     *
     * @returns {module:engine/model/markercollection~MarkerData}
     */
    getData() {
        return {
            range: this.getRange(),
            affectsData: this.affectsData,
            managedUsingOperations: this.managedUsingOperations
        };
    }
    /**
     * Returns current marker start position.
     *
     * @returns {module:engine/model/position~Position}
     */
    getStart() {
        if (!this._liveRange) {
            throw new CKEditorError('marker-destroyed', this);
        }
        return this._liveRange.start.clone();
    }
    /**
     * Returns current marker end position.
     *
     * @returns {module:engine/model/position~Position}
     */
    getEnd() {
        if (!this._liveRange) {
            throw new CKEditorError('marker-destroyed', this);
        }
        return this._liveRange.end.clone();
    }
    /**
     * Returns a range that represents the current state of the marker.
     *
     * Keep in mind that returned value is a {@link module:engine/model/range~Range Range}, not a
     * {@link module:engine/model/liverange~LiveRange LiveRange}. This means that it is up-to-date and relevant only
     * until next model document change. Do not store values returned by this method. Instead, store {@link ~Marker#name}
     * and get `Marker` instance from {@link module:engine/model/markercollection~MarkerCollection MarkerCollection} every
     * time there is a need to read marker properties. This will guarantee that the marker has not been removed and
     * that it's data is up-to-date.
     *
     * @returns {module:engine/model/range~Range}
     */
    getRange() {
        if (!this._liveRange) {
            throw new CKEditorError('marker-destroyed', this);
        }
        return this._liveRange.toRange();
    }
    /**
     * Binds new live range to the marker and detach the old one if is attached.
     *
     * @internal
     * @protected
     * @param {module:engine/model/liverange~LiveRange} liveRange Live range to attach
     * @returns {module:engine/model/liverange~LiveRange} Attached live range.
     */
    _attachLiveRange(liveRange) {
        if (this._liveRange) {
            this._detachLiveRange();
        }
        // Delegating does not work with namespaces. Alternatively, we could delegate all events (using `*`).
        liveRange.delegate('change:range').to(this);
        liveRange.delegate('change:content').to(this);
        this._liveRange = liveRange;
        return liveRange;
    }
    /**
     * Unbinds and destroys currently attached live range.
     *
     * @internal
     * @protected
     */
    _detachLiveRange() {
        this._liveRange.stopDelegating('change:range', this);
        this._liveRange.stopDelegating('change:content', this);
        this._liveRange.detach();
        this._liveRange = null;
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		marker.is( 'marker' ); // -> true
 *		marker.is( 'model:marker' ); // -> true
 *
 *		marker.is( 'view:element' ); // -> false
 *		marker.is( 'documentSelection' ); // -> false
 *
 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 *
 * @param {String} type
 * @returns {Boolean}
 */
Marker.prototype.is = function (type) {
    return type === 'marker' || type === 'model:marker';
};
/**
 * Cannot use a {@link module:engine/model/markercollection~MarkerCollection#destroy destroyed marker} instance.
 *
 * @error marker-destroyed
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module engine/model/operation/operation
 */
/**
 * Abstract base operation class.
 *
 * @abstract
 */
class Operation {
    /**
     * Base operation constructor.
     *
     * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
     * can be applied or `null` if the operation operates on detached (non-document) tree.
     */
    constructor(baseVersion) {
        /**
         * {@link module:engine/model/document~Document#version} on which operation can be applied. If you try to
         * {@link module:engine/model/model~Model#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|null}
         */
        this.baseVersion = baseVersion;
        /**
         * Defines whether operation is executed on attached or detached {@link module:engine/model/item~Item items}.
         *
         * @readonly
         * @member {Boolean} #isDocumentOperation
         */
        this.isDocumentOperation = this.baseVersion !== null;
        /**
         * {@link module:engine/model/batch~Batch Batch} to which the operation is added or `null` if the operation is not
         * added to any batch yet.
         *
         * @member {module:engine/model/batch~Batch|null} #batch
         */
        this.batch = null;
        /**
         * Operation type.
         *
         * @readonly
         * @member {String} #type
         */
        /**
         * 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 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 properties will be applied to the model tree.
         *
         * @protected
         * @method #_execute
         */
    }
    /**
     * Checks whether the operation's parameters are correct and the operation can be correctly executed. Throws
     * an error if operation is not valid.
     *
     * @internal
     * @protected
     * @method #_validate
     */
    _validate() {
    }
    /**
     * Custom toJSON method to solve child-parent circular dependencies.
     *
     * @method #toJSON
     * @returns {Object} Clone of this object with the operation property replaced with string.
     */
    toJSON() {
        // This method creates only a shallow copy, all nested objects should be defined separately.
        // See https://github.com/ckeditor/ckeditor5-engine/issues/1477.
        const json = Object.assign({}, this);
        json.__className = this.constructor.className;
        // Remove reference to the parent `Batch` to avoid circular dependencies.
        delete json.batch;
        // Only document operations are shared with other clients so it is not necessary to keep this information.
        delete json.isDocumentOperation;
        return json;
    }
    /**
     * Name of the operation class used for serialization.
     *
     * @type {String}
     */
    static get className() {
        return '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, document) {
        return new this(json.baseVersion);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Contains functions used for composing model tree by {@link module:engine/model/operation/operation~Operation operations}.
 * Those functions are built on top of {@link module:engine/model/node~Node node}, and it's child classes', APIs.
 *
 * @protected
 * @namespace utils
 */
/**
 * Inserts given nodes at given position.
 *
 * @internal
 * @protected
 * @function module:engine/model/operation/utils~utils.insert
 * @param {module:engine/model/position~Position} position Position at which nodes should be inserted.
 * @param {module:engine/model/node~NodeSet} normalizedNodes Nodes to insert.
 * @returns {module:engine/model/range~Range} Range spanning over inserted elements.
 */
function _insert(position, nodes) {
    const normalizedNodes = _normalizeNodes(nodes);
    // We have to count offset before inserting nodes because they can get merged and we would get wrong offsets.
    const offset = normalizedNodes.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._insertChild(index, normalizedNodes);
    // Merge text nodes, if possible. Merging is needed only at points where inserted nodes "touch" "old" nodes.
    _mergeNodesAtIndex(parent, index + normalizedNodes.length);
    _mergeNodesAtIndex(parent, index);
    return new Range(position, position.getShiftedBy(offset));
}
/**
 * Removed nodes in given range. Only {@link module:engine/model/range~Range#isFlat flat} ranges are accepted.
 *
 * @internal
 * @protected
 * @function module:engine/model/operation/utils~utils._remove
 * @param {module:engine/model/range~Range} range Range containing nodes to remove.
 * @returns {Array.<module:engine/model/node~Node>}
 */
function _remove(range) {
    if (!range.isFlat) {
        /**
         * Trying to remove a range which starts and ends in different element.
         *
         * @error operation-utils-remove-range-not-flat
         */
        throw new CKEditorError('operation-utils-remove-range-not-flat', this);
    }
    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.
 *
 * @internal
 * @protected
 * @function module:engine/model/operation/utils~utils.move
 * @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 _move(sourceRange, targetPosition) {
    if (!sourceRange.isFlat) {
        /**
         * Trying to move a range which starts and ends in different element.
         *
         * @error operation-utils-move-range-not-flat
         */
        throw new CKEditorError('operation-utils-move-range-not-flat', this);
    }
    const nodes = _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 _insert(targetPosition, nodes);
}
/**
 * Sets given attribute on nodes in given range. The attributes are only set on top-level nodes of the range, not on its children.
 *
 * @internal
 * @protected
 * @function module:engine/model/operation/utils~utils._setAttribute
 * @param {module:engine/model/range~Range} range Range containing nodes that should have the attribute set. Must be a flat range.
 * @param {String} key Key of attribute to set.
 * @param {*} value Attribute value.
 */
function _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({ shallow: true })) {
        // 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);
}
/**
 * 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.
 *
 * @protected
 * @function module:engine/model/operation/utils~utils.normalizeNodes
 * @param {module:engine/model/node~NodeSet} nodes Objects to normalize.
 * @returns {Array.<module:engine/model/node~Node>} Normalized nodes.
 */
function _normalizeNodes(nodes) {
    const normalized = [];
    function convert(nodes) {
        if (typeof nodes == 'string') {
            normalized.push(new Text$1(nodes));
        }
        else if (nodes instanceof TextProxy) {
            normalized.push(new Text$1(nodes.data, nodes.getAttributes()));
        }
        else if (nodes instanceof Node$1) {
            normalized.push(nodes);
        }
        else if (isIterable(nodes)) {
            for (const node of nodes) {
                convert(node);
            }
        }
        // Skip unrecognized type.
    }
    convert(nodes);
    // Merge text nodes.
    for (let i = 1; i < normalized.length; i++) {
        const node = normalized[i];
        const prev = normalized[i - 1];
        if (node instanceof Text$1 && prev instanceof Text$1 && _haveSameAttributes(node, prev)) {
            // Doing this instead changing `prev.data` because `data` is readonly.
            normalized.splice(i - 1, 2, new Text$1(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.
//
// @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$1(nodeBefore.data + nodeAfter.data, nodeBefore.getAttributes());
        // Remove separate text nodes.
        element._removeChildren(index - 1, 2);
        // Insert merged text node.
        element._insertChild(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.
//
// @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$1(textNode.data.substr(0, offsetDiff), textNode.getAttributes());
        const secondPart = new Text$1(textNode.data.substr(offsetDiff), textNode.getAttributes());
        element._insertChild(index, [firstPart, secondPart]);
    }
}
// Checks whether two given nodes have same attributes.
//
// @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;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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 extends 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. Must be a flat range.
     * @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|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
     * can be applied or `null` if the operation operates on detached (non-document) tree.
     */
    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.clone();
        /**
         * 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(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(this.range, this.key, this.newValue, this.oldValue, this.baseVersion + 1);
    }
    /**
     * @inheritDoc
     */
    toJSON() {
        const json = super.toJSON();
        json.range = this.range.toJSON();
        return json;
    }
    /**
     * @inheritDoc
     * @internal
     */
    _validate() {
        if (!this.range.isFlat) {
            /**
             * The range to change is not flat.
             *
             * @error attribute-operation-range-not-flat
             */
            throw new CKEditorError('attribute-operation-range-not-flat', this);
        }
        for (const item of this.range.getItems({ shallow: true })) {
            if (this.oldValue !== null && !isEqual(item.getAttribute(this.key), this.oldValue)) {
                /**
                 * Changed node has different attribute value than operation's old attribute value.
                 *
                 * @error attribute-operation-wrong-old-value
                 * @param {module:engine/model/item~Item} item
                 * @param {String} key
                 * @param {*} value
                 */
                throw new CKEditorError('attribute-operation-wrong-old-value', this, { 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', this, { node: item, key: this.key });
            }
        }
    }
    /**
     * @inheritDoc
     * @internal
     */
    _execute() {
        // If value to set is same as old value, don't do anything.
        if (!isEqual(this.oldValue, this.newValue)) {
            // Execution.
            _setAttribute(this.range, this.key, this.newValue);
        }
    }
    /**
     * @inheritDoc
     */
    static get className() {
        return '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(Range.fromJSON(json.range, document), json.key, json.oldValue, json.newValue, json.baseVersion);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// @if CK_DEBUG_ENGINE // const ModelRange = require( '../range' ).default;
/**
 * 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 extends 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|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
     * can be applied or `null` if the operation operates on detached (non-document) tree.
     */
    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 = sourcePosition.clone();
        // `'toNext'` because `sourcePosition` is a bit like a start of the moved range.
        this.sourcePosition.stickiness = 'toNext';
        /**
         * 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 = targetPosition.clone();
        this.targetPosition.stickiness = 'toNone';
    }
    /**
     * @inheritDoc
     */
    get type() {
        if (this.targetPosition.root.rootName == '$graveyard') {
            return 'remove';
        }
        else if (this.sourcePosition.root.rootName == '$graveyard') {
            return 'reinsert';
        }
        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() {
        return new MoveOperation(this.sourcePosition, this.howMany, this.targetPosition, this.baseVersion);
    }
    /**
     * 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);
        return new MoveOperation(this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1);
    }
    /**
     * @inheritDoc
     * @internal
     */
    _validate() {
        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 (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', this);
        }
        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', this);
        }
        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', this);
                }
            }
        }
    }
    /**
     * @inheritDoc
     * @internal
     */
    _execute() {
        _move(Range._createFromPositionAndShift(this.sourcePosition, this.howMany), this.targetPosition);
    }
    /**
     * @inheritDoc
     */
    toJSON() {
        const json = super.toJSON();
        json.sourcePosition = this.sourcePosition.toJSON();
        json.targetPosition = this.targetPosition.toJSON();
        return json;
    }
    /**
     * @inheritDoc
     */
    static get className() {
        return '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.fromJSON(json.sourcePosition, document);
        const targetPosition = Position.fromJSON(json.targetPosition, document);
        return new this(sourcePosition, json.howMany, targetPosition, json.baseVersion);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Operation to insert one or more nodes at given position in the model.
 *
 * @extends module:engine/model/operation/operation~Operation
 */
class InsertOperation extends 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|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
     * can be applied or `null` if the operation operates on detached (non-document) tree.
     */
    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.clone();
        this.position.stickiness = 'toNone';
        /**
         * List of nodes to insert.
         *
         * @readonly
         * @member {module:engine/model/nodelist~NodeList} module:engine/model/operation/insertoperation~InsertOperation#nodeList
         */
        this.nodes = new NodeList(_normalizeNodes(nodes));
        /**
         * Flag deciding how the operation should be transformed. If set to `true`, nodes might get additional attributes
         * during operational transformation. This happens when the operation insertion position is inside of a range
         * where attributes have changed.
         *
         * @member {Boolean} module:engine/model/operation/insertoperation~InsertOperation#shouldReceiveAttributes
         */
        this.shouldReceiveAttributes = false;
    }
    /**
     * @inheritDoc
     */
    get type() {
        return 'insert';
    }
    /**
     * Total offset size of inserted nodes.
     *
     * @returns {Number}
     */
    get howMany() {
        return this.nodes.maxOffset;
    }
    /**
     * 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([...this.nodes].map(node => node._clone(true)));
        const insert = new InsertOperation(this.position, nodes, this.baseVersion);
        insert.shouldReceiveAttributes = this.shouldReceiveAttributes;
        return insert;
    }
    /**
     * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
     *
     * @returns {module:engine/model/operation/moveoperation~MoveOperation}
     */
    getReversed() {
        const graveyard = this.position.root.document.graveyard;
        const gyPosition = new Position(graveyard, [0]);
        return new MoveOperation(this.position, this.nodes.maxOffset, gyPosition, this.baseVersion + 1);
    }
    /**
     * @inheritDoc
     * @internal
     */
    _validate() {
        const targetElement = this.position.parent;
        if (!targetElement || targetElement.maxOffset < this.position.offset) {
            /**
             * Insertion position is invalid.
             *
             * @error insert-operation-position-invalid
             */
            throw new CKEditorError('insert-operation-position-invalid', this);
        }
    }
    /**
     * @inheritDoc
     * @internal
     */
    _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([...originalNodes].map(node => node._clone(true)));
        _insert(this.position, originalNodes);
    }
    /**
     * @inheritDoc
     */
    toJSON() {
        const json = super.toJSON();
        json.position = this.position.toJSON();
        json.nodes = this.nodes.toJSON();
        return json;
    }
    /**
     * @inheritDoc
     */
    static get className() {
        return '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.fromJSON(child));
            }
            else {
                // Otherwise, it is a Text node.
                children.push(Text$1.fromJSON(child));
            }
        }
        const insert = new InsertOperation(Position.fromJSON(json.position, document), children, json.baseVersion);
        insert.shouldReceiveAttributes = json.shouldReceiveAttributes;
        return insert;
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @extends module:engine/model/operation/operation~Operation
 */
class MarkerOperation extends Operation {
    /**
     * @param {String} name Marker name.
     * @param {module:engine/model/range~Range|null} oldRange Marker range before the change.
     * @param {module:engine/model/range~Range|null} newRange Marker range after the change.
     * @param {module:engine/model/markercollection~MarkerCollection} markers Marker collection on which change should be executed.
     * @param {Boolean} affectsData Specifies whether the marker operation affects the data produced by the data pipeline
     * (is persisted in the editor's data).
     * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
     * can be applied or `null` if the operation operates on detached (non-document) tree.
     */
    constructor(name, oldRange, newRange, markers, affectsData, baseVersion) {
        super(baseVersion);
        /**
         * Marker name.
         *
         * @readonly
         * @member {String}
         */
        this.name = name;
        /**
         * Marker range before the change.
         *
         * @readonly
         * @member {module:engine/model/range~Range|null}
         */
        this.oldRange = oldRange ? oldRange.clone() : null;
        /**
         * Marker range after the change.
         *
         * @readonly
         * @member {module:engine/model/range~Range}
         */
        this.newRange = newRange ? newRange.clone() : null;
        /**
         * Specifies whether the marker operation affects the data produced by the data pipeline
         * (is persisted in the editor's data).
         *
         * @readonly
         * @member {Boolean}
         */
        this.affectsData = affectsData;
        /**
         * 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(this.name, this.oldRange, this.newRange, this._markers, this.affectsData, this.baseVersion);
    }
    /**
     * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
     *
     * @returns {module:engine/model/operation/markeroperation~MarkerOperation}
     */
    getReversed() {
        return new MarkerOperation(this.name, this.newRange, this.oldRange, this._markers, this.affectsData, this.baseVersion + 1);
    }
    /**
     * @inheritDoc
     * @internal
     */
    _execute() {
        if (this.newRange) {
            this._markers._set(this.name, this.newRange, true, this.affectsData);
        }
        else {
            this._markers._remove(this.name);
        }
    }
    /**
     * @inheritDoc
     * @internal
     */
    toJSON() {
        const json = super.toJSON();
        if (this.oldRange) {
            json.oldRange = this.oldRange.toJSON();
        }
        if (this.newRange) {
            json.newRange = this.newRange.toJSON();
        }
        delete json._markers;
        return json;
    }
    /**
     * @inheritDoc
     */
    static get className() {
        return 'MarkerOperation';
    }
    /**
     * Creates `MarkerOperation` 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/markeroperation~MarkerOperation}
     */
    static fromJSON(json, document) {
        return new MarkerOperation(json.name, json.oldRange ? Range.fromJSON(json.oldRange, document) : null, json.newRange ? Range.fromJSON(json.newRange, document) : null, document.model.markers, json.affectsData, json.baseVersion);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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 {
    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);
    }
    /** @internal */
    _execute() {
    }
    /**
     * @inheritDoc
     */
    static get className() {
        return 'NoOperation';
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Operation to change element's name.
 *
 * Using this class you can change element's name.
 *
 * @extends module:engine/model/operation/operation~Operation
 */
class RenameOperation extends 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|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
     * can be applied or `null` if the operation operates on detached (non-document) tree.
     */
    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;
        // This position sticks to the next node because it is a position before the node that we want to change.
        this.position.stickiness = 'toNext';
        /**
         * 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(this.position.clone(), 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(this.position.clone(), this.newName, this.oldName, this.baseVersion + 1);
    }
    /**
     * @inheritDoc
     * @internal
     */
    _validate() {
        const element = this.position.nodeAfter;
        if (!(element instanceof 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', this);
        }
        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', this);
        }
    }
    /**
     * @inheritDoc
     * @internal
     */
    _execute() {
        const element = this.position.nodeAfter;
        element.name = this.newName;
    }
    /**
     * @inheritDoc
     */
    toJSON() {
        const json = super.toJSON();
        json.position = this.position.toJSON();
        return json;
    }
    /**
     * @inheritDoc
     */
    static get className() {
        return '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(Position.fromJSON(json.position, document), json.oldName, json.newName, json.baseVersion);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * 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 extends 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|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
     * can be applied or `null` if the operation operates on detached (non-document) tree.
     */
    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;
    }
    /**
     * @inheritDoc
     */
    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(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(this.root, this.key, this.newValue, this.oldValue, this.baseVersion + 1);
    }
    /**
     * @inheritDoc
     * @internal
     */
    _validate() {
        if (this.root != this.root.root || this.root.is('documentFragment')) {
            /**
             * The element to change is not a root element.
             *
             * @error rootattribute-operation-not-a-root
             * @param {module:engine/model/rootelement~RootElement} root
             * @param {String} key
             * @param {*} value
             */
            throw new CKEditorError('rootattribute-operation-not-a-root', this, { root: this.root, key: this.key });
        }
        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', this, { 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', this, { root: this.root, key: this.key });
        }
    }
    /**
     * @inheritDoc
     * @internal
     */
    _execute() {
        if (this.newValue !== null) {
            this.root._setAttribute(this.key, this.newValue);
        }
        else {
            this.root._removeAttribute(this.key);
        }
    }
    /**
     * @inheritDoc
     */
    toJSON() {
        const json = super.toJSON();
        json.root = this.root.toJSON();
        return json;
    }
    /**
     * @inheritDoc
     */
    static get className() {
        return '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.getRoot(json.root)) {
            /**
             * Cannot create RootAttributeOperation for document. Root with specified name does not exist.
             *
             * @error rootattribute-operation-fromjson-no-root
             * @param {String} rootName
             */
            throw new CKEditorError('rootattribute-operation-fromjson-no-root', this, { rootName: json.root });
        }
        return new RootAttributeOperation(document.getRoot(json.root), json.key, json.oldValue, json.newValue, json.baseVersion);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Operation to merge two {@link module:engine/model/element~Element elements}.
 *
 * The merged element is the parent of {@link ~MergeOperation#sourcePosition} and it is merged into the parent of
 * {@link ~MergeOperation#targetPosition}. All nodes from the merged element are moved to {@link ~MergeOperation#targetPosition}.
 *
 * The merged element is moved to the graveyard at {@link ~MergeOperation#graveyardPosition}.
 *
 * @extends module:engine/model/operation/operation~Operation
 */
class MergeOperation extends Operation {
    /**
     * Creates a merge operation.
     *
     * @param {module:engine/model/position~Position} sourcePosition Position inside the merged element. All nodes from that
     * element after that position will be moved to {@link ~#targetPosition}.
     * @param {Number} howMany Summary offset size of nodes which will be moved from the merged element to the new parent.
     * @param {module:engine/model/position~Position} targetPosition Position which the nodes from the merged elements will be moved to.
     * @param {module:engine/model/position~Position} graveyardPosition Position in graveyard to which the merged element will be moved.
     * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
     * can be applied or `null` if the operation operates on detached (non-document) tree.
     */
    constructor(sourcePosition, howMany, targetPosition, graveyardPosition, baseVersion) {
        super(baseVersion);
        /**
         * Position inside the merged element. All nodes from that element after that position will be moved to {@link ~#targetPosition}.
         *
         * @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#sourcePosition
         */
        this.sourcePosition = sourcePosition.clone();
        // This is, and should always remain, the first position in its parent.
        this.sourcePosition.stickiness = 'toPrevious';
        /**
         * Summary offset size of nodes which will be moved from the merged element to the new parent.
         *
         * @member {Number} module:engine/model/operation/mergeoperation~MergeOperation#howMany
         */
        this.howMany = howMany;
        /**
         * Position which the nodes from the merged elements will be moved to.
         *
         * @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#targetPosition
         */
        this.targetPosition = targetPosition.clone();
        // Except of a rare scenario in `MergeOperation` x `MergeOperation` transformation,
        // this is, and should always remain, the last position in its parent.
        this.targetPosition.stickiness = 'toNext';
        /**
         * Position in graveyard to which the merged element will be moved.
         *
         * @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#graveyardPosition
         */
        this.graveyardPosition = graveyardPosition.clone();
    }
    /**
     * @inheritDoc
     */
    get type() {
        return 'merge';
    }
    /**
     * Position before the merged element (which will be deleted).
     *
     * @readonly
     * @type {module:engine/model/position~Position}
     */
    get deletionPosition() {
        return new Position(this.sourcePosition.root, this.sourcePosition.path.slice(0, -1));
    }
    /**
     * Artificial range that contains all the nodes from the merged element that will be moved to {@link ~MergeOperation#sourcePosition}.
     * The range starts at {@link ~MergeOperation#sourcePosition} and ends in the same parent, at `POSITIVE_INFINITY` offset.
     *
     * @readonly
     * @type {module:engine/model/range~Range}
     */
    get movedRange() {
        const end = this.sourcePosition.getShiftedBy(Number.POSITIVE_INFINITY);
        return new Range(this.sourcePosition, end);
    }
    /**
     * Creates and returns an operation that has the same parameters as this operation.
     *
     * @returns {module:engine/model/operation/mergeoperation~MergeOperation} Clone of this operation.
     */
    clone() {
        return new MergeOperation(this.sourcePosition, this.howMany, this.targetPosition, this.graveyardPosition, this.baseVersion);
    }
    /**
     * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
     *
     * @returns {module:engine/model/operation/splitoperation~SplitOperation}
     */
    getReversed() {
        // Positions in this method are transformed by this merge operation because the split operation bases on
        // the context after this merge operation happened (because split operation reverses it).
        // So we need to acknowledge that the merge operation happened and those positions changed a little.
        const targetPosition = this.targetPosition._getTransformedByMergeOperation(this);
        const path = this.sourcePosition.path.slice(0, -1);
        const insertionPosition = new Position(this.sourcePosition.root, path)._getTransformedByMergeOperation(this);
        return new SplitOperation(targetPosition, this.howMany, insertionPosition, this.graveyardPosition, this.baseVersion + 1);
    }
    /**
     * @inheritDoc
     * @internal
     */
    _validate() {
        const sourceElement = this.sourcePosition.parent;
        const targetElement = this.targetPosition.parent;
        // Validate whether merge operation has correct parameters.
        if (!sourceElement.parent) {
            /**
             * Merge source position is invalid. The element to be merged must have a parent node.
             *
             * @error merge-operation-source-position-invalid
             */
            throw new CKEditorError('merge-operation-source-position-invalid', this);
        }
        else if (!targetElement.parent) {
            /**
             * Merge target position is invalid. The element to be merged must have a parent node.
             *
             * @error merge-operation-target-position-invalid
             */
            throw new CKEditorError('merge-operation-target-position-invalid', this);
        }
        else if (this.howMany != sourceElement.maxOffset) {
            /**
             * Merge operation specifies wrong number of nodes to move.
             *
             * @error merge-operation-how-many-invalid
             */
            throw new CKEditorError('merge-operation-how-many-invalid', this);
        }
    }
    /**
     * @inheritDoc
     * @internal
     */
    _execute() {
        const mergedElement = this.sourcePosition.parent;
        const sourceRange = Range._createIn(mergedElement);
        _move(sourceRange, this.targetPosition);
        _move(Range._createOn(mergedElement), this.graveyardPosition);
    }
    /**
     * @inheritDoc
     */
    toJSON() {
        const json = super.toJSON();
        json.sourcePosition = json.sourcePosition.toJSON();
        json.targetPosition = json.targetPosition.toJSON();
        json.graveyardPosition = json.graveyardPosition.toJSON();
        return json;
    }
    /**
     * @inheritDoc
     */
    static get className() {
        return 'MergeOperation';
    }
    /**
     * Creates `MergeOperation` 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/mergeoperation~MergeOperation}
     */
    static fromJSON(json, document) {
        const sourcePosition = Position.fromJSON(json.sourcePosition, document);
        const targetPosition = Position.fromJSON(json.targetPosition, document);
        const graveyardPosition = Position.fromJSON(json.graveyardPosition, document);
        return new this(sourcePosition, json.howMany, targetPosition, graveyardPosition, json.baseVersion);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Operation to split {@link module:engine/model/element~Element an element} at given
 * {@link module:engine/model/operation/splitoperation~SplitOperation#splitPosition split position} into two elements,
 * both containing a part of the element's original content.
 *
 * @extends module:engine/model/operation/operation~Operation
 */
class SplitOperation extends Operation {
    /**
     * Creates a split operation.
     *
     * @param {module:engine/model/position~Position} splitPosition Position at which an element should be split.
     * @param {Number} howMany Total offset size of elements that are in the split element after `position`.
     * @param {module:engine/model/position~Position} insertionPosition Position at which the clone of split element
     * (or element from graveyard) will be inserted.
     * @param {module:engine/model/position~Position|null} graveyardPosition Position in the graveyard root before the element which
     * should be used as a parent of the nodes after `position`. If it is not set, a copy of the the `position` parent will be used.
     * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
     * can be applied or `null` if the operation operates on detached (non-document) tree.
     */
    constructor(splitPosition, howMany, insertionPosition, graveyardPosition, baseVersion) {
        super(baseVersion);
        /**
         * Position at which an element should be split.
         *
         * @member {module:engine/model/position~Position} module:engine/model/operation/splitoperation~SplitOperation#splitPosition
         */
        this.splitPosition = splitPosition.clone();
        // Keep position sticking to the next node. This way any new content added at the place where the element is split
        // will be left in the original element.
        this.splitPosition.stickiness = 'toNext';
        /**
         * Total offset size of elements that are in the split element after `position`.
         *
         * @member {Number} module:engine/model/operation/splitoperation~SplitOperation#howMany
         */
        this.howMany = howMany;
        /**
         * Position at which the clone of split element (or element from graveyard) will be inserted.
         *
         * @member {module:engine/model/position~Position} module:engine/model/operation/splitoperation~SplitOperation#insertionPosition
         */
        this.insertionPosition = insertionPosition;
        /**
         * Position in the graveyard root before the element which should be used as a parent of the nodes after `position`.
         * If it is not set, a copy of the the `position` parent will be used.
         *
         * The default behavior is to clone the split element. Element from graveyard is used during undo.
         *
         * @member {module:engine/model/position~Position|null} #graveyardPosition
         */
        this.graveyardPosition = graveyardPosition ? graveyardPosition.clone() : null;
        if (this.graveyardPosition) {
            this.graveyardPosition.stickiness = 'toNext';
        }
    }
    /**
     * @inheritDoc
     */
    get type() {
        return 'split';
    }
    /**
     * Position inside the new clone of a split element.
     *
     * This is a position where nodes that are after the split position will be moved to.
     *
     * @readonly
     * @type {module:engine/model/position~Position}
     */
    get moveTargetPosition() {
        const path = this.insertionPosition.path.slice();
        path.push(0);
        return new Position(this.insertionPosition.root, path);
    }
    /**
     * Artificial range that contains all the nodes from the split element that will be moved to the new element.
     * The range starts at {@link ~#splitPosition} and ends in the same parent, at `POSITIVE_INFINITY` offset.
     *
     * @readonly
     * @type {module:engine/model/range~Range}
     */
    get movedRange() {
        const end = this.splitPosition.getShiftedBy(Number.POSITIVE_INFINITY);
        return new Range(this.splitPosition, end);
    }
    /**
     * Creates and returns an operation that has the same parameters as this operation.
     *
     * @returns {module:engine/model/operation/splitoperation~SplitOperation} Clone of this operation.
     */
    clone() {
        return new SplitOperation(this.splitPosition, this.howMany, this.insertionPosition, this.graveyardPosition, this.baseVersion);
    }
    /**
     * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
     *
     * @returns {module:engine/model/operation/mergeoperation~MergeOperation}
     */
    getReversed() {
        const graveyard = this.splitPosition.root.document.graveyard;
        const graveyardPosition = new Position(graveyard, [0]);
        return new MergeOperation(this.moveTargetPosition, this.howMany, this.splitPosition, graveyardPosition, this.baseVersion + 1);
    }
    /**
     * @inheritDoc
     * @internal
     */
    _validate() {
        const element = this.splitPosition.parent;
        const offset = this.splitPosition.offset;
        // Validate whether split operation has correct parameters.
        if (!element || element.maxOffset < offset) {
            /**
             * Split position is invalid.
             *
             * @error split-operation-position-invalid
             */
            throw new CKEditorError('split-operation-position-invalid', this);
        }
        else if (!element.parent) {
            /**
             * Cannot split root element.
             *
             * @error split-operation-split-in-root
             */
            throw new CKEditorError('split-operation-split-in-root', this);
        }
        else if (this.howMany != element.maxOffset - this.splitPosition.offset) {
            /**
             * Split operation specifies wrong number of nodes to move.
             *
             * @error split-operation-how-many-invalid
             */
            throw new CKEditorError('split-operation-how-many-invalid', this);
        }
        else if (this.graveyardPosition && !this.graveyardPosition.nodeAfter) {
            /**
             * Graveyard position invalid.
             *
             * @error split-operation-graveyard-position-invalid
             */
            throw new CKEditorError('split-operation-graveyard-position-invalid', this);
        }
    }
    /**
     * @inheritDoc
     * @internal
     */
    _execute() {
        const splitElement = this.splitPosition.parent;
        if (this.graveyardPosition) {
            _move(Range._createFromPositionAndShift(this.graveyardPosition, 1), this.insertionPosition);
        }
        else {
            const newElement = splitElement._clone();
            _insert(this.insertionPosition, newElement);
        }
        const sourceRange = new Range(Position._createAt(splitElement, this.splitPosition.offset), Position._createAt(splitElement, splitElement.maxOffset));
        _move(sourceRange, this.moveTargetPosition);
    }
    /**
     * @inheritDoc
     */
    toJSON() {
        const json = super.toJSON();
        json.splitPosition = this.splitPosition.toJSON();
        json.insertionPosition = this.insertionPosition.toJSON();
        if (this.graveyardPosition) {
            json.graveyardPosition = this.graveyardPosition.toJSON();
        }
        return json;
    }
    /**
     * @inheritDoc
     */
    static get className() {
        return 'SplitOperation';
    }
    /**
     * Helper function that returns a default insertion position basing on given `splitPosition`. The default insertion
     * position is after the split element.
     *
     * @param {module:engine/model/position~Position} splitPosition
     * @returns {module:engine/model/position~Position}
     */
    static getInsertionPosition(splitPosition) {
        const path = splitPosition.path.slice(0, -1);
        path[path.length - 1]++;
        return new Position(splitPosition.root, path, 'toPrevious');
    }
    /**
     * Creates `SplitOperation` 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/splitoperation~SplitOperation}
     */
    static fromJSON(json, document) {
        const splitPosition = Position.fromJSON(json.splitPosition, document);
        const insertionPosition = Position.fromJSON(json.insertionPosition, document);
        const graveyardPosition = json.graveyardPosition ? Position.fromJSON(json.graveyardPosition, document) : null;
        return new this(splitPosition, json.howMany, insertionPosition, graveyardPosition, json.baseVersion);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const operations = {};
operations[AttributeOperation.className] = AttributeOperation;
operations[InsertOperation.className] = InsertOperation;
operations[MarkerOperation.className] = MarkerOperation;
operations[MoveOperation.className] = MoveOperation;
operations[NoOperation.className] = NoOperation;
operations[Operation.className] = Operation;
operations[RenameOperation.className] = RenameOperation;
operations[RootAttributeOperation.className] = RootAttributeOperation;
operations[SplitOperation.className] = SplitOperation;
operations[MergeOperation.className] = MergeOperation;
/**
 * A factory class for creating operations.
 *
 * @abstract
 */
class OperationFactory {
    /**
     * Creates an operation instance from a JSON object (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 operations[json.__className].fromJSON(json, document);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// @if CK_DEBUG_ENGINE // const ModelRange = require( '../range' ).default;
/**
 * Operation to permanently remove node from detached root.
 * Note this operation is only a local operation and won't be send to the other clients.
 *
 * @extends module:engine/model/operation/operation~Operation
 */
class DetachOperation extends Operation {
    /**
     * Creates an insert 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`.
     */
    constructor(sourcePosition, howMany) {
        super(null);
        /**
         * Position before the first {@link module:engine/model/item~Item model item} to detach.
         *
         * @member {module:engine/model/position~Position} #sourcePosition
         */
        this.sourcePosition = sourcePosition.clone();
        /**
         * Offset size of moved range.
         *
         * @member {Number} #howMany
         */
        this.howMany = howMany;
    }
    /**
     * @inheritDoc
     */
    get type() {
        return 'detach';
    }
    /**
     * @inheritDoc
     */
    toJSON() {
        const json = super.toJSON();
        json.sourcePosition = this.sourcePosition.toJSON();
        return json;
    }
    /**
     * @inheritDoc
     * @internal
     */
    _validate() {
        if (this.sourcePosition.root.document) {
            /**
             * Cannot detach document node.
             *
             * @error detach-operation-on-document-node
             */
            throw new CKEditorError('detach-operation-on-document-node', this);
        }
    }
    /**
     * @inheritDoc
     * @internal
     */
    _execute() {
        _remove(Range._createFromPositionAndShift(this.sourcePosition, this.howMany));
    }
    /**
     * @inheritDoc
     */
    static get className() {
        return 'DetachOperation';
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// @if CK_DEBUG_ENGINE // const { stringifyMap } = require( '../dev-utils/utils' );
/**
 * DocumentFragment represents a part of model which does not have a common root but its 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/model~Model#markers model markers} by a
 * {@link module:engine/model/writer~Writer#insert} function.
 */
class DocumentFragment extends TypeCheckable {
    /**
     * Creates an empty `DocumentFragment`.
     *
     * **Note:** Constructor of this class shouldn't be used directly in the code.
     * Use the {@link module:engine/model/writer~Writer#createDocumentFragment} method instead.
     *
     * @protected
     * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children]
     * Nodes to be contained inside the `DocumentFragment`.
     */
    constructor(children) {
        super();
        /**
         * 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/model~Model#markers model markers collection}
         * when DocumentFragment will be inserted to the document.
         *
         * @readonly
         * @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();
        if (children) {
            this._insertChild(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 {@link 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 next sibling. Returns `null`. Added for compatibility reasons.
     *
     * @readonly
     * @type {null}
     */
    get nextSibling() {
        return null;
    }
    /**
     * Artificial previous sibling. Returns `null`. Added for compatibility reasons.
     *
     * @readonly
     * @type {null}
     */
    get previousSibling() {
        return null;
    }
    /**
     * 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;
    }
    /**
     * Artificial owner of `DocumentFragment`. Returns `null`. Added for compatibility reasons.
     *
     * @readonly
     * @type {null}
     */
    get document() {
        return null;
    }
    /**
     * Returns empty array. Added for compatibility reasons.
     *
     * @returns {Array}
     */
    getAncestors() {
        return [];
    }
    /**
     * 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
     * {@link 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) {
        // eslint-disable-next-line @typescript-eslint/no-this-alias, consistent-this
        let node = 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);
    }
    /**
     * 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.fromJSON(child));
            }
            else {
                // Otherwise, it is a Text node.
                children.push(Text$1.fromJSON(child));
            }
        }
        return new DocumentFragment(children);
    }
    /**
     * {@link #_insertChild Inserts} one or more nodes at the end of this document fragment.
     *
     * @internal
     * @protected
     * @param {String|module:engine/model/item~Item|Iterable.<String|module:engine/model/item~Item>} items Items to be inserted.
     */
    _appendChild(items) {
        this._insertChild(this.childCount, items);
    }
    /**
     * 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.
     *
     * @internal
     * @protected
     * @param {Number} index Index at which nodes should be inserted.
     * @param {String|module:engine/model/item~Item|Iterable.<String|module:engine/model/item~Item>} items Items to be inserted.
     */
    _insertChild(index, items) {
        const nodes = normalize$1(items);
        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`.
     *
     * @internal
     * @protected
     * @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;
    }
}
/**
 * Checks whether this object is of the given type.
 *
 *		docFrag.is( 'documentFragment' ); // -> true
 *		docFrag.is( 'model:documentFragment' ); // -> true
 *
 *		docFrag.is( 'view:documentFragment' ); // -> false
 *		docFrag.is( 'element' ); // -> false
 *		docFrag.is( 'node' ); // -> false
 *
 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 *
 * @param {String} type
 * @returns {Boolean}
 */
DocumentFragment.prototype.is = function (type) {
    return type === 'documentFragment' || type === 'model:documentFragment';
};
// Converts strings to Text and non-iterables to arrays.
//
// @param {String|module:engine/model/item~Item|Iterable.<module:engine/model/item~Item>}
// @returns {Iterable.<module:engine/model/node~Node>}
function normalize$1(nodes) {
    // Separate condition because string is iterable.
    if (typeof nodes == 'string') {
        return [new Text$1(nodes)];
    }
    if (!isIterable(nodes)) {
        nodes = [nodes];
    }
    // Array.from to enable .map() on non-arrays.
    return Array.from(nodes)
        .map(node => {
        if (typeof node == 'string') {
            return new Text$1(node);
        }
        if (node instanceof TextProxy) {
            return new Text$1(node.data, node.getAttributes());
        }
        return node;
    });
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * The model can only be modified by using the writer. It should be used whenever you want to create a node, modify
 * child nodes, attributes or text, set the selection's position and its attributes.
 *
 * The instance of the writer is only available in the {@link module:engine/model/model~Model#change `change()`} or
 * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`}.
 *
 *		model.change( writer => {
 *			writer.insertText( 'foo', paragraph, 'end' );
 *		} );
 *
 * Note that the writer should never be stored and used outside of the `change()` and
 * `enqueueChange()` blocks.
 *
 * Note that writer's methods do not check the {@link module:engine/model/schema~Schema}. It is possible
 * to create incorrect model structures by using the writer. Read more about in
 * {@glink framework/guides/deep-dive/schema#who-checks-the-schema "Who checks the schema?"}.
 *
 * @see module:engine/model/model~Model#change
 * @see module:engine/model/model~Model#enqueueChange
 */
class Writer {
    /**
     * Creates a writer instance.
     *
     * **Note:** It is not recommended to use it directly. Use {@link module:engine/model/model~Model#change `Model#change()`} or
     * {@link module:engine/model/model~Model#enqueueChange `Model#enqueueChange()`} instead.
     *
     * @protected
     * @param {module:engine/model/model~Model} model
     * @param {module:engine/model/batch~Batch} batch
     */
    constructor(model, batch) {
        /**
         * Instance of the model on which this writer operates.
         *
         * @readonly
         * @type {module:engine/model/model~Model}
         */
        this.model = model;
        /**
         * The batch to which this writer will add changes.
         *
         * @readonly
         * @type {module:engine/model/batch~Batch}
         */
        this.batch = batch;
    }
    /**
     * Creates a new {@link module:engine/model/text~Text text node}.
     *
     *		writer.createText( 'foo' );
     *		writer.createText( 'foo', { bold: true } );
     *
     * @param {String} data Text data.
     * @param {Object} [attributes] Text attributes.
     * @returns {module:engine/model/text~Text} Created text node.
     */
    createText(data, attributes) {
        return new Text$1(data, attributes);
    }
    /**
     * Creates a new {@link module:engine/model/element~Element element}.
     *
     *		writer.createElement( 'paragraph' );
     *		writer.createElement( 'paragraph', { alignment: 'center' } );
     *
     * @param {String} name Name of the element.
     * @param {Object} [attributes] Elements attributes.
     * @returns {module:engine/model/element~Element} Created element.
     */
    createElement(name, attributes) {
        return new Element(name, attributes);
    }
    /**
     * Creates a new {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
     *
     * @returns {module:engine/model/documentfragment~DocumentFragment} Created document fragment.
     */
    createDocumentFragment() {
        return new DocumentFragment();
    }
    /**
     * Creates a copy of the 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 returned.
     *
     * @param {module:engine/model/element~Element} element The element to clone.
     * @param {Boolean} [deep=true] If set to `true` clones element and all its children recursively. When set to `false`,
     * element will be cloned without any child.
     */
    cloneElement(element, deep = true) {
        return element._clone(deep);
    }
    /**
     * Inserts item on given position.
     *
     *		const paragraph = writer.createElement( 'paragraph' );
     *		writer.insert( paragraph, position );
     *
     * Instead of using position you can use parent and offset:
     *
     *		const text = writer.createText( 'foo' );
     *		writer.insert( text, paragraph, 5 );
     *
     * You can also use `end` instead of the offset to insert at the end:
     *
     *		const text = writer.createText( 'foo' );
     *		writer.insert( text, paragraph, 'end' );
     *
     * Or insert before or after another element:
     *
     *		const paragraph = writer.createElement( 'paragraph' );
     *		writer.insert( paragraph, anotherParagraph, 'after' );
     *
     * These parameters works the same way as {@link #createPositionAt `writer.createPositionAt()`}.
     *
     * Note that if the item already has parent it will be removed from the previous parent.
     *
     * Note that you cannot re-insert a node from a document to a different document or a document fragment. In this case,
     * `model-writer-insert-forbidden-move` is thrown.
     *
     * If you want to move {@link module:engine/model/range~Range range} instead of an
     * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move `Writer#move()`}.
     *
     * **Note:** For a paste-like content insertion mechanism see
     * {@link module:engine/model/model~Model#insertContent `model.insertContent()`}.
     *
     * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment} item Item or document
     * fragment to insert.
     * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * second parameter is a {@link module:engine/model/item~Item model item}.
     */
    insert(item, itemOrPosition, offset = 0) {
        this._assertWriterUsedCorrectly();
        if (item instanceof Text$1 && item.data == '') {
            return;
        }
        const position = Position._createAt(itemOrPosition, offset);
        // If item has a parent already.
        if (item.parent) {
            // We need to check if item is going to be inserted within the same document.
            if (isSameTree(item.root, position.root)) {
                // If it's we just need to move it.
                this.move(Range._createOn(item), position);
                return;
            }
            // If it isn't the same root.
            else {
                if (item.root.document) {
                    /**
                     * Cannot move a node from a document to a different tree.
                     * It is forbidden to move a node that was already in a document outside of it.
                     *
                     * @error model-writer-insert-forbidden-move
                     */
                    throw new CKEditorError('model-writer-insert-forbidden-move', this);
                }
                else {
                    // Move between two different document fragments or from document fragment to a document is possible.
                    // In that case, remove the item from it's original parent.
                    this.remove(item);
                }
            }
        }
        const version = position.root.document ? position.root.document.version : null;
        const insert = new InsertOperation(position, item, version);
        if (item instanceof Text$1) {
            insert.shouldReceiveAttributes = true;
        }
        this.batch.addOperation(insert);
        this.model.applyOperation(insert);
        // When element is a DocumentFragment we need to move its markers to Document#markers.
        if (item instanceof DocumentFragment) {
            for (const [markerName, markerRange] of item.markers) {
                // We need to migrate marker range from DocumentFragment to Document.
                const rangeRootPosition = Position._createAt(markerRange.root, 0);
                const range = new Range(markerRange.start._getCombined(rangeRootPosition, position), markerRange.end._getCombined(rangeRootPosition, position));
                const options = { range, usingOperation: true, affectsData: true };
                if (this.model.markers.has(markerName)) {
                    this.updateMarker(markerName, options);
                }
                else {
                    this.addMarker(markerName, options);
                }
            }
        }
    }
    /**
     * Creates and inserts text on given position. You can optionally set text attributes:
     *
     *		writer.insertText( 'foo', position );
     *		writer.insertText( 'foo', { bold: true }, position );
     *
     * Instead of using position you can use parent and offset or define that text should be inserted at the end
     * or before or after other node:
     *
     *		// Inserts 'foo' in paragraph, at offset 5:
     *		writer.insertText( 'foo', paragraph, 5 );
     *		// Inserts 'foo' at the end of a paragraph:
     *		writer.insertText( 'foo', paragraph, 'end' );
     *		// Inserts 'foo' after an image:
     *		writer.insertText( 'foo', image, 'after' );
     *
     * These parameters work in the same way as {@link #createPositionAt `writer.createPositionAt()`}.
     *
     * @param {String} dattexta Text data.
     * @param {Object} [attributes] Text attributes.
     * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * third parameter is a {@link module:engine/model/item~Item model item}.
     */
    insertText(text, attributes, // Too complicated when not using `any`.
    itemOrPosition, // Too complicated when not using `any`.
    offset // Too complicated when not using `any`.
    ) {
        if (attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position) {
            this.insert(this.createText(text), attributes, itemOrPosition);
        }
        else {
            this.insert(this.createText(text, attributes), itemOrPosition, offset);
        }
    }
    /**
     * Creates and inserts element on given position. You can optionally set attributes:
     *
     *		writer.insertElement( 'paragraph', position );
     *		writer.insertElement( 'paragraph', { alignment: 'center' }, position );
     *
     * Instead of using position you can use parent and offset or define that text should be inserted at the end
     * or before or after other node:
     *
     *		// Inserts paragraph in the root at offset 5:
     *		writer.insertElement( 'paragraph', root, 5 );
     *		// Inserts paragraph at the end of a blockquote:
     *		writer.insertElement( 'paragraph', blockquote, 'end' );
     *		// Inserts after an image:
     *		writer.insertElement( 'paragraph', image, 'after' );
     *
     * These parameters works the same way as {@link #createPositionAt `writer.createPositionAt()`}.
     *
     * @param {String} name Name of the element.
     * @param {Object} [attributes] Elements attributes.
     * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * third parameter is a {@link module:engine/model/item~Item model item}.
     */
    insertElement(name, attributes, // Too complicated when not using `any`.
    itemOrPositionOrOffset, // Too complicated when not using `any`.
    offset // Too complicated when not using `any`.
    ) {
        if (attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position) {
            this.insert(this.createElement(name), attributes, itemOrPositionOrOffset);
        }
        else {
            this.insert(this.createElement(name, attributes), itemOrPositionOrOffset, offset);
        }
    }
    /**
     * Inserts item at the end of the given parent.
     *
     *		const paragraph = writer.createElement( 'paragraph' );
     *		writer.append( paragraph, root );
     *
     * Note that if the item already has parent it will be removed from the previous parent.
     *
     * If you want to move {@link module:engine/model/range~Range range} instead of an
     * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move `Writer#move()`}.
     *
     * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment}
     * item Item or document fragment to insert.
     * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
     */
    append(item, parent) {
        this.insert(item, parent, 'end');
    }
    /**
     * Creates text node and inserts it at the end of the parent. You can optionally set text attributes:
     *
     *		writer.appendText( 'foo', paragraph );
     *		writer.appendText( 'foo', { bold: true }, paragraph );
     *
     * @param {String} text Text data.
     * @param {Object} [attributes] Text attributes.
     * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
     */
    appendText(text, attributes, parent) {
        if (attributes instanceof DocumentFragment || attributes instanceof Element) {
            this.insert(this.createText(text), attributes, 'end');
        }
        else {
            this.insert(this.createText(text, attributes), parent, 'end');
        }
    }
    /**
     * Creates element and inserts it at the end of the parent. You can optionally set attributes:
     *
     *		writer.appendElement( 'paragraph', root );
     *		writer.appendElement( 'paragraph', { alignment: 'center' }, root );
     *
     * @param {String} name Name of the element.
     * @param {Object} [attributes] Elements attributes.
     * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
     */
    appendElement(name, attributes, parent) {
        if (attributes instanceof DocumentFragment || attributes instanceof Element) {
            this.insert(this.createElement(name), attributes, 'end');
        }
        else {
            this.insert(this.createElement(name, attributes), parent, 'end');
        }
    }
    /**
     * 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}.
     *
     * @param {String} key Attribute key.
     * @param {*} value Attribute new value.
     * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
     * Model item or range on which the attribute will be set.
     */
    setAttribute(key, value, itemOrRange) {
        this._assertWriterUsedCorrectly();
        if (itemOrRange instanceof Range) {
            const ranges = itemOrRange.getMinimalFlatRanges();
            for (const range of ranges) {
                setAttributeOnRange(this, key, value, range);
            }
        }
        else {
            setAttributeOnItem(this, key, value, itemOrRange);
        }
    }
    /**
     * Sets values of attributes on a {@link module:engine/model/item~Item model item}
     * or on a {@link module:engine/model/range~Range range}.
     *
     *		writer.setAttributes( {
     *			bold: true,
     *			italic: true
     *		}, range );
     *
     * @param {Object} attributes Attributes keys and values.
     * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
     * Model item or range on which the attributes will be set.
     */
    setAttributes(attributes, itemOrRange) {
        for (const [key, val] of toMap(attributes)) {
            this.setAttribute(key, val, itemOrRange);
        }
    }
    /**
     * 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}.
     *
     * @param {String} key Attribute key.
     * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
     * Model item or range from which the attribute will be removed.
     */
    removeAttribute(key, itemOrRange) {
        this._assertWriterUsedCorrectly();
        if (itemOrRange instanceof Range) {
            const ranges = itemOrRange.getMinimalFlatRanges();
            for (const range of ranges) {
                setAttributeOnRange(this, key, null, range);
            }
        }
        else {
            setAttributeOnItem(this, key, null, itemOrRange);
        }
    }
    /**
     * Removes all attributes from all elements in the range or from the given item.
     *
     * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
     * Model item or range from which all attributes will be removed.
     */
    clearAttributes(itemOrRange) {
        this._assertWriterUsedCorrectly();
        const removeAttributesFromItem = (item) => {
            for (const attribute of item.getAttributeKeys()) {
                this.removeAttribute(attribute, item);
            }
        };
        if (!(itemOrRange instanceof Range)) {
            removeAttributesFromItem(itemOrRange);
        }
        else {
            for (const item of itemOrRange.getItems()) {
                removeAttributesFromItem(item);
            }
        }
    }
    /**
     * Moves all items in the source range to the target position.
     *
     *		writer.move( sourceRange, targetPosition );
     *
     * Instead of the target position you can use parent and offset or define that range should be moved to the end
     * or before or after chosen item:
     *
     *		// Moves all items in the range to the paragraph at offset 5:
     *		writer.move( sourceRange, paragraph, 5 );
     *		// Moves all items in the range to the end of a blockquote:
     *		writer.move( sourceRange, blockquote, 'end' );
     *		// Moves all items in the range to a position after an image:
     *		writer.move( sourceRange, image, 'after' );
     *
     * These parameters works the same way as {@link #createPositionAt `writer.createPositionAt()`}.
     *
     * Note that items can be moved only within the same tree. It means that you can move items within the same root
     * (element or document fragment) or between {@link module:engine/model/document~Document#roots documents roots},
     * but you can not move items from document fragment to the document or from one detached element to another. Use
     * {@link module:engine/model/writer~Writer#insert} in such cases.
     *
     * @param {module:engine/model/range~Range} range Source range.
     * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * second parameter is a {@link module:engine/model/item~Item model item}.
     */
    move(range, itemOrPosition, offset) {
        this._assertWriterUsedCorrectly();
        if (!(range instanceof Range)) {
            /**
             * Invalid range to move.
             *
             * @error writer-move-invalid-range
             */
            throw new CKEditorError('writer-move-invalid-range', this);
        }
        if (!range.isFlat) {
            /**
             * Range to move is not flat.
             *
             * @error writer-move-range-not-flat
             */
            throw new CKEditorError('writer-move-range-not-flat', this);
        }
        const position = Position._createAt(itemOrPosition, offset);
        // Do not move anything if the move target is same as moved range start.
        if (position.isEqual(range.start)) {
            return;
        }
        // If part of the marker is removed, create additional marker operation for undo purposes.
        this._addOperationForAffectedMarkers('move', range);
        if (!isSameTree(range.root, position.root)) {
            /**
             * Range is going to be moved within not the same document. Please use
             * {@link module:engine/model/writer~Writer#insert insert} instead.
             *
             * @error writer-move-different-document
             */
            throw new CKEditorError('writer-move-different-document', this);
        }
        const version = range.root.document ? range.root.document.version : null;
        const operation = new MoveOperation(range.start, range.end.offset - range.start.offset, position, version);
        this.batch.addOperation(operation);
        this.model.applyOperation(operation);
    }
    /**
     * Removes given model {@link module:engine/model/item~Item item} or {@link module:engine/model/range~Range range}.
     *
     * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove.
     */
    remove(itemOrRange) {
        this._assertWriterUsedCorrectly();
        const rangeToRemove = itemOrRange instanceof Range ? itemOrRange : Range._createOn(itemOrRange);
        const ranges = rangeToRemove.getMinimalFlatRanges().reverse();
        for (const flat of ranges) {
            // If part of the marker is removed, create additional marker operation for undo purposes.
            this._addOperationForAffectedMarkers('move', flat);
            applyRemoveOperation(flat.start, flat.end.offset - flat.start.offset, this.batch, this.model);
        }
    }
    /**
     * Merges two siblings at the given position.
     *
     * Node before and after the position have to be an element. Otherwise `writer-merge-no-element-before` or
     * `writer-merge-no-element-after` error will be thrown.
     *
     * @param {module:engine/model/position~Position} position Position between merged elements.
     */
    merge(position) {
        this._assertWriterUsedCorrectly();
        const nodeBefore = position.nodeBefore;
        const nodeAfter = position.nodeAfter;
        // If part of the marker is removed, create additional marker operation for undo purposes.
        this._addOperationForAffectedMarkers('merge', position);
        if (!(nodeBefore instanceof Element)) {
            /**
             * Node before merge position must be an element.
             *
             * @error writer-merge-no-element-before
             */
            throw new CKEditorError('writer-merge-no-element-before', this);
        }
        if (!(nodeAfter instanceof Element)) {
            /**
             * Node after merge position must be an element.
             *
             * @error writer-merge-no-element-after
             */
            throw new CKEditorError('writer-merge-no-element-after', this);
        }
        if (!position.root.document) {
            this._mergeDetached(position);
        }
        else {
            this._merge(position);
        }
    }
    /**
     * Shortcut for {@link module:engine/model/model~Model#createPositionFromPath `Model#createPositionFromPath()`}.
     *
     * @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}.
     * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
     * See {@link module:engine/model/position~PositionStickiness}.
     * @returns {module:engine/model/position~Position}
     */
    createPositionFromPath(root, path, stickiness) {
        return this.model.createPositionFromPath(root, path, stickiness);
    }
    /**
     * Shortcut for {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}.
     *
     * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * first parameter is a {@link module:engine/model/item~Item model item}.
     * @returns {module:engine/model/position~Position}
     */
    createPositionAt(itemOrPosition, offset) {
        return this.model.createPositionAt(itemOrPosition, offset);
    }
    /**
     * Shortcut for {@link module:engine/model/model~Model#createPositionAfter `Model#createPositionAfter()`}.
     *
     * @param {module:engine/model/item~Item} item Item after which the position should be placed.
     * @returns {module:engine/model/position~Position}
     */
    createPositionAfter(item) {
        return this.model.createPositionAfter(item);
    }
    /**
     * Shortcut for {@link module:engine/model/model~Model#createPositionBefore `Model#createPositionBefore()`}.
     *
     * @param {module:engine/model/item~Item} item Item after which the position should be placed.
     * @returns {module:engine/model/position~Position}
     */
    createPositionBefore(item) {
        return this.model.createPositionBefore(item);
    }
    /**
     * Shortcut for {@link module:engine/model/model~Model#createRange `Model#createRange()`}.
     *
     * @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.
     * @returns {module:engine/model/range~Range}
     */
    createRange(start, end) {
        return this.model.createRange(start, end);
    }
    /**
     * Shortcut for {@link module:engine/model/model~Model#createRangeIn `Model#createRangeIn()`}.
     *
     * @param {module:engine/model/element~Element} element Element which is a parent for the range.
     * @returns {module:engine/model/range~Range}
     */
    createRangeIn(element) {
        return this.model.createRangeIn(element);
    }
    /**
     * Shortcut for {@link module:engine/model/model~Model#createRangeOn `Model#createRangeOn()`}.
     *
     * @param {module:engine/model/element~Element} element Element which is a parent for the range.
     * @returns {module:engine/model/range~Range}
     */
    createRangeOn(element) {
        return this.model.createRangeOn(element);
    }
    /**
     * Shortcut for {@link module:engine/model/model~Model#createSelection `Model#createSelection()`}.
     *
     * @param {module:engine/model/selection~Selectable} selectable
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     * @returns {module:engine/model/selection~Selection}
     */
    createSelection(...args) {
        return this.model.createSelection(...args);
    }
    /**
     * Performs merge action in a detached tree.
     *
     * @private
     * @param {module:engine/model/position~Position} position Position between merged elements.
     */
    _mergeDetached(position) {
        const nodeBefore = position.nodeBefore;
        const nodeAfter = position.nodeAfter;
        this.move(Range._createIn(nodeAfter), Position._createAt(nodeBefore, 'end'));
        this.remove(nodeAfter);
    }
    /**
     * Performs merge action in a non-detached tree.
     *
     * @private
     * @param {module:engine/model/position~Position} position Position between merged elements.
     */
    _merge(position) {
        const targetPosition = Position._createAt(position.nodeBefore, 'end');
        const sourcePosition = Position._createAt(position.nodeAfter, 0);
        const graveyard = position.root.document.graveyard;
        const graveyardPosition = new Position(graveyard, [0]);
        const version = position.root.document.version;
        const merge = new MergeOperation(sourcePosition, position.nodeAfter.maxOffset, targetPosition, graveyardPosition, version);
        this.batch.addOperation(merge);
        this.model.applyOperation(merge);
    }
    /**
     * Renames the given element.
     *
     * @param {module:engine/model/element~Element} element The element to rename.
     * @param {String} newName New element name.
     */
    rename(element, newName) {
        this._assertWriterUsedCorrectly();
        if (!(element instanceof Element)) {
            /**
             * Trying to rename an object which is not an instance of Element.
             *
             * @error writer-rename-not-element-instance
             */
            throw new CKEditorError('writer-rename-not-element-instance', this);
        }
        const version = element.root.document ? element.root.document.version : null;
        const renameOperation = new RenameOperation(Position._createBefore(element), element.name, newName, version);
        this.batch.addOperation(renameOperation);
        this.model.applyOperation(renameOperation);
    }
    /**
     * Splits elements starting from the given position and going to the top of the model tree as long as given
     * `limitElement` is reached. When `limitElement` is not defined then only the parent of the given position will be split.
     *
     * The element needs to have a parent. It cannot be a root element nor a document fragment.
     * The `writer-split-element-no-parent` error will be thrown if you try to split an element with no parent.
     *
     * @param {module:engine/model/position~Position} position Position of split.
     * @param {module:engine/model/node~Node} [limitElement] Stop splitting when this element will be reached.
     * @returns {Object} result Split result.
     * @returns {module:engine/model/position~Position} result.position Position between split elements.
     * @returns {module:engine/model/range~Range} result.range Range that stars from the end of the first split element and ends
     * at the beginning of the first copy element.
     */
    split(position, limitElement) {
        this._assertWriterUsedCorrectly();
        let splitElement = position.parent;
        if (!splitElement.parent) {
            /**
             * Element with no parent can not be split.
             *
             * @error writer-split-element-no-parent
             */
            throw new CKEditorError('writer-split-element-no-parent', this);
        }
        // When limit element is not defined lets set splitElement parent as limit.
        if (!limitElement) {
            limitElement = splitElement.parent;
        }
        if (!position.parent.getAncestors({ includeSelf: true }).includes(limitElement)) {
            /**
             * Limit element is not a position ancestor.
             *
             * @error writer-split-invalid-limit-element
             */
            throw new CKEditorError('writer-split-invalid-limit-element', this);
        }
        // We need to cache elements that will be created as a result of the first split because
        // we need to create a range from the end of the first split element to the beginning of the
        // first copy element. This should be handled by LiveRange but it doesn't work on detached nodes.
        let firstSplitElement;
        let firstCopyElement;
        do {
            const version = splitElement.root.document ? splitElement.root.document.version : null;
            const howMany = splitElement.maxOffset - position.offset;
            const insertionPosition = SplitOperation.getInsertionPosition(position);
            const split = new SplitOperation(position, howMany, insertionPosition, null, version);
            this.batch.addOperation(split);
            this.model.applyOperation(split);
            // Cache result of the first split.
            if (!firstSplitElement && !firstCopyElement) {
                firstSplitElement = splitElement;
                firstCopyElement = position.parent.nextSibling;
            }
            position = this.createPositionAfter(position.parent);
            splitElement = position.parent;
        } while (splitElement !== limitElement);
        return {
            position,
            range: new Range(Position._createAt(firstSplitElement, 'end'), Position._createAt(firstCopyElement, 0))
        };
    }
    /**
     * Wraps the given range with the given element or with a new element (if a string was passed).
     *
     * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat `Range#isFlat`}).
     * If not, an error will be thrown.
     *
     * @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.
     */
    wrap(range, elementOrString) {
        this._assertWriterUsedCorrectly();
        if (!range.isFlat) {
            /**
             * Range to wrap is not flat.
             *
             * @error writer-wrap-range-not-flat
             */
            throw new CKEditorError('writer-wrap-range-not-flat', this);
        }
        const element = elementOrString instanceof Element ? elementOrString : new Element(elementOrString);
        if (element.childCount > 0) {
            /**
             * Element to wrap with is not empty.
             *
             * @error writer-wrap-element-not-empty
             */
            throw new CKEditorError('writer-wrap-element-not-empty', this);
        }
        if (element.parent !== null) {
            /**
             * Element to wrap with is already attached to a tree model.
             *
             * @error writer-wrap-element-attached
             */
            throw new CKEditorError('writer-wrap-element-attached', this);
        }
        this.insert(element, range.start);
        // Shift the range-to-wrap because we just inserted an element before that range.
        const shiftedRange = new Range(range.start.getShiftedBy(1), range.end.getShiftedBy(1));
        this.move(shiftedRange, Position._createAt(element, 0));
    }
    /**
     * 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.
     *
     * @param {module:engine/model/element~Element} element Element to unwrap.
     */
    unwrap(element) {
        this._assertWriterUsedCorrectly();
        if (element.parent === null) {
            /**
             * Trying to unwrap an element which has no parent.
             *
             * @error writer-unwrap-element-no-parent
             */
            throw new CKEditorError('writer-unwrap-element-no-parent', this);
        }
        this.move(Range._createIn(element), this.createPositionAfter(element));
        this.remove(element);
    }
    /**
     * Adds a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
     * changes in the document and updates its range automatically, when model tree changes.
     *
     * As the first parameter you can set marker name.
     *
     * The required `options.usingOperation` parameter lets you decide if the marker should be managed by operations or not. See
     * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
     * markers managed by operations and not-managed by operations.
     *
     * The `options.affectsData` parameter, which defaults to `false`, allows you to define if a marker affects the data. It should be
     * `true` when the marker change changes the data returned by the
     * {@link module:core/editor/utils/dataapimixin~DataApi#getData `editor.getData()`} method.
     * When set to `true` it fires the {@link module:engine/model/document~Document#event:change:data `change:data`} event.
     * When set to `false` it fires the {@link module:engine/model/document~Document#event:change `change`} event.
     *
     * Create marker directly base on marker's name:
     *
     *		addMarker( markerName, { range, usingOperation: false } );
     *
     * Create marker using operation:
     *
     *		addMarker( markerName, { range, usingOperation: true } );
     *
     * Create marker that affects the editor data:
     *
     *		addMarker( markerName, { range, usingOperation: false, affectsData: true } );
     *
     * Note: For efficiency reasons, it's best to create and keep as little markers as possible.
     *
     * @see module:engine/model/markercollection~Marker
     * @param {String} name Name of a marker to create - must be unique.
     * @param {Object} options
     * @param {Boolean} options.usingOperation Flag indicating that the marker should be added by MarkerOperation.
     * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
     * @param {module:engine/model/range~Range} options.range Marker range.
     * @param {Boolean} [options.affectsData=false] Flag indicating that the marker changes the editor data.
     * @returns {module:engine/model/markercollection~Marker} Marker that was set.
     */
    addMarker(name, options) {
        this._assertWriterUsedCorrectly();
        if (!options || typeof options.usingOperation != 'boolean') {
            /**
             * The `options.usingOperation` parameter is required when adding a new marker.
             *
             * @error writer-addmarker-no-usingoperation
             */
            throw new CKEditorError('writer-addmarker-no-usingoperation', this);
        }
        const usingOperation = options.usingOperation;
        const range = options.range;
        const affectsData = options.affectsData === undefined ? false : options.affectsData;
        if (this.model.markers.has(name)) {
            /**
             * Marker with provided name already exists.
             *
             * @error writer-addmarker-marker-exists
             */
            throw new CKEditorError('writer-addmarker-marker-exists', this);
        }
        if (!range) {
            /**
             * Range parameter is required when adding a new marker.
             *
             * @error writer-addmarker-no-range
             */
            throw new CKEditorError('writer-addmarker-no-range', this);
        }
        if (!usingOperation) {
            return this.model.markers._set(name, range, usingOperation, affectsData);
        }
        applyMarkerOperation(this, name, null, range, affectsData);
        return this.model.markers.get(name);
    }
    /**
     * Adds, updates or refreshes a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
     * changes in the document and updates its range automatically, when model tree changes. Still, it is possible to change the
     * marker's range directly using this method.
     *
     * As the first parameter you can set marker name or instance. If none of them is provided, new marker, with a unique
     * name is created and returned.
     *
     * **Note**: If you want to change the {@link module:engine/view/element~Element view element} of the marker while its data in the model
     * remains the same, use the dedicated {@link module:engine/controller/editingcontroller~EditingController#reconvertMarker} method.
     *
     * The `options.usingOperation` parameter lets you change if the marker should be managed by operations or not. See
     * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
     * markers managed by operations and not-managed by operations. It is possible to change this option for an existing marker.
     *
     * The `options.affectsData` parameter, which defaults to `false`, allows you to define if a marker affects the data. It should be
     * `true` when the marker change changes the data returned by
     * the {@link module:core/editor/utils/dataapimixin~DataApi#getData `editor.getData()`} method.
     * When set to `true` it fires the {@link module:engine/model/document~Document#event:change:data `change:data`} event.
     * When set to `false` it fires the {@link module:engine/model/document~Document#event:change `change`} event.
     *
     * Update marker directly base on marker's name:
     *
     *		updateMarker( markerName, { range } );
     *
     * Update marker using operation:
     *
     *		updateMarker( marker, { range, usingOperation: true } );
     *		updateMarker( markerName, { range, usingOperation: true } );
     *
     * Change marker's option (start using operations to manage it):
     *
     *		updateMarker( marker, { usingOperation: true } );
     *
     * Change marker's option (inform the engine, that the marker does not affect the data anymore):
     *
     *		updateMarker( markerName, { affectsData: false } );
     *
     * @see module:engine/model/markercollection~Marker
     * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of a marker to update, or a marker instance.
     * @param {Object} [options] If options object is not defined then marker will be refreshed by triggering
     * downcast conversion for this marker with the same data.
     * @param {module:engine/model/range~Range} [options.range] Marker range to update.
     * @param {Boolean} [options.usingOperation] Flag indicated whether the marker should be added by MarkerOperation.
     * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
     * @param {Boolean} [options.affectsData] Flag indicating that the marker changes the editor data.
     */
    updateMarker(markerOrName, options) {
        this._assertWriterUsedCorrectly();
        const markerName = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
        const currentMarker = this.model.markers.get(markerName);
        if (!currentMarker) {
            /**
             * Marker with provided name does not exist and will not be updated.
             *
             * @error writer-updatemarker-marker-not-exists
             */
            throw new CKEditorError('writer-updatemarker-marker-not-exists', this);
        }
        if (!options) {
            /**
             * The usage of `writer.updateMarker()` only to reconvert (refresh) a
             * {@link module:engine/model/markercollection~Marker model marker} was deprecated and may not work in the future.
             * Please update your code to use
             * {@link module:engine/controller/editingcontroller~EditingController#reconvertMarker `editor.editing.reconvertMarker()`}
             * instead.
             *
             * @error writer-updatemarker-reconvert-using-editingcontroller
             * @param {String} markerName The name of the updated marker.
             */
            logWarning('writer-updatemarker-reconvert-using-editingcontroller', { markerName });
            this.model.markers._refresh(currentMarker);
            return;
        }
        const hasUsingOperationDefined = typeof options.usingOperation == 'boolean';
        const affectsDataDefined = typeof options.affectsData == 'boolean';
        // Use previously defined marker's affectsData if the property is not provided.
        const affectsData = affectsDataDefined ? options.affectsData : currentMarker.affectsData;
        if (!hasUsingOperationDefined && !options.range && !affectsDataDefined) {
            /**
             * One of the options is required - provide range, usingOperations or affectsData.
             *
             * @error writer-updatemarker-wrong-options
             */
            throw new CKEditorError('writer-updatemarker-wrong-options', this);
        }
        const currentRange = currentMarker.getRange();
        const updatedRange = options.range ? options.range : currentRange;
        if (hasUsingOperationDefined && options.usingOperation !== currentMarker.managedUsingOperations) {
            // The marker type is changed so it's necessary to create proper operations.
            if (options.usingOperation) {
                // If marker changes to a managed one treat this as synchronizing existing marker.
                // Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
                applyMarkerOperation(this, markerName, null, updatedRange, affectsData);
            }
            else {
                // If marker changes to a marker that do not use operations then we need to create additional operation
                // that removes that marker first.
                applyMarkerOperation(this, markerName, currentRange, null, affectsData);
                // Although not managed the marker itself should stay in model and its range should be preserver or changed to passed range.
                this.model.markers._set(markerName, updatedRange, undefined, affectsData);
            }
            return;
        }
        // Marker's type doesn't change so update it accordingly.
        if (currentMarker.managedUsingOperations) {
            applyMarkerOperation(this, markerName, currentRange, updatedRange, affectsData);
        }
        else {
            this.model.markers._set(markerName, updatedRange, undefined, affectsData);
        }
    }
    /**
     * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
     * The marker is removed accordingly to how it has been created, so if the marker was created using operation,
     * it will be destroyed using operation.
     *
     * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove.
     */
    removeMarker(markerOrName) {
        this._assertWriterUsedCorrectly();
        const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
        if (!this.model.markers.has(name)) {
            /**
             * Trying to remove marker which does not exist.
             *
             * @error writer-removemarker-no-marker
             */
            throw new CKEditorError('writer-removemarker-no-marker', this);
        }
        const marker = this.model.markers.get(name);
        if (!marker.managedUsingOperations) {
            this.model.markers._remove(name);
            return;
        }
        const oldRange = marker.getRange();
        applyMarkerOperation(this, name, oldRange, null, marker.affectsData);
    }
    /**
     * Sets the document's selection (ranges and direction) to the specified location based on the given
     * {@link module:engine/model/selection~Selectable selectable} or creates an empty selection if no arguments were passed.
     *
     *		// Sets selection to the given range.
     *		const range = writer.createRange( start, end );
     *		writer.setSelection( range );
     *
     *		// Sets selection to given ranges.
     *		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
     *		writer.setSelection( ranges );
     *
     *		// Sets selection to other selection.
     *		const otherSelection = writer.createSelection();
     *		writer.setSelection( otherSelection );
     *
     *		// Sets selection to the given document selection.
     *		const documentSelection = model.document.selection;
     *		writer.setSelection( documentSelection );
     *
     *		// Sets collapsed selection at the given position.
     *		const position = writer.createPosition( root, path );
     *		writer.setSelection( position );
     *
     *		// Sets collapsed selection at the position of the given node and an offset.
     *		writer.setSelection( paragraph, offset );
     *
     * 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.
     *
     *		writer.setSelection( paragraph, 'in' );
     *
     * Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends just after the item.
     *
     *		writer.setSelection( paragraph, 'on' );
     *
     *		// Removes all selection's ranges.
     *		writer.setSelection( null );
     *
     * `Writer#setSelection()` allow passing additional options (`backward`) as the last argument.
     *
     *		// Sets selection as backward.
     *		writer.setSelection( range, { backward: true } );
     *
     * Throws `writer-incorrect-use` error when the writer is used outside the `change()` block.
     *
     * @param {module:engine/model/selection~Selectable} selectable
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     */
    setSelection(...args) {
        this._assertWriterUsedCorrectly();
        this.model.document.selection._setTo(...args);
    }
    /**
     * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
     *
     * The location can be specified in the same form as
     * {@link #createPositionAt `writer.createPositionAt()`} parameters.
     *
     * @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}.
     */
    setSelectionFocus(itemOrPosition, offset) {
        this._assertWriterUsedCorrectly();
        this.model.document.selection._setFocus(itemOrPosition, offset);
    }
    /**
     * Sets attribute(s) on the selection. If attribute with the same key already is set, it's value is overwritten.
     *
     * Using key and value pair:
     *
     * 	writer.setSelectionAttribute( 'italic', true );
     *
     * Using key-value object:
     *
     * 	writer.setSelectionAttribute( { italic: true, bold: false } );
     *
     * Using iterable object:
     *
     * 	writer.setSelectionAttribute( new Map( [ [ 'italic', true ] ] ) );
     *
     * @param {String|Object|Iterable.<*>} keyOrObjectOrIterable Key of the attribute to set
     * or object / iterable of key => value attribute pairs.
     * @param {*} [value] Attribute value.
     */
    setSelectionAttribute(keyOrObjectOrIterable, value) {
        this._assertWriterUsedCorrectly();
        if (typeof keyOrObjectOrIterable === 'string') {
            this._setSelectionAttribute(keyOrObjectOrIterable, value);
        }
        else {
            for (const [key, value] of toMap(keyOrObjectOrIterable)) {
                this._setSelectionAttribute(key, value);
            }
        }
    }
    /**
     * Removes attribute(s) with given key(s) from the selection.
     *
     * Remove one attribute:
     *
     *		writer.removeSelectionAttribute( 'italic' );
     *
     * Remove multiple attributes:
     *
     *		writer.removeSelectionAttribute( [ 'italic', 'bold' ] );
     *
     * @param {String|Iterable.<String>} keyOrIterableOfKeys Key of the attribute to remove or an iterable of attribute keys to remove.
     */
    removeSelectionAttribute(keyOrIterableOfKeys) {
        this._assertWriterUsedCorrectly();
        if (typeof keyOrIterableOfKeys === 'string') {
            this._removeSelectionAttribute(keyOrIterableOfKeys);
        }
        else {
            for (const key of keyOrIterableOfKeys) {
                this._removeSelectionAttribute(key);
            }
        }
    }
    /**
     * Temporarily changes the {@link module:engine/model/documentselection~DocumentSelection#isGravityOverridden gravity}
     * of the selection from left to right.
     *
     * The gravity defines from which direction the selection inherits its attributes. If it's the default left gravity,
     * then the selection (after being moved by the user) inherits attributes from its left-hand side.
     * This method allows to temporarily override this behavior by forcing the gravity to the right.
     *
     * For the following model fragment:
     *
     *		<$text bold="true" linkHref="url">bar[]</$text><$text bold="true">biz</$text>
     *
     * * Default gravity: selection will have the `bold` and `linkHref` attributes.
     * * Overridden gravity: selection will have `bold` attribute.
     *
     * **Note**: It returns an unique identifier which is required to restore the gravity. It guarantees the symmetry
     * of the process.
     *
     * @returns {String} The unique id which allows restoring the gravity.
     */
    overrideSelectionGravity() {
        return this.model.document.selection._overrideGravity();
    }
    /**
     * Restores {@link ~Writer#overrideSelectionGravity} gravity to default.
     *
     * Restoring the gravity is only possible using the unique identifier returned by
     * {@link ~Writer#overrideSelectionGravity}. Note that the gravity remains overridden as long as won't be restored
     * the same number of times it was overridden.
     *
     * @param {String} uid The unique id returned by {@link ~Writer#overrideSelectionGravity}.
     */
    restoreSelectionGravity(uid) {
        this.model.document.selection._restoreGravity(uid);
    }
    /**
     * @private
     * @param {String} key Key of the attribute to remove.
     * @param {*} value Attribute value.
     */
    _setSelectionAttribute(key, value) {
        const selection = this.model.document.selection;
        // Store attribute in parent element if the selection is collapsed in an empty node.
        if (selection.isCollapsed && selection.anchor.parent.isEmpty) {
            const storeKey = DocumentSelection._getStoreAttributeKey(key);
            this.setAttribute(storeKey, value, selection.anchor.parent);
        }
        selection._setAttribute(key, value);
    }
    /**
     * @private
     * @param {String} key Key of the attribute to remove.
     */
    _removeSelectionAttribute(key) {
        const selection = this.model.document.selection;
        // Remove stored attribute from parent element if the selection is collapsed in an empty node.
        if (selection.isCollapsed && selection.anchor.parent.isEmpty) {
            const storeKey = DocumentSelection._getStoreAttributeKey(key);
            this.removeAttribute(storeKey, selection.anchor.parent);
        }
        selection._removeAttribute(key);
    }
    /**
     * Throws `writer-detached-writer-tries-to-modify-model` error when the writer is used outside of the `change()` block.
     *
     * @private
     */
    _assertWriterUsedCorrectly() {
        /**
         * Trying to use a writer outside a {@link module:engine/model/model~Model#change `change()`} or
         * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`} blocks.
         *
         * The writer can only be used inside these blocks which ensures that the model
         * can only be changed during such "sessions".
         *
         * @error writer-incorrect-use
         */
        if (this.model._currentWriter !== this) {
            throw new CKEditorError('writer-incorrect-use', this);
        }
    }
    /**
     * For given action `type` and `positionOrRange` where the action happens, this function finds all affected markers
     * and applies a marker operation with the new marker range equal to the current range. Thanks to this, the marker range
     * can be later correctly processed during undo.
     *
     * @private
     * @param {'move'|'merge'} type Writer action type.
     * @param {module:engine/model/position~Position|module:engine/model/range~Range} positionOrRange Position or range
     * where the writer action happens.
     */
    _addOperationForAffectedMarkers(type, positionOrRange) {
        for (const marker of this.model.markers) {
            if (!marker.managedUsingOperations) {
                continue;
            }
            const markerRange = marker.getRange();
            let isAffected = false;
            if (type === 'move') {
                const range = positionOrRange;
                isAffected =
                    range.containsPosition(markerRange.start) ||
                        range.start.isEqual(markerRange.start) ||
                        range.containsPosition(markerRange.end) ||
                        range.end.isEqual(markerRange.end);
            }
            else {
                // if type === 'merge'.
                const position = positionOrRange;
                const elementBefore = position.nodeBefore;
                const elementAfter = position.nodeAfter;
                //               Start:  <p>Foo[</p><p>Bar]</p>
                //         After merge:  <p>Foo[Bar]</p>
                // After undoing split:  <p>Foo</p><p>[Bar]</p>     <-- incorrect, needs remembering for undo.
                //
                const affectedInLeftElement = markerRange.start.parent == elementBefore && markerRange.start.isAtEnd;
                //               Start:  <p>[Foo</p><p>]Bar</p>
                //         After merge:  <p>[Foo]Bar</p>
                // After undoing split:  <p>[Foo]</p><p>Bar</p>     <-- incorrect, needs remembering for undo.
                //
                const affectedInRightElement = markerRange.end.parent == elementAfter && markerRange.end.offset == 0;
                //               Start:  <p>[Foo</p>]<p>Bar</p>
                //         After merge:  <p>[Foo]Bar</p>
                // After undoing split:  <p>[Foo]</p><p>Bar</p>     <-- incorrect, needs remembering for undo.
                //
                const affectedAfterLeftElement = markerRange.end.nodeAfter == elementAfter;
                //               Start:  <p>Foo</p>[<p>Bar]</p>
                //         After merge:  <p>Foo[Bar]</p>
                // After undoing split:  <p>Foo</p><p>[Bar]</p>     <-- incorrect, needs remembering for undo.
                //
                const affectedBeforeRightElement = markerRange.start.nodeAfter == elementAfter;
                isAffected = affectedInLeftElement || affectedInRightElement || affectedAfterLeftElement || affectedBeforeRightElement;
            }
            if (isAffected) {
                this.updateMarker(marker.name, { range: markerRange });
            }
        }
    }
}
// Sets given attribute to each node in given range. When attribute value is null then attribute will be removed.
//
// Because attribute operation needs to have the same attribute value on the whole range, this function splits
// the range into smaller parts.
//
// Given `range` must be flat.
//
// @private
// @param {module:engine/model/writer~Writer} writer
// @param {String} key Attribute key.
// @param {*} value Attribute new value.
// @param {module:engine/model/range~Range} range Model range on which the attribute will be set.
function setAttributeOnRange(writer, key, value, range) {
    const model = writer.model;
    const doc = model.document;
    // 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.
    let valueBefore;
    // Value after the currently position.
    let valueAfter;
    for (const val of range.getWalker({ shallow: true })) {
        valueAfter = val.item.getAttribute(key);
        // At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
        // because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).
        if (position && valueBefore != valueAfter) {
            // if valueBefore == value there is nothing to change, so we add operation only if these values are different.
            if (valueBefore != value) {
                addOperation();
            }
            lastSplitPosition = position;
        }
        position = val.nextPosition;
        valueBefore = valueAfter;
    }
    // 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 != lastSplitPosition && valueBefore != value) {
        addOperation();
    }
    function addOperation() {
        const range = new Range(lastSplitPosition, position);
        const version = range.root.document ? doc.version : null;
        const operation = new AttributeOperation(range, key, valueBefore, value, version);
        writer.batch.addOperation(operation);
        model.applyOperation(operation);
    }
}
// Sets given attribute to the given node. When attribute value is null then attribute will be removed.
//
// @private
// @param {module:engine/model/writer~Writer} writer
// @param {String} key Attribute key.
// @param {*} value Attribute new value.
// @param {module:engine/model/item~Item} item Model item on which the attribute will be set.
function setAttributeOnItem(writer, key, value, item) {
    const model = writer.model;
    const doc = model.document;
    const previousValue = item.getAttribute(key);
    let range, operation;
    if (previousValue != value) {
        const isRootChanged = item.root === item;
        if (isRootChanged) {
            // If we change attributes of root element, we have to use `RootAttributeOperation`.
            const version = item.document ? doc.version : null;
            operation = new RootAttributeOperation(item, key, previousValue, value, version);
        }
        else {
            range = new Range(Position._createBefore(item), writer.createPositionAfter(item));
            const version = range.root.document ? doc.version : null;
            operation = new AttributeOperation(range, key, previousValue, value, version);
        }
        writer.batch.addOperation(operation);
        model.applyOperation(operation);
    }
}
// Creates and applies marker operation to {@link module:engine/model/operation/operation~Operation operation}.
//
// @private
// @param {module:engine/model/writer~Writer} writer
// @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 {Boolean} affectsData
function applyMarkerOperation(writer, name, oldRange, newRange, affectsData) {
    const model = writer.model;
    const doc = model.document;
    const operation = new MarkerOperation(name, oldRange, newRange, model.markers, !!affectsData, doc.version);
    writer.batch.addOperation(operation);
    model.applyOperation(operation);
}
// Creates `MoveOperation` or `DetachOperation` that removes `howMany` nodes starting from `position`.
// The operation will be applied on given model instance and added to given operation instance.
//
// @private
// @param {module:engine/model/position~Position} position Position from which nodes are removed.
// @param {Number} howMany Number of nodes to remove.
// @param {Batch} batch Batch to which the operation will be added.
// @param {module:engine/model/model~Model} model Model instance on which operation will be applied.
function applyRemoveOperation(position, howMany, batch, model) {
    let operation;
    if (position.root.document) {
        const doc = model.document;
        const graveyardPosition = new Position(doc.graveyard, [0]);
        operation = new MoveOperation(position, howMany, graveyardPosition, doc.version);
    }
    else {
        operation = new DetachOperation(position, howMany);
    }
    batch.addOperation(operation);
    model.applyOperation(operation);
}
// Returns `true` if both root elements are the same element or both are documents root elements.
//
// Elements in the same tree can be moved (for instance you can move element form one documents root to another, or
// within the same document fragment), but when element supposed to be moved from document fragment to the document, or
// to another document it should be removed and inserted to avoid problems with OT. This is because features like undo or
// collaboration may track changes on the document but ignore changes on detached fragments and should not get
// unexpected `move` operation.
function isSameTree(rootA, rootB) {
    // If it is the same root this is the same tree.
    if (rootA === rootB) {
        return true;
    }
    // If both roots are documents root it is operation within the document what we still treat as the same tree.
    if (rootA instanceof RootElement && rootB instanceof RootElement) {
        return true;
    }
    return false;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Injects selection post-fixer to the model.
 *
 * The role of the selection post-fixer is to ensure that the selection is in a correct place
 * after a {@link module:engine/model/model~Model#change `change()`} block was executed.
 *
 * The correct position means that:
 *
 * * All collapsed selection ranges are in a place where the {@link module:engine/model/schema~Schema}
 * allows a `$text`.
 * * None of the selection's non-collapsed ranges crosses a {@link module:engine/model/schema~Schema#isLimit limit element}
 * boundary (a range must be rooted within one limit element).
 * * Only {@link module:engine/model/schema~Schema#isSelectable selectable elements} can be selected from the outside
 * (e.g. `[<paragraph>foo</paragraph>]` is invalid). This rule applies independently to both selection ends, so this
 * selection is correct: `<paragraph>f[oo</paragraph><imageBlock></imageBlock>]`.
 *
 * If the position is not correct, the post-fixer will automatically correct it.
 *
 * ## Fixing a non-collapsed selection
 *
 * See as an example a selection that starts in a P1 element and ends inside the text of a TD element
 * (`[` and `]` are range boundaries and `(l)` denotes an element defined as `isLimit=true`):
 *
 *		root
 *		 |- element P1
 *		 |   |- "foo"                                      root
 *		 |- element TABLE (l)                   P1         TABLE             P2
 *		 |   |- element TR (l)                 f o[o     TR      TR         b a r
 *		 |   |   |- element TD (l)                       TD      TD
 *		 |   |       |- "aaa"                          a]a a    b b b
 *		 |   |- element TR (l)
 *		 |   |   |- element TD (l)                           ||
 *		 |   |       |- "bbb"                                ||
 *		 |- element P2                                       VV
 *		 |   |- "bar"
 *		                                                   root
 *		                                        P1         TABLE]            P2
 *		                                       f o[o     TR      TR         b a r
 *		                                                 TD      TD
 *		                                               a a a    b b b
 *
 * In the example above, the TABLE, TR and TD are defined as `isLimit=true` in the schema. The range which is not contained within
 * a single limit element must be expanded to select the outermost limit element. The range end is inside the text node of the TD element.
 * As the TD element is a child of the TR and TABLE elements, where both are defined as `isLimit=true` in the schema, the range must be
 * expanded to select the whole TABLE element.
 *
 * **Note** If the selection contains multiple ranges, the method returns a minimal set of ranges that are not intersecting after expanding
 * them to select `isLimit=true` elements.
 *
 * @param {module:engine/model/model~Model} model
 */
function injectSelectionPostFixer(model) {
    model.document.registerPostFixer(writer => selectionPostFixer(writer, model));
}
// The selection post-fixer.
//
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/model~Model} model
function selectionPostFixer(writer, model) {
    const selection = model.document.selection;
    const schema = model.schema;
    const ranges = [];
    let wasFixed = false;
    for (const modelRange of selection.getRanges()) {
        // Go through all ranges in selection and try fixing each of them.
        // Those ranges might overlap but will be corrected later.
        const correctedRange = tryFixingRange(modelRange, schema);
        // "Selection fixing" algorithms sometimes get lost. In consequence, it may happen
        // that a new range is returned but, in fact, it has the same positions as the original
        // range anyway. If this range is not discarded, a new selection will be set and that,
        // for instance, would destroy the selection attributes. Let's make sure that the post-fixer
        // actually worked first before setting a new selection.
        //
        // https://github.com/ckeditor/ckeditor5/issues/6693
        if (correctedRange && !correctedRange.isEqual(modelRange)) {
            ranges.push(correctedRange);
            wasFixed = true;
        }
        else {
            ranges.push(modelRange);
        }
    }
    // If any of ranges were corrected update the selection.
    if (wasFixed) {
        writer.setSelection(mergeIntersectingRanges(ranges), { backward: selection.isBackward });
    }
    return false;
}
// Tries fixing a range if it's incorrect.
//
// @param {module:engine/model/range~Range} range
// @param {module:engine/model/schema~Schema} schema
// @returns {module:engine/model/range~Range|null} Returns fixed range or null if range is valid.
function tryFixingRange(range, schema) {
    if (range.isCollapsed) {
        return tryFixingCollapsedRange(range, schema);
    }
    return tryFixingNonCollapsedRage(range, schema);
}
// Tries to fix collapsed ranges.
//
// * Fixes situation when a range is in a place where $text is not allowed
//
// @param {module:engine/model/range~Range} range Collapsed range to fix.
// @param {module:engine/model/schema~Schema} schema
// @returns {module:engine/model/range~Range|null} Returns fixed range or null if range is valid.
function tryFixingCollapsedRange(range, schema) {
    const originalPosition = range.start;
    const nearestSelectionRange = schema.getNearestSelectionRange(originalPosition);
    // This might be null, i.e. when the editor data is empty or the selection is inside a limit element
    // that doesn't allow text inside.
    // In the first case, there is no need to fix the selection range.
    // In the second, let's go up to the outer selectable element
    if (!nearestSelectionRange) {
        const ancestorObject = originalPosition.getAncestors().reverse().find(item => schema.isObject(item));
        if (ancestorObject) {
            return Range._createOn(ancestorObject);
        }
        return null;
    }
    if (!nearestSelectionRange.isCollapsed) {
        return nearestSelectionRange;
    }
    const fixedPosition = nearestSelectionRange.start;
    // Fixed position is the same as original - no need to return corrected range.
    if (originalPosition.isEqual(fixedPosition)) {
        return null;
    }
    return new Range(fixedPosition);
}
// Tries to fix an expanded range.
//
// @param {module:engine/model/range~Range} range Expanded range to fix.
// @param {module:engine/model/schema~Schema} schema
// @returns {module:engine/model/range~Range|null} Returns fixed range or null if range is valid.
function tryFixingNonCollapsedRage(range, schema) {
    const { start, end } = range;
    const isTextAllowedOnStart = schema.checkChild(start, '$text');
    const isTextAllowedOnEnd = schema.checkChild(end, '$text');
    const startLimitElement = schema.getLimitElement(start);
    const endLimitElement = schema.getLimitElement(end);
    // Ranges which both end are inside the same limit element (or root) might needs only minor fix.
    if (startLimitElement === endLimitElement) {
        // Range is valid when both position allows to place a text:
        // - <block>f[oobarba]z</block>
        // This would be "fixed" by a next check but as it will be the same it's better to return null so the selection stays the same.
        if (isTextAllowedOnStart && isTextAllowedOnEnd) {
            return null;
        }
        // Range that is on non-limit element (or is partially) must be fixed so it is placed inside the block around $text:
        // - [<block>foo</block>]    ->    <block>[foo]</block>
        // - [<block>foo]</block>    ->    <block>[foo]</block>
        // - <block>f[oo</block>]    ->    <block>f[oo]</block>
        // - [<block>foo</block><selectable></selectable>]    ->    <block>[foo</block><selectable></selectable>]
        if (checkSelectionOnNonLimitElements(start, end, schema)) {
            const isStartBeforeSelectable = start.nodeAfter && schema.isSelectable(start.nodeAfter);
            const fixedStart = isStartBeforeSelectable ? null : schema.getNearestSelectionRange(start, 'forward');
            const isEndAfterSelectable = end.nodeBefore && schema.isSelectable(end.nodeBefore);
            const fixedEnd = isEndAfterSelectable ? null : schema.getNearestSelectionRange(end, 'backward');
            // The schema.getNearestSelectionRange might return null - if that happens use original position.
            const rangeStart = fixedStart ? fixedStart.start : start;
            const rangeEnd = fixedEnd ? fixedEnd.end : end;
            return new Range(rangeStart, rangeEnd);
        }
    }
    const isStartInLimit = startLimitElement && !startLimitElement.is('rootElement');
    const isEndInLimit = endLimitElement && !endLimitElement.is('rootElement');
    // At this point we eliminated valid positions on text nodes so if one of range positions is placed inside a limit element
    // then the range crossed limit element boundaries and needs to be fixed.
    if (isStartInLimit || isEndInLimit) {
        const bothInSameParent = (start.nodeAfter && end.nodeBefore) && start.nodeAfter.parent === end.nodeBefore.parent;
        const expandStart = isStartInLimit && (!bothInSameParent || !isSelectable(start.nodeAfter, schema));
        const expandEnd = isEndInLimit && (!bothInSameParent || !isSelectable(end.nodeBefore, schema));
        // Although we've already found limit element on start/end positions we must find the outer-most limit element.
        // as limit elements might be nested directly inside (ie table > tableRow > tableCell).
        let fixedStart = start;
        let fixedEnd = end;
        if (expandStart) {
            fixedStart = Position._createBefore(findOutermostLimitAncestor(startLimitElement, schema));
        }
        if (expandEnd) {
            fixedEnd = Position._createAfter(findOutermostLimitAncestor(endLimitElement, schema));
        }
        return new Range(fixedStart, fixedEnd);
    }
    // Range was not fixed at this point so it is valid - ie it was placed around limit element already.
    return null;
}
// Finds the outer-most ancestor.
//
// @param {module:engine/model/node~Node} startingNode
// @param {module:engine/model/schema~Schema} schema
// @returns {module:engine/model/node~Node}
function findOutermostLimitAncestor(startingNode, schema) {
    let isLimitNode = startingNode;
    let parent = isLimitNode;
    // Find outer most isLimit block as such blocks might be nested (ie. in tables).
    while (schema.isLimit(parent) && parent.parent) {
        isLimitNode = parent;
        parent = parent.parent;
    }
    return isLimitNode;
}
// Checks whether any of range boundaries is placed around non-limit elements.
//
// @param {module:engine/model/position~Position} start
// @param {module:engine/model/position~Position} end
// @param {module:engine/model/schema~Schema} schema
// @returns {Boolean}
function checkSelectionOnNonLimitElements(start, end, schema) {
    const startIsOnBlock = (start.nodeAfter && !schema.isLimit(start.nodeAfter)) || schema.checkChild(start, '$text');
    const endIsOnBlock = (end.nodeBefore && !schema.isLimit(end.nodeBefore)) || schema.checkChild(end, '$text');
    // We should fix such selection when one of those nodes needs fixing.
    return startIsOnBlock || endIsOnBlock;
}
/**
 * Returns a minimal non-intersecting array of ranges without duplicates.
 *
 * @param {Array.<module:engine/model/range~Range>} Ranges to merge.
 * @returns {Array.<module:engine/model/range~Range>} Array of unique and nonIntersecting ranges.
 */
function mergeIntersectingRanges(ranges) {
    const rangesToMerge = [...ranges];
    const rangeIndexesToRemove = new Set();
    let currentRangeIndex = 1;
    while (currentRangeIndex < rangesToMerge.length) {
        const currentRange = rangesToMerge[currentRangeIndex];
        const previousRanges = rangesToMerge.slice(0, currentRangeIndex);
        for (const [previousRangeIndex, previousRange] of previousRanges.entries()) {
            if (rangeIndexesToRemove.has(previousRangeIndex)) {
                continue;
            }
            if (currentRange.isEqual(previousRange)) {
                rangeIndexesToRemove.add(previousRangeIndex);
            }
            else if (currentRange.isIntersecting(previousRange)) {
                rangeIndexesToRemove.add(previousRangeIndex);
                rangeIndexesToRemove.add(currentRangeIndex);
                const mergedRange = currentRange.getJoined(previousRange);
                rangesToMerge.push(mergedRange);
            }
        }
        currentRangeIndex++;
    }
    const nonIntersectingRanges = rangesToMerge.filter((_, index) => !rangeIndexesToRemove.has(index));
    return nonIntersectingRanges;
}
// Checks if node exists and if it's a selectable.
//
// @param {module:engine/model/node~Node} node
// @param {module:engine/model/schema~Schema} schema
// @returns {Boolean}
function isSelectable(node, schema) {
    return node && schema.isSelectable(node);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * `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 extends EmitterMixin(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]
     */
    constructor(root, path, stickiness = 'toNone') {
        super(root, path, stickiness);
        if (!this.root.is('rootElement')) {
            /**
             * LivePosition's root has to be an instance of RootElement.
             *
             * @error model-liveposition-root-not-rootelement
             */
            throw new CKEditorError('model-liveposition-root-not-rootelement', root);
        }
        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();
    }
    /**
     * Creates a {@link module:engine/model/position~Position position instance}, which is equal to this live position.
     *
     * @returns {module:engine/model/position~Position}
     */
    toPosition() {
        return new Position(this.root, this.path.slice(), this.stickiness);
    }
    /**
     * Creates a `LivePosition` instance that is equal to position.
     *
     * @param {module:engine/model/position~Position} position
     * @param {module:engine/model/position~PositionStickiness} [stickiness]
     * @returns {module:engine/model/liveposition~LivePosition}
     */
    static fromPosition(position, stickiness) {
        return new this(position.root, position.path.slice(), stickiness ? stickiness : position.stickiness);
    }
}
/**
 * Checks whether this object is of the given.
 *
 *		livePosition.is( 'position' ); // -> true
 *		livePosition.is( 'model:position' ); // -> true
 *		livePosition.is( 'liveposition' ); // -> true
 *		livePosition.is( 'model:livePosition' ); // -> true
 *
 *		livePosition.is( 'view:position' ); // -> false
 *		livePosition.is( 'documentSelection' ); // -> false
 *
 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 *
 * @param {String} type
 * @returns {Boolean}
 */
LivePosition.prototype.is = function (type) {
    return type === 'livePosition' || type === 'model:livePosition' ||
        // From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
        type == 'position' || type === 'model:position';
};
// 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}.
//
// @private
function bindWithDocument() {
    this.listenTo(this.root.document.model, 'applyOperation', (event, args) => {
        const operation = args[0];
        if (!operation.isDocumentOperation) {
            return;
        }
        transform$1.call(this, operation);
    }, { priority: 'low' });
}
// Updates this position accordingly to the updates applied to the model. Bases on change events.
//
// @private
// @param {module:engine/model/operation/operation~Operation} operation Executed operation.
function transform$1(operation) {
    const result = this.getTransformedByOperation(operation);
    if (!this.isEqual(result)) {
        const oldPosition = this.toPosition();
        this.path = result.path;
        this.root = result.root;
        this.fire('change', oldPosition);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Deletes content of the selection and merge siblings. The resulting selection is always collapsed.
 *
 * **Note:** Use {@link module:engine/model/model~Model#deleteContent} instead of this function.
 * This function is only exposed to be reusable in algorithms
 * which change the {@link module:engine/model/model~Model#deleteContent}
 * method's behavior.
 *
 * @param {module:engine/model/model~Model} model The model in context of which the insertion
 * should be performed.
 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
 * Selection of which the content should be deleted.
 * @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#isObject object} and {@link module:engine/model/schema~Schema#isLimit 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`).
 *
 * @param {Boolean} [options.doNotAutoparagraph=false] Whether to create a paragraph if after content deletion selection is moved
 * to a place where text cannot be inserted.
 *
 * For example `<paragraph>x</paragraph>[<imageBlock src="foo.jpg"></imageBlock>]` will become:
 *
 * * `<paragraph>x</paragraph><paragraph>[]</paragraph>` with the option disabled (`doNotAutoparagraph == false`)
 * * `<paragraph>x</paragraph>[]` with the option enabled (`doNotAutoparagraph == true`).
 *
 * If you use this option you need to make sure to handle invalid selections yourself or leave
 * them to the selection post-fixer (may not always work).
 *
 * **Note:** If there is no valid position for the selection, the paragraph will always be created:
 *
 * `[<imageBlock src="foo.jpg"></imageBlock>]` -> `<paragraph>[]</paragraph>`.
 */
function deleteContent(model, selection, options = {}) {
    if (selection.isCollapsed) {
        return;
    }
    const selRange = selection.getFirstRange();
    // If the selection is already removed, don't do anything.
    if (selRange.root.rootName == '$graveyard') {
        return;
    }
    const schema = model.schema;
    model.change(writer => {
        // 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(writer, selection);
            return;
        }
        // Collect attributes to copy in case of autoparagraphing.
        const attributesForAutoparagraph = {};
        if (!options.doNotAutoparagraph) {
            const selectedElement = selection.getSelectedElement();
            if (selectedElement) {
                Object.assign(attributesForAutoparagraph, schema.getAttributesWithProperty(selectedElement, 'copyOnReplace', true));
            }
        }
        // Get the live positions for the range adjusted to span only blocks selected from the user perspective.
        const [startPosition, endPosition] = getLivePositionsForSelectedBlocks(selRange);
        // 2. Remove the content if there is any.
        if (!startPosition.isTouching(endPosition)) {
            writer.remove(writer.createRange(startPosition, endPosition));
        }
        // 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(writer, startPosition, endPosition);
            // TMP this will be replaced with a postfixer.
            // 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(startPosition.parent.getChildren(), writer);
        }
        collapseSelectionAt(writer, selection, startPosition);
        // 4. Add a paragraph to set selection in it.
        // Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
        // If autoparagraphing is off, we assume that you know what you do so we leave the selection wherever it was.
        if (!options.doNotAutoparagraph && shouldAutoparagraph(schema, startPosition)) {
            insertParagraph(writer, startPosition, selection, attributesForAutoparagraph);
        }
        startPosition.detach();
        endPosition.detach();
    });
}
// Returns the live positions for the range adjusted to span only blocks selected from the user perspective. Example:
//
//     <heading1>[foo</heading1>
//     <paragraph>bar</paragraph>
//     <heading1>]abc</heading1>  <-- this block is not considered as selected
//
// This is the same behavior as in Selection#getSelectedBlocks() "special case".
function getLivePositionsForSelectedBlocks(range) {
    const model = range.root.document.model;
    const startPosition = range.start;
    let endPosition = range.end;
    // If the end of selection is at the start position of last block in the selection, then
    // shrink it to not include that trailing block. Note that this should happen only for not empty selection.
    if (model.hasContent(range, { ignoreMarkers: true })) {
        const endBlock = getParentBlock(endPosition);
        if (endBlock && endPosition.isTouching(model.createPositionAt(endBlock, 0))) {
            // Create forward selection as a probe to find a valid position after excluding last block from the range.
            const selection = model.createSelection(range);
            // Modify the forward selection in backward direction to shrink it and remove first position of following block from it.
            // This is how modifySelection works and here we are making use of it.
            model.modifySelection(selection, { direction: 'backward' });
            const newEndPosition = selection.getLastPosition();
            // For such a model and selection:
            //     <paragraph>A[</paragraph><imageBlock></imageBlock><paragraph>]B</paragraph>
            //
            // After modifySelection(), we would end up with this:
            //     <paragraph>A[</paragraph>]<imageBlock></imageBlock><paragraph>B</paragraph>
            //
            // So we need to check if there is no content in the skipped range (because we want to include the <imageBlock>).
            const skippedRange = model.createRange(newEndPosition, endPosition);
            if (!model.hasContent(skippedRange, { ignoreMarkers: true })) {
                endPosition = newEndPosition;
            }
        }
    }
    return [
        LivePosition.fromPosition(startPosition, 'toPrevious'),
        LivePosition.fromPosition(endPosition, 'toNext')
    ];
}
// Finds the lowest element in position's ancestors which is a block.
// Returns null if a limit element is encountered before reaching a block element.
function getParentBlock(position) {
    const element = position.parent;
    const schema = element.root.document.model.schema;
    const ancestors = element.getAncestors({ parentFirst: true, includeSelf: true });
    for (const element of ancestors) {
        if (schema.isLimit(element)) {
            return null;
        }
        if (schema.isBlock(element)) {
            return element;
        }
    }
}
// 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(writer, startPosition, endPosition) {
    const model = writer.model;
    // Verify if there is a need and possibility to merge.
    if (!checkShouldMerge(writer.model.schema, startPosition, endPosition)) {
        return;
    }
    // If the start element on the common ancestor level is empty, and the end element on the same level is not empty
    // then merge those to the right element so that it's properties are preserved (name, attributes).
    // Because of OT merging is used instead of removing elements.
    //
    // Merge left:
    //     <heading1>foo[</heading1>    ->  <heading1>foo[]bar</heading1>
    //     <paragraph>]bar</paragraph>  ->               --^
    //
    // Merge right:
    //     <heading1>[</heading1>       ->
    //     <paragraph>]bar</paragraph>  ->  <paragraph>[]bar</paragraph>
    //
    // Merge left:
    //     <blockQuote>                     ->  <blockQuote>
    //         <heading1>foo[</heading1>    ->      <heading1>foo[]bar</heading1>
    //         <paragraph>]bar</paragraph>  ->                   --^
    //     </blockQuote>                    ->  </blockQuote>
    //
    // Merge right:
    //     <blockQuote>                     ->  <blockQuote>
    //         <heading1>[</heading1>       ->
    //         <paragraph>]bar</paragraph>  ->      <paragraph>[]bar</paragraph>
    //     </blockQuote>                    ->  </blockQuote>
    // Merging should not go deeper than common ancestor.
    const [startAncestor, endAncestor] = getAncestorsJustBelowCommonAncestor(startPosition, endPosition);
    // Branches can't be merged if one of the positions is directly inside a common ancestor.
    //
    // Example:
    //     <blockQuote>
    //         <paragraph>[foo</paragraph>]
    //         <table> ... </table>
    //     <blockQuote>
    //
    if (!startAncestor || !endAncestor) {
        return;
    }
    if (!model.hasContent(startAncestor, { ignoreMarkers: true }) && model.hasContent(endAncestor, { ignoreMarkers: true })) {
        mergeBranchesRight(writer, startPosition, endPosition, startAncestor.parent);
    }
    else {
        mergeBranchesLeft(writer, startPosition, endPosition, startAncestor.parent);
    }
}
// Merging blocks to the left (properties of the left block are preserved).
// Simple example:
//     <heading1>foo[</heading1>    ->  <heading1>foo[bar</heading1>]
//     <paragraph>]bar</paragraph>  ->              --^
//
// Nested example:
//     <blockQuote>                     ->  <blockQuote>
//         <heading1>foo[</heading1>    ->      <heading1>foo[bar</heading1>
//     </blockQuote>                    ->  </blockQuote>]    ^
//     <blockBlock>                     ->                    |
//         <paragraph>]bar</paragraph>  ->                 ---
//     </blockBlock>                    ->
//
function mergeBranchesLeft(writer, startPosition, endPosition, commonAncestor) {
    const startElement = startPosition.parent;
    const endElement = endPosition.parent;
    // Merging reached the common ancestor element, stop here.
    if (startElement == commonAncestor || endElement == commonAncestor) {
        return;
    }
    // Remember next positions to merge in next recursive step (also used as modification points pointers).
    startPosition = writer.createPositionAfter(startElement);
    endPosition = writer.createPositionBefore(endElement);
    // Move endElement just after startElement if they aren't siblings.
    if (!endPosition.isEqual(startPosition)) {
        //
        //     <blockQuote>                     ->  <blockQuote>
        //         <heading1>foo[</heading1>    ->      <heading1>foo</heading1>[<paragraph>bar</paragraph>
        //     </blockQuote>                    ->  </blockQuote>                ^
        //     <blockBlock>                     ->  <blockBlock>                 |
        //         <paragraph>]bar</paragraph>  ->      ]                     ---
        //     </blockBlock>                    ->  </blockBlock>
        //
        writer.insert(endElement, startPosition);
    }
    // Merge two siblings (nodes on sides of startPosition):
    //
    //     <blockQuote>                                             ->  <blockQuote>
    //         <heading1>foo</heading1>[<paragraph>bar</paragraph>  ->      <heading1>foo[bar</heading1>
    //     </blockQuote>                                            ->  </blockQuote>
    //     <blockBlock>                                             ->  <blockBlock>
    //         ]                                                    ->      ]
    //     </blockBlock>                                            ->  </blockBlock>
    //
    // Or in simple case (without moving elements in above if):
    //     <heading1>foo</heading1>[<paragraph>bar</paragraph>]  ->  <heading1>foo[bar</heading1>]
    //
    writer.merge(startPosition);
    // Remove empty end ancestors:
    //
    //     <blockQuote>                      ->  <blockQuote>
    //         <heading1>foo[bar</heading1>  ->      <heading1>foo[bar</heading1>
    //     </blockQuote>                     ->  </blockQuote>
    //     <blockBlock>                      ->
    //         ]                             ->  ]
    //     </blockBlock>                     ->
    //
    while (endPosition.parent.isEmpty) {
        const parentToRemove = endPosition.parent;
        endPosition = writer.createPositionBefore(parentToRemove);
        writer.remove(parentToRemove);
    }
    // Verify if there is a need and possibility to merge next level.
    if (!checkShouldMerge(writer.model.schema, startPosition, endPosition)) {
        return;
    }
    // Continue merging next level (blockQuote with blockBlock in the examples above if it would not be empty and got removed).
    mergeBranchesLeft(writer, startPosition, endPosition, commonAncestor);
}
// Merging blocks to the right (properties of the right block are preserved).
// Simple example:
//     <heading1>foo[</heading1>    ->            --v
//     <paragraph>]bar</paragraph>  ->  [<paragraph>foo]bar</paragraph>
//
// Nested example:
//     <blockQuote>                     ->
//         <heading1>foo[</heading1>    ->              ---
//     </blockQuote>                    ->                 |
//     <blockBlock>                     ->  [<blockBlock>  v
//         <paragraph>]bar</paragraph>  ->      <paragraph>foo]bar</paragraph>
//     </blockBlock>                    ->  </blockBlock>
//
function mergeBranchesRight(writer, startPosition, endPosition, commonAncestor) {
    const startElement = startPosition.parent;
    const endElement = endPosition.parent;
    // Merging reached the common ancestor element, stop here.
    if (startElement == commonAncestor || endElement == commonAncestor) {
        return;
    }
    // Remember next positions to merge in next recursive step (also used as modification points pointers).
    startPosition = writer.createPositionAfter(startElement);
    endPosition = writer.createPositionBefore(endElement);
    // Move startElement just before endElement if they aren't siblings.
    if (!endPosition.isEqual(startPosition)) {
        //
        //     <blockQuote>                     ->  <blockQuote>
        //         <heading1>foo[</heading1>    ->      [                   ---
        //     </blockQuote>                    ->  </blockQuote>              |
        //     <blockBlock>                     ->  <blockBlock>               v
        //         <paragraph>]bar</paragraph>  ->      <heading1>foo</heading1>]<paragraph>bar</paragraph>
        //     </blockBlock>                    ->  </blockBlock>
        //
        writer.insert(startElement, endPosition);
    }
    // Remove empty end ancestors:
    //
    //     <blockQuote>                                             ->
    //         [                                                    ->  [
    //     </blockQuote>                                            ->
    //     <blockBlock>                                             ->  <blockBlock>
    //         <heading1>foo</heading1>]<paragraph>bar</paragraph>  ->      <heading1>foo</heading1>]<paragraph>bar</paragraph>
    //     </blockBlock>                                            ->  </blockBlock>
    //
    while (startPosition.parent.isEmpty) {
        const parentToRemove = startPosition.parent;
        startPosition = writer.createPositionBefore(parentToRemove);
        writer.remove(parentToRemove);
    }
    // Update endPosition after inserting and removing elements.
    endPosition = writer.createPositionBefore(endElement);
    // Merge right two siblings (nodes on sides of endPosition):
    //                                                              ->
    //     [                                                        ->  [
    //                                                              ->
    //     <blockBlock>                                             ->  <blockBlock>
    //         <heading1>foo</heading1>]<paragraph>bar</paragraph>  ->      <paragraph>foo]bar</paragraph>
    //     </blockBlock>                                            ->  </blockBlock>
    //
    // Or in simple case (without moving elements in above if):
    //     [<heading1>foo</heading1>]<paragraph>bar</paragraph>  ->  [<heading1>foo]bar</heading1>
    //
    mergeRight(writer, endPosition);
    // Verify if there is a need and possibility to merge next level.
    if (!checkShouldMerge(writer.model.schema, startPosition, endPosition)) {
        return;
    }
    // Continue merging next level (blockQuote with blockBlock in the examples above if it would not be empty and got removed).
    mergeBranchesRight(writer, startPosition, endPosition, commonAncestor);
}
// There is no right merge operation so we need to simulate it.
function mergeRight(writer, position) {
    const startElement = position.nodeBefore;
    const endElement = position.nodeAfter;
    if (startElement.name != endElement.name) {
        writer.rename(startElement, endElement.name);
    }
    writer.clearAttributes(startElement);
    writer.setAttributes(Object.fromEntries(endElement.getAttributes()), startElement);
    writer.merge(position);
}
// Verifies if merging is needed and possible. It's not needed if both positions are in the same element
// and it's not possible if some element is a limit or the range crosses a limit element.
function checkShouldMerge(schema, startPosition, endPosition) {
    const startElement = startPosition.parent;
    const endElement = endPosition.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 (startElement == endElement) {
        return false;
    }
    // If one of the positions is a limit element, then there's nothing to merge because we don't want to cross the limit boundaries.
    if (schema.isLimit(startElement) || schema.isLimit(endElement)) {
        return false;
    }
    // Check if operations we'll need to do won't need to cross object or limit boundaries.
    // E.g., we can't merge endElement into startElement in this case:
    // <limit><startElement>x[</startElement></limit><endElement>]</endElement>
    return isCrossingLimitElement(startPosition, endPosition, schema);
}
// Returns the elements that are the ancestors of the provided positions that are direct children of the common ancestor.
function getAncestorsJustBelowCommonAncestor(positionA, positionB) {
    const ancestorsA = positionA.getAncestors();
    const ancestorsB = positionB.getAncestors();
    let i = 0;
    while (ancestorsA[i] && ancestorsA[i] == ancestorsB[i]) {
        i++;
    }
    return [ancestorsA[i], ancestorsB[i]];
}
function shouldAutoparagraph(schema, position) {
    const isTextAllowed = schema.checkChild(position, '$text');
    const isParagraphAllowed = schema.checkChild(position, 'paragraph');
    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 isCrossingLimitElement(leftPos, rightPos, schema) {
    const rangeToCheck = new Range(leftPos, rightPos);
    for (const value of rangeToCheck.getWalker()) {
        if (schema.isLimit(value.item)) {
            return false;
        }
    }
    return true;
}
function insertParagraph(writer, position, selection, attributes = {}) {
    const paragraph = writer.createElement('paragraph');
    writer.model.schema.setAllowedAttributes(paragraph, attributes, writer);
    writer.insert(paragraph, position);
    collapseSelectionAt(writer, selection, writer.createPositionAt(paragraph, 0));
}
function replaceEntireContentWithParagraph(writer, selection) {
    const limitElement = writer.model.schema.getLimitElement(selection);
    writer.remove(writer.createRangeIn(limitElement));
    insertParagraph(writer, writer.createPositionAt(limitElement, 0), 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.checkChild(limitElement, 'paragraph');
}
// Helper function that sets the selection. Depending whether given `selection` is a document selection or not,
// uses a different method to set it.
function collapseSelectionAt(writer, selection, positionOrRange) {
    if (selection instanceof DocumentSelection) {
        writer.setSelection(positionOrRange);
    }
    else {
        selection.setTo(positionOrRange);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module engine/model/utils/getselectedcontent
 */
/**
 * Gets a clone of the selected content.
 *
 * For example, for the following selection:
 *
 * ```html
 * <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:
 *
 * ```html
 * <quote><h>st</h></quote><p>se</p>
 * ```
 *
 * @param {module:engine/model/model~Model} model The model in context of which
 * the selection modification should be performed.
 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
 * The selection of which content will be returned.
 * @returns {module:engine/model/documentfragment~DocumentFragment}
 */
function getSelectedContent(model, selection) {
    return model.change(writer => {
        const frag = writer.createDocumentFragment();
        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 = writer.createRange(writer.createPositionAt(commonParent, range.start.path[commonPath.length]), writer.createPositionAt(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')) {
                writer.appendText(item.data, item.getAttributes(), frag);
            }
            else {
                writer.append(writer.cloneElement(item, true), frag);
            }
        }
        // ## 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, writer.createPositionAt(frag, 0), howMany)[0];
            const leftExcessRange = writer.createRange(writer.createPositionAt(frag, 0), newRange.start);
            const rightExcessRange = writer.createRange(newRange.end, writer.createPositionAt(frag, 'end'));
            removeRangeContent(rightExcessRange, writer);
            removeRangeContent(leftExcessRange, writer);
        }
        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, writer) {
    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 => writer.createRangeOn(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);
        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 = writer.createRangeOn(parent);
            parent = parent.parent;
            writer.remove(removeRange);
        }
    });
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Inserts content into the editor (specified selection) as one would expect the paste functionality to work.
 *
 * It takes care of removing the selected content, splitting elements (if needed), inserting elements and merging elements appropriately.
 *
 * Some examples:
 *
 * 		<p>x^</p> + <p>y</p> => <p>x</p><p>y</p> => <p>xy[]</p>
 * 		<p>x^y</p> + <p>z</p> => <p>x</p>^<p>y</p> + <p>z</p> => <p>x</p><p>z</p><p>y</p> => <p>xz[]y</p>
 * 		<p>x^y</p> + <img /> => <p>x</p>^<p>y</p> + <img /> => <p>x</p><img /><p>y</p>
 * 		<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>z</p> + <p>y</p> => <p>x</p><p>y[]</p><p>z</p>
 *
 * If an instance of {@link module:engine/model/selection~Selection} is passed as `selectable` it will be modified
 * to the insertion selection (equal to a range to be selected after insertion).
 *
 * If `selectable` is not passed, the content will be inserted using the current selection of the model document.
 *
 * **Note:** Use {@link module:engine/model/model~Model#insertContent} instead of this function.
 * This function is only exposed to be reusable in algorithms which change the {@link module:engine/model/model~Model#insertContent}
 * method's behavior.
 *
 * @param {module:engine/model/model~Model} model The model 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~Selectable} [selectable=model.document.selection]
 * Selection into which the content should be inserted.
 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
 * @returns {module:engine/model/range~Range} Range which contains all the performed changes. This is a range that, if removed,
 * would return the model to the state before the insertion. If no changes were preformed by `insertContent`, returns a range collapsed
 * at the insertion position.
 */
function insertContent(model, content, selectable, placeOrOffset) {
    return model.change(writer => {
        let selection;
        if (!selectable) {
            selection = model.document.selection;
        }
        else if (selectable instanceof Selection || selectable instanceof DocumentSelection) {
            selection = selectable;
        }
        else {
            selection = writer.createSelection(selectable, placeOrOffset);
        }
        if (!selection.isCollapsed) {
            model.deleteContent(selection, { doNotAutoparagraph: true });
        }
        const insertion = new Insertion(model, writer, selection.anchor);
        let nodesToInsert;
        if (content.is('documentFragment')) {
            nodesToInsert = content.getChildren();
        }
        else {
            nodesToInsert = [content];
        }
        insertion.handleNodes(nodesToInsert);
        const newRange = insertion.getSelectionRange();
        /* istanbul ignore else */
        if (newRange) {
            if (selection instanceof DocumentSelection) {
                writer.setSelection(newRange);
            }
            else {
                selection.setTo(newRange);
            }
        }
        const affectedRange = insertion.getAffectedRange() || model.createRange(selection.anchor);
        insertion.destroy();
        return affectedRange;
    });
}
/**
 * Utility class for performing content insertion.
 *
 * @private
 */
class Insertion {
    constructor(model, writer, position) {
        /**
         * The model in context of which the insertion should be performed.
         *
         * @member {module:engine/model~Model} #model
         */
        this.model = model;
        /**
         * Batch to which operations will be added.
         *
         * @member {module:engine/controller/writer~Batch} #writer
         */
        this.writer = writer;
        /**
         * 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 = model.schema;
        /**
         * The temporary DocumentFragment used for grouping multiple nodes for single insert operation.
         *
         * @private
         * @type {module:engine/model/documentfragment~DocumentFragment}
         */
        this._documentFragment = writer.createDocumentFragment();
        /**
         * The current position in the temporary DocumentFragment.
         *
         * @private
         * @type {module:engine/model/position~Position}
         */
        this._documentFragmentPosition = writer.createPositionAt(this._documentFragment, 0);
        /**
         * The reference to the first inserted node.
         *
         * @private
         * @type {module:engine/model/node~Node}
         */
        this._firstNode = null;
        /**
         * The reference to the last inserted node.
         *
         * @private
         * @type {module:engine/model/node~Node}
         */
        this._lastNode = null;
        /**
         * The reference to the last auto paragraph node.
         *
         * @private
         * @type {module:engine/model/node~Node}
         */
        this._lastAutoParagraph = null;
        /**
         * The array of nodes that should be cleaned of not allowed attributes.
         *
         * @private
         * @type {Array.<module:engine/model/node~Node>}
         */
        this._filterAttributesOf = [];
        /**
         * Beginning of the affected range. See {@link module:engine/model/utils/insertcontent~Insertion#getAffectedRange}.
         *
         * @private
         * @member {module:engine/model/liveposition~LivePosition|null} #_affectedStart
         */
        this._affectedStart = null;
        /**
         * End of the affected range. See {@link module:engine/model/utils/insertcontent~Insertion#getAffectedRange}.
         *
         * @private
         * @member {module:engine/model/liveposition~LivePosition|null} #_affectedEnd
         */
        this._affectedEnd = null;
    }
    /**
     * Handles insertion of a set of nodes.
     *
     * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes to insert.
     */
    handleNodes(nodes) {
        for (const node of Array.from(nodes)) {
            this._handleNode(node);
        }
        // Insert nodes collected in temporary DocumentFragment.
        this._insertPartialFragment();
        // If there was an auto paragraph then we might need to adjust the end of insertion.
        if (this._lastAutoParagraph) {
            this._updateLastNodeFromAutoParagraph(this._lastAutoParagraph);
        }
        // After the content was inserted we may try to merge it with its next sibling if the selection was in it initially.
        // Merging with the previous sibling was performed just after inserting the first node to the document.
        this._mergeOnRight();
        // TMP this will become a post-fixer.
        this.schema.removeDisallowedAttributes(this._filterAttributesOf, this.writer);
        this._filterAttributesOf = [];
    }
    /**
     * Updates the last node after the auto paragraphing.
     *
     * @private
     * @param {module:engine/model/node~Node} node The last auto paragraphing node.
     */
    _updateLastNodeFromAutoParagraph(node) {
        const positionAfterLastNode = this.writer.createPositionAfter(this._lastNode);
        const positionAfterNode = this.writer.createPositionAfter(node);
        // If the real end was after the last auto paragraph then update relevant properties.
        if (positionAfterNode.isAfter(positionAfterLastNode)) {
            this._lastNode = node;
            /* istanbul ignore if */
            if (this.position.parent != node || !this.position.isAtEnd) {
                // 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 at the end of the last auto paragraph.
                // Note: This error is documented in other place in this file.
                throw new CKEditorError('insertcontent-invalid-insertion-position', this);
            }
            this.position = positionAfterNode;
            this._setAffectedBoundaries(this.position);
        }
    }
    /**
     * 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._createOn(this._nodeToSelect);
        }
        return this.model.schema.getNearestSelectionRange(this.position);
    }
    /**
     * Returns a range which contains all the performed changes. This is a range that, if removed, would return the model to the state
     * before the insertion. Returns `null` if no changes were done.
     *
     * @returns {module:engine/model/range~Range|null}
     */
    getAffectedRange() {
        if (!this._affectedStart) {
            return null;
        }
        return new Range(this._affectedStart, this._affectedEnd);
    }
    /**
     * Destroys `Insertion` instance.
     */
    destroy() {
        if (this._affectedStart) {
            this._affectedStart.detach();
        }
        if (this._affectedEnd) {
            this._affectedEnd.detach();
        }
    }
    /**
     * Handles insertion of a single node.
     *
     * @private
     * @param {module:engine/model/node~Node} node
     */
    _handleNode(node) {
        // 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.schema.isObject(node)) {
            this._handleObject(node);
            return;
        }
        // Try to find a place for the given node.
        // Check if a node can be inserted in the given position or it would be accepted if a paragraph would be inserted.
        // Inserts the auto paragraph if it would allow for insertion.
        let isAllowed = this._checkAndAutoParagraphToAllowedPosition(node);
        if (!isAllowed) {
            // 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.
            isAllowed = this._checkAndSplitToAllowedPosition(node);
            if (!isAllowed) {
                this._handleDisallowedNode(node);
                return;
            }
        }
        // Add node to the current temporary DocumentFragment.
        this._appendToFragment(node);
        // Store the first and last nodes for easy access for merging with sibling nodes.
        if (!this._firstNode) {
            this._firstNode = node;
        }
        this._lastNode = node;
    }
    /**
     * Inserts the temporary DocumentFragment into the model.
     *
     * @private
     */
    _insertPartialFragment() {
        if (this._documentFragment.isEmpty) {
            return;
        }
        const livePosition = LivePosition.fromPosition(this.position, 'toNext');
        this._setAffectedBoundaries(this.position);
        // If the very first node of the whole insertion process is inserted, insert it separately for OT reasons (undo).
        // Note: there can be multiple calls to `_insertPartialFragment()` during one insertion process.
        // Note: only the very first node can be merged so we have to do separate operation only for it.
        if (this._documentFragment.getChild(0) == this._firstNode) {
            this.writer.insert(this._firstNode, this.position);
            // We must merge the first node just after inserting it to avoid problems with OT.
            // (See: https://github.com/ckeditor/ckeditor5/pull/8773#issuecomment-760945652).
            this._mergeOnLeft();
            this.position = livePosition.toPosition();
        }
        // Insert the remaining nodes from document fragment.
        if (!this._documentFragment.isEmpty) {
            this.writer.insert(this._documentFragment, this.position);
        }
        this._documentFragmentPosition = this.writer.createPositionAt(this._documentFragment, 0);
        this.position = livePosition.toPosition();
        livePosition.detach();
    }
    /**
     * @private
     * @param {module:engine/model/element~Element} node The object element.
     */
    _handleObject(node) {
        // Try finding it a place in the tree.
        if (this._checkAndSplitToAllowedPosition(node)) {
            this._appendToFragment(node);
        }
        // Try autoparagraphing.
        else {
            this._tryAutoparagraphing(node);
        }
    }
    /**
     * @private
     * @param {module:engine/model/node~Node} node The disallowed node which needs to be handled.
     */
    _handleDisallowedNode(node) {
        // If the node is an element, try inserting its children (strip the parent).
        if (node.is('element')) {
            this.handleNodes(node.getChildren());
        }
        // If text is not allowed, try autoparagraphing it.
        else {
            this._tryAutoparagraphing(node);
        }
    }
    /**
     * Append a node to the temporary DocumentFragment.
     *
     * @private
     * @param {module:engine/model/node~Node} node The node to insert.
     */
    _appendToFragment(node) {
        /* istanbul ignore if */
        if (!this.schema.checkChild(this.position, node)) {
            // 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.
            /**
             * Given node cannot be inserted on the given position.
             *
             * @error insertcontent-wrong-position
             * @param {module:engine/model/node~Node} node Node to insert.
             * @param {module:engine/model/position~Position} position Position to insert the node at.
             */
            throw new CKEditorError('insertcontent-wrong-position', this, { node, position: this.position });
        }
        this.writer.insert(node, this._documentFragmentPosition);
        this._documentFragmentPosition = this._documentFragmentPosition.getShiftedBy(node.offsetSize);
        // The last inserted object should be selected because we can't put a collapsed selection after it.
        if (this.schema.isObject(node) && !this.schema.checkChild(this.position, '$text')) {
            this._nodeToSelect = node;
        }
        else {
            this._nodeToSelect = null;
        }
        this._filterAttributesOf.push(node);
    }
    /**
     * Sets `_affectedStart` and `_affectedEnd` to the given `position`. Should be used before a change is done during insertion process to
     * mark the affected range.
     *
     * This method is used before inserting a node or splitting a parent node. `_affectedStart` and `_affectedEnd` are also changed
     * during merging, but the logic there is more complicated so it is left out of this function.
     *
     * @private
     * @param {module:engine/model/position~Position} position
     */
    _setAffectedBoundaries(position) {
        // Set affected boundaries stickiness so that those position will "expand" when something is inserted in between them:
        // <paragraph>Foo][bar</paragraph> -> <paragraph>Foo]xx[bar</paragraph>
        // This is why it cannot be a range but two separate positions.
        if (!this._affectedStart) {
            this._affectedStart = LivePosition.fromPosition(position, 'toPrevious');
        }
        // If `_affectedEnd` is before the new boundary position, expand `_affectedEnd`. This can happen if first inserted node was
        // inserted into the parent but the next node is moved-out of that parent:
        // (1) <paragraph>Foo][</paragraph> -> <paragraph>Foo]xx[</paragraph>
        // (2) <paragraph>Foo]xx[</paragraph> -> <paragraph>Foo]xx</paragraph><widget></widget>[
        if (!this._affectedEnd || this._affectedEnd.isBefore(position)) {
            if (this._affectedEnd) {
                this._affectedEnd.detach();
            }
            this._affectedEnd = LivePosition.fromPosition(position, 'toNext');
        }
    }
    /**
     * Merges the previous sibling of the first node if it should be merged.
     *
     * After the content was inserted we may try to merge it with its siblings.
     * This should happen only if the selection was in those elements initially.
     *
     * @private
     */
    _mergeOnLeft() {
        const node = this._firstNode;
        if (!(node instanceof Element)) {
            return;
        }
        if (!this._canMergeLeft(node)) {
            return;
        }
        const mergePosLeft = LivePosition._createBefore(node);
        mergePosLeft.stickiness = 'toNext';
        const livePosition = LivePosition.fromPosition(this.position, 'toNext');
        // If `_affectedStart` is sames as merge position, it means that the element "marked" by `_affectedStart` is going to be
        // removed and its contents will be moved. This won't transform `LivePosition` so `_affectedStart` needs to be moved
        // by hand to properly reflect affected range. (Due to `_affectedStart` and `_affectedEnd` stickiness, the "range" is
        // shown as `][`).
        //
        // Example - insert `<paragraph>Abc</paragraph><paragraph>Xyz</paragraph>` at the end of `<paragraph>Foo^</paragraph>`:
        //
        // <paragraph>Foo</paragraph><paragraph>Bar</paragraph>   -->
        // <paragraph>Foo</paragraph>]<paragraph>Abc</paragraph><paragraph>Xyz</paragraph>[<paragraph>Bar</paragraph>   -->
        // <paragraph>Foo]Abc</paragraph><paragraph>Xyz</paragraph>[<paragraph>Bar</paragraph>
        //
        // Note, that if we are here then something must have been inserted, so `_affectedStart` and `_affectedEnd` have to be set.
        if (this._affectedStart.isEqual(mergePosLeft)) {
            this._affectedStart.detach();
            this._affectedStart = LivePosition._createAt(mergePosLeft.nodeBefore, 'end', 'toPrevious');
        }
        // We need to update the references to the first and last nodes if they will be merged into the previous sibling node
        // because the reference would point to the removed node.
        //
        // <p>A^A</p> + <p>X</p>
        //
        // <p>A</p>^<p>A</p>
        // <p>A</p><p>X</p><p>A</p>
        // <p>AX</p><p>A</p>
        // <p>AXA</p>
        if (this._firstNode === this._lastNode) {
            this._firstNode = mergePosLeft.nodeBefore;
            this._lastNode = mergePosLeft.nodeBefore;
        }
        this.writer.merge(mergePosLeft);
        // If only one element (the merged one) is in the "affected range", also move the affected range end appropriately.
        //
        // Example - insert `<paragraph>Abc</paragraph>` at the of `<paragraph>Foo^</paragraph>`:
        //
        // <paragraph>Foo</paragraph><paragraph>Bar</paragraph>   -->
        // <paragraph>Foo</paragraph>]<paragraph>Abc</paragraph>[<paragraph>Bar</paragraph>   -->
        // <paragraph>Foo]Abc</paragraph>[<paragraph>Bar</paragraph>   -->
        // <paragraph>Foo]Abc[</paragraph><paragraph>Bar</paragraph>
        if (mergePosLeft.isEqual(this._affectedEnd) && this._firstNode === this._lastNode) {
            this._affectedEnd.detach();
            this._affectedEnd = LivePosition._createAt(mergePosLeft.nodeBefore, 'end', 'toNext');
        }
        this.position = livePosition.toPosition();
        livePosition.detach();
        // After merge elements that were marked by _insert() to be filtered might be gone so
        // we need to mark the new container.
        this._filterAttributesOf.push(this.position.parent);
        mergePosLeft.detach();
    }
    /**
     * Merges the next sibling of the last node if it should be merged.
     *
     * After the content was inserted we may try to merge it with its siblings.
     * This should happen only if the selection was in those elements initially.
     *
     * @private
     */
    _mergeOnRight() {
        const node = this._lastNode;
        if (!(node instanceof Element)) {
            return;
        }
        if (!this._canMergeRight(node)) {
            return;
        }
        const mergePosRight = LivePosition._createAfter(node);
        mergePosRight.stickiness = 'toNext';
        /* 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.
            /**
             * An internal error occurred when merging inserted content with its siblings.
             * The insertion position should equal the merge position.
             *
             * If you encountered this error, report it back to the CKEditor 5 team
             * with as many details as possible regarding the content being inserted and the insertion position.
             *
             * @error insertcontent-invalid-insertion-position
             */
            throw new CKEditorError('insertcontent-invalid-insertion-position', this);
        }
        // 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._createAt(mergePosRight.nodeBefore, 'end');
        // Explanation of setting position stickiness to `'toPrevious'`:
        // 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 livePosition = LivePosition.fromPosition(this.position, 'toPrevious');
        // See comment in `_mergeOnLeft()` on moving `_affectedStart`.
        if (this._affectedEnd.isEqual(mergePosRight)) {
            this._affectedEnd.detach();
            this._affectedEnd = LivePosition._createAt(mergePosRight.nodeBefore, 'end', 'toNext');
        }
        // We need to update the references to the first and last nodes if they will be merged into the previous sibling node
        // because the reference would point to the removed node.
        //
        // <p>A^A</p> + <p>X</p>
        //
        // <p>A</p>^<p>A</p>
        // <p>A</p><p>X</p><p>A</p>
        // <p>AX</p><p>A</p>
        // <p>AXA</p>
        if (this._firstNode === this._lastNode) {
            this._firstNode = mergePosRight.nodeBefore;
            this._lastNode = mergePosRight.nodeBefore;
        }
        this.writer.merge(mergePosRight);
        // See comment in `_mergeOnLeft()` on moving `_affectedStart`.
        if (mergePosRight.getShiftedBy(-1).isEqual(this._affectedStart) && this._firstNode === this._lastNode) {
            this._affectedStart.detach();
            this._affectedStart = LivePosition._createAt(mergePosRight.nodeBefore, 0, 'toPrevious');
        }
        this.position = livePosition.toPosition();
        livePosition.detach();
        // After merge elements that were marked by _insert() to be filtered might be gone so
        // we need to mark the new container.
        this._filterAttributesOf.push(this.position.parent);
        mergePosRight.detach();
    }
    /**
     * Checks whether specified node can be merged with previous sibling element.
     *
     * @private
     * @param {module:engine/model/node~Node} node The node which could potentially be merged.
     * @returns {Boolean}
     */
    _canMergeLeft(node) {
        const previousSibling = node.previousSibling;
        return (previousSibling instanceof Element) &&
            this.canMergeWith.has(previousSibling) &&
            this.model.schema.checkMerge(previousSibling, node);
    }
    /**
     * Checks whether specified node can be merged with next sibling element.
     *
     * @private
     * @param {module:engine/model/node~Node} node The node which could potentially be merged.
     * @returns {Boolean}
     */
    _canMergeRight(node) {
        const nextSibling = node.nextSibling;
        return (nextSibling instanceof Element) &&
            this.canMergeWith.has(nextSibling) &&
            this.model.schema.checkMerge(node, nextSibling);
    }
    /**
     * Tries wrapping the node in a new paragraph and inserting it this way.
     *
     * @private
     * @param {module:engine/model/node~Node} node The node which needs to be autoparagraphed.
     */
    _tryAutoparagraphing(node) {
        const paragraph = this.writer.createElement('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(this.position.parent, paragraph) && this.schema.checkChild(paragraph, node)) {
            paragraph._appendChild(node);
            this._handleNode(paragraph);
        }
    }
    /**
     * Checks if a node can be inserted in the given position or it would be accepted if a paragraph would be inserted.
     * It also handles inserting the paragraph.
     *
     * @private
     * @param {module:engine/model/node~Node} node The node.
     * @returns {Boolean} Whether an allowed position was found.
     * `false` is returned if the node isn't allowed at the current position or in auto paragraph, `true` if was.
     */
    _checkAndAutoParagraphToAllowedPosition(node) {
        if (this.schema.checkChild(this.position.parent, node)) {
            return true;
        }
        // Do not auto paragraph 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.schema.checkChild(this.position.parent, 'paragraph') || !this.schema.checkChild('paragraph', node)) {
            return false;
        }
        // Insert nodes collected in temporary DocumentFragment if the position parent needs change to process further nodes.
        this._insertPartialFragment();
        // Insert a paragraph and move insertion position to it.
        const paragraph = this.writer.createElement('paragraph');
        this.writer.insert(paragraph, this.position);
        this._setAffectedBoundaries(this.position);
        this._lastAutoParagraph = paragraph;
        this.position = this.writer.createPositionAt(paragraph, 0);
        return true;
    }
    /**
     * @private
     * @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(this.position.parent, node);
        if (!allowedIn) {
            return false;
        }
        // Insert nodes collected in temporary DocumentFragment if the position parent needs change to process further nodes.
        if (allowedIn != this.position.parent) {
            this._insertPartialFragment();
        }
        while (allowedIn != this.position.parent) {
            if (this.position.isAtStart) {
                // If insertion position is at the beginning of the parent, move it out instead of splitting.
                // <p>^Foo</p> -> ^<p>Foo</p>
                const parent = this.position.parent;
                this.position = this.writer.createPositionBefore(parent);
                // Special case – parent is empty (<p>^</p>).
                //
                // 1. parent.isEmpty
                // We can remove the element after moving insertion position out of it.
                //
                // 2. parent.parent === allowedIn
                // However parent should remain in place when allowed element is above limit element in document tree.
                // For example there shouldn't be allowed to remove empty paragraph from tableCell, when is pasted
                // content allowed in $root.
                if (parent.isEmpty && parent.parent === allowedIn) {
                    this.writer.remove(parent);
                }
            }
            else if (this.position.isAtEnd) {
                // If insertion position is at the end of the parent, move it out instead of splitting.
                // <p>Foo^</p> -> <p>Foo</p>^
                this.position = this.writer.createPositionAfter(this.position.parent);
            }
            else {
                const tempPos = this.writer.createPositionAfter(this.position.parent);
                this._setAffectedBoundaries(this.position);
                this.writer.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.
     *
     * @private
     * @param {module:engine/model/element~Element} contextElement The element in which context the node should be checked.
     * @param {module:engine/model/node~Node} childNode The node to check.
     * @returns {module:engine/model/element~Element|null}
     */
    _getAllowedIn(contextElement, childNode) {
        if (this.schema.checkChild(contextElement, childNode)) {
            return contextElement;
        }
        // If the child wasn't allowed in the context element and the element is a limit there's no point in
        // checking any further towards the root. This is it: the limit is unsplittable and there's nothing
        // we can do about it. Without this check, the algorithm will analyze parent of the limit and may create
        // an illusion of the child being allowed. There's no way to insert it down there, though. It results in
        // infinite loops.
        if (this.schema.isLimit(contextElement)) {
            return null;
        }
        return this._getAllowedIn(contextElement.parent, childNode);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// Returns a model range which is optimal (in terms of UX) for inserting a widget block.
//
// For instance, if a selection is in the middle of a paragraph, the collapsed range before this paragraph
// will be returned so that it is not split. If the selection is at the end of a paragraph,
// the collapsed range after this paragraph will be returned.
//
// Note: If the selection is placed in an empty block, the range in that block will be returned. If that range
// is then passed to {@link module:engine/model/model~Model#insertContent}, the block will be fully replaced
// by the inserted widget block.
//
// **Note:** Use {@link module:widget/utils#findOptimalInsertionRange} instead of this function outside engine.
// This function is only exposed to be used by {@link module:widget/utils#findOptimalInsertionRange findOptimalInsertionRange()}
// in the `widget` package and inside the `engine` package.
//
// @private
// @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
// The selection based on which the insertion position should be calculated.
// @param {module:engine/model/model~Model} model Model instance.
// @param {'auto'|'before'|'after'} [place='auto'] The place where to look for optimal insertion range.
// The default `auto` value will determine itself the best position for insertion.
// The `before` value will try to find a position before selection.
// The `after` value will try to find a position after selection.
// @returns {module:engine/model/range~Range} The optimal range.
function findOptimalInsertionRange$1(selection, model, place = 'auto') {
    const selectedElement = selection.getSelectedElement();
    if (selectedElement && model.schema.isObject(selectedElement) && !model.schema.isInline(selectedElement)) {
        if (place == 'before' || place == 'after') {
            return model.createRange(model.createPositionAt(selectedElement, place));
        }
        return model.createRangeOn(selectedElement);
    }
    const firstBlock = first(selection.getSelectedBlocks());
    // There are no block elements within ancestors (in the current limit element).
    if (!firstBlock) {
        return model.createRange(selection.focus);
    }
    // If inserting into an empty block – return position in that block. It will get
    // replaced with the image by insertContent(). #42.
    if (firstBlock.isEmpty) {
        return model.createRange(model.createPositionAt(firstBlock, 0));
    }
    const positionAfter = model.createPositionAfter(firstBlock);
    // If selection is at the end of the block - return position after the block.
    if (selection.focus.isTouching(positionAfter)) {
        return model.createRange(positionAfter);
    }
    // Otherwise, return position before the block.
    return model.createRange(model.createPositionBefore(firstBlock));
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Inserts an {@glink framework/guides/deep-dive/schema#object-elements object element} at a specific position in the editor content.
 *
 * **Note:** Use {@link module:engine/model/model~Model#insertObject} instead of this function.
 * This function is only exposed to be reusable in algorithms which change the {@link module:engine/model/model~Model#insertObject}
 * method's behavior.
 *
 * **Note**: For more documentation and examples, see {@link module:engine/model/model~Model#insertObject}.
 *
 * @param {module:engine/model/model~Model} model The model in context of which the insertion
 * should be performed.
 * @param {module:engine/model/element~Element} object An object to be inserted into the model document.
 * @param {module:engine/model/selection~Selectable} [selectable=model.document.selection]
 * A selectable where the content should be inserted. If not specified, the current
 * {@link module:engine/model/document~Document#selection document selection} will be used instead.
 * @param {Number|'before'|'end'|'after'|'on'|'in'} placeOrOffset Specifies the exact place or offset for the insertion to take place,
 * relative to `selectable`.
 * @param {Object} [options] Additional options.
 * @param {'auto'|'before'|'after'} [options.findOptimalPosition] An option that, when set, adjusts the insertion position (relative to
 * `selectable` and `placeOrOffset`) so that the content of `selectable` is not split upon insertion (a.k.a. non-destructive insertion).
 * * When `'auto'`, the algorithm will decide whether to insert the object before or after `selectable` to avoid content splitting.
 * * When `'before'`, the closest position before `selectable` will be used that will not result in content splitting.
 * * When `'after'`, the closest position after `selectable` will be used that will not result in content splitting.
 *
 * Note that this option works only for block objects. Inline objects are inserted into text and do not split blocks.
 * @param {'on'|'after'} [options.setSelection] An option that, when set, moves the
 * {@link module:engine/model/document~Document#selection document selection} after inserting the object.
 * * When `'on'`, the document selection will be set on the inserted object.
 * * When `'after'`, the document selection will move to the closest text node after the inserted object. If there is no
 * such text node, a paragraph will be created and the document selection will be moved inside it.
 * @returns {module:engine/model/range~Range} A range which contains all the performed changes. This is a range that, if removed,
 * would return the model to the state before the insertion. If no changes were preformed by `insertObject()`, returns a range collapsed
 * at the insertion position.
 */
function insertObject(model, object, selectable, placeOrOffset, options = {}) {
    if (!model.schema.isObject(object)) {
        /**
         * Tried to insert an element with {@link module:engine/model/utils/insertobject insertObject()} function
         * that is not defined as an object in schema.
         * See {@link module:engine/model/schema~SchemaItemDefinition#isObject `SchemaItemDefinition`}.
         * If you want to insert content that is not an object you might want to use
         * {@link module:engine/model/utils/insertcontent insertContent()} function.
         * @error insertobject-element-not-an-object
         */
        throw new CKEditorError('insertobject-element-not-an-object', model, { object });
    }
    // Normalize selectable to a selection instance.
    let originalSelection;
    if (!selectable) {
        originalSelection = model.document.selection;
    }
    else if (selectable instanceof Selection || selectable instanceof DocumentSelection) {
        originalSelection = selectable;
    }
    else {
        originalSelection = model.createSelection(selectable, placeOrOffset);
    }
    // Adjust the insertion selection.
    let insertionSelection = originalSelection;
    if (options.findOptimalPosition && model.schema.isBlock(object)) {
        insertionSelection = model.createSelection(findOptimalInsertionRange$1(originalSelection, model, options.findOptimalPosition));
    }
    // Collect attributes to be copied on the inserted object.
    const firstSelectedBlock = first(originalSelection.getSelectedBlocks());
    const attributesToCopy = {};
    if (firstSelectedBlock) {
        Object.assign(attributesToCopy, model.schema.getAttributesWithProperty(firstSelectedBlock, 'copyOnReplace', true));
    }
    return model.change(writer => {
        // Remove the selected content to find out what the parent of the inserted object would be.
        // It would be removed inside model.insertContent() anyway.
        if (!insertionSelection.isCollapsed) {
            model.deleteContent(insertionSelection, { doNotAutoparagraph: true });
        }
        let elementToInsert = object;
        const insertionPositionParent = insertionSelection.anchor.parent;
        // Autoparagraphing of an inline objects.
        if (!model.schema.checkChild(insertionPositionParent, object) &&
            model.schema.checkChild(insertionPositionParent, 'paragraph') &&
            model.schema.checkChild('paragraph', object)) {
            elementToInsert = writer.createElement('paragraph');
            writer.insert(object, elementToInsert);
        }
        // Apply attributes that are allowed on the inserted object (or paragraph if autoparagraphed).
        model.schema.setAllowedAttributes(elementToInsert, attributesToCopy, writer);
        // Insert the prepared content at the optionally adjusted selection.
        const affectedRange = model.insertContent(elementToInsert, insertionSelection);
        // Nothing got inserted.
        if (affectedRange.isCollapsed) {
            return affectedRange;
        }
        if (options.setSelection) {
            updateSelection(writer, object, options.setSelection, attributesToCopy);
        }
        return affectedRange;
    });
}
// Updates document selection based on given `place` parameter in relation to `contextElement` element.
//
// @private
// @param {module:engine/model/writer~Writer} writer An instance of the model writer.
// @param {module:engine/model/element~Element} contextElement An element to set the attributes on.
// @param {'on'|'after'} place The place where selection should be set in relation to the `contextElement` element.
// Value `on` will set selection on the passed `contextElement`. Value `after` will set selection after `contextElement`.
// @param {Object} attributes Attributes keys and values to set on a paragraph that this function can create when
// `place` parameter is equal to `after` but there is no element with `$text` node to set selection in.
function updateSelection(writer, contextElement, place, paragraphAttributes) {
    const model = writer.model;
    if (place == 'after') {
        let nextElement = contextElement.nextSibling;
        // Check whether an element next to the inserted element is defined and can contain a text.
        const canSetSelection = nextElement && model.schema.checkChild(nextElement, '$text');
        // If the element is missing, but a paragraph could be inserted next to the element, let's add it.
        if (!canSetSelection && model.schema.checkChild(contextElement.parent, 'paragraph')) {
            nextElement = writer.createElement('paragraph');
            model.schema.setAllowedAttributes(nextElement, paragraphAttributes, writer);
            model.insertContent(nextElement, writer.createPositionAfter(contextElement));
        }
        // Put the selection inside the element, at the beginning.
        if (nextElement) {
            writer.setSelection(nextElement, 0);
        }
    }
    else if (place == 'on') {
        writer.setSelection(contextElement, 'on');
    }
    else {
        /**
         * The unsupported `options.setSelection` parameter was passed
         * to the {@link module:engine/model/utils/insertobject insertObject()} function.
         * Check the {@link module:engine/model/utils/insertobject insertObject()} API documentation for allowed
         * `options.setSelection` parameter values.
         *
         * @error insertobject-invalid-place-parameter-value
         */
        throw new CKEditorError('insertobject-invalid-place-parameter-value', model);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const wordBoundaryCharacters = ' ,.?!:;"-()';
/**
 * 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".
 *  * `'word'` - moves selection by a whole word.
 *
 * **Note:** if you extend a forward selection in a backward direction you will in fact shrink it.
 *
 * **Note:** Use {@link module:engine/model/model~Model#modifySelection} instead of this function.
 * This function is only exposed to be reusable in algorithms
 * which change the {@link module:engine/model/model~Model#modifySelection}
 * method's behavior.
 *
 * @param {module:engine/model/model~Model} model The model in context of which
 * the selection modification should be performed.
 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} 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'|'word'} [options.unit='character'] The unit by which selection should be modified.
 * @param {Boolean} [options.treatEmojiAsSingleUnit=false] Whether multi-characer emoji sequences should be handled as single unit.
 */
function modifySelection(model, selection, options = {}) {
    const schema = model.schema;
    const isForward = options.direction != 'backward';
    const unit = options.unit ? options.unit : 'character';
    const treatEmojiAsSingleUnit = !!options.treatEmojiAsSingleUnit;
    const focus = selection.focus;
    const walker = new TreeWalker({
        boundaries: getSearchRange(focus, isForward),
        singleCharacters: true,
        direction: isForward ? 'forward' : 'backward'
    });
    const data = { walker, schema, isForward, unit, treatEmojiAsSingleUnit };
    let next;
    while ((next = walker.next())) {
        if (next.done) {
            return;
        }
        const position = tryExtendingTo(data, next.value);
        if (position) {
            if (selection instanceof DocumentSelection) {
                model.change(writer => {
                    writer.setSelectionFocus(position);
                });
            }
            else {
                selection.setFocus(position);
            }
            return;
        }
    }
}
// Checks whether the selection can be extended to the the walker's next value (next position).
// @param {{ walker, unit, isForward, schema, treatEmojiAsSingleUnit }} data
// @param {module:engine/view/treewalker~TreeWalkerValue} value
function tryExtendingTo(data, value) {
    const { isForward, walker, unit, schema, treatEmojiAsSingleUnit } = data;
    const { type, item, nextPosition } = value;
    // If found text, we can certainly put the focus in it. Let's just find a correct position
    // based on the unit.
    if (type == 'text') {
        if (data.unit === 'word') {
            return getCorrectWordBreakPosition(walker, isForward);
        }
        return getCorrectPosition(walker, unit, treatEmojiAsSingleUnit);
    }
    // Entering an element.
    if (type == (isForward ? 'elementStart' : 'elementEnd')) {
        // If it's a selectable, we can select it now.
        if (schema.isSelectable(item)) {
            return Position._createAt(item, isForward ? 'after' : 'before');
        }
        // If text allowed on this position, extend to this place.
        if (schema.checkChild(nextPosition, '$text')) {
            return nextPosition;
        }
    }
    // Leaving an element.
    else {
        // If leaving a limit element, stop.
        if (schema.isLimit(item)) {
            // NOTE: Fast-forward the walker until the end.
            walker.skip(() => true);
            return;
        }
        // If text allowed on this position, extend to this place.
        if (schema.checkChild(nextPosition, '$text')) {
            return 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.
//
// @param {module:engine/model/treewalker~TreeWalker} walker
// @param {String} unit The unit by which selection should be modified.
// @param {Boolean} treatEmojiAsSingleUnit
function getCorrectPosition(walker, unit, treatEmojiAsSingleUnit) {
    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)) ||
            (treatEmojiAsSingleUnit && isInsideEmojiSequence(data, offset))) {
            walker.next();
            offset = walker.position.offset - textNode.startOffset;
        }
    }
    return walker.position;
}
// Finds a correct position of a word break by walking in a text node and checking whether selection can be extended to given position
// or should be extended further.
//
// @param {module:engine/model/treewalker~TreeWalker} walker
// @param {Boolean} isForward Is the direction in which the selection should be modified is forward.
function getCorrectWordBreakPosition(walker, isForward) {
    let textNode = walker.position.textNode;
    if (textNode) {
        let offset = walker.position.offset - textNode.startOffset;
        while (!isAtWordBoundary(textNode.data, offset, isForward) && !isAtNodeBoundary(textNode, offset, isForward)) {
            walker.next();
            // Check of adjacent text nodes with different attributes (like BOLD).
            // Example          : 'foofoo []bar<$text bold="true">bar</$text> bazbaz'
            // should expand to : 'foofoo [bar<$text bold="true">bar</$text>] bazbaz'.
            const nextNode = isForward ? walker.position.nodeAfter : walker.position.nodeBefore;
            // Scan only text nodes. Ignore inline elements (like `<softBreak>`).
            if (nextNode && nextNode.is('$text')) {
                // Check boundary char of an adjacent text node.
                const boundaryChar = nextNode.data.charAt(isForward ? 0 : nextNode.data.length - 1);
                // Go to the next node if the character at the boundary of that node belongs to the same word.
                if (!wordBoundaryCharacters.includes(boundaryChar)) {
                    // If adjacent text node belongs to the same word go to it & reset values.
                    walker.next();
                    textNode = walker.position.textNode;
                }
            }
            offset = walker.position.offset - textNode.startOffset;
        }
    }
    return walker.position;
}
function getSearchRange(start, isForward) {
    const root = start.root;
    const searchEnd = Position._createAt(root, isForward ? 'end' : 0);
    if (isForward) {
        return new Range(start, searchEnd);
    }
    else {
        return new Range(searchEnd, start);
    }
}
// Checks if selection is on word boundary.
//
// @param {String} data The text node value to investigate.
// @param {Number} offset Position offset.
// @param {Boolean} isForward Is the direction in which the selection should be modified is forward.
function isAtWordBoundary(data, offset, isForward) {
    // The offset to check depends on direction.
    const offsetToCheck = offset + (isForward ? 0 : -1);
    return wordBoundaryCharacters.includes(data.charAt(offsetToCheck));
}
// Checks if selection is on node boundary.
//
// @param {module:engine/model/text~Text} textNode The text node to investigate.
// @param {Number} offset Position offset.
// @param {Boolean} isForward Is the direction in which the selection should be modified is forward.
function isAtNodeBoundary(textNode, offset, isForward) {
    return offset === (isForward ? textNode.endOffset : 0);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// @if CK_DEBUG_ENGINE // const { dumpTrees } = require( '../dev-utils/utils' );
// @if CK_DEBUG_ENGINE // const { OperationReplayer } = require( '../dev-utils/operationreplayer' ).default;
/**
 * Editor's data model. Read about the model in the
 * {@glink framework/guides/architecture/editing-engine engine architecture guide}.
 *
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class Model$1 extends Observable {
    constructor() {
        super();
        /**
         * Model's marker collection.
         *
         * @readonly
         * @member {module:engine/model/markercollection~MarkerCollection}
         */
        this.markers = new MarkerCollection();
        /**
         * Model's document.
         *
         * @readonly
         * @member {module:engine/model/document~Document}
         */
        this.document = new Document(this);
        /**
         * Model's schema.
         *
         * @readonly
         * @member {module:engine/model/schema~Schema}
         */
        this.schema = new Schema();
        /**
         * All callbacks added by {@link module:engine/model/model~Model#change} or
         * {@link module:engine/model/model~Model#enqueueChange} methods waiting to be executed.
         *
         * @private
         * @type {Array.<Function>}
         */
        this._pendingChanges = [];
        /**
         * The last created and currently used writer instance.
         *
         * @private
         * @member {module:engine/model/writer~Writer}
         */
        this._currentWriter = null;
        ['insertContent', 'insertObject', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation']
            .forEach(methodName => this.decorate(methodName));
        // Adding operation validation with `highest` priority, so it is called before any other feature would like
        // to do anything with the operation. If the operation has incorrect parameters it should throw on the earliest occasion.
        this.on('applyOperation', (evt, args) => {
            const operation = args[0];
            operation._validate();
        }, { priority: 'highest' });
        // Register some default abstract entities.
        this.schema.register('$root', {
            isLimit: true
        });
        this.schema.register('$container', {
            allowIn: ['$root', '$container']
        });
        this.schema.register('$block', {
            allowIn: ['$root', '$container'],
            isBlock: true
        });
        this.schema.register('$blockObject', {
            allowWhere: '$block',
            isBlock: true,
            isObject: true
        });
        this.schema.register('$inlineObject', {
            allowWhere: '$text',
            allowAttributesOf: '$text',
            isInline: true,
            isObject: true
        });
        this.schema.register('$text', {
            allowIn: '$block',
            isInline: true,
            isContent: true
        });
        this.schema.register('$clipboardHolder', {
            allowContentOf: '$root',
            allowChildren: '$text',
            isLimit: true
        });
        this.schema.register('$documentFragment', {
            allowContentOf: '$root',
            allowChildren: '$text',
            isLimit: true
        });
        // An element needed by the `upcastElementToMarker` converter.
        // This element temporarily represents a marker boundary during the conversion process and is removed
        // at the end of the conversion. `UpcastDispatcher` or at least `Conversion` class looks like a
        // better place for this registration but both know nothing about `Schema`.
        this.schema.register('$marker');
        this.schema.addChildCheck((context, childDefinition) => {
            if (childDefinition.name === '$marker') {
                return true;
            }
        });
        injectSelectionPostFixer(this);
        // Post-fixer which takes care of adding empty paragraph elements to the empty roots.
        this.document.registerPostFixer(autoParagraphEmptyRoots);
        // @if CK_DEBUG_ENGINE // this.on( 'applyOperation', () => {
        // @if CK_DEBUG_ENGINE // 	dumpTrees( this.document, this.document.version );
        // @if CK_DEBUG_ENGINE // }, { priority: 'lowest' } );
    }
    /**
     * The `change()` method is the primary way of changing the model. You should use it to modify all document nodes
     * (including detached nodes – i.e. nodes not added to the {@link module:engine/model/model~Model#document model document}),
     * the {@link module:engine/model/document~Document#selection document's selection}, and
     * {@link module:engine/model/model~Model#markers model markers}.
     *
     *		model.change( writer => {
     *			writer.insertText( 'foo', paragraph, 'end' );
     *		} );
     *
     * All changes inside the change block use the same {@link module:engine/model/batch~Batch} so they are combined
     * into a single undo step.
     *
     *		model.change( writer => {
     *			writer.insertText( 'foo', paragraph, 'end' ); // foo.
     *
     *			model.change( writer => {
     *				writer.insertText( 'bar', paragraph, 'end' ); // foobar.
     *			} );
     *
     * 			writer.insertText( 'bom', paragraph, 'end' ); // foobarbom.
     *		} );
     *
     * The callback of the `change()` block is executed synchronously.
     *
     * You can also return a value from the change block.
     *
     *		const img = model.change( writer => {
     *			return writer.createElement( 'img' );
     *		} );
     *
     * @see #enqueueChange
     * @param {Function} callback Callback function which may modify the model.
     * @returns {*} Value returned by the callback.
     */
    change(callback) {
        try {
            if (this._pendingChanges.length === 0) {
                // If this is the outermost block, create a new batch and start `_runPendingChanges` execution flow.
                this._pendingChanges.push({ batch: new Batch(), callback });
                return this._runPendingChanges()[0];
            }
            else {
                // If this is not the outermost block, just execute the callback.
                return callback(this._currentWriter);
            }
        }
        catch (err) {
            // @if CK_DEBUG // throw err;
            /* istanbul ignore next */
            CKEditorError.rethrowUnexpectedError(err, this);
        }
    }
    /**
     * The `enqueueChange()` method performs similar task as the {@link #change `change()` method}, with two major differences.
     *
     * First, the callback of `enqueueChange()` is executed when all other enqueued changes are done. It might be executed
     * immediately if it is not nested in any other change block, but if it is nested in another (enqueue)change block,
     * it will be delayed and executed after the outermost block.
     *
     *		model.change( writer => {
     *			console.log( 1 );
     *
     *			model.enqueueChange( writer => {
     *				console.log( 2 );
     *			} );
     *
     * 			console.log( 3 );
     *		} ); // Will log: 1, 3, 2.
     *
     * In addition to that, the changes enqueued with `enqueueChange()` will be converted separately from the changes
     * done in the outer `change()` block.
     *
     * Second, it lets you define the {@link module:engine/model/batch~Batch} into which you want to add your changes.
     * By default, a new batch with the default {@link module:engine/model/batch~Batch#constructor batch type} is created.
     * In the sample above, the `change` and `enqueueChange` blocks will use a different batch (and a different
     * {@link module:engine/model/writer~Writer} instance since each of them operates on a separate batch).
     *
     *		model.enqueueChange( { isUndoable: false }, writer => {
     *			writer.insertText( 'foo', paragraph, 'end' );
     *		} );
     *
     * When using the `enqueueChange()` block you can also add some changes to the batch you used before.
     *
     *		model.enqueueChange( batch, writer => {
     *			writer.insertText( 'foo', paragraph, 'end' );
     *		} );
     *
     * In order to make a nested `enqueueChange()` create a single undo step together with the changes done in the outer `change()`
     * block, you can obtain the batch instance from the  {@link module:engine/model/writer~Writer#batch writer} of the outer block.
     *
     * @param {module:engine/model/batch~Batch|Object} [batchOrType] A batch or a
     * {@link module:engine/model/batch~Batch#constructor batch type} that should be used in the callback. If not defined, a new batch with
     * the default type will be created.
     * @param {Function} callback Callback function which may modify the model.
     */
    enqueueChange(batchOrType, callback) {
        try {
            if (!batchOrType) {
                batchOrType = new Batch();
            }
            else if (typeof batchOrType === 'function') {
                callback = batchOrType;
                batchOrType = new Batch();
            }
            else if (!(batchOrType instanceof Batch)) {
                batchOrType = new Batch(batchOrType);
            }
            this._pendingChanges.push({ batch: batchOrType, callback });
            if (this._pendingChanges.length == 1) {
                this._runPendingChanges();
            }
        }
        catch (err) {
            // @if CK_DEBUG // throw err;
            /* istanbul ignore next */
            CKEditorError.rethrowUnexpectedError(err, this);
        }
    }
    /**
     * {@link module:utils/observablemixin~ObservableMixin#decorate Decorated} function for applying
     * {@link module:engine/model/operation/operation~Operation operations} to the model.
     *
     * This is a low-level way of changing the model. It is exposed for very specific use cases (like the undo feature).
     * Normally, to modify the model, you will want to use {@link module:engine/model/writer~Writer `Writer`}.
     * See also {@glink framework/guides/architecture/editing-engine#changing-the-model Changing the model} section
     * of the {@glink framework/guides/architecture/editing-engine Editing architecture} guide.
     *
     * @param {module:engine/model/operation/operation~Operation} operation The operation to apply.
     */
    applyOperation(operation) {
        // @if CK_DEBUG_ENGINE // console.log( 'Applying ' + operation );
        // @if CK_DEBUG_ENGINE // if ( !this._operationLogs ) {
        // @if CK_DEBUG_ENGINE //	this._operationLogs = [];
        // @if CK_DEBUG_ENGINE // }
        // @if CK_DEBUG_ENGINE // this._operationLogs.push( JSON.stringify( operation ) );
        // @if CK_DEBUG_ENGINE //if ( !this._appliedOperations ) {
        // @if CK_DEBUG_ENGINE //	this._appliedOperations = [];
        // @if CK_DEBUG_ENGINE //}
        // @if CK_DEBUG_ENGINE //this._appliedOperations.push( operation );
        operation._execute();
    }
    // @if CK_DEBUG_ENGINE // getAppliedOperation() {
    // @if CK_DEBUG_ENGINE //	if ( !this._appliedOperations ) {
    // @if CK_DEBUG_ENGINE //		return '';
    // @if CK_DEBUG_ENGINE //	}
    // @if CK_DEBUG_ENGINE //	return this._appliedOperations.map( JSON.stringify ).join( '-------' );
    // @if CK_DEBUG_ENGINE // }
    // @if CK_DEBUG_ENGINE // createReplayer( stringifiedOperations ) {
    // @if CK_DEBUG_ENGINE //	return new OperationReplayer( this, '-------', stringifiedOperations );
    // @if CK_DEBUG_ENGINE // }
    /**
     * Inserts content at the position in the editor specified by the selection, as one would expect the paste
     * functionality to work.
     *
     * **Note**: If you want to insert an {@glink framework/guides/deep-dive/schema#object-elements object element}
     * (e.g. a {@link module:widget/utils~toWidget widget}), see {@link #insertObject} instead.
     *
     * This is a high-level method. It takes the {@link #schema schema} into consideration when inserting
     * the content, clears the given selection's content before inserting nodes and moves the selection
     * to its target position at the end of the process.
     * It can split elements, merge them, wrap bare text nodes with paragraphs, etc. &mdash; just like the
     * pasting feature should do.
     *
     * For lower-level methods see {@link module:engine/model/writer~Writer `Writer`}.
     *
     * This method, unlike {@link module:engine/model/writer~Writer `Writer`}'s methods, does not have to be used
     * inside a {@link #change `change()` block}.
     *
     * # Conversion and schema
     *
     * Inserting elements and text nodes into the model is not enough to make CKEditor 5 render that content
     * to the user. CKEditor 5 implements a model-view-controller architecture and what `model.insertContent()` does
     * is only adding nodes to the model. Additionally, you need to define
     * {@glink framework/guides/architecture/editing-engine#conversion converters} between the model and view
     * and define those nodes in the {@glink framework/guides/architecture/editing-engine#schema schema}.
     *
     * So, while this method may seem similar to CKEditor 4 `editor.insertHtml()` (in fact, both methods
     * are used for paste-like content insertion), the CKEditor 5 method cannot be use to insert arbitrary HTML
     * unless converters are defined for all elements and attributes in that HTML.
     *
     * # Examples
     *
     * Using `insertContent()` with a manually created model structure:
     *
     *		// Let's create a document fragment containing such content as:
     *		//
     *		// <paragraph>foo</paragraph>
     *		// <blockQuote>
     *		//    <paragraph>bar</paragraph>
     *		// </blockQuote>
     *		const docFrag = editor.model.change( writer => {
     *			const p1 = writer.createElement( 'paragraph' );
     *			const p2 = writer.createElement( 'paragraph' );
     *			const blockQuote = writer.createElement( 'blockQuote' );
     *			const docFrag = writer.createDocumentFragment();
     *
     *			writer.append( p1, docFrag );
     *			writer.append( blockQuote, docFrag );
     *			writer.append( p2, blockQuote );
     *			writer.insertText( 'foo', p1 );
     *			writer.insertText( 'bar', p2 );
     *
     *			return docFrag;
     *		} );
     *
     *		// insertContent() does not have to be used in a change() block. It can, though,
     *		// so this code could be moved to the callback defined above.
     *		editor.model.insertContent( docFrag );
     *
     * Using `insertContent()` with an HTML string converted to a model document fragment (similar to the pasting mechanism):
     *
     *		// You can create your own HtmlDataProcessor instance or use editor.data.processor
     *		// if you have not overridden the default one (which is the HtmlDataProcessor instance).
     *		const htmlDP = new HtmlDataProcessor( viewDocument );
     *
     *		// Convert an HTML string to a view document fragment:
     *		const viewFragment = htmlDP.toView( htmlString );
     *
     *		// Convert the view document fragment to a model document fragment
     *		// in the context of $root. This conversion takes the schema into
     *		// account so if, for example, the view document fragment contained a bare text node,
     *		// this text node cannot be a child of $root, so it will be automatically
     *		// wrapped with a <paragraph>. You can define the context yourself (in the second parameter),
     *		// and e.g. convert the content like it would happen in a <paragraph>.
     *		// Note: The clipboard feature uses a custom context called $clipboardHolder
     *		// which has a loosened schema.
     *		const modelFragment = editor.data.toModel( viewFragment );
     *
     *		editor.model.insertContent( modelFragment );
     *
     * By default this method will use the document selection but it can also be used with a position, range or selection instance.
     *
     *		// Insert text at the current document selection position.
     *		editor.model.change( writer => {
     *			editor.model.insertContent( writer.createText( 'x' ) );
     *		} );
     *
     *		// Insert text at a given position - the document selection will not be modified.
     *		editor.model.change( writer => {
     *			editor.model.insertContent( writer.createText( 'x' ), doc.getRoot(), 2 );
     *
     *			// Which is a shorthand for:
     *			editor.model.insertContent( writer.createText( 'x' ), writer.createPositionAt( doc.getRoot(), 2 ) );
     *		} );
     *
     * If you want the document selection to be moved to the inserted content, use the
     * {@link module:engine/model/writer~Writer#setSelection `setSelection()`} method of the writer after inserting
     * the content:
     *
     *		editor.model.change( writer => {
     *			const paragraph = writer.createElement( 'paragraph' );
     *
     *			// Insert an empty paragraph at the beginning of the root.
     *			editor.model.insertContent( paragraph, writer.createPositionAt( editor.model.document.getRoot(), 0 ) );
     *
     *			// Move the document selection to the inserted paragraph.
     *			writer.setSelection( paragraph, 'in' );
     *		} );
     *
     * If an instance of the {@link module:engine/model/selection~Selection model selection} is passed as `selectable`,
     * the new content will be inserted at the passed selection (instead of document selection):
     *
     *		editor.model.change( writer => {
     *			// Create a selection in a paragraph that will be used as a place of insertion.
     *			const selection = writer.createSelection( paragraph, 'in' );
     *
     *			// Insert the new text at the created selection.
     *			editor.model.insertContent( writer.createText( 'x' ), selection );
     *
     *			// insertContent() modifies the passed selection instance so it can be used to set the document selection.
     *			// Note: This is not necessary when you passed the document selection to insertContent().
     *			writer.setSelection( selection );
     *		} );
     *
     * @fires insertContent
     * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
     * @param {module:engine/model/selection~Selectable} [selectable=model.document.selection]
     * The selection into which the content should be inserted. If not provided the current model document selection will be used.
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] To be used when a model item was passed as `selectable`.
     * This param defines a position in relation to that item.
     * @returns {module:engine/model/range~Range} Range which contains all the performed changes. This is a range that, if removed,
     * would return the model to the state before the insertion. If no changes were preformed by `insertContent`, returns a range collapsed
     * at the insertion position.
     */
    insertContent(content, selectable, placeOrOffset) {
        return insertContent(this, content, selectable, placeOrOffset);
    }
    /**
     * Inserts an {@glink framework/guides/deep-dive/schema#object-elements object element} at a specific position in the editor content.
     *
     * This is a high-level API:
     * * It takes the {@link #schema schema} into consideration,
     * * It clears the content of passed `selectable` before inserting,
     * * It can move the selection at the end of the process,
     * * It will copy the selected block's attributes to preserve them upon insertion,
     * * It can split elements or wrap inline objects with paragraphs if they are not allowed in target position,
     * * etc.
     *
     * # Notes
     *
     * * If you want to insert a non-object content, see {@link #insertContent} instead.
     * * For lower-level API, see {@link module:engine/model/writer~Writer `Writer`}.
     * * Unlike {@link module:engine/model/writer~Writer `Writer`}, this method does not have to be used inside
     * a {@link #change `change()` block}.
     * * Inserting object into the model is not enough to make CKEditor 5 render that content to the user.
     * CKEditor 5 implements a model-view-controller architecture and what `model.insertObject()` does
     * is only adding nodes to the model. Additionally, you need to define
     * {@glink framework/guides/architecture/editing-engine#conversion converters} between the model and view
     * and define those nodes in the {@glink framework/guides/architecture/editing-engine#schema schema}.
     *
     * # Examples
     *
     * Use the following code to insert an object at the current selection and keep the selection on the inserted element:
     *
     *		const rawHtmlEmbedElement = writer.createElement( 'rawHtml' );
     *
     *		model.insertObject( rawHtmlEmbedElement, null, null, {
     *			setSelection: 'on'
     *		} );
     *
     * Use the following code to insert an object at the current selection and nudge the selection after the inserted object:
     *
     *		const pageBreakElement = writer.createElement( 'pageBreak' );
     *
     *		model.insertObject( pageBreakElement, null, null, {
     *			setSelection: 'after'
     *		} );
     *
     * Use the following code to insert an object at the current selection and avoid splitting the content (non-destructive insertion):
     *
     *		const tableElement = writer.createElement( 'table' );
     *
     *		model.insertObject( tableElement, null, null, {
     *			findOptimalPosition: 'auto'
     *		} );
     *
     * Use the following code to insert an object at the specific range (also: replace the content of the range):
     *
     *		const tableElement = writer.createElement( 'table' );
     *		const range = model.createRangeOn( model.document.getRoot().getChild( 1 ) );
     *
     *		model.insertObject( tableElement, range );
     *
     * @param {module:engine/model/element~Element} object An object to be inserted into the model document.
     * @param {module:engine/model/selection~Selectable} [selectable=model.document.selection]
     * A selectable where the content should be inserted. If not specified, the current
     * {@link module:engine/model/document~Document#selection document selection} will be used instead.
     * @param {Number|'before'|'end'|'after'|'on'|'in'} placeOrOffset Specifies the exact place or offset for the insertion to take place,
     * relative to `selectable`.
     * @param {Object} [options] Additional options.
     * @param {'auto'|'before'|'after'} [options.findOptimalPosition] An option that, when set, adjusts the insertion position (relative to
     * `selectable` and `placeOrOffset`) so that the content of `selectable` is not split upon insertion (a.k.a. non-destructive insertion).
     * * When `'auto'`, the algorithm will decide whether to insert the object before or after `selectable` to avoid content splitting.
     * * When `'before'`, the closest position before `selectable` will be used that will not result in content splitting.
     * * When `'after'`, the closest position after `selectable` will be used that will not result in content splitting.
     *
     * Note that this option only works for block objects. Inline objects are inserted into text and do not split blocks.
     * @param {'on'|'after'} [options.setSelection] An option that, when set, moves the
     * {@link module:engine/model/document~Document#selection document selection} after inserting the object.
     * * When `'on'`, the document selection will be set on the inserted object.
     * * When `'after'`, the document selection will move to the closest text node after the inserted object. If there is no
     * such text node, a paragraph will be created and the document selection will be moved inside it.
     * @returns {module:engine/model/range~Range} A range which contains all the performed changes. This is a range that, if removed,
     * would return the model to the state before the insertion. If no changes were preformed by `insertObject()`, returns a range collapsed
     * at the insertion position.
     */
    insertObject(object, selectable, placeOrOffset, options) {
        return insertObject(this, object, selectable, placeOrOffset, options);
    }
    /**
     * Deletes content of the selection and merge siblings. The resulting selection is always collapsed.
     *
     * **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|module:engine/model/documentselection~DocumentSelection} selection
     * Selection of which the content should be deleted.
     * @param {Object} [options]
     * @param {Boolean} [options.leaveUnmerged=false] Whether to merge elements after removing the content of the selection.
     *
     * For example `<heading1>x[x</heading1><paragraph>y]y</paragraph>` will become:
     *
     * * `<heading1>x^y</heading1>` with the option disabled (`leaveUnmerged == false`)
     * * `<heading1>x^</heading1><paragraph>y</paragraph>` with enabled (`leaveUnmerged == true`).
     *
     * Note: {@link module:engine/model/schema~Schema#isObject object} and {@link module:engine/model/schema~Schema#isLimit 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 `<heading1>[x</heading1><paragraph>y]</paragraph>` will become:
     *
     * * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
     * * `<heading1>^</heading1>` with enabled (`doNotResetEntireContent == true`)
     *
     * @param {Boolean} [options.doNotAutoparagraph=false] Whether to create a paragraph if after content deletion selection is moved
     * to a place where text cannot be inserted.
     *
     * For example `<paragraph>x</paragraph>[<imageBlock src="foo.jpg"></imageBlock>]` will become:
     *
     * * `<paragraph>x</paragraph><paragraph>[]</paragraph>` with the option disabled (`doNotAutoparagraph == false`)
     * * `<paragraph>x[]</paragraph>` with the option enabled (`doNotAutoparagraph == true`).
     *
     * **Note:** if there is no valid position for the selection, the paragraph will always be created:
     *
     * `[<imageBlock src="foo.jpg"></imageBlock>]` -> `<paragraph>[]</paragraph>`.
     *
     * @param {'forward'|'backward'} [options.direction='backward'] The direction in which the content is being consumed.
     * Deleting backward corresponds to using the <kbd>Backspace</kbd> key, while deleting content forward corresponds to
     * the <kbd>Shift</kbd>+<kbd>Backspace</kbd> keystroke.
     */
    deleteContent(selection, options) {
        deleteContent(this, selection, options);
    }
    /**
     * 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".
     *  * `'word'` - moves selection by a whole word.
     *
     * **Note:** if you extend a forward selection in a backward direction you will in fact shrink it.
     *
     * @fires modifySelection
     * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} 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'|'word'} [options.unit='character'] The unit by which selection should be modified.
     * @param {Boolean} [options.treatEmojiAsSingleUnit=false] Whether multi-characer emoji sequences should be handled as single unit.
     */
    modifySelection(selection, options) {
        modifySelection(this, selection, options);
    }
    /**
     * Gets a clone of the selected content.
     *
     * For example, for the following selection:
     *
     * ```html
     * <paragraph>x</paragraph>
     * <blockQuote>
     *	<paragraph>y</paragraph>
     *	<heading1>fir[st</heading1>
     * </blockQuote>
     * <paragraph>se]cond</paragraph>
     * <paragraph>z</paragraph>
     * ```
     *
     * It will return a document fragment with such a content:
     *
     * ```html
     * <blockQuote>
     *	<heading1>st</heading1>
     * </blockQuote>
     * <paragraph>se</paragraph>
     * ```
     *
     * @fires getSelectedContent
     * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
     * The selection of which content will be returned.
     * @returns {module:engine/model/documentfragment~DocumentFragment}
     */
    getSelectedContent(selection) {
        return getSelectedContent(this, selection);
    }
    /**
     * Checks whether the given {@link module:engine/model/range~Range range} or
     * {@link module:engine/model/element~Element element} has any meaningful content.
     *
     * Meaningful content is:
     *
     * * any text node (`options.ignoreWhitespaces` allows controlling whether this text node must also contain
     * any non-whitespace characters),
     * * or any {@link module:engine/model/schema~Schema#isContent content element},
     * * or any {@link module:engine/model/markercollection~Marker marker} which
     * {@link module:engine/model/markercollection~Marker#_affectsData affects data}.
     *
     * This means that a range containing an empty `<paragraph></paragraph>` is not considered to have a meaningful content.
     * However, a range containing an `<imageBlock></imageBlock>` (which would normally be marked in the schema as an object element)
     * is considered non-empty.
     *
     * @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
     * @param {Object} [options]
     * @param {Boolean} [options.ignoreWhitespaces] Whether text node with whitespaces only should be considered empty.
     * @param {Boolean} [options.ignoreMarkers] Whether markers should be ignored.
     * @returns {Boolean}
     */
    hasContent(rangeOrElement, options = {}) {
        const range = rangeOrElement instanceof Range ? rangeOrElement : Range._createIn(rangeOrElement);
        if (range.isCollapsed) {
            return false;
        }
        const { ignoreWhitespaces = false, ignoreMarkers = false } = options;
        // Check if there are any markers which affects data in this given range.
        if (!ignoreMarkers) {
            for (const intersectingMarker of this.markers.getMarkersIntersectingRange(range)) {
                if (intersectingMarker.affectsData) {
                    return true;
                }
            }
        }
        for (const item of range.getItems()) {
            if (this.schema.isContent(item)) {
                if (item.is('$textProxy')) {
                    if (!ignoreWhitespaces) {
                        return true;
                    }
                    else if (item.data.search(/\S/) !== -1) {
                        return true;
                    }
                }
                else {
                    return true;
                }
            }
        }
        return false;
    }
    /**
     * Creates a position from the given root and path in that root.
     *
     * Note: This method is also available as
     * {@link module:engine/model/writer~Writer#createPositionFromPath `Writer#createPositionFromPath()`}.
     *
     * @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}.
     * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
     * See {@link module:engine/model/position~PositionStickiness}.
     * @returns {module:engine/model/position~Position}
     */
    createPositionFromPath(root, path, stickiness) {
        return new Position(root, path, stickiness);
    }
    /**
     * Creates position at the given location. The location can be specified as:
     *
     * * a {@link module:engine/model/position~Position position},
     * * a parent element and offset in that element,
     * * a parent element and `'end'` (the position will be set at the end of that element),
     * * a {@link module:engine/model/item~Item model item} and `'before'` or `'after'`
     * (the position will be set before or after the given model item).
     *
     * This method is a shortcut to other factory methods such as:
     *
     * * {@link module:engine/model/model~Model#createPositionBefore `createPositionBefore()`},
     * * {@link module:engine/model/model~Model#createPositionAfter `createPositionAfter()`}.
     *
     * Note: This method is also available as
     * {@link module:engine/model/writer~Writer#createPositionAt `Writer#createPositionAt()`},
     *
     * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * first parameter is a {@link module:engine/model/item~Item model item}.
     */
    createPositionAt(itemOrPosition, offset) {
        return Position._createAt(itemOrPosition, offset);
    }
    /**
     * Creates a new position after the given {@link module:engine/model/item~Item model item}.
     *
     * Note: This method is also available as
     * {@link module:engine/model/writer~Writer#createPositionAfter `Writer#createPositionAfter()`}.
     *
     * @param {module:engine/model/item~Item} item Item after which the position should be placed.
     * @returns {module:engine/model/position~Position}
     */
    createPositionAfter(item) {
        return Position._createAfter(item);
    }
    /**
     * Creates a new position before the given {@link module:engine/model/item~Item model item}.
     *
     * Note: This method is also available as
     * {@link module:engine/model/writer~Writer#createPositionBefore `Writer#createPositionBefore()`}.
     *
     * @param {module:engine/model/item~Item} item Item before which the position should be placed.
     * @returns {module:engine/model/position~Position}
     */
    createPositionBefore(item) {
        return Position._createBefore(item);
    }
    /**
     * Creates a range spanning from the `start` position to the `end` position.
     *
     * Note: This method is also available as
     * {@link module:engine/model/writer~Writer#createRange `Writer#createRange()`}:
     *
     *		model.change( writer => {
     *			const range = writer.createRange( start, end );
     *		} );
     *
     * @param {module:engine/model/position~Position} start Start position.
     * @param {module:engine/model/position~Position} [end] End position. If not set, the range will be collapsed
     * to the `start` position.
     * @returns {module:engine/model/range~Range}
     */
    createRange(start, end) {
        return new Range(start, end);
    }
    /**
     * Creates a range inside the given element which starts before the first child of
     * that element and ends after the last child of that element.
     *
     * Note: This method is also available as
     * {@link module:engine/model/writer~Writer#createRangeIn `Writer#createRangeIn()`}:
     *
     *		model.change( writer => {
     *			const range = writer.createRangeIn( paragraph );
     *		} );
     *
     * @param {module:engine/model/element~Element} element Element which is a parent for the range.
     * @returns {module:engine/model/range~Range}
     */
    createRangeIn(element) {
        return Range._createIn(element);
    }
    /**
     * Creates a range that starts before the given {@link module:engine/model/item~Item model item} and ends after it.
     *
     * Note: This method is also available on `writer` instance as
     * {@link module:engine/model/writer~Writer#createRangeOn `Writer.createRangeOn()`}:
     *
     *		model.change( writer => {
     *			const range = writer.createRangeOn( paragraph );
     *		} );
     *
     * @param {module:engine/model/item~Item} item
     * @returns {module:engine/model/range~Range}
     */
    createRangeOn(item) {
        return Range._createOn(item);
    }
    /**
     * Creates a new selection instance based on the given {@link module:engine/model/selection~Selectable selectable}
     * or creates an empty selection if no arguments were passed.
     *
     * Note: This method is also available as
     * {@link module:engine/model/writer~Writer#createSelection `Writer#createSelection()`}.
     *
     *		// Creates empty selection without ranges.
     *		const selection = writer.createSelection();
     *
     *		// Creates selection at the given range.
     *		const range = writer.createRange( start, end );
     *		const selection = writer.createSelection( range );
     *
     *		// Creates selection at the given ranges
     *		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
     *		const selection = writer.createSelection( ranges );
     *
     *		// Creates selection from the other selection.
     *		// Note: It doesn't copies selection attributes.
     *		const otherSelection = writer.createSelection();
     *		const selection = writer.createSelection( otherSelection );
     *
     *		// Creates selection from the given document selection.
     *		// Note: It doesn't copies selection attributes.
     *		const documentSelection = model.document.selection;
     *		const selection = writer.createSelection( documentSelection );
     *
     *		// Creates selection at the given position.
     *		const position = writer.createPositionFromPath( root, path );
     *		const selection = writer.createSelection( position );
     *
     *		// Creates selection at the given offset in the given element.
     *		const paragraph = writer.createElement( 'paragraph' );
     *		const selection = writer.createSelection( paragraph, offset );
     *
     *		// 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.
     *		const selection = writer.createSelection( paragraph, 'in' );
     *
     *		// Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends
     *		// just after the item.
     *		const selection = writer.createSelection( paragraph, 'on' );
     *
     *		// Additional options (`'backward'`) can be specified as the last argument.
     *
     *		// Creates backward selection.
     *		const selection = writer.createSelection( range, { backward: true } );
     *
     * @param {module:engine/model/selection~Selectable} selectable
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [optionsOrPlaceOrOffset] Sets place or offset of the selection.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     * @returns {module:engine/model/selection~Selection}
     */
    createSelection(...args) {
        return new Selection(...args);
    }
    /**
     * Creates a {@link module:engine/model/batch~Batch} instance.
     *
     * **Note:** In most cases creating a batch instance is not necessary as they are created when using:
     *
     * * {@link #change `change()`},
     * * {@link #enqueueChange `enqueueChange()`}.
     *
     * @param {Object} [type] {@link module:engine/model/batch~Batch#constructor The type} of the batch.
     * @returns {module:engine/model/batch~Batch}
     */
    createBatch(type) {
        return new Batch(type);
    }
    /**
     * Creates an operation instance from a JSON object (parsed JSON string).
     *
     * This is an alias for {@link module:engine/model/operation/operationfactory~OperationFactory.fromJSON `OperationFactory.fromJSON()`}.
     *
     * @param {Object} json Deserialized JSON object.
     * @returns {module:engine/model/operation/operation~Operation}
     */
    createOperationFromJSON(json) {
        return OperationFactory.fromJSON(json, this.document);
    }
    /**
     * Removes all events listeners set by model instance and destroys {@link module:engine/model/document~Document}.
     */
    destroy() {
        this.document.destroy();
        this.stopListening();
    }
    /**
     * Common part of {@link module:engine/model/model~Model#change} and {@link module:engine/model/model~Model#enqueueChange}
     * which calls callbacks and returns array of values returned by these callbacks.
     *
     * @private
     * @returns {Array.<*>} Array of values returned by callbacks.
     */
    _runPendingChanges() {
        const ret = [];
        this.fire('_beforeChanges');
        try {
            while (this._pendingChanges.length) {
                // Create a new writer using batch instance created for this chain of changes.
                const currentBatch = this._pendingChanges[0].batch;
                this._currentWriter = new Writer(this, currentBatch);
                // Execute changes callback and gather the returned value.
                const callbackReturnValue = this._pendingChanges[0].callback(this._currentWriter);
                ret.push(callbackReturnValue);
                this.document._handleChangeBlock(this._currentWriter);
                this._pendingChanges.shift();
                this._currentWriter = null;
            }
        }
        finally {
            this._pendingChanges.length = 0;
            this._currentWriter = null;
            this.fire('_afterChanges');
        }
        return ret;
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A keystroke handler for editor editing. Its instance is available
 * in {@link module:core/editor/editor~Editor#keystrokes} so plugins
 * can register their keystrokes.
 *
 * E.g. an undo plugin would do this:
 *
 *		editor.keystrokes.set( 'Ctrl+Z', 'undo' );
 *		editor.keystrokes.set( 'Ctrl+Shift+Z', 'redo' );
 *		editor.keystrokes.set( 'Ctrl+Y', 'redo' );
 *
 * @extends module:utils/keystrokehandler~KeystrokeHandler
 */
class EditingKeystrokeHandler extends KeystrokeHandler {
	/**
	 * Creates an instance of the keystroke handler.
	 *
	 * @param {module:core/editor/editor~Editor} editor
	 */
	constructor( editor ) {
		super();

		/**
		 * The editor instance.
		 *
		 * @readonly
		 * @member {module:core/editor/editor~Editor}
		 */
		this.editor = editor;
	}

	/**
	 * Registers a handler for the specified keystroke.
	 *
	 * The handler can be specified as a command name or a callback.
	 *
	 * @param {String|Array.<String|Number>} keystroke Keystroke defined in a format accepted by
	 * the {@link module:utils/keyboard~parseKeystroke} function.
	 * @param {Function|String} callback If a string is passed, then the keystroke will
	 * {@link module:core/editor/editor~Editor#execute execute a command}.
	 * If a function, then it will be called with the
	 * {@link module:engine/view/observer/keyobserver~KeyEventData key event data} object and
	 * a `cancel()` helper to both `preventDefault()` and `stopPropagation()` of the event.
	 * @param {Object} [options={}] Additional options.
	 * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of the keystroke
	 * callback. The higher the priority value the sooner the callback will be executed. Keystrokes having the same priority
	 * are called in the order they were added.
	 */
	set( keystroke, callback, options = {} ) {
		if ( typeof callback == 'string' ) {
			const commandName = callback;

			callback = ( evtData, cancel ) => {
				this.editor.execute( commandName );
				cancel();
			};
		}

		super.set( keystroke, callback, options );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The class representing a basic, generic editor.
 *
 * Check out the list of its subclasses to learn about specific editor implementations.
 *
 * All editor implementations (like {@link module:editor-classic/classiceditor~ClassicEditor} or
 * {@link module:editor-inline/inlineeditor~InlineEditor}) should extend this class. They can add their
 * own methods and properties.
 *
 * When you are implementing a plugin, this editor represents the API
 * which your plugin can expect to get when using its {@link module:core/plugin~Plugin#editor} property.
 *
 * This API should be sufficient in order to implement the "editing" part of your feature
 * (schema definition, conversion, commands, keystrokes, etc.).
 * It does not define the editor UI, which is available only if
 * the specific editor implements also the {@link module:core/editor/editorwithui~EditorWithUI} interface
 * (as most editor implementations do).
 *
 * @abstract
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class Editor {
	/**
	 * Creates a new instance of the editor class.
	 *
	 * Usually, not to be used directly. See the static {@link module:core/editor/editor~Editor.create `create()`} method.
	 *
	 * @param {Object} [config={}] The editor configuration.
	 */
	constructor( config = {} ) {
		// Prefer the language passed as the argument to the constructor instead of the constructor's `defaultConfig`, if both are set.
		const language = config.language || ( this.constructor.defaultConfig && this.constructor.defaultConfig.language );

		/**
		 * The editor context.
		 * When it is not provided through the configuration, the editor creates it.
		 *
		 * @protected
		 * @type {module:core/context~Context}
		 */
		this._context = config.context || new Context( { language } );
		this._context._addEditor( this, !config.context );

		// Clone the plugins to make sure that the plugin array will not be shared
		// between editors and make the watchdog feature work correctly.
		const availablePlugins = Array.from( this.constructor.builtinPlugins || [] );

		/**
		 * Stores all configurations specific to this editor instance.
		 *
		 *		editor.config.get( 'image.toolbar' );
		 *		// -> [ 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ]
		 *
		 * @readonly
		 * @member {module:utils/config~Config}
		 */
		this.config = new Config( config, this.constructor.defaultConfig );
		this.config.define( 'plugins', availablePlugins );
		this.config.define( this._context._getEditorConfig() );

		/**
		 * The plugins loaded and in use by this editor instance.
		 *
		 *		editor.plugins.get( 'ClipboardPipeline' ); // -> An instance of the clipboard pipeline plugin.
		 *
		 * @readonly
		 * @member {module:core/plugincollection~PluginCollection}
		 */
		this.plugins = new PluginCollection( this, availablePlugins, this._context.plugins );

		/**
		 * The locale instance.
		 *
		 * @readonly
		 * @type {module:utils/locale~Locale}
		 */
		this.locale = this._context.locale;

		/**
		 * Shorthand for {@link module:utils/locale~Locale#t}.
		 *
		 * @see module:utils/locale~Locale#t
		 * @method #t
		 */
		this.t = this.locale.t;

		/**
		 * A set of lock IDs for the {@link #isReadOnly} getter.
		 *
		 * @private
		 * @type {Set.<String|Symbol>}
		 */
		this._readOnlyLocks = new Set();

		/**
		 * Commands registered to the editor.
		 *
		 * Use the shorthand {@link #execute `editor.execute()`} method to execute commands:
		 *
		 *		// Execute the bold command:
		 *		editor.execute( 'bold' );
		 *
		 *		// Check the state of the bold command:
		 *		editor.commands.get( 'bold' ).value;
		 *
		 * @readonly
		 * @member {module:core/commandcollection~CommandCollection}
		 */
		this.commands = new CommandCollection();

		/**
		 * Indicates the editor life-cycle state.
		 *
		 * The editor is in one of the following states:
		 *
		 * * `initializing` &ndash; During the editor initialization (before
		 * {@link module:core/editor/editor~Editor.create `Editor.create()`}) finished its job.
		 * * `ready` &ndash; After the promise returned by the {@link module:core/editor/editor~Editor.create `Editor.create()`}
		 * method is resolved.
		 * * `destroyed` &ndash; Once the {@link #destroy `editor.destroy()`} method was called.
		 *
		 * @observable
		 * @member {'initializing'|'ready'|'destroyed'} #state
		 */
		this.set( 'state', 'initializing' );
		this.once( 'ready', () => ( this.state = 'ready' ), { priority: 'high' } );
		this.once( 'destroy', () => ( this.state = 'destroyed' ), { priority: 'high' } );

		/**
		 * The editor's model.
		 *
		 * The central point of the editor's abstract data model.
		 *
		 * @readonly
		 * @member {module:engine/model/model~Model}
		 */
		this.model = new Model$1();

		const stylesProcessor = new StylesProcessor();

		/**
		 * The {@link module:engine/controller/datacontroller~DataController data controller}.
		 * Used e.g. for setting and retrieving the editor data.
		 *
		 * @readonly
		 * @member {module:engine/controller/datacontroller~DataController}
		 */
		this.data = new DataController( this.model, stylesProcessor );

		/**
		 * The {@link module:engine/controller/editingcontroller~EditingController editing controller}.
		 * Controls user input and rendering the content for editing.
		 *
		 * @readonly
		 * @member {module:engine/controller/editingcontroller~EditingController}
		 */
		this.editing = new EditingController( this.model, stylesProcessor );
		this.editing.view.document.bind( 'isReadOnly' ).to( this );

		/**
		 * Conversion manager through which you can register model-to-view and view-to-model converters.
		 *
		 * See the {@link module:engine/conversion/conversion~Conversion} documentation to learn how to add converters.
		 *
		 * @readonly
		 * @member {module:engine/conversion/conversion~Conversion}
		 */
		this.conversion = new Conversion( [ this.editing.downcastDispatcher, this.data.downcastDispatcher ], this.data.upcastDispatcher );
		this.conversion.addAlias( 'dataDowncast', this.data.downcastDispatcher );
		this.conversion.addAlias( 'editingDowncast', this.editing.downcastDispatcher );

		/**
		 * An instance of the {@link module:core/editingkeystrokehandler~EditingKeystrokeHandler}.
		 *
		 * It allows setting simple keystrokes:
		 *
		 *		// Execute the bold command on Ctrl+E:
		 *		editor.keystrokes.set( 'Ctrl+E', 'bold' );
		 *
		 *		// Execute your own callback:
		 *		editor.keystrokes.set( 'Ctrl+E', ( data, cancel ) => {
		 *			console.log( data.keyCode );
		 *
		 *			// Prevent the default (native) action and stop the underlying keydown event
		 *			// so no other editor feature will interfere.
		 *			cancel();
		 *		} );
		 *
		 * Note: Certain typing-oriented keystrokes (like <kbd>Backspace</kbd> or <kbd>Enter</kbd>) are handled
		 * by a low-level mechanism and trying to listen to them via the keystroke handler will not work reliably.
		 * To handle these specific keystrokes, see the events fired by the
		 * {@link module:engine/view/document~Document editing view document} (`editor.editing.view.document`).
		 *
		 * @readonly
		 * @member {module:core/editingkeystrokehandler~EditingKeystrokeHandler}
		 */
		this.keystrokes = new EditingKeystrokeHandler( this );
		this.keystrokes.listenTo( this.editing.view.document );
	}

	/**
	 * Defines whether the editor is in the read-only mode.
	 *
	 * In read-only mode the editor {@link #commands commands} are disabled so it is not possible
	 * to modify the document by using them. Also, the editable element(s) become non-editable.
	 *
	 * In order to make the editor read-only, you need to call the {@link #enableReadOnlyMode} method:
	 *
	 *		editor.enableReadOnlyMode( 'feature-id' );
	 *
     * Later, to turn off the read-only mode, call {@link #disableReadOnlyMode}:
	 *
	 * 		editor.disableReadOnlyMode( 'feature-id' );
	 *
	 * @readonly
	 * @observable
	 * @member {Boolean} #isReadOnly
	 */
	get isReadOnly() {
		return this._readOnlyLocks.size > 0;
	}

	set isReadOnly( value ) {
		/**
		 * The {@link #isReadOnly Editor#isReadOnly} property is read-only since version `34.0.0` and can be set only using
		 * {@link #enableReadOnlyMode `Editor#enableReadOnlyMode( lockId )`} and
		 * {@link #disableReadOnlyMode `Editor#disableReadOnlyMode( lockId )`}.
		 *
		 * Usage before version `34.0.0`:
		 *
		 *		editor.isReadOnly = true;
		 * 		editor.isReadOnly = false;
		 *
		 * Usage since version `34.0.0`:
		 *
		 *		editor.enableReadOnlyMode( 'my-feature-id' );
		 * 		editor.disableReadOnlyMode( 'my-feature-id' );
		 *
		 * @error editor-isreadonly-has-no-setter
		 */
		throw new CKEditorError( 'editor-isreadonly-has-no-setter' );
	}

	/**
	 * Turns on the read-only mode in the editor.
	 *
	 * Editor can be switched to or out of the read-only mode by many features, under various circumstances. The editor supports locking
	 * mechanism for the read-only mode. It enables easy control over the read-only mode when many features wants to turn it on or off at
	 * the same time, without conflicting with each other. It guarantees that you will not make the editor editable accidentally (which
	 * could lead to errors).
	 *
	 * Each read-only mode request is identified by a unique id (also called "lock"). If multiple plugins requested to turn on the
	 * read-only mode, then, the editor will become editable only after all these plugins turn the read-only mode off (using the same ids).
	 *
	 * Note, that you cannot force the editor to disable the read-only mode if other plugins set it.
	 *
	 * After the first `enableReadOnlyMode()` call, the {@link #isReadOnly `isReadOnly` property} will be set to `true`:
	 *
	 *		editor.isReadOnly; // `false`.
	 * 		editor.enableReadOnlyMode( 'my-feature-id' );
	 * 		editor.isReadOnly; // `true`.
	 *
	 * You can turn off the read-only mode ("clear the lock") using the {@link #disableReadOnlyMode `disableReadOnlyMode()`} method:
	 *
	 * 		editor.enableReadOnlyMode( 'my-feature-id' );
	 * 		// ...
	 * 		editor.disableReadOnlyMode( 'my-feature-id' );
	 * 		editor.isReadOnly; // `false`.
	 *
	 * All "locks" need to be removed to enable editing:
	 *
	 * 		editor.enableReadOnlyMode( 'my-feature-id' );
	 * 		editor.enableReadOnlyMode( 'my-other-feature-id' );
	 * 		// ...
	 * 		editor.disableReadOnlyMode( 'my-feature-id' );
	 * 		editor.isReadOnly; // `true`.
	 * 		editor.disableReadOnlyMode( 'my-other-feature-id' );
	 * 		editor.isReadOnly; // `false`.
	 *
	 * @param {String|Symbol} lockId A unique ID for setting the editor to the read-only state.
	 */
	enableReadOnlyMode( lockId ) {
		if ( typeof lockId !== 'string' && typeof lockId !== 'symbol' ) {
			/**
			 * The lock ID is missing or it is not a string or symbol.
			 *
			 * @error editor-read-only-lock-id-invalid
			 */
			throw new CKEditorError( 'editor-read-only-lock-id-invalid', null, { lockId } );
		}

		if ( this._readOnlyLocks.has( lockId ) ) {
			return;
		}

		this._readOnlyLocks.add( lockId );

		if ( this._readOnlyLocks.size === 1 ) {
			// Manually fire the `change:isReadOnly` event as only getter is provided.
			this.fire( 'change:isReadOnly', 'isReadOnly', true, false );
		}
	}

	/**
	 * Removes the read-only lock from the editor with given lock ID.
	 *
	 * When no lock is present on the editor anymore, then the {@link #isReadOnly `isReadOnly` property} will be set to `false`.
	 *
	 * @param {String|Symbol} lockId The lock ID for setting the editor to the read-only state.
	 */
	disableReadOnlyMode( lockId ) {
		if ( typeof lockId !== 'string' && typeof lockId !== 'symbol' ) {
			throw new CKEditorError( 'editor-read-only-lock-id-invalid', null, { lockId } );
		}

		if ( !this._readOnlyLocks.has( lockId ) ) {
			return;
		}

		this._readOnlyLocks.delete( lockId );

		if ( this._readOnlyLocks.size === 0 ) {
			// Manually fire the `change:isReadOnly` event as only getter is provided.
			this.fire( 'change:isReadOnly', 'isReadOnly', false, true );
		}
	}

	/**
	 * Loads and initializes plugins specified in the configuration.
	 *
	 * @returns {Promise.<module:core/plugin~LoadedPlugins>} A promise which resolves
	 * once the initialization is completed, providing an array of loaded plugins.
	 */
	initPlugins() {
		const config = this.config;
		const plugins = config.get( 'plugins' );
		const removePlugins = config.get( 'removePlugins' ) || [];
		const extraPlugins = config.get( 'extraPlugins' ) || [];
		const substitutePlugins = config.get( 'substitutePlugins' ) || [];

		return this.plugins.init( plugins.concat( extraPlugins ), removePlugins, substitutePlugins );
	}

	/**
	 * Destroys the editor instance, releasing all resources used by it.
	 *
	 * **Note** The editor cannot be destroyed during the initialization phase so if it is called
	 * while the editor {@link #state is being initialized}, it will wait for the editor initialization before destroying it.
	 *
	 * @fires destroy
	 * @returns {Promise} A promise that resolves once the editor instance is fully destroyed.
	 */
	destroy() {
		let readyPromise = Promise.resolve();

		if ( this.state == 'initializing' ) {
			readyPromise = new Promise( resolve => this.once( 'ready', resolve ) );
		}

		return readyPromise
			.then( () => {
				this.fire( 'destroy' );
				this.stopListening();
				this.commands.destroy();
			} )
			.then( () => this.plugins.destroy() )
			.then( () => {
				this.model.destroy();
				this.data.destroy();
				this.editing.destroy();
				this.keystrokes.destroy();
			} )
			// Remove the editor from the context.
			// When the context was created by this editor, the context will be destroyed.
			.then( () => this._context._removeEditor( this ) );
	}

	/**
	 * Executes the specified command with given parameters.
	 *
	 * Shorthand for:
	 *
	 *		editor.commands.get( commandName ).execute( ... );
	 *
	 * @param {String} commandName The name of the command to execute.
	 * @param {*} [...commandParams] Command parameters.
	 * @returns {*} The value returned by the {@link module:core/commandcollection~CommandCollection#execute `commands.execute()`}.
	 */
	execute( ...args ) {
		try {
			return this.commands.execute( ...args );
		} catch ( err ) {
			// @if CK_DEBUG // throw err;
			/* istanbul ignore next */
			CKEditorError.rethrowUnexpectedError( err, this );
		}
	}

	/**
	 * Focuses the editor.
	 *
	 * **Note** To explicitly focus the editing area of the editor, use the
	 * {@link module:engine/view/view~View#focus `editor.editing.view.focus()`} method of the editing view.
	 *
	 * Check out the {@glink framework/guides/deep-dive/ui/focus-tracking#focus-in-the-editor-ui Focus in the editor UI} section
	 * of the {@glink framework/guides/deep-dive/ui/focus-tracking Deep dive into focus tracking} guide to learn more.
	 */
	focus() {
		this.editing.view.focus();
	}

	/**
	 * Creates and initializes a new editor instance.
	 *
	 * This is an abstract method. Every editor type needs to implement its own initialization logic.
	 *
	 * See the `create()` methods of the existing editor types to learn how to use them:
	 *
	 * * {@link module:editor-classic/classiceditor~ClassicEditor.create `ClassicEditor.create()`}
	 * * {@link module:editor-balloon/ballooneditor~BalloonEditor.create `BalloonEditor.create()`}
	 * * {@link module:editor-decoupled/decouplededitor~DecoupledEditor.create `DecoupledEditor.create()`}
	 * * {@link module:editor-inline/inlineeditor~InlineEditor.create `InlineEditor.create()`}
	 *
	 * @abstract
	 * @method module:core/editor/editor~Editor.create
	 */
}

mix( Editor, ObservableMixin );

/**
 * Fired when the {@link module:engine/controller/datacontroller~DataController#event:ready data} and all additional
 * editor components are ready.
 *
 * Note: This event is most useful for plugin developers. When integrating the editor with your website or
 * application, you do not have to listen to `editor#ready` because when the promise returned by the static
 * {@link module:core/editor/editor~Editor.create `Editor.create()`} event is resolved, the editor is already ready.
 * In fact, since the first moment when the editor instance is available to you is inside `then()`'s callback,
 * you cannot even add a listener to the `editor#ready` event.
 *
 * See also the {@link #state `editor.state`} property.
 *
 * @event ready
 */

/**
 * Fired when this editor instance is destroyed. The editor at this point is not usable and this event should be used to
 * perform the clean-up in any plugin.
 *
 *
 * See also the {@link #state `editor.state`} property.
 *
 * @event destroy
 */

/**
 * This error is thrown when trying to pass a `<textarea>` element to a `create()` function of an editor class.
 *
 * The only editor type which can be initialized on `<textarea>` elements is
 * the {@glink installation/getting-started/predefined-builds#classic-editor classic editor}.
 * This editor hides the passed element and inserts its own UI next to it. Other types of editors reuse the passed element as their root
 * editable element and therefore `<textarea>` is not appropriate for them. Use a `<div>` or another text container instead:
 *
 *		<div id="editor">
 *			<p>Initial content.</p>
 *		</div>
 *
 * @error editor-wrong-element
 */

/**
 * An array of plugins built into this editor class.
 *
 * It is used in CKEditor 5 builds to provide a list of plugins which are later automatically initialized
 * during the editor initialization.
 *
 * They will be automatically initialized by the editor, unless listed in `config.removePlugins` and
 * unless `config.plugins` is passed.
 *
 *		// Build some plugins into the editor class first.
 *		ClassicEditor.builtinPlugins = [ FooPlugin, BarPlugin ];
 *
 *		// Normally, you need to define config.plugins, but since ClassicEditor.builtinPlugins was
 *		// defined, now you can call create() without any configuration.
 *		ClassicEditor
 *			.create( sourceElement )
 *			.then( editor => {
 *				editor.plugins.get( FooPlugin ); // -> An instance of the Foo plugin.
 *				editor.plugins.get( BarPlugin ); // -> An instance of the Bar plugin.
 *			} );
 *
 *		ClassicEditor
 *			.create( sourceElement, {
 *				// Do not initialize these plugins (note: it is defined by a string):
 *				removePlugins: [ 'Foo' ]
 *			} )
 *			.then( editor => {
 *				editor.plugins.get( FooPlugin ); // -> Undefined.
 *				editor.config.get( BarPlugin ); // -> An instance of the Bar plugin.
 *			} );
 *
 *		ClassicEditor
 *			.create( sourceElement, {
 *				// Load only this plugin. It can also be defined by a string if
 *				// this plugin was built into the editor class.
 *				plugins: [ FooPlugin ]
 *			} )
 *			.then( editor => {
 *				editor.plugins.get( FooPlugin ); // -> An instance of the Foo plugin.
 *				editor.config.get( BarPlugin ); // -> Undefined.
 *			} );
 *
 * See also {@link module:core/editor/editor~Editor.defaultConfig}.
 *
 * @static
 * @member {Array.<Function>} module:core/editor/editor~Editor.builtinPlugins
 */

/**
 * The default configuration which is built into the editor class.
 *
 * It is used in CKEditor 5 builds to provide the default configuration options which are later used during the editor initialization.
 *
 *		ClassicEditor.defaultConfig = {
 *			foo: 1,
 *			bar: 2
 *		};
 *
 *		ClassicEditor
 *			.create( sourceElement )
 *			.then( editor => {
 *				editor.config.get( 'foo' ); // -> 1
 *				editor.config.get( 'bar' ); // -> 2
 *			} );
 *
 *		// The default options can be overridden by the configuration passed to create().
 *		ClassicEditor
 *			.create( sourceElement, { bar: 3 } )
 *			.then( editor => {
 *				editor.config.get( 'foo' ); // -> 1
 *				editor.config.get( 'bar' ); // -> 3
 *			} );
 *
 * See also {@link module:core/editor/editor~Editor.builtinPlugins}.
 *
 * @static
 * @member {Object} module:core/editor/editor~Editor.defaultConfig
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A helper class implementing the UI component ({@link module:ui/view~View view}) factory.
 *
 * It allows functions producing specific UI components to be registered under their unique names
 * in the factory. A registered component can be then instantiated by providing its name.
 * Note that names are case insensitive.
 *
 *		// The editor provides localization tools for the factory.
 *		const factory = new ComponentFactory( editor );
 *
 *		factory.add( 'foo', locale => new FooView( locale ) );
 *		factory.add( 'bar', locale => new BarView( locale ) );
 *
 *		// An instance of FooView.
 *		const fooInstance = factory.create( 'foo' );
 *
 *		// Names are case insensitive so this is also allowed:
 *		const barInstance = factory.create( 'Bar' );
 *
 * The {@link module:core/editor/editor~Editor#locale editor locale} is passed to the factory
 * function when {@link module:ui/componentfactory~ComponentFactory#create} is called.
 */
class ComponentFactory {
	/**
	 * Creates an instance of the factory.
	 *
	 * @constructor
	 * @param {module:core/editor/editor~Editor} editor The editor instance.
	 */
	constructor( editor ) {
		/**
		 * The editor instance that the factory belongs to.
		 *
		 * @readonly
		 * @member {module:core/editor/editor~Editor}
		 */
		this.editor = editor;

		/**
		 * Registered component factories.
		 *
		 * @private
		 * @member {Map}
		 */
		this._components = new Map();
	}

	/**
	 * Returns an iterator of registered component names. Names are returned in lower case.
	 *
	 * @returns {Iterable.<String>}
	 */
	* names() {
		for ( const value of this._components.values() ) {
			yield value.originalName;
		}
	}

	/**
	 * Registers a component factory function that will be used by the
	 * {@link #create create} method and called with the
	 * {@link module:core/editor/editor~Editor#locale editor locale} as an argument,
	 * allowing localization of the {@link module:ui/view~View view}.
	 *
	 * @param {String} name The name of the component.
	 * @param {Function} callback The callback that returns the component.
	 */
	add( name, callback ) {
		this._components.set( getNormalized( name ), { callback, originalName: name } );
	}

	/**
	 * Creates an instance of a component registered in the factory under a specific name.
	 *
	 * When called, the {@link module:core/editor/editor~Editor#locale editor locale} is passed to
	 * the previously {@link #add added} factory function, allowing localization of the
	 * {@link module:ui/view~View view}.
	 *
	 * @param {String} name The name of the component.
	 * @returns {module:ui/view~View} The instantiated component view.
	 */
	create( name ) {
		if ( !this.has( name ) ) {
			/**
			 * The required component is not registered in the component factory. Please make sure
			 * the provided name is correct and the component has been correctly
			 * {@link #add added} to the factory.
			 *
			 * @error componentfactory-item-missing
			 * @param {String} name The name of the missing component.
			 */
			throw new CKEditorError(
				'componentfactory-item-missing',
				this,
				{ name }
			);
		}

		return this._components.get( getNormalized( name ) ).callback( this.editor.locale );
	}

	/**
	 * Checks if a component of a given name is registered in the factory.
	 *
	 * @param {String} name The name of the component.
	 * @returns {Boolean}
	 */
	has( name ) {
		return this._components.has( getNormalized( name ) );
	}
}

//
// Ensures that the component name used as the key in the internal map is in lower case.
//
// @private
// @param {String} name
// @returns {String}
function getNormalized( name ) {
	return String( name ).toLowerCase();
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Collects {@link module:ui/view~View} instances.
 *
 *		const parentView = new ParentView( locale );
 *		const collection = new ViewCollection( locale );
 *
 *		collection.setParent( parentView.element );
 *
 *		const viewA = new ChildView( locale );
 *		const viewB = new ChildView( locale );
 *
 * View collection renders and manages view {@link module:ui/view~View#element elements}:
 *
 *		collection.add( viewA );
 *		collection.add( viewB );
 *
 *		console.log( parentView.element.firsChild ); // -> viewA.element
 *		console.log( parentView.element.lastChild ); // -> viewB.element
 *
 * It {@link module:ui/viewcollection~ViewCollection#delegate propagates} DOM events too:
 *
 *		// Delegate #click and #keydown events from viewA and viewB to the parentView.
 *		collection.delegate( 'click' ).to( parentView );
 *
 *		parentView.on( 'click', ( evt ) => {
 *			console.log( `${ evt.source } has been clicked.` );
 *		} );
 *
 *		// This event will be delegated to the parentView.
 *		viewB.fire( 'click' );
 *
 * **Note**: A view collection can be used directly in the {@link module:ui/template~TemplateDefinition definition}
 * of a {@link module:ui/template~Template template}.
 *
 * @extends module:utils/collection~Collection
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class ViewCollection extends Collection {
	/**
	 * Creates a new instance of the {@link module:ui/viewcollection~ViewCollection}.
	 *
	 * @param {Iterable.<module:ui/view~View>} [initialItems] The initial items of the collection.
	 */
	constructor( initialItems = [] ) {
		super( initialItems, {
			// An #id Number attribute should be legal and not break the `ViewCollection` instance.
			// https://github.com/ckeditor/ckeditor5-ui/issues/93
			idProperty: 'viewUid'
		} );

		// Handle {@link module:ui/view~View#element} in DOM when a new view is added to the collection.
		this.on( 'add', ( evt, view, index ) => {
			this._renderViewIntoCollectionParent( view, index );
		} );

		// Handle {@link module:ui/view~View#element} in DOM when a view is removed from the collection.
		this.on( 'remove', ( evt, view ) => {
			if ( view.element && this._parentElement ) {
				view.element.remove();
			}
		} );

		/**
		 * A parent element within which child views are rendered and managed in DOM.
		 *
		 * @protected
		 * @member {HTMLElement}
		 */
		this._parentElement = null;
	}

	/**
	 * Destroys the view collection along with child views.
	 * See the view {@link module:ui/view~View#destroy} method.
	 */
	destroy() {
		this.map( view => view.destroy() );
	}

	/**
	 * Sets the parent HTML element of this collection. When parent is set, {@link #add adding} and
	 * {@link #remove removing} views in the collection synchronizes their
	 * {@link module:ui/view~View#element elements} in the parent element.
	 *
	 * @param {HTMLElement} element A new parent element.
	 */
	setParent( elementOrDocFragment ) {
		this._parentElement = elementOrDocFragment;

		// Take care of the initial collection items passed to the constructor.
		for ( const view of this ) {
			this._renderViewIntoCollectionParent( view );
		}
	}

	/**
	 * Delegates selected events coming from within views in the collection to any
	 * {@link module:utils/emittermixin~Emitter}.
	 *
	 * For the following views and collection:
	 *
	 *		const viewA = new View();
	 *		const viewB = new View();
	 *		const viewC = new View();
	 *
	 *		const views = parentView.createCollection();
	 *
	 *		views.delegate( 'eventX' ).to( viewB );
	 *		views.delegate( 'eventX', 'eventY' ).to( viewC );
	 *
	 *		views.add( viewA );
	 *
	 * the `eventX` is delegated (fired by) `viewB` and `viewC` along with `customData`:
	 *
	 *		viewA.fire( 'eventX', customData );
	 *
	 * and `eventY` is delegated (fired by) `viewC` along with `customData`:
	 *
	 *		viewA.fire( 'eventY', customData );
	 *
	 * See {@link module:utils/emittermixin~Emitter#delegate}.
	 *
	 * @param {...String} events {@link module:ui/view~View} event names to be delegated to another
	 * {@link module:utils/emittermixin~Emitter}.
	 * @returns {Object}
	 * @returns {Function} return.to A function which accepts the destination of
	 * {@link module:utils/emittermixin~Emitter#delegate delegated} events.
	 */
	delegate( ...events ) {
		if ( !events.length || !isStringArray( events ) ) {
			/**
			 * All event names must be strings.
			 *
			 * @error ui-viewcollection-delegate-wrong-events
			 */
			throw new CKEditorError(
				'ui-viewcollection-delegate-wrong-events',
				this
			);
		}

		return {
			/**
			 * Selects destination for {@link module:utils/emittermixin~Emitter#delegate} events.
			 *
			 * @memberOf module:ui/viewcollection~ViewCollection#delegate
			 * @function module:ui/viewcollection~ViewCollection#delegate.to
			 * @param {module:utils/emittermixin~Emitter} dest An `Emitter` instance which is
			 * the destination for delegated events.
			 */
			to: dest => {
				// Activate delegating on existing views in this collection.
				for ( const view of this ) {
					for ( const evtName of events ) {
						view.delegate( evtName ).to( dest );
					}
				}

				// Activate delegating on future views in this collection.
				this.on( 'add', ( evt, view ) => {
					for ( const evtName of events ) {
						view.delegate( evtName ).to( dest );
					}
				} );

				// Deactivate delegating when view is removed from this collection.
				this.on( 'remove', ( evt, view ) => {
					for ( const evtName of events ) {
						view.stopDelegating( evtName, dest );
					}
				} );
			}
		};
	}

	/**
	 * This method {@link module:ui/view~View#render renders} a new view added to the collection.
	 *
	 * If the {@link #_parentElement parent element} of the collection is set, this method also adds
	 * the view's {@link module:ui/view~View#element} as a child of the parent in DOM at a specified index.
	 *
	 * **Note**: If index is not specified, the view's element is pushed as the last child
	 * of the parent element.
	 *
	 * @private
	 * @param {module:ui/view~View} view A new view added to the collection.
	 * @param {Number} [index] An index the view holds in the collection. When not specified,
	 * the view is added at the end.
	 */
	_renderViewIntoCollectionParent( view, index ) {
		if ( !view.isRendered ) {
			view.render();
		}

		if ( view.element && this._parentElement ) {
			this._parentElement.insertBefore( view.element, this._parentElement.children[ index ] );
		}
	}

	/**
	 * Removes a child view from the collection. If the {@link #setParent parent element} of the
	 * collection has been set, the {@link module:ui/view~View#element element} of the view is also removed
	 * in DOM, reflecting the order of the collection.
	 *
	 * See the {@link #add} method.
	 *
	 * @method #remove
	 * @param {module:ui/view~View|Number|String} subject The view to remove, its id or index in the collection.
	 * @returns {Object} The removed view.
	 */
}

// 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' );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const xhtmlNs = 'http://www.w3.org/1999/xhtml';

/**
 * A basic Template class. It renders a DOM HTML element or text from a
 * {@link module:ui/template~TemplateDefinition definition} and supports element attributes, children,
 * bindings to {@link module:utils/observablemixin~Observable observables} and DOM event propagation.
 *
 * A simple template can look like this:
 *
 *		const bind = Template.bind( observable, emitter );
 *
 *		new Template( {
 *			tag: 'p',
 *			attributes: {
 *				class: 'foo',
 *				style: {
 *					backgroundColor: 'yellow'
 *				}
 *			},
 *			on: {
 *				click: bind.to( 'clicked' )
 *			},
 *			children: [
 *				'A paragraph.'
 *			]
 *		} ).render();
 *
 * and it will render the following HTML element:
 *
 *		<p class="foo" style="background-color: yellow;">A paragraph.</p>
 *
 * Additionally, the `observable` will always fire `clicked` upon clicking `<p>` in the DOM.
 *
 * See {@link module:ui/template~TemplateDefinition} to know more about templates and complex
 * template definitions.
 *
* @mixes module:utils/emittermixin~EmitterMixin
 */
class Template {
	/**
	 * Creates an instance of the {@link ~Template} class.
	 *
	 * @param {module:ui/template~TemplateDefinition} def The definition of the template.
	 */
	constructor( def ) {
		Object.assign( this, normalize( clone( def ) ) );

		/**
		 * Indicates whether this particular Template instance has been
		 * {@link #render rendered}.
		 *
		 * @readonly
		 * @protected
		 * @member {Boolean}
		 */
		this._isRendered = false;

		/**
		 * The tag (`tagName`) of this template, e.g. `div`. It also indicates that the template
		 * renders to an HTML element.
		 *
		 * @member {String} #tag
		 */

		/**
		 * The text of the template. It also indicates that the template renders to a DOM text node.
		 *
		 * @member {Array.<String|module:ui/template~TemplateValueSchema>} #text
		 */

		/**
		 * The attributes of the template, e.g. `{ id: [ 'ck-id' ] }`, corresponding with
		 * the attributes of an HTML element.
		 *
		 * **Note**: This property only makes sense when {@link #tag} is defined.
		 *
		 * @member {Object} #attributes
		 */

		/**
		 * The children of the template. They can be either:
		 * * independent instances of {@link ~Template} (sub–templates),
		 * * native DOM Nodes.
		 *
		 * **Note**: This property only makes sense when {@link #tag} is defined.
		 *
		 * @member {Array.<module:ui/template~Template|Node>} #children
		 */

		/**
		 * The DOM event listeners of the template.
		 *
		 * @member {Object} #eventListeners
		 */

		/**
		 * The data used by the {@link #revert} method to restore a node to its original state.
		 *
		 * See: {@link #apply}.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/template~RenderData}
		 */
		this._revertData = null;
	}

	/**
	 * Renders a DOM Node (an HTML element or text) out of the template.
	 *
	 *		const domNode = new Template( { ... } ).render();
	 *
	 * See: {@link #apply}.
	 *
	 * @returns {HTMLElement|Text}
	 */
	render() {
		const node = this._renderNode( {
			intoFragment: true
		} );

		this._isRendered = true;

		return node;
	}

	/**
	 * Applies the template to an existing DOM Node, either HTML element or text.
	 *
	 * **Note:** No new DOM nodes will be created. Applying extends:
	 *
	 * {@link module:ui/template~TemplateDefinition attributes},
	 * {@link module:ui/template~TemplateDefinition event listeners}, and
	 * `textContent` of {@link module:ui/template~TemplateDefinition children} only.
	 *
	 * **Note:** Existing `class` and `style` attributes are extended when a template
	 * is applied to an HTML element, while other attributes and `textContent` are overridden.
	 *
	 * **Note:** The process of applying a template can be easily reverted using the
	 * {@link module:ui/template~Template#revert} method.
	 *
	 *		const element = document.createElement( 'div' );
	 *		const observable = new Model( { divClass: 'my-div' } );
	 *		const emitter = Object.create( EmitterMixin );
	 *		const bind = Template.bind( observable, emitter );
	 *
	 *		new Template( {
	 *			attributes: {
	 *				id: 'first-div',
	 *				class: bind.to( 'divClass' )
	 *			},
	 *			on: {
	 *				click: bind( 'elementClicked' ) // Will be fired by the observable.
	 *			},
	 *			children: [
	 *				'Div text.'
	 *			]
	 *		} ).apply( element );
	 *
	 *		console.log( element.outerHTML ); // -> '<div id="first-div" class="my-div"></div>'
	 *
	 * @see module:ui/template~Template#render
	 * @see module:ui/template~Template#revert
	 * @param {Node} node Root node for the template to apply.
	 */
	apply( node ) {
		this._revertData = getEmptyRevertData();

		this._renderNode( {
			node,
			isApplying: true,
			revertData: this._revertData
		} );

		return node;
	}

	/**
	 * Reverts a template {@link module:ui/template~Template#apply applied} to a DOM node.
	 *
	 * @param {Node} node The root node for the template to revert. In most of the cases, it is the
	 * same node used by {@link module:ui/template~Template#apply}.
	 */
	revert( node ) {
		if ( !this._revertData ) {
			/**
			 * Attempting to revert a template which has not been applied yet.
			 *
			 * @error ui-template-revert-not-applied
			 */
			throw new CKEditorError(
				'ui-template-revert-not-applied',
				[ this, node ]
			);
		}

		this._revertTemplateFromNode( node, this._revertData );
	}

	/**
	 * Returns an iterator which traverses the template in search of {@link module:ui/view~View}
	 * instances and returns them one by one.
	 *
	 *		const viewFoo = new View();
	 *		const viewBar = new View();
	 *		const viewBaz = new View();
	 *		const template = new Template( {
	 *			tag: 'div',
	 *			children: [
	 *				viewFoo,
	 *				{
	 *					tag: 'div',
	 *					children: [
	 *						viewBar
	 *					]
	 *				},
	 *				viewBaz
	 *			]
	 *		} );
	 *
	 *		// Logs: viewFoo, viewBar, viewBaz
	 *		for ( const view of template.getViews() ) {
	 *			console.log( view );
	 *		}
	 *
	 * @returns {Iterable.<module:ui/view~View>}
	 */
	* getViews() {
		function* search( def ) {
			if ( def.children ) {
				for ( const child of def.children ) {
					if ( isView( child ) ) {
						yield child;
					} else if ( isTemplate( child ) ) {
						yield* search( child );
					}
				}
			}
		}

		yield* search( this );
	}

	/**
	 * An entry point to the interface which binds DOM nodes to
	 * {@link module:utils/observablemixin~Observable observables}.
	 * There are two types of bindings:
	 *
	 * * HTML element attributes or text `textContent` synchronized with attributes of an
	 * {@link module:utils/observablemixin~Observable}. Learn more about {@link module:ui/template~BindChain#to}
	 * and {@link module:ui/template~BindChain#if}.
	 *
	 *		const bind = Template.bind( observable, emitter );
	 *
	 *		new Template( {
	 *			attributes: {
	 *				// Binds the element "class" attribute to observable#classAttribute.
	 *				class: bind.to( 'classAttribute' )
	 *			}
	 *		} ).render();
	 *
	 * * DOM events fired on HTML element propagated through
	 * {@link module:utils/observablemixin~Observable}. Learn more about {@link module:ui/template~BindChain#to}.
	 *
	 *		const bind = Template.bind( observable, emitter );
	 *
	 *		new Template( {
	 *			on: {
	 *				// Will be fired by the observable.
	 *				click: bind( 'elementClicked' )
	 *			}
	 *		} ).render();
	 *
	 * Also see {@link module:ui/view~View#bindTemplate}.
	 *
	 * @param {module:utils/observablemixin~Observable} observable An observable which provides boundable attributes.
	 * @param {module:utils/emittermixin~Emitter} emitter An emitter that listens to observable attribute
	 * changes or DOM Events (depending on the kind of the binding). Usually, a {@link module:ui/view~View} instance.
	 * @returns {module:ui/template~BindChain}
	 */
	static bind( observable, emitter ) {
		return {
			to( eventNameOrFunctionOrAttribute, callback ) {
				return new TemplateToBinding( {
					eventNameOrFunction: eventNameOrFunctionOrAttribute,
					attribute: eventNameOrFunctionOrAttribute,
					observable, emitter, callback
				} );
			},

			if( attribute, valueIfTrue, callback ) {
				return new TemplateIfBinding( {
					observable, emitter, attribute, valueIfTrue, callback
				} );
			}
		};
	}

	/**
	 * Extends an existing {@link module:ui/template~Template} instance with some additional content
	 * from another {@link module:ui/template~TemplateDefinition}.
	 *
	 *		const bind = Template.bind( observable, emitter );
	 *
	 *		const template = new Template( {
	 *			tag: 'p',
	 *			attributes: {
	 *				class: 'a',
	 *				data-x: bind.to( 'foo' )
	 *			},
	 *			children: [
	 *				{
	 *					tag: 'span',
	 *					attributes: {
	 *						class: 'b'
	 *					},
	 *					children: [
	 *						'Span'
	 *					]
	 *				}
	 *			]
	 *		 } );
	 *
	 *		// Instance-level extension.
	 *		Template.extend( template, {
	 *			attributes: {
	 *				class: 'b',
	 *				data-x: bind.to( 'bar' )
	 *			},
	 *			children: [
	 *				{
	 *					attributes: {
	 *						class: 'c'
	 *					}
	 *				}
	 *			]
	 *		} );
	 *
	 *		// Child extension.
	 *		Template.extend( template.children[ 0 ], {
	 *			attributes: {
	 *				class: 'd'
	 *			}
	 *		} );
	 *
	 * the `outerHTML` of `template.render()` is:
	 *
	 *		<p class="a b" data-x="{ observable.foo } { observable.bar }">
	 *			<span class="b c d">Span</span>
	 *		</p>
	 *
	 * @param {module:ui/template~Template} template An existing template instance to be extended.
	 * @param {module:ui/template~TemplateDefinition} def Additional definition to be applied to a template.
	 */
	static extend( template, def ) {
		if ( template._isRendered ) {
			/**
			 * Extending a template after rendering may not work as expected. To make sure
			 * the {@link module:ui/template~Template.extend extending} works for an element,
			 * make sure it happens before {@link #render} is called.
			 *
			 * @error template-extend-render
			 */
			throw new CKEditorError(
				'template-extend-render',
				[ this, template ]
			);
		}

		extendTemplate( template, normalize( clone( def ) ) );
	}

	/**
	 * Renders a DOM Node (either an HTML element or text) out of the template.
	 *
	 * @protected
	 * @param {module:ui/template~RenderData} data Rendering data.
	 */
	_renderNode( data ) {
		let isInvalid;

		if ( data.node ) {
			// When applying, a definition cannot have "tag" and "text" at the same time.
			isInvalid = this.tag && this.text;
		} else {
			// When rendering, a definition must have either "tag" or "text": XOR( this.tag, this.text ).
			isInvalid = this.tag ? this.text : !this.text;
		}

		if ( isInvalid ) {
			/**
			 * Node definition cannot have the "tag" and "text" properties at the same time.
			 * Node definition must have either "tag" or "text" when rendering a new Node.
			 *
			 * @error ui-template-wrong-syntax
			 */
			throw new CKEditorError(
				'ui-template-wrong-syntax',
				this
			);
		}

		if ( this.text ) {
			return this._renderText( data );
		} else {
			return this._renderElement( data );
		}
	}

	/**
	 * Renders an HTML element out of the template.
	 *
	 * @protected
	 * @param {module:ui/template~RenderData} data Rendering data.
	 */
	_renderElement( data ) {
		let node = data.node;

		if ( !node ) {
			node = data.node = document.createElementNS( this.ns || xhtmlNs, this.tag );
		}

		this._renderAttributes( data );
		this._renderElementChildren( data );
		this._setUpListeners( data );

		return node;
	}

	/**
	 * Renders a text node out of {@link module:ui/template~Template#text}.
	 *
	 * @protected
	 * @param {module:ui/template~RenderData} data Rendering data.
	 */
	_renderText( data ) {
		let node = data.node;

		// Save the original textContent to revert it in #revert().
		if ( node ) {
			data.revertData.text = node.textContent;
		} else {
			node = data.node = document.createTextNode( '' );
		}

		// Check if this Text Node is bound to Observable. Cases:
		//
		//		text: [ Template.bind( ... ).to( ... ) ]
		//
		//		text: [
		//			'foo',
		//			Template.bind( ... ).to( ... ),
		//			...
		//		]
		//
		if ( hasTemplateBinding( this.text ) ) {
			this._bindToObservable( {
				schema: this.text,
				updater: getTextUpdater( node ),
				data
			} );
		}
		// Simply set text. Cases:
		//
		//		text: [ 'all', 'are', 'static' ]
		//
		//		text: [ 'foo' ]
		//
		else {
			node.textContent = this.text.join( '' );
		}

		return node;
	}

	/**
	 * Renders HTML element attributes out of {@link module:ui/template~Template#attributes}.
	 *
	 * @protected
	 * @param {module:ui/template~RenderData} data Rendering data.
	 */
	_renderAttributes( data ) {
		let attrName, attrValue, domAttrValue, attrNs;

		if ( !this.attributes ) {
			return;
		}

		const node = data.node;
		const revertData = data.revertData;

		for ( attrName in this.attributes ) {
			// Current attribute value in DOM.
			domAttrValue = node.getAttribute( attrName );

			// The value to be set.
			attrValue = this.attributes[ attrName ];

			// Save revert data.
			if ( revertData ) {
				revertData.attributes[ attrName ] = domAttrValue;
			}

			// Detect custom namespace:
			//
			//		class: {
			//			ns: 'abc',
			//			value: Template.bind( ... ).to( ... )
			//		}
			//
			attrNs = ( isObject( attrValue[ 0 ] ) && attrValue[ 0 ].ns ) ? attrValue[ 0 ].ns : null;

			// Activate binding if one is found. Cases:
			//
			//		class: [
			//			Template.bind( ... ).to( ... )
			//		]
			//
			//		class: [
			//			'bar',
			//			Template.bind( ... ).to( ... ),
			//			'baz'
			//		]
			//
			//		class: {
			//			ns: 'abc',
			//			value: Template.bind( ... ).to( ... )
			//		}
			//
			if ( hasTemplateBinding( attrValue ) ) {
				// Normalize attributes with additional data like namespace:
				//
				//		class: {
				//			ns: 'abc',
				//			value: [ ... ]
				//		}
				//
				const valueToBind = attrNs ? attrValue[ 0 ].value : attrValue;

				// Extend the original value of attributes like "style" and "class",
				// don't override them.
				if ( revertData && shouldExtend( attrName ) ) {
					valueToBind.unshift( domAttrValue );
				}

				this._bindToObservable( {
					schema: valueToBind,
					updater: getAttributeUpdater( node, attrName, attrNs ),
					data
				} );
			}

			// Style attribute could be an Object so it needs to be parsed in a specific way.
			//
			//		style: {
			//			width: '100px',
			//			height: Template.bind( ... ).to( ... )
			//		}
			//
			else if ( attrName == 'style' && typeof attrValue[ 0 ] !== 'string' ) {
				this._renderStyleAttribute( attrValue[ 0 ], data );
			}

			// Otherwise simply set the static attribute:
			//
			//		class: [ 'foo' ]
			//
			//		class: [ 'all', 'are', 'static' ]
			//
			//		class: [
			//			{
			//				ns: 'abc',
			//				value: [ 'foo' ]
			//			}
			//		]
			//
			else {
				// Extend the original value of attributes like "style" and "class",
				// don't override them.
				if ( revertData && domAttrValue && shouldExtend( attrName ) ) {
					attrValue.unshift( domAttrValue );
				}

				attrValue = attrValue
					// Retrieve "values" from:
					//
					//		class: [
					//			{
					//				ns: 'abc',
					//				value: [ ... ]
					//			}
					//		]
					//
					.map( val => val ? ( val.value || val ) : val )
					// Flatten the array.
					.reduce( ( prev, next ) => prev.concat( next ), [] )
					// Convert into string.
					.reduce( arrayValueReducer, '' );

				if ( !isFalsy( attrValue ) ) {
					node.setAttributeNS( attrNs, attrName, attrValue );
				}
			}
		}
	}

	/**
	 * Renders the `style` attribute of an HTML element based on
	 * {@link module:ui/template~Template#attributes}.
	 *
	 * A style attribute is an {Object} with static values:
	 *
	 *		attributes: {
	 *			style: {
	 *				color: 'red'
	 *			}
	 *		}
	 *
	 * or values bound to {@link module:ui/model~Model} properties:
	 *
	 *		attributes: {
	 *			style: {
	 *				color: bind.to( ... )
	 *			}
	 *		}
	 *
	 * Note: The `style` attribute is rendered without setting the namespace. It does not seem to be
	 * needed.
	 *
	 * @private
	 * @param {Object} styles Styles located in `attributes.style` of {@link module:ui/template~TemplateDefinition}.
	 * @param {module:ui/template~RenderData} data Rendering data.
	 */
	_renderStyleAttribute( styles, data ) {
		const node = data.node;

		for ( const styleName in styles ) {
			const styleValue = styles[ styleName ];

			// Cases:
			//
			//		style: {
			//			color: bind.to( 'attribute' )
			//		}
			//
			if ( hasTemplateBinding( styleValue ) ) {
				this._bindToObservable( {
					schema: [ styleValue ],
					updater: getStyleUpdater( node, styleName ),
					data
				} );
			}

			// Cases:
			//
			//		style: {
			//			color: 'red'
			//		}
			//
			else {
				node.style[ styleName ] = styleValue;
			}
		}
	}

	/**
	 * Recursively renders HTML element's children from {@link module:ui/template~Template#children}.
	 *
	 * @protected
	 * @param {module:ui/template~RenderData} data Rendering data.
	 */
	_renderElementChildren( data ) {
		const node = data.node;
		const container = data.intoFragment ? document.createDocumentFragment() : node;
		const isApplying = data.isApplying;
		let childIndex = 0;

		for ( const child of this.children ) {
			if ( isViewCollection( child ) ) {
				if ( !isApplying ) {
					child.setParent( node );

					// Note: ViewCollection renders its children.
					for ( const view of child ) {
						container.appendChild( view.element );
					}
				}
			} else if ( isView( child ) ) {
				if ( !isApplying ) {
					if ( !child.isRendered ) {
						child.render();
					}

					container.appendChild( child.element );
				}
			} else if ( isNode( child ) ) {
				container.appendChild( child );
			} else {
				if ( isApplying ) {
					const revertData = data.revertData;
					const childRevertData = getEmptyRevertData();

					revertData.children.push( childRevertData );

					child._renderNode( {
						node: container.childNodes[ childIndex++ ],
						isApplying: true,
						revertData: childRevertData
					} );
				} else {
					container.appendChild( child.render() );
				}
			}
		}

		if ( data.intoFragment ) {
			node.appendChild( container );
		}
	}

	/**
	 * Activates `on` event listeners from the {@link module:ui/template~TemplateDefinition}
	 * on an HTML element.
	 *
	 * @protected
	 * @param {module:ui/template~RenderData} data Rendering data.
	 */
	_setUpListeners( data ) {
		if ( !this.eventListeners ) {
			return;
		}

		for ( const key in this.eventListeners ) {
			const revertBindings = this.eventListeners[ key ].map( schemaItem => {
				const [ domEvtName, domSelector ] = key.split( '@' );

				return schemaItem.activateDomEventListener( domEvtName, domSelector, data );
			} );

			if ( data.revertData ) {
				data.revertData.bindings.push( revertBindings );
			}
		}
	}

	/**
	 * For a given {@link module:ui/template~TemplateValueSchema} containing {@link module:ui/template~TemplateBinding}
	 * activates the binding and sets its initial value.
	 *
	 * Note: {@link module:ui/template~TemplateValueSchema} can be for HTML element attributes or
	 * text node `textContent`.
	 *
	 * @protected
	 * @param {Object} options Binding options.
	 * @param {module:ui/template~TemplateValueSchema} options.schema
	 * @param {Function} options.updater A function which updates the DOM (like attribute or text).
	 * @param {module:ui/template~RenderData} options.data Rendering data.
	 */
	_bindToObservable( { schema, updater, data } ) {
		const revertData = data.revertData;

		// Set initial values.
		syncValueSchemaValue( schema, updater, data );

		const revertBindings = schema
			// Filter "falsy" (false, undefined, null, '') value schema components out.
			.filter( item => !isFalsy( item ) )
			// Filter inactive bindings from schema, like static strings ('foo'), numbers (42), etc.
			.filter( item => item.observable )
			// Once only the actual binding are left, let the emitter listen to observable change:attribute event.
			// TODO: Reduce the number of listeners attached as many bindings may listen
			// to the same observable attribute.
			.map( templateBinding => templateBinding.activateAttributeListener( schema, updater, data ) );

		if ( revertData ) {
			revertData.bindings.push( revertBindings );
		}
	}

	/**
	 * Reverts {@link module:ui/template~RenderData#revertData template data} from a node to
	 * return it to the original state.
	 *
	 * @protected
	 * @param {HTMLElement|Text} node A node to be reverted.
	 * @param {Object} revertData An object that stores information about what changes have been made by
	 * {@link #apply} to the node. See {@link module:ui/template~RenderData#revertData} for more information.
	 */
	_revertTemplateFromNode( node, revertData ) {
		for ( const binding of revertData.bindings ) {
			// Each binding may consist of several observable+observable#attribute.
			// like the following has 2:
			//
			//		class: [
			//			'x',
			//			bind.to( 'foo' ),
			//			'y',
			//			bind.to( 'bar' )
			//		]
			//
			for ( const revertBinding of binding ) {
				revertBinding();
			}
		}

		if ( revertData.text ) {
			node.textContent = revertData.text;

			return;
		}

		for ( const attrName in revertData.attributes ) {
			const attrValue = revertData.attributes[ attrName ];

			// When the attribute has **not** been set before #apply().
			if ( attrValue === null ) {
				node.removeAttribute( attrName );
			} else {
				node.setAttribute( attrName, attrValue );
			}
		}

		for ( let i = 0; i < revertData.children.length; ++i ) {
			this._revertTemplateFromNode( node.childNodes[ i ], revertData.children[ i ] );
		}
	}
}

mix( Template, EmitterMixin );

/**
 * Describes a binding created by the {@link module:ui/template~Template.bind} interface.
 *
 * @protected
 */
class TemplateBinding {
	/**
	 * Creates an instance of the {@link module:ui/template~TemplateBinding} class.
	 *
	 * @param {module:ui/template~TemplateDefinition} def The definition of the binding.
	 */
	constructor( def ) {
		Object.assign( this, def );

		/**
		 * An observable instance of the binding. It either:
		 *
		 * * provides the attribute with the value,
		 * * or passes the event when a corresponding DOM event is fired.
		 *
		 * @member {module:utils/observablemixin~ObservableMixin} module:ui/template~TemplateBinding#observable
		 */

		/**
		 * An {@link module:utils/emittermixin~Emitter} used by the binding to:
		 *
		 * * listen to the attribute change in the {@link module:ui/template~TemplateBinding#observable},
		 * * or listen to the event in the DOM.
		 *
		 * @member {module:utils/emittermixin~EmitterMixin} module:ui/template~TemplateBinding#emitter
		 */

		/**
		 * The name of the {@link module:ui/template~TemplateBinding#observable observed attribute}.
		 *
		 * @member {String} module:ui/template~TemplateBinding#attribute
		 */

		/**
		 * A custom function to process the value of the {@link module:ui/template~TemplateBinding#attribute}.
		 *
		 * @member {Function} [module:ui/template~TemplateBinding#callback]
		 */
	}

	/**
	 * Returns the value of the binding. It is the value of the {@link module:ui/template~TemplateBinding#attribute} in
	 * {@link module:ui/template~TemplateBinding#observable}. The value may be processed by the
	 * {@link module:ui/template~TemplateBinding#callback}, if such has been passed to the binding.
	 *
	 * @param {Node} [node] A native DOM node, passed to the custom {@link module:ui/template~TemplateBinding#callback}.
	 * @returns {*} The value of {@link module:ui/template~TemplateBinding#attribute} in
	 * {@link module:ui/template~TemplateBinding#observable}.
	 */
	getValue( node ) {
		const value = this.observable[ this.attribute ];

		return this.callback ? this.callback( value, node ) : value;
	}

	/**
	 * Activates the listener which waits for changes of the {@link module:ui/template~TemplateBinding#attribute} in
	 * {@link module:ui/template~TemplateBinding#observable}, then updates the DOM with the aggregated
	 * value of {@link module:ui/template~TemplateValueSchema}.
	 *
	 * @param {module:ui/template~TemplateValueSchema} schema A full schema to generate an attribute or text in the DOM.
	 * @param {Function} updater A DOM updater function used to update the native DOM attribute or text.
	 * @param {module:ui/template~RenderData} data Rendering data.
	 * @returns {Function} A function to sever the listener binding.
	 */
	activateAttributeListener( schema, updater, data ) {
		const callback = () => syncValueSchemaValue( schema, updater, data );

		this.emitter.listenTo( this.observable, 'change:' + this.attribute, callback );

		// Allows revert of the listener.
		return () => {
			this.emitter.stopListening( this.observable, 'change:' + this.attribute, callback );
		};
	}
}

/**
 * Describes either:
 *
 * * a binding to an {@link module:utils/observablemixin~Observable},
 * * or a native DOM event binding.
 *
 * It is created by the {@link module:ui/template~BindChain#to} method.
 *
 * @protected
 */
class TemplateToBinding extends TemplateBinding {
	/**
	 * Activates the listener for the native DOM event, which when fired, is propagated by
	 * the {@link module:ui/template~TemplateBinding#emitter}.
	 *
	 * @param {String} domEvtName The name of the native DOM event.
	 * @param {String} domSelector The selector in the DOM to filter delegated events.
	 * @param {module:ui/template~RenderData} data Rendering data.
	 * @returns {Function} A function to sever the listener binding.
	 */
	activateDomEventListener( domEvtName, domSelector, data ) {
		const callback = ( evt, domEvt ) => {
			if ( !domSelector || domEvt.target.matches( domSelector ) ) {
				if ( typeof this.eventNameOrFunction == 'function' ) {
					this.eventNameOrFunction( domEvt );
				} else {
					this.observable.fire( this.eventNameOrFunction, domEvt );
				}
			}
		};

		this.emitter.listenTo( data.node, domEvtName, callback );

		// Allows revert of the listener.
		return () => {
			this.emitter.stopListening( data.node, domEvtName, callback );
		};
	}
}

/**
 * Describes a binding to {@link module:utils/observablemixin~ObservableMixin} created by the {@link module:ui/template~BindChain#if}
 * method.
 *
 * @protected
 */
class TemplateIfBinding extends TemplateBinding {
	/**
	 * @inheritDoc
	 */
	getValue( node ) {
		const value = super.getValue( node );

		return isFalsy( value ) ? false : ( this.valueIfTrue || true );
	}

	/**
	 * The value of the DOM attribute or text to be set if the {@link module:ui/template~TemplateBinding#attribute} in
	 * {@link module:ui/template~TemplateBinding#observable} is `true`.
	 *
	 * @member {String} [module:ui/template~TemplateIfBinding#valueIfTrue]
	 */
}

// Checks whether given {@link module:ui/template~TemplateValueSchema} contains a
// {@link module:ui/template~TemplateBinding}.
//
// @param {module:ui/template~TemplateValueSchema} schema
// @returns {Boolean}
function hasTemplateBinding( schema ) {
	if ( !schema ) {
		return false;
	}

	// Normalize attributes with additional data like namespace:
	//
	//		class: {
	//			ns: 'abc',
	//			value: [ ... ]
	//		}
	//
	if ( schema.value ) {
		schema = schema.value;
	}

	if ( Array.isArray( schema ) ) {
		return schema.some( hasTemplateBinding );
	} else if ( schema instanceof TemplateBinding ) {
		return true;
	}

	return false;
}

// Assembles the value using {@link module:ui/template~TemplateValueSchema} and stores it in a form of
// an Array. Each entry of the Array corresponds to one of {@link module:ui/template~TemplateValueSchema}
// items.
//
// @param {module:ui/template~TemplateValueSchema} schema
// @param {Node} node DOM Node updated when {@link module:utils/observablemixin~ObservableMixin} changes.
// @returns {Array}
function getValueSchemaValue( schema, node ) {
	return schema.map( schemaItem => {
		// Process {@link module:ui/template~TemplateBinding} bindings.
		if ( schemaItem instanceof TemplateBinding ) {
			return schemaItem.getValue( node );
		}

		// All static values like strings, numbers, and "falsy" values (false, null, undefined, '', etc.) just pass.
		return schemaItem;
	} );
}

// A function executed each time the bound Observable attribute changes, which updates the DOM with a value
// constructed from {@link module:ui/template~TemplateValueSchema}.
//
// @param {module:ui/template~TemplateValueSchema} schema
// @param {Function} updater A function which updates the DOM (like attribute or text).
// @param {Node} node DOM Node updated when {@link module:utils/observablemixin~ObservableMixin} changes.
function syncValueSchemaValue( schema, updater, { node } ) {
	let value = getValueSchemaValue( schema, node );

	// Check if schema is a single Template.bind.if, like:
	//
	//		class: Template.bind.if( 'foo' )
	//
	if ( schema.length == 1 && schema[ 0 ] instanceof TemplateIfBinding ) {
		value = value[ 0 ];
	} else {
		value = value.reduce( arrayValueReducer, '' );
	}

	if ( isFalsy( value ) ) {
		updater.remove();
	} else {
		updater.set( value );
	}
}

// Returns an object consisting of `set` and `remove` functions, which
// can be used in the context of DOM Node to set or reset `textContent`.
// @see module:ui/view~View#_bindToObservable
//
// @param {Node} node DOM Node to be modified.
// @returns {Object}
function getTextUpdater( node ) {
	return {
		set( value ) {
			node.textContent = value;
		},

		remove() {
			node.textContent = '';
		}
	};
}

// Returns an object consisting of `set` and `remove` functions, which
// can be used in the context of DOM Node to set or reset an attribute.
// @see module:ui/view~View#_bindToObservable
//
// @param {Node} node DOM Node to be modified.
// @param {String} attrName Name of the attribute to be modified.
// @param {String} [ns=null] Namespace to use.
// @returns {Object}
function getAttributeUpdater( el, attrName, ns ) {
	return {
		set( value ) {
			el.setAttributeNS( ns, attrName, value );
		},

		remove() {
			el.removeAttributeNS( ns, attrName );
		}
	};
}

// Returns an object consisting of `set` and `remove` functions, which
// can be used in the context of CSSStyleDeclaration to set or remove a style.
// @see module:ui/view~View#_bindToObservable
//
// @param {Node} node DOM Node to be modified.
// @param {String} styleName Name of the style to be modified.
// @returns {Object}
function getStyleUpdater( el, styleName ) {
	return {
		set( value ) {
			el.style[ styleName ] = value;
		},

		remove() {
			el.style[ styleName ] = null;
		}
	};
}

// Clones definition of the template.
//
// @param {module:ui/template~TemplateDefinition} def
// @returns {module:ui/template~TemplateDefinition}
function clone( def ) {
	const clone = cloneDeepWith( def, value => {
		// Don't clone the `Template.bind`* bindings because of the references to Observable
		// and DomEmitterMixin instances inside, which would also be traversed and cloned by greedy
		// cloneDeepWith algorithm. There's no point in cloning Observable/DomEmitterMixins
		// along with the definition.
		//
		// Don't clone Template instances if provided as a child. They're simply #render()ed
		// and nothing should interfere.
		//
		// Also don't clone View instances if provided as a child of the Template. The template
		// instance will be extracted from the View during the normalization and there's no need
		// to clone it.
		if ( value && ( value instanceof TemplateBinding || isTemplate( value ) || isView( value ) || isViewCollection( value ) ) ) {
			return value;
		}
	} );

	return clone;
}

// Normalizes given {@link module:ui/template~TemplateDefinition}.
//
// See:
//  * {@link normalizeAttributes}
//  * {@link normalizeListeners}
//  * {@link normalizePlainTextDefinition}
//  * {@link normalizeTextDefinition}
//
// @param {module:ui/template~TemplateDefinition} def
// @returns {module:ui/template~TemplateDefinition} Normalized definition.
function normalize( def ) {
	if ( typeof def == 'string' ) {
		def = normalizePlainTextDefinition( def );
	} else if ( def.text ) {
		normalizeTextDefinition( def );
	}

	if ( def.on ) {
		def.eventListeners = normalizeListeners( def.on );

		// Template mixes EmitterMixin, so delete #on to avoid collision.
		delete def.on;
	}

	if ( !def.text ) {
		if ( def.attributes ) {
			normalizeAttributes( def.attributes );
		}

		const children = [];

		if ( def.children ) {
			if ( isViewCollection( def.children ) ) {
				children.push( def.children );
			} else {
				for ( const child of def.children ) {
					if ( isTemplate( child ) || isView( child ) || isNode( child ) ) {
						children.push( child );
					} else {
						children.push( new Template( child ) );
					}
				}
			}
		}

		def.children = children;
	}

	return def;
}

// Normalizes "attributes" section of {@link module:ui/template~TemplateDefinition}.
//
//		attributes: {
//			a: 'bar',
//			b: {@link module:ui/template~TemplateBinding},
//			c: {
//				value: 'bar'
//			}
//		}
//
// becomes
//
//		attributes: {
//			a: [ 'bar' ],
//			b: [ {@link module:ui/template~TemplateBinding} ],
//			c: {
//				value: [ 'bar' ]
//			}
//		}
//
// @param {Object} attributes
function normalizeAttributes( attributes ) {
	for ( const a in attributes ) {
		if ( attributes[ a ].value ) {
			attributes[ a ].value = toArray( attributes[ a ].value );
		}

		arrayify( attributes, a );
	}
}

// Normalizes "on" section of {@link module:ui/template~TemplateDefinition}.
//
//		on: {
//			a: 'bar',
//			b: {@link module:ui/template~TemplateBinding},
//			c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
//		}
//
// becomes
//
//		on: {
//			a: [ 'bar' ],
//			b: [ {@link module:ui/template~TemplateBinding} ],
//			c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
//		}
//
// @param {Object} listeners
// @returns {Object} Object containing normalized listeners.
function normalizeListeners( listeners ) {
	for ( const l in listeners ) {
		arrayify( listeners, l );
	}

	return listeners;
}

// Normalizes "string" {@link module:ui/template~TemplateDefinition}.
//
//		"foo"
//
// becomes
//
//		{ text: [ 'foo' ] },
//
// @param {String} def
// @returns {module:ui/template~TemplateDefinition} Normalized template definition.
function normalizePlainTextDefinition( def ) {
	return {
		text: [ def ]
	};
}

// Normalizes text {@link module:ui/template~TemplateDefinition}.
//
//		children: [
//			{ text: 'def' },
//			{ text: {@link module:ui/template~TemplateBinding} }
//		]
//
// becomes
//
//		children: [
//			{ text: [ 'def' ] },
//			{ text: [ {@link module:ui/template~TemplateBinding} ] }
//		]
//
// @param {module:ui/template~TemplateDefinition} def
function normalizeTextDefinition( def ) {
	def.text = toArray( def.text );
}

// Wraps an entry in Object in an Array, if not already one.
//
//		{
//			x: 'y',
//			a: [ 'b' ]
//		}
//
// becomes
//
//		{
//			x: [ 'y' ],
//			a: [ 'b' ]
//		}
//
// @param {Object} obj
// @param {String} key
function arrayify( obj, key ) {
	obj[ key ] = toArray( obj[ key ] );
}

// A helper which concatenates the value avoiding unwanted
// leading white spaces.
//
// @param {String} prev
// @param {String} cur
// @returns {String}
function arrayValueReducer( prev, cur ) {
	if ( isFalsy( cur ) ) {
		return prev;
	} else if ( isFalsy( prev ) ) {
		return cur;
	} else {
		return `${ prev } ${ cur }`;
	}
}

// Extends one object defined in the following format:
//
//		{
//			key1: [Array1],
//			key2: [Array2],
//			...
//			keyN: [ArrayN]
//		}
//
// with another object of the same data format.
//
// @param {Object} obj Base object.
// @param {Object} ext Object extending base.
// @returns {String}
function extendObjectValueArray( obj, ext ) {
	for ( const a in ext ) {
		if ( obj[ a ] ) {
			obj[ a ].push( ...ext[ a ] );
		} else {
			obj[ a ] = ext[ a ];
		}
	}
}

// A helper for {@link module:ui/template~Template#extend}. Recursively extends {@link module:ui/template~Template} instance
// with content from {@link module:ui/template~TemplateDefinition}. See {@link module:ui/template~Template#extend} to learn more.
//
// @param {module:ui/template~Template} def A template instance to be extended.
// @param {module:ui/template~TemplateDefinition} def A definition which is to extend the template instance.
// @param {Object} Error context.
function extendTemplate( template, def ) {
	if ( def.attributes ) {
		if ( !template.attributes ) {
			template.attributes = {};
		}

		extendObjectValueArray( template.attributes, def.attributes );
	}

	if ( def.eventListeners ) {
		if ( !template.eventListeners ) {
			template.eventListeners = {};
		}

		extendObjectValueArray( template.eventListeners, def.eventListeners );
	}

	if ( def.text ) {
		template.text.push( ...def.text );
	}

	if ( def.children && def.children.length ) {
		if ( template.children.length != def.children.length ) {
			/**
			 * The number of children in extended definition does not match.
			 *
			 * @error ui-template-extend-children-mismatch
			 */
			throw new CKEditorError(
				'ui-template-extend-children-mismatch',
				template
			);
		}

		let childIndex = 0;

		for ( const childDef of def.children ) {
			extendTemplate( template.children[ childIndex++ ], childDef );
		}
	}
}

// Checks if value is "falsy".
// Note: 0 (Number) is not "falsy" in this context.
//
// @private
// @param {*} value Value to be checked.
function isFalsy( value ) {
	return !value && value !== 0;
}

// Checks if the item is an instance of {@link module:ui/view~View}
//
// @private
// @param {*} value Value to be checked.
function isView( item ) {
	return item instanceof View;
}

// Checks if the item is an instance of {@link module:ui/template~Template}
//
// @private
// @param {*} value Value to be checked.
function isTemplate( item ) {
	return item instanceof Template;
}

// Checks if the item is an instance of {@link module:ui/viewcollection~ViewCollection}
//
// @private
// @param {*} value Value to be checked.
function isViewCollection( item ) {
	return item instanceof ViewCollection;
}

// Creates an empty skeleton for {@link module:ui/template~Template#revert}
// data.
//
// @private
function getEmptyRevertData() {
	return {
		children: [],
		bindings: [],
		attributes: {}
	};
}

// Checks whether an attribute should be extended when
// {@link module:ui/template~Template#apply} is called.
//
// @private
// @param {String} attrName Attribute name to check.
function shouldExtend( attrName ) {
	return attrName == 'class' || attrName == 'style';
}

/**
 * A definition of the {@link module:ui/template~Template}. It describes what kind of
 * node a template will render (HTML element or text), attributes of an element, DOM event
 * listeners and children.
 *
 * Also see:
 * * {@link module:ui/template~TemplateValueSchema} to learn about HTML element attributes,
 * * {@link module:ui/template~TemplateListenerSchema} to learn about DOM event listeners.
 *
 * A sample definition on an HTML element can look like this:
 *
 *		new Template( {
 *			tag: 'p',
 *			children: [
 *				{
 *					tag: 'span',
 *					attributes: { ... },
 *					children: [ ... ],
 *				},
 *				{
 *					text: 'static–text'
 *				},
 *				'also-static–text',
 *			],
 *			attributes: {
 *				class: {@link module:ui/template~TemplateValueSchema},
 *				id: {@link module:ui/template~TemplateValueSchema},
 *				style: {@link module:ui/template~TemplateValueSchema}
 *
 *				// ...
 *			},
 *			on: {
 *				'click': {@link module:ui/template~TemplateListenerSchema}
 *
 *				// Document.querySelector format is also accepted.
 *				'keyup@a.some-class': {@link module:ui/template~TemplateListenerSchema}
 *
 *				// ...
 *			}
 *		} );
 *
 * A {@link module:ui/view~View}, another {@link module:ui/template~Template} or a native DOM node
 * can also become a child of a template. When a view is passed, its {@link module:ui/view~View#element} is used:
 *
 *		const view = new SomeView();
 *		const childTemplate = new Template( { ... } );
 *		const childNode = document.createElement( 'b' );
 *
 *		new Template( {
 *			tag: 'p',
 *
 *			children: [
 *				// view#element will be added as a child of this <p>.
 *				view,
 *
 * 				// The output of childTemplate.render() will be added here.
 *				childTemplate,
 *
 *				// Native DOM nodes are included directly in the rendered output.
 *				childNode
 *			]
 *		} );
 *
 * An entire {@link module:ui/viewcollection~ViewCollection} can be used as a child in the definition:
 *
 *		const collection = new ViewCollection();
 *		collection.add( someView );
 *
 *		new Template( {
 *			tag: 'p',
 *
 *			children: collection
 *		} );
 *
 * @typedef module:ui/template~TemplateDefinition
 * @type Object
 *
 * @property {String} tag See the template {@link module:ui/template~Template#tag} property.
 *
 * @property {Array.<module:ui/template~TemplateDefinition>} [children]
 * See the template {@link module:ui/template~Template#children} property.
 *
 * @property {Object.<String, module:ui/template~TemplateValueSchema>} [attributes]
 * See the template {@link module:ui/template~Template#attributes} property.
 *
 * @property {String|module:ui/template~TemplateValueSchema|Array.<String|module:ui/template~TemplateValueSchema>} [text]
 * See the template {@link module:ui/template~Template#text} property.
 *
 * @property {Object.<String, module:ui/template~TemplateListenerSchema>} [on]
 * See the template {@link module:ui/template~Template#eventListeners} property.
 */

/**
 * Describes a value of an HTML element attribute or `textContent`. It allows combining multiple
 * data sources like static values and {@link module:utils/observablemixin~Observable} attributes.
 *
 * Also see:
 * * {@link module:ui/template~TemplateDefinition} to learn where to use it,
 * * {@link module:ui/template~Template.bind} to learn how to configure
 * {@link module:utils/observablemixin~Observable} attribute bindings,
 * * {@link module:ui/template~Template#render} to learn how to render a template,
 * * {@link module:ui/template~BindChain#to `to()`} and {@link module:ui/template~BindChain#if `if()`}
 * methods to learn more about bindings.
 *
 * Attribute values can be described in many different ways:
 *
 *		// Bind helper will create bindings to attributes of the observable.
 *		const bind = Template.bind( observable, emitter );
 *
 *		new Template( {
 *			tag: 'p',
 *			attributes: {
 *				// A plain string schema.
 *				'class': 'static-text',
 *
 *				// An object schema, binds to the "foo" attribute of the
 *				// observable and follows its value.
 *				'class': bind.to( 'foo' ),
 *
 *				// An array schema, combines the above.
 *				'class': [
 *					'static-text',
 *					bind.to( 'bar', () => { ... } ),
 *
 * 					// Bindings can also be conditional.
 *					bind.if( 'baz', 'class-when-baz-is-true' )
 *				],
 *
 *				// An array schema, with a custom namespace, e.g. useful for creating SVGs.
 *				'class': {
 *					ns: 'http://ns.url',
 *					value: [
 *						bind.if( 'baz', 'value-when-true' ),
 *						'static-text'
 *					]
 *				},
 *
 *				// An object schema, specific for styles.
 *				style: {
 *					color: 'red',
 *					backgroundColor: bind.to( 'qux', () => { ... } )
 *				}
 *			}
 *		} );
 *
 * Text nodes can also have complex values:
 *
 *		const bind = Template.bind( observable, emitter );
 *
 *		// Will render a "foo" text node.
 *		new Template( {
 *			text: 'foo'
 *		} );
 *
 *		// Will render a "static text: {observable.foo}" text node.
 *		// The text of the node will be updated as the "foo" attribute changes.
 *		new Template( {
 *			text: [
 *				'static text: ',
 *				bind.to( 'foo', () => { ... } )
 *			]
 *		} );
 *
 * @typedef module:ui/template~TemplateValueSchema
 * @type {Object|String|Array}
 */

/**
 * Describes an event listener attached to an HTML element. Such listener can propagate DOM events
 * through an {@link module:utils/observablemixin~Observable} instance, execute custom callbacks
 * or both, if necessary.
 *
 * Also see:
 * * {@link module:ui/template~TemplateDefinition} to learn more about template definitions,
 * * {@link module:ui/template~BindChain#to `to()`} method to learn more about bindings.
 *
 * Check out different ways of attaching event listeners below:
 *
 *		// Bind helper will propagate events through the observable.
 *		const bind = Template.bind( observable, emitter );
 *
 *		new Template( {
 *			tag: 'p',
 *			on: {
 *				// An object schema. The observable will fire the "clicked" event upon DOM "click".
 *				click: bind.to( 'clicked' )
 *
 *				// An object schema. It will work for "click" event on "a.foo" children only.
 *				'click@a.foo': bind.to( 'clicked' )
 *
 *				// An array schema, makes the observable propagate multiple events.
 *				click: [
 *					bind.to( 'clicked' ),
 *					bind.to( 'executed' )
 *				],
 *
 *				// An array schema with a custom callback.
 *				'click@a.foo': {
 *					bind.to( 'clicked' ),
 *					bind.to( evt => {
 *						console.log( `${ evt.target } has been clicked!` );
 *					} }
 *				}
 *			}
 *		} );
 *
 * @typedef module:ui/template~TemplateListenerSchema
 * @type {Object|String|Array}
 */

/**
 * The return value of {@link ~Template.bind `Template.bind()`}. It provides `to()` and `if()`
 * methods to create the {@link module:utils/observablemixin~Observable observable} attribute and event bindings.
 *
 * @interface module:ui/template~BindChain
 */

/**
 * Binds an {@link module:utils/observablemixin~Observable observable} to either:
 *
 * * an HTML element attribute or a text node `textContent`, so it remains in sync with the observable
 * attribute as it changes,
 * * or an HTML element DOM event, so the DOM events are propagated through an observable.
 *
 * Some common use cases of `to()` bindings are presented below:
 *
 *		const bind = Template.bind( observable, emitter );
 *
 *		new Template( {
 *			tag: 'p',
 *			attributes: {
 *				// class="..." attribute gets bound to `observable#a`
 *				class: bind.to( 'a' )
 *			},
 *			children: [
 *				// <p>...</p> gets bound to observable#b; always `toUpperCase()`.
 *				{
 *					text: bind.to( 'b', ( value, node ) => value.toUpperCase() )
 *				}
 *			],
 *			on: {
 *				click: [
 *					// An observable will fire "clicked" upon "click" in the DOM.
 *					bind.to( 'clicked' ),
 *
 *					// A custom callback will be executed upon "click" in the DOM.
 *					bind.to( () => {
 *						...
 *					} )
 *				]
 *			}
 *		} ).render();
 *
 * Learn more about using `to()` in the {@link module:ui/template~TemplateValueSchema} and
 * {@link module:ui/template~TemplateListenerSchema}.
 *
 * @method #to
 * @param {String|Function} eventNameOrFunctionOrAttribute An attribute name of
 * {@link module:utils/observablemixin~Observable} or a DOM event name or an event callback.
 * @param {Function} [callback] Allows for processing of the value. Accepts `Node` and `value` as arguments.
 * @returns {module:ui/template~TemplateBinding}
 */

/**
 * Binds an {@link module:utils/observablemixin~Observable observable} to an HTML element attribute or a text
 * node `textContent` so it remains in sync with the observable attribute as it changes.
 *
 * Unlike {@link module:ui/template~BindChain#to}, it controls the presence of the attribute or `textContent`
 * depending on the "falseness" of an {@link module:utils/observablemixin~Observable} attribute.
 *
 *		const bind = Template.bind( observable, emitter );
 *
 *		new Template( {
 *			tag: 'input',
 *			attributes: {
 *				// <input checked> when `observable#a` is not undefined/null/false/''
 *				// <input> when `observable#a` is undefined/null/false
 *				checked: bind.if( 'a' )
 *			},
 *			children: [
 *				{
 *					// <input>"b-is-not-set"</input> when `observable#b` is undefined/null/false/''
 *					// <input></input> when `observable#b` is not "falsy"
 *					text: bind.if( 'b', 'b-is-not-set', ( value, node ) => !value )
 *				}
 *			]
 *		} ).render();
 *
 * Learn more about using `if()` in the {@link module:ui/template~TemplateValueSchema}.
 *
 * @method #if
 * @param {String} attribute An attribute name of {@link module:utils/observablemixin~Observable} used in the binding.
 * @param {String} [valueIfTrue] Value set when the {@link module:utils/observablemixin~Observable} attribute is not
 * undefined/null/false/'' (empty string).
 * @param {Function} [callback] Allows for processing of the value. Accepts `Node` and `value` as arguments.
 * @returns {module:ui/template~TemplateBinding}
 */

/**
 * The {@link module:ui/template~Template#_renderNode} configuration.
 *
 * @private
 * @interface module:ui/template~RenderData
 */

/**
 * Tells {@link module:ui/template~Template#_renderNode} to render
 * children into `DocumentFragment` first and then append the fragment
 * to the parent element. It is a speed optimization.
 *
 * @member {Boolean} #intoFragment
 */

/**
 * A node which is being rendered.
 *
 * @member {HTMLElement|Text} #node
 */

/**
 * Indicates whether the {@module:ui/template~RenderNodeOptions#node} has
 * been provided by {@module:ui/template~Template#apply}.
 *
 * @member {Boolean} #isApplying
 */

/**
 * An object storing the data that helps {@module:ui/template~Template#revert}
 * bringing back an element to its initial state, i.e. before
 * {@module:ui/template~Template#apply} was called.
 *
 * @member {Object} #revertData
 */var css_248z$V = ".ck-hidden{display:none!important}.ck-reset_all :not(.ck-reset_all-excluded *),.ck.ck-reset,.ck.ck-reset_all{box-sizing:border-box;height:auto;position:static;width:auto}:root{--ck-z-default:1;--ck-z-modal:calc(var(--ck-z-default) + 999)}.ck-transitions-disabled,.ck-transitions-disabled *{transition:none!important}:root{--ck-color-base-foreground:#fafafa;--ck-color-base-background:#fff;--ck-color-base-border:#ccced1;--ck-color-base-action:#53a336;--ck-color-base-focus:#6cb5f9;--ck-color-base-text:#333;--ck-color-base-active:#2977ff;--ck-color-base-active-focus:#0d65ff;--ck-color-base-error:#db3700;--ck-color-focus-border-coordinates:218,81.8%,56.9%;--ck-color-focus-border:hsl(var(--ck-color-focus-border-coordinates));--ck-color-focus-outer-shadow:#cae1fc;--ck-color-focus-disabled-shadow:rgba(119,186,248,.3);--ck-color-focus-error-shadow:rgba(255,64,31,.3);--ck-color-text:var(--ck-color-base-text);--ck-color-shadow-drop:rgba(0,0,0,.15);--ck-color-shadow-drop-active:rgba(0,0,0,.2);--ck-color-shadow-inner:rgba(0,0,0,.1);--ck-color-button-default-background:transparent;--ck-color-button-default-hover-background:#f0f0f0;--ck-color-button-default-active-background:#f0f0f0;--ck-color-button-default-disabled-background:transparent;--ck-color-button-on-background:#f0f7ff;--ck-color-button-on-hover-background:#dbecff;--ck-color-button-on-active-background:#dbecff;--ck-color-button-on-disabled-background:#f0f2f4;--ck-color-button-on-color:#2977ff;--ck-color-button-action-background:var(--ck-color-base-action);--ck-color-button-action-hover-background:#4d9d30;--ck-color-button-action-active-background:#4d9d30;--ck-color-button-action-disabled-background:#7ec365;--ck-color-button-action-text:var(--ck-color-base-background);--ck-color-button-save:#008a00;--ck-color-button-cancel:#db3700;--ck-color-switch-button-off-background:#939393;--ck-color-switch-button-off-hover-background:#7d7d7d;--ck-color-switch-button-on-background:var(--ck-color-button-action-background);--ck-color-switch-button-on-hover-background:#4d9d30;--ck-color-switch-button-inner-background:var(--ck-color-base-background);--ck-color-switch-button-inner-shadow:rgba(0,0,0,.1);--ck-color-dropdown-panel-background:var(--ck-color-base-background);--ck-color-dropdown-panel-border:var(--ck-color-base-border);--ck-color-input-background:var(--ck-color-base-background);--ck-color-input-border:var(--ck-color-base-border);--ck-color-input-error-border:var(--ck-color-base-error);--ck-color-input-text:var(--ck-color-base-text);--ck-color-input-disabled-background:#f2f2f2;--ck-color-input-disabled-border:var(--ck-color-base-border);--ck-color-input-disabled-text:#757575;--ck-color-list-background:var(--ck-color-base-background);--ck-color-list-button-hover-background:var(--ck-color-button-default-hover-background);--ck-color-list-button-on-background:var(--ck-color-button-on-color);--ck-color-list-button-on-background-focus:var(--ck-color-button-on-color);--ck-color-list-button-on-text:var(--ck-color-base-background);--ck-color-panel-background:var(--ck-color-base-background);--ck-color-panel-border:var(--ck-color-base-border);--ck-color-toolbar-background:var(--ck-color-base-background);--ck-color-toolbar-border:var(--ck-color-base-border);--ck-color-tooltip-background:var(--ck-color-base-text);--ck-color-tooltip-text:var(--ck-color-base-background);--ck-color-engine-placeholder-text:#707070;--ck-color-upload-bar-background:#6cb5f9;--ck-color-link-default:#0000f0;--ck-color-link-selected-background:rgba(31,176,255,.1);--ck-color-link-fake-selection:rgba(31,176,255,.3);--ck-disabled-opacity:.5;--ck-focus-outer-shadow-geometry:0 0 0 3px;--ck-focus-outer-shadow:var(--ck-focus-outer-shadow-geometry) var(--ck-color-focus-outer-shadow);--ck-focus-disabled-outer-shadow:var(--ck-focus-outer-shadow-geometry) var(--ck-color-focus-disabled-shadow);--ck-focus-error-outer-shadow:var(--ck-focus-outer-shadow-geometry) var(--ck-color-focus-error-shadow);--ck-focus-ring:1px solid var(--ck-color-focus-border);--ck-font-size-base:13px;--ck-line-height-base:1.84615;--ck-font-face:Helvetica,Arial,Tahoma,Verdana,Sans-Serif;--ck-font-size-tiny:0.7em;--ck-font-size-small:0.75em;--ck-font-size-normal:1em;--ck-font-size-big:1.4em;--ck-font-size-large:1.8em;--ck-ui-component-min-height:2.3em}.ck-reset_all :not(.ck-reset_all-excluded *),.ck.ck-reset,.ck.ck-reset_all{word-wrap:break-word;background:transparent;border:0;margin:0;padding:0;text-decoration:none;transition:none;vertical-align:middle}.ck-reset_all :not(.ck-reset_all-excluded *),.ck.ck-reset_all{border-collapse:collapse;color:var(--ck-color-text);cursor:auto;float:none;font:normal normal normal var(--ck-font-size-base)/var(--ck-line-height-base) var(--ck-font-face);text-align:left;white-space:nowrap}.ck-reset_all .ck-rtl :not(.ck-reset_all-excluded *){text-align:right}.ck-reset_all iframe:not(.ck-reset_all-excluded *){vertical-align:inherit}.ck-reset_all textarea:not(.ck-reset_all-excluded *){white-space:pre-wrap}.ck-reset_all input[type=password]:not(.ck-reset_all-excluded *),.ck-reset_all input[type=text]:not(.ck-reset_all-excluded *),.ck-reset_all textarea:not(.ck-reset_all-excluded *){cursor:text}.ck-reset_all input[type=password][disabled]:not(.ck-reset_all-excluded *),.ck-reset_all input[type=text][disabled]:not(.ck-reset_all-excluded *),.ck-reset_all textarea[disabled]:not(.ck-reset_all-excluded *){cursor:default}.ck-reset_all fieldset:not(.ck-reset_all-excluded *){border:2px groove #dfdee3;padding:10px}.ck-reset_all button:not(.ck-reset_all-excluded *)::-moz-focus-inner{border:0;padding:0}.ck[dir=rtl],.ck[dir=rtl] .ck{text-align:right}:root{--ck-border-radius:2px;--ck-inner-shadow:2px 2px 3px var(--ck-color-shadow-inner) inset;--ck-drop-shadow:0 1px 2px 1px var(--ck-color-shadow-drop);--ck-drop-shadow-active:0 3px 6px 1px var(--ck-color-shadow-drop-active);--ck-spacing-unit:0.6em;--ck-spacing-large:calc(var(--ck-spacing-unit)*1.5);--ck-spacing-standard:var(--ck-spacing-unit);--ck-spacing-medium:calc(var(--ck-spacing-unit)*0.8);--ck-spacing-small:calc(var(--ck-spacing-unit)*0.5);--ck-spacing-tiny:calc(var(--ck-spacing-unit)*0.3);--ck-spacing-extra-tiny:calc(var(--ck-spacing-unit)*0.16)}";
styleInject(css_248z$V);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The basic view class, which represents an HTML element created out of a
 * {@link module:ui/view~View#template}. Views are building blocks of the user interface and handle
 * interaction
 *
 * Views {@link module:ui/view~View#registerChild aggregate} children in
 * {@link module:ui/view~View#createCollection collections} and manage the life cycle of DOM
 * listeners e.g. by handling rendering and destruction.
 *
 * See the {@link module:ui/template~TemplateDefinition} syntax to learn more about shaping view
 * elements, attributes and listeners.
 *
 *		class SampleView extends View {
 *			constructor( locale ) {
 *				super( locale );
 *
 *				const bind = this.bindTemplate;
 *
 *				// Views define their interface (state) using observable attributes.
 *				this.set( 'elementClass', 'bar' );
 *
 *				this.setTemplate( {
 *					tag: 'p',
 *
 *					// The element of the view can be defined with its children.
 *					children: [
 *						'Hello',
 *						{
 *							tag: 'b',
 *							children: [ 'world!' ]
 *						}
 *					],
 *					attributes: {
 *						class: [
 *							'foo',
 *
 *							// Observable attributes control the state of the view in DOM.
 *							bind.to( 'elementClass' )
 *						]
 *					},
 *					on: {
 *						// Views listen to DOM events and propagate them.
 *						click: bind.to( 'clicked' )
 *					}
 *				} );
 *			}
 *		}
 *
 *		const view = new SampleView( locale );
 *
 *		view.render();
 *
 *		// Append <p class="foo bar">Hello<b>world</b></p> to the <body>
 *		document.body.appendChild( view.element );
 *
 *		// Change the class attribute to <p class="foo baz">Hello<b>world</b></p>
 *		view.elementClass = 'baz';
 *
 *		// Respond to the "click" event in DOM by executing a custom action.
 *		view.on( 'clicked', () => {
 *			console.log( 'The view has been clicked!' );
 *		} );
 *
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class View {
	/**
	 * Creates an instance of the {@link module:ui/view~View} class.
	 *
	 * Also see {@link #render}.
	 *
	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
	 */
	constructor( locale ) {
		/**
		 * An HTML element of the view. `null` until {@link #render rendered}
		 * from the {@link #template}.
		 *
		 *		class SampleView extends View {
		 *			constructor() {
		 *				super();
		 *
		 *				// A template instance the #element will be created from.
		 *				this.setTemplate( {
		 *					tag: 'p'
		 *
		 *					// ...
		 *				} );
		 *			}
		 *		}
		 *
		 *		const view = new SampleView();
		 *
		 *		// Renders the #template.
		 *		view.render();
		 *
		 *		// Append the HTML element of the view to <body>.
		 *		document.body.appendChild( view.element );
		 *
		 * **Note**: The element of the view can also be assigned directly:
		 *
		 *		view.element = document.querySelector( '#my-container' );
		 *
		 * @member {HTMLElement}
		 */
		this.element = null;

		/**
		 * Set `true` when the view has already been {@link module:ui/view~View#render rendered}.
		 *
		 * @readonly
		 * @member {Boolean} #isRendered
		 */
		this.isRendered = false;

		/**
		 * A set of tools to localize the user interface.
		 *
		 * Also see {@link module:core/editor/editor~Editor#locale}.
		 *
		 * @readonly
		 * @member {module:utils/locale~Locale}
		 */
		this.locale = locale;

		/**
		 * Shorthand for {@link module:utils/locale~Locale#t}.
		 *
		 * Note: If {@link #locale} instance hasn't been passed to the view this method may not
		 * be available.
		 *
		 * @see module:utils/locale~Locale#t
		 * @method
		 */
		this.t = locale && locale.t;

		/**
		 * Collections registered with {@link #createCollection}.
		 *
		 * @protected
		 * @member {Set.<module:ui/viewcollection~ViewCollection>}
		 */
		this._viewCollections = new Collection();

		/**
		 * A collection of view instances, which have been added directly
		 * into the {@link module:ui/template~Template#children}.
		 *
		 * @protected
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this._unboundChildren = this.createCollection();

		// Pass parent locale to its children.
		this._viewCollections.on( 'add', ( evt, collection ) => {
			collection.locale = locale;
		} );

		/**
		 * Template of this view. It provides the {@link #element} representing
		 * the view in DOM, which is {@link #render rendered}.
		 *
		 * @member {module:ui/template~Template} #template
		 */

		/**
		 * Cached {@link module:ui/template~BindChain bind chain} object created by the
		 * {@link #template}. See {@link #bindTemplate}.
		 *
		 * @private
		 * @member {Object} #_bindTemplate
		 */

		this.decorate( 'render' );
	}

	/**
	 * Shorthand for {@link module:ui/template~Template.bind}, a binding
	 * {@link module:ui/template~BindChain interface} pre–configured for the view instance.
	 *
	 * It provides {@link module:ui/template~BindChain#to `to()`} and
	 * {@link module:ui/template~BindChain#if `if()`} methods that initialize bindings with
	 * observable attributes and attach DOM listeners.
	 *
	 *		class SampleView extends View {
	 *			constructor( locale ) {
	 *				super( locale );
	 *
	 *				const bind = this.bindTemplate;
	 *
	 *				// These {@link module:utils/observablemixin~Observable observable} attributes will control
	 *				// the state of the view in DOM.
	 *				this.set( {
	 *					elementClass: 'foo',
	 *				 	isEnabled: true
	 *				 } );
	 *
	 *				this.setTemplate( {
	 *					tag: 'p',
	 *
	 *					attributes: {
	 *						// The class HTML attribute will follow elementClass
	 *						// and isEnabled view attributes.
	 *						class: [
	 *							bind.to( 'elementClass' )
	 *							bind.if( 'isEnabled', 'present-when-enabled' )
	 *						]
	 *					},
	 *
	 *					on: {
	 *						// The view will fire the "clicked" event upon clicking <p> in DOM.
	 *						click: bind.to( 'clicked' )
	 *					}
	 *				} );
	 *			}
	 *		}
	 *
	 * @method #bindTemplate
	 */
	get bindTemplate() {
		if ( this._bindTemplate ) {
			return this._bindTemplate;
		}

		return ( this._bindTemplate = Template.bind( this, this ) );
	}

	/**
	 * Creates a new collection of views, which can be used as
	 * {@link module:ui/template~Template#children} of this view.
	 *
	 *		class SampleView extends View {
	 *			constructor( locale ) {
	 *				super( locale );
	 *
	 *				const child = new ChildView( locale );
	 *				this.items = this.createCollection( [ child ] );
 	 *
	 *				this.setTemplate( {
	 *					tag: 'p',
	 *
	 *					// `items` collection will render here.
	 *					children: this.items
	 *				} );
	 *			}
	 *		}
	 *
	 *		const view = new SampleView( locale );
	 *		view.render();
	 *
	 *		// It will append <p><child#element></p> to the <body>.
	 *		document.body.appendChild( view.element );
	 *
	 * @param {Iterable.<module:ui/view~View>} [views] Initial views of the collection.
	 * @returns {module:ui/viewcollection~ViewCollection} A new collection of view instances.
	 */
	createCollection( views ) {
		const collection = new ViewCollection( views );

		this._viewCollections.add( collection );

		return collection;
	}

	/**
	 * Registers a new child view under the view instance. Once registered, a child
	 * view is managed by its parent, including {@link #render rendering}
	 * and {@link #destroy destruction}.
	 *
	 * To revert this, use {@link #deregisterChild}.
	 *
	 *		class SampleView extends View {
	 *			constructor( locale ) {
	 *				super( locale );
	 *
	 *				this.childA = new SomeChildView( locale );
	 *				this.childB = new SomeChildView( locale );
	 *
	 *				this.setTemplate( { tag: 'p' } );
	 *
	 *				// Register the children.
	 *				this.registerChild( [ this.childA, this.childB ] );
	 *			}
	 *
	 *			render() {
	 *				super.render();
	 *
	 *				this.element.appendChild( this.childA.element );
	 *				this.element.appendChild( this.childB.element );
	 *			}
	 *		}
	 *
	 *		const view = new SampleView( locale );
	 *
	 *		view.render();
	 *
	 *		// Will append <p><childA#element><b></b><childB#element></p>.
	 *		document.body.appendChild( view.element );
	 *
	 * **Note**: There's no need to add child views if they're already referenced in the
	 * {@link #template}:
	 *
	 *		class SampleView extends View {
	 *			constructor( locale ) {
	 *				super( locale );
	 *
	 *				this.childA = new SomeChildView( locale );
	 *				this.childB = new SomeChildView( locale );
	 *
	 *				this.setTemplate( {
	 *					tag: 'p',
	 *
 	 *					// These children will be added automatically. There's no
 	 *					// need to call {@link #registerChild} for any of them.
	 *					children: [ this.childA, this.childB ]
	 *				} );
	 *			}
	 *
	 *			// ...
	 *		}
	 *
	 * @param {module:ui/view~View|Iterable.<module:ui/view~View>} children Children views to be registered.
	 */
	registerChild( children ) {
		if ( !isIterable( children ) ) {
			children = [ children ];
		}

		for ( const child of children ) {
			this._unboundChildren.add( child );
		}
	}

	/**
	 * The opposite of {@link #registerChild}. Removes a child view from this view instance.
	 * Once removed, the child is no longer managed by its parent, e.g. it can safely
	 * become a child of another parent view.
	 *
	 * @see #registerChild
	 * @param {module:ui/view~View|Iterable.<module:ui/view~View>} children Child views to be removed.
	 */
	deregisterChild( children ) {
		if ( !isIterable( children ) ) {
			children = [ children ];
		}

		for ( const child of children ) {
			this._unboundChildren.remove( child );
		}
	}

	/**
	 * Sets the {@link #template} of the view with with given definition.
	 *
	 * A shorthand for:
	 *
	 *		view.setTemplate( definition );
	 *
	 * @param {module:ui/template~TemplateDefinition} definition Definition of view's template.
	 */
	setTemplate( definition ) {
		this.template = new Template( definition );
	}

	/**
	 * {@link module:ui/template~Template.extend Extends} the {@link #template} of the view with
	 * with given definition.
	 *
	 * A shorthand for:
	 *
	 *		Template.extend( view.template, definition );
	 *
	 * **Note**: Is requires the {@link #template} to be already set. See {@link #setTemplate}.
	 *
	 * @param {module:ui/template~TemplateDefinition} definition Definition which
	 * extends the {@link #template}.
	 */
	extendTemplate( definition ) {
		Template.extend( this.template, definition );
	}

	/**
	 * Recursively renders the view.
	 *
	 * Once the view is rendered:
	 * * the {@link #element} becomes an HTML element out of {@link #template},
	 * * the {@link #isRendered} flag is set `true`.
	 *
	 * **Note**: The children of the view:
	 * * defined directly in the {@link #template}
	 * * residing in collections created by the {@link #createCollection} method,
	 * * and added by {@link #registerChild}
	 * are also rendered in the process.
	 *
	 * In general, `render()` method is the right place to keep the code which refers to the
	 * {@link #element} and should be executed at the very beginning of the view's life cycle.
	 *
	 * It is possible to {@link module:ui/template~Template.extend} the {@link #template} before
	 * the view is rendered. To allow an early customization of the view (e.g. by its parent),
	 * such references should be done in `render()`.
	 *
	 *		class SampleView extends View {
	 *			constructor() {
	 *				this.setTemplate( {
	 *					// ...
	 *				} );
	 *			},
	 *
	 *			render() {
	 *				// View#element becomes available.
	 *				super.render();
	 *
	 *				// The "scroll" listener depends on #element.
	 *				this.listenTo( window, 'scroll', () => {
	 *					// A reference to #element would render the #template and make it non-extendable.
	 *					if ( window.scrollY > 0 ) {
	 *						this.element.scrollLeft = 100;
	 *					} else {
	 *						this.element.scrollLeft = 0;
	 *					}
	 *				} );
	 *			}
	 *		}
	 *
	 *		const view = new SampleView();
	 *
	 *		// Let's customize the view before it gets rendered.
	 *		view.extendTemplate( {
	 *			attributes: {
	 *				class: [
	 *					'additional-class'
	 *				]
	 *			}
	 *		} );
	 *
	 *		// Late rendering allows customization of the view.
	 *		view.render();
	 */
	render() {
		if ( this.isRendered ) {
			/**
			 * This View has already been rendered.
			 *
			 * @error ui-view-render-already-rendered
			 */
			throw new CKEditorError( 'ui-view-render-already-rendered', this );
		}

		// Render #element of the view.
		if ( this.template ) {
			this.element = this.template.render();

			// Auto–register view children from #template.
			this.registerChild( this.template.getViews() );
		}

		this.isRendered = true;
	}

	/**
	 * Recursively destroys the view instance and child views added by {@link #registerChild} and
	 * residing in collections created by the {@link #createCollection}.
	 *
	 * Destruction disables all event listeners:
	 * * created on the view, e.g. `view.on( 'event', () => {} )`,
	 * * defined in the {@link #template} for DOM events.
	 */
	destroy() {
		this.stopListening();

		this._viewCollections.map( c => c.destroy() );

		// Template isn't obligatory for views.
		if ( this.template && this.template._revertData ) {
			this.template.revert( this.element );
		}
	}

	/**
	 * Event fired by the {@link #render} method. Actual rendering is executed as a listener to
	 * this event with the default priority.
	 *
	 * See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples.
	 *
	 * @event render
	 */
}

mix( View, DomEmitterMixin );
mix( View, ObservableMixin );/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * For a given element, returns the nearest ancestor element which CSS position is not "static".
 *
 * @param {HTMLElement} [element] The native DOM element to be checked.
 * @returns {HTMLElement|null}
 */
function getPositionedAncestor(element) {
    if (!element || !element.parentNode) {
        return null;
    }
    if (element.offsetParent === global$2.document.body) {
        return null;
    }
    return element.offsetParent;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// @if CK_DEBUG_POSITION // import { RectDrawer } from '@ckeditor/ckeditor5-minimap/src/utils';
/**
 * Calculates the `position: absolute` coordinates of a given element so it can be positioned with respect to the
 * target in the visually most efficient way, taking various restrictions like viewport or limiter geometry
 * into consideration.
 *
 *		// The element which is to be positioned.
 *		const element = document.body.querySelector( '#toolbar' );
 *
 *		// A target to which the element is positioned relatively.
 *		const target = document.body.querySelector( '#container' );
 *
 *		// Finding the optimal coordinates for the positioning.
 *		const { left, top, name } = getOptimalPosition( {
 *			element: element,
 *			target: target,
 *
 * 			// The algorithm will chose among these positions to meet the requirements such
 * 			// as "limiter" element or "fitInViewport", set below. The positions are considered
 * 			// in the order of the array.
 *			positions: [
 *				//
 *			 	//	[ Target ]
 *				//	+-----------------+
 *				//	|     Element     |
 *				//	+-----------------+
 *				//
 *				targetRect => ( {
 *					top: targetRect.bottom,
 *					left: targetRect.left,
 *					name: 'mySouthEastPosition'
 *				} ),
 *
 *				//
 *				//	+-----------------+
 *				//	|     Element     |
 *				//	+-----------------+
 *				//	[ Target ]
 *				//
 *				( targetRect, elementRect ) => ( {
 *					top: targetRect.top - elementRect.height,
 *					left: targetRect.left,
 *					name: 'myNorthEastPosition'
 *				} )
 *			],
 *
 *			// Find a position such guarantees the element remains within visible boundaries of <body>.
 *			limiter: document.body,
 *
 *			// Find a position such guarantees the element remains within visible boundaries of the browser viewport.
 *			fitInViewport: true
 *		} );
 *
 *		// The best position which fits into document.body and the viewport. May be useful
 *		// to set proper class on the `element`.
 *		console.log( name ); // -> "myNorthEastPosition"
 *
 *		// Using the absolute coordinates which has been found to position the element
 *		// as in the diagram depicting the "myNorthEastPosition" position.
 *		element.style.top = top;
 *		element.style.left = left;
 *
 * @param {module:utils/dom/position~Options} options The input data and configuration of the helper.
 * @returns {module:utils/dom/position~Position}
 */
function getOptimalPosition({ element, target, positions, limiter, fitInViewport, viewportOffsetConfig }) {
    // If the {@link module:utils/dom/position~Options#target} is a function, use what it returns.
    // https://github.com/ckeditor/ckeditor5-utils/issues/157
    if (isFunction(target)) {
        target = target();
    }
    // If the {@link module:utils/dom/position~Options#limiter} is a function, use what it returns.
    // https://github.com/ckeditor/ckeditor5-ui/issues/260
    if (isFunction(limiter)) {
        limiter = limiter();
    }
    const positionedElementAncestor = getPositionedAncestor(element);
    const elementRect = new Rect(element);
    const targetRect = new Rect(target);
    let bestPosition;
    // @if CK_DEBUG_POSITION // RectDrawer.clear();
    // @if CK_DEBUG_POSITION // RectDrawer.draw( targetRect, { outlineWidth: '5px' }, 'Target' );
    const viewportRect = fitInViewport && getConstrainedViewportRect(viewportOffsetConfig) || null;
    const positionOptions = { targetRect, elementRect, positionedElementAncestor, viewportRect };
    // If there are no limits, just grab the very first position and be done with that drama.
    if (!limiter && !fitInViewport) {
        bestPosition = new PositionObject(positions[0], positionOptions);
    }
    else {
        const limiterRect = limiter && new Rect(limiter).getVisible();
        // @if CK_DEBUG_POSITION // if ( viewportRect ) {
        // @if CK_DEBUG_POSITION //		RectDrawer.draw( viewportRect, { outlineWidth: '5px' }, 'Viewport' );
        // @if CK_DEBUG_POSITION // }
        // @if CK_DEBUG_POSITION // if ( limiter ) {
        // @if CK_DEBUG_POSITION // 	RectDrawer.draw( limiterRect, { outlineWidth: '5px', outlineColor: 'green' }, 'Visible limiter' );
        // @if CK_DEBUG_POSITION // }
        Object.assign(positionOptions, { limiterRect, viewportRect });
        // If there's no best position found, i.e. when all intersections have no area because
        // rects have no width or height, then just use the first available position.
        bestPosition = getBestPosition(positions, positionOptions) || new PositionObject(positions[0], positionOptions);
    }
    return bestPosition;
}
// Returns a viewport `Rect` shrunk by the viewport offset config from all sides.
//
// @private
// @param {Object} An object containing viewportOffset config.
// @returns {module:utils/dom/rect~Rect} A shrunken rect of the viewport.
function getConstrainedViewportRect(viewportOffsetConfig) {
    viewportOffsetConfig = Object.assign({ top: 0, bottom: 0, left: 0, right: 0 }, viewportOffsetConfig);
    const viewportRect = new Rect(global$2.window);
    viewportRect.top += viewportOffsetConfig.top;
    viewportRect.height -= viewportOffsetConfig.top;
    viewportRect.bottom -= viewportOffsetConfig.bottom;
    viewportRect.height -= viewportOffsetConfig.bottom;
    return viewportRect;
}
// For a given array of positioning functions, returns such that provides the best
// fit of the `elementRect` into the `limiterRect` and `viewportRect`.
//
// @private
//
// @param {module:utils/dom/position~Options#positions} positions Functions returning
// {@link module:utils/dom/position~Position}to be checked, in the order of preference.
// @param {Object} options
// @param {module:utils/dom/rect~Rect} options.elementRect The positioned element rect.
// @param {module:utils/dom/rect~Rect} options.targetRect The target element rect.
// @param {module:utils/dom/rect~Rect} options.viewportRect The viewport rect.
// @param {module:utils/dom/rect~Rect} [options.limiterRect] The limiter rect.
// @param {HTMLElement|null} [options.positionedElementAncestor] Nearest element ancestor element which CSS position is not "static".
//
// @returns {module:utils/dom/position~Position|null} An array containing the name of the position and it's rect.
function getBestPosition(positions, options) {
    const { elementRect } = options;
    // This is when element is fully visible.
    const elementRectArea = elementRect.getArea();
    const positionInstances = positions
        .map(positioningFunction => new PositionObject(positioningFunction, options))
        // Some positioning functions may return `null` if they don't want to participate.
        .filter(position => !!position.name);
    let maxFitFactor = 0;
    let bestPosition = null;
    for (const position of positionInstances) {
        const { limiterIntersectionArea, viewportIntersectionArea } = position;
        // If a such position is found that element is fully contained by the limiter then, obviously,
        // there will be no better one, so finishing.
        if (limiterIntersectionArea === elementRectArea) {
            return position;
        }
        // To maximize both viewport and limiter intersection areas we use distance on _viewportIntersectionArea
        // and _limiterIntersectionArea plane (without sqrt because we are looking for max value).
        const fitFactor = viewportIntersectionArea ** 2 + limiterIntersectionArea ** 2;
        if (fitFactor > maxFitFactor) {
            maxFitFactor = fitFactor;
            bestPosition = position;
        }
    }
    return bestPosition;
}
// For a given absolute Rect coordinates object and a positioned element ancestor, it updates its
// coordinates that make up for the position and the scroll of the ancestor.
//
// This is necessary because while Rects (and DOMRects) are relative to the browser's viewport, their coordinates
// are used in real–life to position elements with `position: absolute`, which are scoped by any positioned
// (and scrollable) ancestors.
//
// @private
//
// @param {module:utils/dom/rect~Rect} rect A rect with absolute rect coordinates.
// @param {HTMLElement} positionedElementAncestor An ancestor element that should be considered.
function shiftRectToCompensatePositionedAncestor(rect, positionedElementAncestor) {
    const ancestorPosition = getRectForAbsolutePositioning(new Rect(positionedElementAncestor));
    const ancestorBorderWidths = getBorderWidths(positionedElementAncestor);
    let moveX = 0;
    let moveY = 0;
    // (https://github.com/ckeditor/ckeditor5-ui-default/issues/126)
    // If there's some positioned ancestor of the panel, then its `Rect` must be taken into
    // consideration. `Rect` is always relative to the viewport while `position: absolute` works
    // with respect to that positioned ancestor.
    moveX -= ancestorPosition.left;
    moveY -= ancestorPosition.top;
    // (https://github.com/ckeditor/ckeditor5-utils/issues/139)
    // If there's some positioned ancestor of the panel, not only its position must be taken into
    // consideration (see above) but also its internal scrolls. Scroll have an impact here because `Rect`
    // is relative to the viewport (it doesn't care about scrolling), while `position: absolute`
    // must compensate that scrolling.
    moveX += positionedElementAncestor.scrollLeft;
    moveY += positionedElementAncestor.scrollTop;
    // (https://github.com/ckeditor/ckeditor5-utils/issues/139)
    // If there's some positioned ancestor of the panel, then its `Rect` includes its CSS `borderWidth`
    // while `position: absolute` positioning does not consider it.
    // E.g. `{ position: absolute, top: 0, left: 0 }` means upper left corner of the element,
    // not upper-left corner of its border.
    moveX -= ancestorBorderWidths.left;
    moveY -= ancestorBorderWidths.top;
    rect.moveBy(moveX, moveY);
}
// DOMRect (also Rect) works in a scroll–independent geometry but `position: absolute` doesn't.
// This function converts Rect to `position: absolute` coordinates.
//
// @private
// @param {module:utils/dom/rect~Rect} rect A rect to be converted.
// @returns {module:utils/dom/rect~Rect} Object containing `left` and `top` properties, in absolute coordinates.
function getRectForAbsolutePositioning(rect) {
    const { scrollX, scrollY } = global$2.window;
    return rect.clone().moveBy(scrollX, scrollY);
}
// A position class which instances are created and used by the {@link module:utils/dom/position~getOptimalPosition} helper.
//
// {@link module:utils/dom/position~Position#top} and {@link module:utils/dom/position~Position#left} properties of the position instance
// translate directly to the `top` and `left` properties in CSS "`position: absolute` coordinate system". If set on the positioned element
// in DOM, they will make it display it in the right place in the viewport.
// @private
// @implements {Position}
class PositionObject {
    // Creates an instance of the {@link module:utils/dom/position~PositionObject} class.
    //
    // @param {module:utils/dom/position~PositioningFunction} positioningFunction function The function that defines the expected
    // coordinates the positioned element should move to.
    // @param {Object} [options] options object.
    // @param {module:utils/dom/rect~Rect} options.elementRect The positioned element rect.
    // @param {module:utils/dom/rect~Rect} options.targetRect The target element rect.
    // @param {module:utils/dom/rect~Rect|null} options.viewportRect The viewport rect.
    // @param {module:utils/dom/rect~Rect} [options.limiterRect] The limiter rect.
    // @param {HTMLElement|null} [options.positionedElementAncestor] Nearest element ancestor element which CSS position is not "static".
    constructor(positioningFunction, options) {
        const positioningFunctionOutput = positioningFunction(options.targetRect, options.elementRect, options.viewportRect);
        // Nameless position for a function that didn't participate.
        if (!positioningFunctionOutput) {
            return;
        }
        const { left, top, name, config } = positioningFunctionOutput;
        this.name = name;
        this.config = config;
        this._positioningFunctionCorrdinates = { left, top };
        this._options = options;
    }
    // The left value in pixels in the CSS `position: absolute` coordinate system.
    // Set it on the positioned element in DOM to move it to the position.
    //
    // @readonly
    // @type {Number}
    get left() {
        return this._absoluteRect.left;
    }
    // The top value in pixels in the CSS `position: absolute` coordinate system.
    // Set it on the positioned element in DOM to move it to the position.
    //
    // @readonly
    // @type {Number}
    get top() {
        return this._absoluteRect.top;
    }
    // An intersection area between positioned element and limiter within viewport constraints.
    //
    // @readonly
    // @type {Number}
    get limiterIntersectionArea() {
        const limiterRect = this._options.limiterRect;
        if (limiterRect) {
            const viewportRect = this._options.viewportRect;
            if (viewportRect) {
                // Consider only the part of the limiter which is visible in the viewport. So the limiter is getting limited.
                const limiterViewportIntersectRect = limiterRect.getIntersection(viewportRect);
                if (limiterViewportIntersectRect) {
                    // If the limiter is within the viewport, then check the intersection between that part of the
                    // limiter and actual position.
                    return limiterViewportIntersectRect.getIntersectionArea(this._rect);
                }
            }
            else {
                return limiterRect.getIntersectionArea(this._rect);
            }
        }
        return 0;
    }
    // An intersection area between positioned element and viewport.
    //
    // @readonly
    // @type {Number}
    get viewportIntersectionArea() {
        const viewportRect = this._options.viewportRect;
        if (viewportRect) {
            return viewportRect.getIntersectionArea(this._rect);
        }
        return 0;
    }
    // An already positioned element rect. A clone of the element rect passed to the constructor
    // but placed in the viewport according to the positioning function.
    //
    // @private
    // @readonly
    // @type {module:utils/dom/rect~Rect}
    get _rect() {
        if (this._cachedRect) {
            return this._cachedRect;
        }
        this._cachedRect = this._options.elementRect.clone().moveTo(this._positioningFunctionCorrdinates.left, this._positioningFunctionCorrdinates.top);
        return this._cachedRect;
    }
    // An already absolutely positioned element rect. See ({@link #_rect}).
    //
    // @private
    // @readonly
    // @type {module:utils/dom/rect~Rect}
    get _absoluteRect() {
        if (this._cachedAbsoluteRect) {
            return this._cachedAbsoluteRect;
        }
        this._cachedAbsoluteRect = getRectForAbsolutePositioning(this._rect);
        if (this._options.positionedElementAncestor) {
            shiftRectToCompensatePositionedAncestor(this._cachedAbsoluteRect, this._options.positionedElementAncestor);
        }
        return this._cachedAbsoluteRect;
    }
}var css_248z$U = ":root{--ck-balloon-panel-arrow-z-index:calc(var(--ck-z-default) - 3)}.ck.ck-balloon-panel{display:none;position:absolute;z-index:var(--ck-z-modal)}.ck.ck-balloon-panel.ck-balloon-panel_with-arrow:after,.ck.ck-balloon-panel.ck-balloon-panel_with-arrow:before{content:\"\";position:absolute}.ck.ck-balloon-panel.ck-balloon-panel_with-arrow:before{z-index:var(--ck-balloon-panel-arrow-z-index)}.ck.ck-balloon-panel.ck-balloon-panel_with-arrow:after{z-index:calc(var(--ck-balloon-panel-arrow-z-index) + 1)}.ck.ck-balloon-panel[class*=arrow_n]:before{z-index:var(--ck-balloon-panel-arrow-z-index)}.ck.ck-balloon-panel[class*=arrow_n]:after{z-index:calc(var(--ck-balloon-panel-arrow-z-index) + 1)}.ck.ck-balloon-panel[class*=arrow_s]:before{z-index:var(--ck-balloon-panel-arrow-z-index)}.ck.ck-balloon-panel[class*=arrow_s]:after{z-index:calc(var(--ck-balloon-panel-arrow-z-index) + 1)}.ck.ck-balloon-panel.ck-balloon-panel_visible{display:block}:root{--ck-balloon-border-width:1px;--ck-balloon-arrow-offset:2px;--ck-balloon-arrow-height:10px;--ck-balloon-arrow-half-width:8px;--ck-balloon-arrow-drop-shadow:0 2px 2px var(--ck-color-shadow-drop)}.ck.ck-balloon-panel{border-radius:0}.ck-rounded-corners .ck.ck-balloon-panel,.ck.ck-balloon-panel.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-balloon-panel{background:var(--ck-color-panel-background);border:var(--ck-balloon-border-width) solid var(--ck-color-panel-border);box-shadow:var(--ck-drop-shadow),0 0;min-height:15px}.ck.ck-balloon-panel.ck-balloon-panel_with-arrow:after,.ck.ck-balloon-panel.ck-balloon-panel_with-arrow:before{border-style:solid;height:0;width:0}.ck.ck-balloon-panel[class*=arrow_n]:after,.ck.ck-balloon-panel[class*=arrow_n]:before{border-width:0 var(--ck-balloon-arrow-half-width) var(--ck-balloon-arrow-height) var(--ck-balloon-arrow-half-width)}.ck.ck-balloon-panel[class*=arrow_n]:before{border-color:transparent transparent var(--ck-color-panel-border) transparent;margin-top:calc(var(--ck-balloon-border-width)*-1)}.ck.ck-balloon-panel[class*=arrow_n]:after{border-color:transparent transparent var(--ck-color-panel-background) transparent;margin-top:calc(var(--ck-balloon-arrow-offset) - var(--ck-balloon-border-width))}.ck.ck-balloon-panel[class*=arrow_s]:after,.ck.ck-balloon-panel[class*=arrow_s]:before{border-width:var(--ck-balloon-arrow-height) var(--ck-balloon-arrow-half-width) 0 var(--ck-balloon-arrow-half-width)}.ck.ck-balloon-panel[class*=arrow_s]:before{border-color:var(--ck-color-panel-border) transparent transparent;filter:drop-shadow(var(--ck-balloon-arrow-drop-shadow));margin-bottom:calc(var(--ck-balloon-border-width)*-1)}.ck.ck-balloon-panel[class*=arrow_s]:after{border-color:var(--ck-color-panel-background) transparent transparent transparent;margin-bottom:calc(var(--ck-balloon-arrow-offset) - var(--ck-balloon-border-width))}.ck.ck-balloon-panel[class*=arrow_e]:after,.ck.ck-balloon-panel[class*=arrow_e]:before{border-width:var(--ck-balloon-arrow-half-width) 0 var(--ck-balloon-arrow-half-width) var(--ck-balloon-arrow-height)}.ck.ck-balloon-panel[class*=arrow_e]:before{border-color:transparent transparent transparent var(--ck-color-panel-border);margin-right:calc(var(--ck-balloon-border-width)*-1)}.ck.ck-balloon-panel[class*=arrow_e]:after{border-color:transparent transparent transparent var(--ck-color-panel-background);margin-right:calc(var(--ck-balloon-arrow-offset) - var(--ck-balloon-border-width))}.ck.ck-balloon-panel[class*=arrow_w]:after,.ck.ck-balloon-panel[class*=arrow_w]:before{border-width:var(--ck-balloon-arrow-half-width) var(--ck-balloon-arrow-height) var(--ck-balloon-arrow-half-width) 0}.ck.ck-balloon-panel[class*=arrow_w]:before{border-color:transparent var(--ck-color-panel-border) transparent transparent;margin-left:calc(var(--ck-balloon-border-width)*-1)}.ck.ck-balloon-panel[class*=arrow_w]:after{border-color:transparent var(--ck-color-panel-background) transparent transparent;margin-left:calc(var(--ck-balloon-arrow-offset) - var(--ck-balloon-border-width))}.ck.ck-balloon-panel.ck-balloon-panel_arrow_n:after,.ck.ck-balloon-panel.ck-balloon-panel_arrow_n:before{left:50%;margin-left:calc(var(--ck-balloon-arrow-half-width)*-1);top:calc(var(--ck-balloon-arrow-height)*-1)}.ck.ck-balloon-panel.ck-balloon-panel_arrow_nw:after,.ck.ck-balloon-panel.ck-balloon-panel_arrow_nw:before{left:calc(var(--ck-balloon-arrow-half-width)*2);top:calc(var(--ck-balloon-arrow-height)*-1)}.ck.ck-balloon-panel.ck-balloon-panel_arrow_ne:after,.ck.ck-balloon-panel.ck-balloon-panel_arrow_ne:before{right:calc(var(--ck-balloon-arrow-half-width)*2);top:calc(var(--ck-balloon-arrow-height)*-1)}.ck.ck-balloon-panel.ck-balloon-panel_arrow_s:after,.ck.ck-balloon-panel.ck-balloon-panel_arrow_s:before{bottom:calc(var(--ck-balloon-arrow-height)*-1);left:50%;margin-left:calc(var(--ck-balloon-arrow-half-width)*-1)}.ck.ck-balloon-panel.ck-balloon-panel_arrow_sw:after,.ck.ck-balloon-panel.ck-balloon-panel_arrow_sw:before{bottom:calc(var(--ck-balloon-arrow-height)*-1);left:calc(var(--ck-balloon-arrow-half-width)*2)}.ck.ck-balloon-panel.ck-balloon-panel_arrow_se:after,.ck.ck-balloon-panel.ck-balloon-panel_arrow_se:before{bottom:calc(var(--ck-balloon-arrow-height)*-1);right:calc(var(--ck-balloon-arrow-half-width)*2)}.ck.ck-balloon-panel.ck-balloon-panel_arrow_sme:after,.ck.ck-balloon-panel.ck-balloon-panel_arrow_sme:before{bottom:calc(var(--ck-balloon-arrow-height)*-1);margin-right:calc(var(--ck-balloon-arrow-half-width)*2);right:25%}.ck.ck-balloon-panel.ck-balloon-panel_arrow_smw:after,.ck.ck-balloon-panel.ck-balloon-panel_arrow_smw:before{bottom:calc(var(--ck-balloon-arrow-height)*-1);left:25%;margin-left:calc(var(--ck-balloon-arrow-half-width)*2)}.ck.ck-balloon-panel.ck-balloon-panel_arrow_nme:after,.ck.ck-balloon-panel.ck-balloon-panel_arrow_nme:before{margin-right:calc(var(--ck-balloon-arrow-half-width)*2);right:25%;top:calc(var(--ck-balloon-arrow-height)*-1)}.ck.ck-balloon-panel.ck-balloon-panel_arrow_nmw:after,.ck.ck-balloon-panel.ck-balloon-panel_arrow_nmw:before{left:25%;margin-left:calc(var(--ck-balloon-arrow-half-width)*2);top:calc(var(--ck-balloon-arrow-height)*-1)}.ck.ck-balloon-panel.ck-balloon-panel_arrow_e:after,.ck.ck-balloon-panel.ck-balloon-panel_arrow_e:before{margin-top:calc(var(--ck-balloon-arrow-half-width)*-1);right:calc(var(--ck-balloon-arrow-height)*-1);top:50%}.ck.ck-balloon-panel.ck-balloon-panel_arrow_w:after,.ck.ck-balloon-panel.ck-balloon-panel_arrow_w:before{left:calc(var(--ck-balloon-arrow-height)*-1);margin-top:calc(var(--ck-balloon-arrow-half-width)*-1);top:50%}";
styleInject(css_248z$U);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const toPx$5 = toUnit( 'px' );
const defaultLimiterElement = global$2.document.body;

/**
 * The balloon panel view class.
 *
 * A floating container which can
 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#pin pin} to any
 * {@link module:utils/dom/position~Options#target target} in the DOM and remain in that position
 * e.g. when the web page is scrolled.
 *
 * The balloon panel can be used to display contextual, non-blocking UI like forms, toolbars and
 * the like in its {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#content} view
 * collection.
 *
 * There is a number of {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}
 * that the balloon can use, automatically switching from one to another when the viewport space becomes
 * scarce to keep the balloon visible to the user as long as it is possible. The balloon will also
 * accept any custom position set provided by the user compatible with the
 * {@link module:utils/dom/position~Options options}.
 *
 *		const panel = new BalloonPanelView( locale );
 *		const childView = new ChildView();
 *		const positions = BalloonPanelView.defaultPositions;
 *
 *		panel.render();
 *
 *		// Add a child view to the panel's content collection.
 *		panel.content.add( childView );
 *
 *		// Start pinning the panel to an element with the "target" id DOM.
 *		// The balloon will remain pinned until unpin() is called.
 *		panel.pin( {
 *			target: document.querySelector( '#target' ),
 *			positions: [
 *				positions.northArrowSouth,
 *				positions.southArrowNorth
 *			]
 *		} );
 *
 * @extends module:ui/view~View
 */
class BalloonPanelView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * The absolute top position of the balloon panel in pixels.
		 *
		 * @observable
		 * @default 0
		 * @member {Number} #top
		 */
		this.set( 'top', 0 );

		/**
		 * The absolute left position of the balloon panel in pixels.
		 *
		 * @observable
		 * @default 0
		 * @member {Number} #left
		 */
		this.set( 'left', 0 );

		/**
		 * The balloon panel's current position. The position name is reflected in the CSS class set
		 * to the balloon, i.e. `.ck-balloon-panel_arrow_nw` for the "arrow_nw" position. The class
		 * controls the minor aspects of the balloon's visual appearance like the placement
		 * of an {@link #withArrow arrow}. To support a new position, an additional CSS must be created.
		 *
		 * Default position names correspond with
		 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
		 *
		 * See the {@link #attachTo} and {@link #pin} methods to learn about custom balloon positions.
		 *
		 * @observable
		 * @default 'arrow_nw'
		 * @member {'arrow_nw'|'arrow_ne'|'arrow_sw'|'arrow_se'} #position
		 */
		this.set( 'position', 'arrow_nw' );

		/**
		 * Controls whether the balloon panel is visible or not.
		 *
		 * @observable
		 * @default false
		 * @member {Boolean} #isVisible
		 */
		this.set( 'isVisible', false );

		/**
		 * Controls whether the balloon panel has an arrow. The presence of the arrow
		 * is reflected in the `ck-balloon-panel_with-arrow` CSS class.
		 *
		 * @observable
		 * @default true
		 * @member {Boolean} #withArrow
		 */
		this.set( 'withArrow', true );

		/**
		 * An additional CSS class added to the {@link #element}.
		 *
		 * @observable
		 * @member {String} #class
		 */
		this.set( 'class' );

		/**
		 * A callback that starts pinning the panel when {@link #isVisible} gets
		 * `true`. Used by {@link #pin}.
		 *
		 * @private
		 * @member {Function} #_pinWhenIsVisibleCallback
		 */

		/**
		 * A collection of the child views that creates the balloon panel contents.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.content = this.createCollection();

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-balloon-panel',
					bind.to( 'position', value => `ck-balloon-panel_${ value }` ),
					bind.if( 'isVisible', 'ck-balloon-panel_visible' ),
					bind.if( 'withArrow', 'ck-balloon-panel_with-arrow' ),
					bind.to( 'class' )
				],

				style: {
					top: bind.to( 'top', toPx$5 ),
					left: bind.to( 'left', toPx$5 )
				}
			},

			children: this.content
		} );
	}

	/**
	 * Shows the panel.
	 *
	 * See {@link #isVisible}.
	 */
	show() {
		this.isVisible = true;
	}

	/**
	 * Hides the panel.
	 *
	 * See {@link #isVisible}.
	 */
	hide() {
		this.isVisible = false;
	}

	/**
	 * Attaches the panel to a specified {@link module:utils/dom/position~Options#target} with a
	 * smart positioning heuristics that chooses from available positions to make sure the panel
	 * is visible to the user i.e. within the limits of the viewport.
	 *
	 * This method accepts configuration {@link module:utils/dom/position~Options options}
	 * to set the `target`, optional `limiter` and `positions` the balloon should choose from.
	 *
	 *		const panel = new BalloonPanelView( locale );
	 *		const positions = BalloonPanelView.defaultPositions;
	 *
	 *		panel.render();
	 *
	 *		// Attach the panel to an element with the "target" id DOM.
	 *		panel.attachTo( {
	 *			target: document.querySelector( '#target' ),
	 *			positions: [
	 *				positions.northArrowSouth,
	 *				positions.southArrowNorth
	 *			]
	 *		} );
	 *
	 * **Note**: Attaching the panel will also automatically {@link #show} it.
	 *
	 * **Note**: An attached panel will not follow its target when the window is scrolled or resized.
	 * See the {@link #pin} method for a more permanent positioning strategy.
	 *
	 * @param {module:utils/dom/position~Options} options Positioning options compatible with
	 * {@link module:utils/dom/position~getOptimalPosition}. Default `positions` array is
	 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
	 */
	attachTo( options ) {
		this.show();

		const defaultPositions = BalloonPanelView.defaultPositions;
		const positionOptions = Object.assign( {}, {
			element: this.element,
			positions: [
				defaultPositions.southArrowNorth,
				defaultPositions.southArrowNorthMiddleWest,
				defaultPositions.southArrowNorthMiddleEast,
				defaultPositions.southArrowNorthWest,
				defaultPositions.southArrowNorthEast,
				defaultPositions.northArrowSouth,
				defaultPositions.northArrowSouthMiddleWest,
				defaultPositions.northArrowSouthMiddleEast,
				defaultPositions.northArrowSouthWest,
				defaultPositions.northArrowSouthEast,
				defaultPositions.viewportStickyNorth
			],
			limiter: defaultLimiterElement,
			fitInViewport: true
		}, options );

		const optimalPosition = BalloonPanelView._getOptimalPosition( positionOptions );

		// Usually browsers make some problems with super accurate values like 104.345px
		// so it is better to use int values.
		const left = parseInt( optimalPosition.left );
		const top = parseInt( optimalPosition.top );

		const { name: position, config = {} } = optimalPosition;
		const { withArrow = true } = config;

		Object.assign( this, { top, left, position, withArrow } );
	}

	/**
	 * Works the same way as the {@link #attachTo} method except that the position of the panel is
	 * continuously updated when:
	 *
	 * * any ancestor of the {@link module:utils/dom/position~Options#target}
	 * or {@link module:utils/dom/position~Options#limiter} is scrolled,
	 * * the browser window gets resized or scrolled.
	 *
	 * Thanks to that, the panel always sticks to the {@link module:utils/dom/position~Options#target}
	 * and is immune to the changing environment.
	 *
	 *		const panel = new BalloonPanelView( locale );
	 *		const positions = BalloonPanelView.defaultPositions;
	 *
	 *		panel.render();
	 *
	 *		// Pin the panel to an element with the "target" id DOM.
	 *		panel.pin( {
	 *			target: document.querySelector( '#target' ),
	 *			positions: [
	 *				positions.northArrowSouth,
	 *				positions.southArrowNorth
	 *			]
	 *		} );
	 *
	 * To leave the pinned state, use the {@link #unpin} method.
	 *
	 * **Note**: Pinning the panel will also automatically {@link #show} it.
	 *
	 * @param {module:utils/dom/position~Options} options Positioning options compatible with
	 * {@link module:utils/dom/position~getOptimalPosition}. Default `positions` array is
	 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
	 */
	pin( options ) {
		this.unpin();

		this._pinWhenIsVisibleCallback = () => {
			if ( this.isVisible ) {
				this._startPinning( options );
			} else {
				this._stopPinning();
			}
		};

		this._startPinning( options );

		// Control the state of the listeners depending on whether the panel is visible
		// or not.
		// TODO: Use on() (https://github.com/ckeditor/ckeditor5-utils/issues/144).
		this.listenTo( this, 'change:isVisible', this._pinWhenIsVisibleCallback );
	}

	/**
	 * Stops pinning the panel, as set up by {@link #pin}.
	 */
	unpin() {
		if ( this._pinWhenIsVisibleCallback ) {
			// Deactivate listeners attached by pin().
			this._stopPinning();

			// Deactivate the panel pin() control logic.
			// TODO: Use off() (https://github.com/ckeditor/ckeditor5-utils/issues/144).
			this.stopListening( this, 'change:isVisible', this._pinWhenIsVisibleCallback );

			this._pinWhenIsVisibleCallback = null;

			this.hide();
		}
	}

	/**
	 * Starts managing the pinned state of the panel. See {@link #pin}.
	 *
	 * @private
	 * @param {module:utils/dom/position~Options} options Positioning options compatible with
	 * {@link module:utils/dom/position~getOptimalPosition}.
	 */
	_startPinning( options ) {
		this.attachTo( options );

		const targetElement = getDomElement( options.target );
		const limiterElement = options.limiter ? getDomElement( options.limiter ) : defaultLimiterElement;

		// Then we need to listen on scroll event of eny element in the document.
		this.listenTo( global$2.document, 'scroll', ( evt, domEvt ) => {
			const scrollTarget = domEvt.target;

			// The position needs to be updated if the positioning target is within the scrolled element.
			const isWithinScrollTarget = targetElement && scrollTarget.contains( targetElement );

			// The position needs to be updated if the positioning limiter is within the scrolled element.
			const isLimiterWithinScrollTarget = limiterElement && scrollTarget.contains( limiterElement );

			// The positioning target and/or limiter can be a Rect, object etc..
			// There's no way to optimize the listener then.
			if ( isWithinScrollTarget || isLimiterWithinScrollTarget || !targetElement || !limiterElement ) {
				this.attachTo( options );
			}
		}, { useCapture: true } );

		// We need to listen on window resize event and update position.
		this.listenTo( global$2.window, 'resize', () => {
			this.attachTo( options );
		} );
	}

	/**
	 * Stops managing the pinned state of the panel. See {@link #pin}.
	 *
	 * @private
	 */
	_stopPinning() {
		this.stopListening( global$2.document, 'scroll' );
		this.stopListening( global$2.window, 'resize' );
	}
}

// Returns the DOM element for given object or null, if there is none,
// e.g. when the passed object is a Rect instance or so.
//
// @private
// @param {*} object
// @returns {HTMLElement|null}
function getDomElement( object ) {
	if ( isElement( object ) ) {
		return object;
	}

	if ( isRange( object ) ) {
		return object.commonAncestorContainer;
	}

	if ( typeof object == 'function' ) {
		return getDomElement( object() );
	}

	return null;
}

/**
 * A side offset of the arrow tip from the edge of the balloon. Controlled by CSS.
 *
 *		 ┌───────────────────────┐
 *		 │                       │
 *		 │         Balloon       │
 *		 │         Content       │
 *		 │                       │
 *		 └──+    +───────────────┘
 *		 |   \  /
 *		 |    \/
 *		>┼─────┼< ─────────────────────── side offset
 *
 *
 * @default 25
 * @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowSideOffset
 */
BalloonPanelView.arrowSideOffset = 25;

/**
 * A height offset of the arrow from the edge of the balloon. Controlled by CSS.
 *
 *		 ┌───────────────────────┐
 *		 │                       │
 *		 │         Balloon       │
 *		 │         Content       │      ╱-- arrow height offset
 *		 │                       │      V
 *		 └──+    +───────────────┘ --- ─┼───────
 *		     \  /                       │
 *		      \/                        │
 *		────────────────────────────────┼───────
 *		                                ^
 *
 *
 *		>┼────┼<  arrow height offset
 *		 │    │
 *		 │    ┌────────────────────────┐
 *		 │    │                        │
 *		 │   ╱                         │
 *		 │ ╱            Balloon        │
 *		 │ ╲            Content        │
 *		 │   ╲                         │
 *		 │    │                        │
 *		 │    └────────────────────────┘
 *
 *
 * @default 10
 * @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowHeightOffset
 */
BalloonPanelView.arrowHeightOffset = 10;

/**
 * A vertical offset of the balloon panel from the edge of the viewport if sticky.
 * It helps in accessing toolbar buttons underneath the balloon panel.
 *
 *		  ┌───────────────────────────────────────────────────┐
 *		  │                      Target                       │
 *		  │                                                   │
 *		  │                            /── vertical offset    │
 *		┌─────────────────────────────V─────────────────────────┐
 *		│ Toolbar            ┌─────────────┐                    │
 *		├────────────────────│   Balloon   │────────────────────┤
 *		│ │                  └─────────────┘                  │ │
 *		│ │                                                   │ │
 *		│ │                                                   │ │
 *		│ │                                                   │ │
 *		│ └───────────────────────────────────────────────────┘ │
 *		│                        Viewport                       │
 *		└───────────────────────────────────────────────────────┘
 *
 * @default 20
 * @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.stickyVerticalOffset
 */
BalloonPanelView.stickyVerticalOffset = 20;

/**
 * Function used to calculate the optimal position for the balloon.
 *
 * @protected
 * @member {Function} module:ui/panel/balloon/balloonpanelview~BalloonPanelView._getOptimalPosition
 */
BalloonPanelView._getOptimalPosition = getOptimalPosition;

/**
 * A default set of positioning functions used by the balloon panel view
 * when attaching using the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo} method.
 *
 * The available positioning functions are as follows:
 *
 *
 *
 * **North west**
 *
 * * `northWestArrowSouthWest`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		 V
 *		 [ Target ]
 *
 * * `northWestArrowSouthMiddleWest`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		    V
 *		    [ Target ]
 *
 * * `northWestArrowSouth`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		         V
 *		         [ Target ]
 *
 * * `northWestArrowSouthMiddleEast`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		             V
 *		             [ Target ]
 *
 * * `northWestArrowSouthEast`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		                 V
 *		                 [ Target ]
 *
 *
 *
 * **North**
 *
 * * `northArrowSouthWest`
 *
 *		    +-----------------+
 *		    |     Balloon     |
 *		    +-----------------+
 *		     V
 *		[ Target ]
 *
 * * `northArrowSouthMiddleWest`
 *
 *		 +-----------------+
 *		 |     Balloon     |
 *		 +-----------------+
 *		     V
 *		[ Target ]
 *
 * * `northArrowSouth`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		         V
 *		    [ Target ]
 *
 * * `northArrowSouthMiddleEast`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		             V
 *		        [ Target ]
 *
 * * `northArrowSouthEast`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		                V
 *		           [ Target ]
 *
 * **North east**
 *
 * * `northEastArrowSouthWest`
 *
 *		        +-----------------+
 *		        |     Balloon     |
 *		        +-----------------+
 *		         V
 *		[ Target ]
 *
 *
 * * `northEastArrowSouthMiddleWest`
 *
 *		     +-----------------+
 *		     |     Balloon     |
 *		     +-----------------+
 *		         V
 *		[ Target ]
 *
 * * `northEastArrowSouth`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		         V
 *		[ Target ]
 *
 * * `northEastArrowSouthMiddleEast`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		             V
 *		    [ Target ]
 *
 * * `northEastArrowSouthEast`
 *
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *		                 V
 *		        [ Target ]
 *
 *
 *
 * **South**
 *
 *
 * * `southArrowNorthWest`
 *
 *		[ Target ]
 *		     ^
 *		    +-----------------+
 *		    |     Balloon     |
 *		    +-----------------+
 *
 * * `southArrowNorthMiddleWest`
 *
 *		   [ Target ]
 *		        ^
 *		    +-----------------+
 *		    |     Balloon     |
 *		    +-----------------+
 *
 * * `southArrowNorth`
 *
 *		    [ Target ]
 *		         ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 * * `southArrowNorthMiddleEast`
 *
 *		            [ Target ]
 *		                 ^
 *		   +-----------------+
 *		   |     Balloon     |
 *		   +-----------------+
 *
 * * `southArrowNorthEast`
 *
 *		            [ Target ]
 *		                 ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 *
 *
 * **South west**
 *
 * * `southWestArrowNorthWest`
 *
 *		 [ Target ]
 *		 ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 * * `southWestArrowNorthMiddleWest`
 *
 *		     [ Target ]
 *		     ^
 *		 +-----------------+
 *		 |     Balloon     |
 *		 +-----------------+
 *
 * * `southWestArrowNorth`
 *
 *		         [ Target ]
 *		         ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 * * `southWestArrowNorthMiddleEast`
 *
 *		              [ Target ]
 *		              ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 * * `southWestArrowNorthEast`
 *
 *		                 [ Target ]
 *		                 ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 *
 *
 * **South east**
 *
 * * `southEastArrowNorthWest`
 *
 *		[ Target ]
 *		         ^
 *		        +-----------------+
 *		        |     Balloon     |
 *		        +-----------------+
 * * `southEastArrowNorthMiddleWest`
 *
 *		   [ Target ]
 *		            ^
 *		        +-----------------+
 *		        |     Balloon     |
 *		        +-----------------+
 *
 * * `southEastArrowNorth`
 *
 *		[ Target ]
 *		         ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 * * `southEastArrowNorthMiddleEast`
 *
 *		     [ Target ]
 *		              ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 * * `southEastArrowNorthEast`
 *
 *		        [ Target ]
 *		                 ^
 *		+-----------------+
 *		|     Balloon     |
 *		+-----------------+
 *
 *
 *
 * **West**
 *
 * * `westArrowEast`
 *
 *		+-----------------+
 *		|     Balloon     |>[ Target ]
 *		+-----------------+
 *
 * **East**
 *
 * * `eastArrowWest`
 *
 *		           +-----------------+
 *		[ Target ]<|     Balloon     |
 *		           +-----------------+
 *
 *
 *
 * **Sticky**
 *
 * * `viewportStickyNorth`
 *
 *		    +---------------------------+
 *		    |        [ Target ]         |
 *		    |                           |
 *		+-----------------------------------+
 *		|   |    +-----------------+    |   |
 *		|   |    |     Balloon     |    |   |
 *		|   |    +-----------------+    |   |
 *		|   |                           |   |
 *		|   |                           |   |
 *		|   |                           |   |
 *		|   |                           |   |
 *		|   +---------------------------+   |
 *		|             Viewport              |
 *		+-----------------------------------+
 *
 * See {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo}.
 *
 * Positioning functions must be compatible with {@link module:utils/dom/position~Position}.
 *
 * Default positioning functions with customized offsets can be generated using
 * {@link module:ui/panel/balloon/balloonpanelview~generatePositions}.
 *
 * The name that the position function returns will be reflected in the balloon panel's class that
 * controls the placement of the "arrow". See {@link #position} to learn more.
 *
 * @member {Object.<String,module:utils/dom/position~PositioningFunction>}
 * module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions
 */
BalloonPanelView.defaultPositions = generatePositions();

/**
 * Returns available {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
 * {@link module:utils/dom/position~PositioningFunction positioning functions} adjusted by the specific offsets.
 *
 * @protected
 * @param {Object} [options] Options to generate positions. If not specified, this helper will simply return
 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
 * @param {Number} [options.sideOffset] A custom side offset (in pixels) of each position. If
 * not specified, {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowSideOffset the default value}
 * will be used.
 * @param {Number} [options.heightOffset] A custom height offset (in pixels) of each position. If
 * not specified, {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowHeightOffset the default value}
 * will be used.
 * @param {Number} [options.stickyVerticalOffset] A custom offset (in pixels) of the `viewportStickyNorth` positioning function.
 * If not specified, {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.stickyVerticalOffset the default value}
 * will be used.
 * @param {Object} [options.config] Additional configuration of the balloon balloon panel view.
 * Currently only {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#withArrow} is supported. Learn more
 * about {@link module:utils/dom/position~PositioningFunction positioning functions}.
 * @returns {Object.<String,module:utils/dom/position~PositioningFunction>}
 */
function generatePositions( {
	sideOffset = BalloonPanelView.arrowSideOffset,
	heightOffset = BalloonPanelView.arrowHeightOffset,
	stickyVerticalOffset = BalloonPanelView.stickyVerticalOffset,
	config
} = {} ) {
	return {
		// ------- North west

		northWestArrowSouthWest: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.left - sideOffset,
			name: 'arrow_sw',
			...( config && { config } )
		} ),

		northWestArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.left - ( balloonRect.width * .25 ) - sideOffset,
			name: 'arrow_smw',
			...( config && { config } )
		} ),

		northWestArrowSouth: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.left - balloonRect.width / 2,
			name: 'arrow_s',
			...( config && { config } )
		} ),

		northWestArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.left - ( balloonRect.width * .75 ) + sideOffset,
			name: 'arrow_sme',
			...( config && { config } )
		} ),

		northWestArrowSouthEast: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.left - balloonRect.width + sideOffset,
			name: 'arrow_se',
			...( config && { config } )
		} ),

		// ------- North

		northArrowSouthWest: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.left + targetRect.width / 2 - sideOffset,
			name: 'arrow_sw',
			...( config && { config } )
		} ),

		northArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .25 ) - sideOffset,
			name: 'arrow_smw',
			...( config && { config } )
		} ),

		northArrowSouth: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
			name: 'arrow_s',
			...( config && { config } )
		} ),

		northArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .75 ) + sideOffset,
			name: 'arrow_sme',
			...( config && { config } )
		} ),

		northArrowSouthEast: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.left + targetRect.width / 2 - balloonRect.width + sideOffset,
			name: 'arrow_se',
			...( config && { config } )
		} ),

		// ------- North east

		northEastArrowSouthWest: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.right - sideOffset,
			name: 'arrow_sw',
			...( config && { config } )
		} ),

		northEastArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.right - ( balloonRect.width * .25 ) - sideOffset,
			name: 'arrow_smw',
			...( config && { config } )
		} ),

		northEastArrowSouth: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.right - balloonRect.width / 2,
			name: 'arrow_s',
			...( config && { config } )
		} ),

		northEastArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.right - ( balloonRect.width * .75 ) + sideOffset,
			name: 'arrow_sme',
			...( config && { config } )
		} ),

		northEastArrowSouthEast: ( targetRect, balloonRect ) => ( {
			top: getNorthTop( targetRect, balloonRect ),
			left: targetRect.right - balloonRect.width + sideOffset,
			name: 'arrow_se',
			...( config && { config } )
		} ),

		// ------- South west

		southWestArrowNorthWest: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.left - sideOffset,
			name: 'arrow_nw',
			...( config && { config } )
		} ),

		southWestArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.left - ( balloonRect.width * .25 ) - sideOffset,
			name: 'arrow_nmw',
			...( config && { config } )
		} ),

		southWestArrowNorth: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.left - balloonRect.width / 2,
			name: 'arrow_n',
			...( config && { config } )
		} ),

		southWestArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.left - ( balloonRect.width * .75 ) + sideOffset,
			name: 'arrow_nme',
			...( config && { config } )
		} ),

		southWestArrowNorthEast: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.left - balloonRect.width + sideOffset,
			name: 'arrow_ne',
			...( config && { config } )
		} ),

		// ------- South

		southArrowNorthWest: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.left + targetRect.width / 2 - sideOffset,
			name: 'arrow_nw',
			...( config && { config } )
		} ),

		southArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.25 ) - sideOffset,
			name: 'arrow_nmw',
			...( config && { config } )
		} ),

		southArrowNorth: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
			name: 'arrow_n',
			...( config && { config } )
		} ),

		southArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.75 ) + sideOffset,
			name: 'arrow_nme',
			...( config && { config } )
		} ),

		southArrowNorthEast: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.left + targetRect.width / 2 - balloonRect.width + sideOffset,
			name: 'arrow_ne',
			...( config && { config } )
		} ),

		// ------- South east

		southEastArrowNorthWest: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.right - sideOffset,
			name: 'arrow_nw',
			...( config && { config } )
		} ),

		southEastArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.right - ( balloonRect.width * .25 ) - sideOffset,
			name: 'arrow_nmw',
			...( config && { config } )
		} ),

		southEastArrowNorth: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.right - balloonRect.width / 2,
			name: 'arrow_n',
			...( config && { config } )
		} ),

		southEastArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.right - ( balloonRect.width * .75 ) + sideOffset,
			name: 'arrow_nme',
			...( config && { config } )
		} ),

		southEastArrowNorthEast: ( targetRect, balloonRect ) => ( {
			top: getSouthTop( targetRect),
			left: targetRect.right - balloonRect.width + sideOffset,
			name: 'arrow_ne',
			...( config && { config } )
		} ),

		// ------- West

		westArrowEast: ( targetRect, balloonRect ) => ( {
			top: targetRect.top + targetRect.height / 2 - balloonRect.height / 2,
			left: targetRect.left - balloonRect.width - heightOffset,
			name: 'arrow_e',
			...( config && { config } )
		} ),

		// ------- East

		eastArrowWest: ( targetRect, balloonRect ) => ( {
			top: targetRect.top + targetRect.height / 2 - balloonRect.height / 2,
			left: targetRect.right + heightOffset,
			name: 'arrow_w',
			...( config && { config } )
		} ),

		// ------- Sticky

		viewportStickyNorth: ( targetRect, balloonRect, viewportRect ) => {
			if ( !targetRect.getIntersection( viewportRect ) ) {
				return null;
			}

			return {
				top: viewportRect.top + stickyVerticalOffset,
				left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
				name: 'arrowless',
				config: {
					withArrow: false,
					...config
				}
			};
		}
	};

	// Returns the top coordinate for positions starting with `north*`.
	//
	// @private
	// @param {utils/dom/rect~Rect} targetRect A rect of the target.
	// @param {utils/dom/rect~Rect} elementRect A rect of the balloon.
	// @returns {Number}
	function getNorthTop( targetRect, balloonRect ) {
		return targetRect.top - balloonRect.height - heightOffset;
	}

	// Returns the top coordinate for positions starting with `south*`.
	//
	// @private
	// @param {utils/dom/rect~Rect} targetRect A rect of the target.
	// @param {utils/dom/rect~Rect} elementRect A rect of the balloon.
	// @returns {Number}
	function getSouthTop( targetRect ) {
		return targetRect.bottom + heightOffset;
	}
}var css_248z$T = ".ck.ck-balloon-panel.ck-tooltip{--ck-balloon-border-width:0px;--ck-balloon-arrow-offset:0px;--ck-balloon-arrow-half-width:4px;--ck-balloon-arrow-height:4px;--ck-color-panel-background:var(--ck-color-tooltip-background);padding:0 var(--ck-spacing-medium);pointer-events:none;z-index:calc(var(--ck-z-modal) + 100)}.ck.ck-balloon-panel.ck-tooltip .ck-tooltip__text{color:var(--ck-color-tooltip-text);font-size:.9em;line-height:1.5}.ck.ck-balloon-panel.ck-tooltip{box-shadow:none}.ck.ck-balloon-panel.ck-tooltip:before{display:none}";
styleInject(css_248z$T);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const BALLOON_CLASS = 'ck-tooltip';

/**
 * A tooltip manager class for the UI of the editor.
 *
 * **Note**: Most likely you do not have to use the `TooltipManager` API listed below in order to display tooltips. Popular
 * {@glink framework/guides/architecture/ui-library UI components} support tooltips out-of-the-box via observable properties
 * (see {@link module:ui/button/buttonview~ButtonView#tooltip} and {@link module:ui/button/buttonview~ButtonView#tooltipPosition}).
 *
 * # Displaying tooltips
 *
 * To display a tooltip, set `data-cke-tooltip-text` attribute on any DOM element:
 *
 *		domElement.dataset.ckeTooltipText = 'My tooltip';
 *
 * The tooltip will show up whenever the user moves the mouse over the element or the element gets focus in DOM.
 *
 * # Positioning tooltips
 *
 * To change the position of the tooltip, use the `data-cke-tooltip-position` attribute (`s`, `se`, `sw`, `n`, `e`, or `w`):
 *
 *		domElement.dataset.ckeTooltipText = 'Tooltip to the north';
 *		domElement.dataset.ckeTooltipPosition = 'n';
 *
 * # Disabling tooltips
 *
 * In order to disable the tooltip  temporarily, use the `data-cke-tooltip-disabled` attribute:
 *
 *		domElement.dataset.ckeTooltipText = 'Disabled. For now.';
 *		domElement.dataset.ckeTooltipDisabled = 'true';
 *
 *
 * # Styling tooltips
 *
 * By default, the tooltip has `.ck-tooltip` class and its text inner `.ck-tooltip__text`.
 *
 * If your tooltip requires custom styling, using `data-cke-tooltip-class` attribute will add additional class to the balloon
 * displaying the tooltip:
 *
 *		domElement.dataset.ckeTooltipText = 'Tooltip with a red text';
 *		domElement.dataset.ckeTooltipClass = 'my-class';
 *
 *		.ck.ck-tooltip.my-class { color: red }
 *
 * **Note**: This class is a singleton. All editor instances re-use the same instance loaded by
 * {@link module:core/editor/editorui~EditorUI} of the first editor.
 *
 * @mixes module:utils/domemittermixin~DomEmitterMixin
 */
class TooltipManager {
	/**
	 * Creates an instance of the tooltip manager.
	 *
	 * @param {module:core/editor/editor~Editor} editor
	 */
	constructor( editor ) {
		TooltipManager._editors.add( editor );

		// TooltipManager must be a singleton. Multiple instances would mean multiple tooltips attached
		// to the same DOM element with data-cke-tooltip-* attributes.
		if ( TooltipManager._instance ) {
			return TooltipManager._instance;
		}

		TooltipManager._instance = this;

		/**
		 * The view rendering text of the tooltip.
		 *
		 * @readonly
		 * @member {module:ui/view~View} #tooltipTextView
		 */
		this.tooltipTextView = new View( editor.locale );
		this.tooltipTextView.set( 'text', '' );
		this.tooltipTextView.setTemplate( {
			tag: 'span',
			attributes: {
				class: [
					'ck',
					'ck-tooltip__text'
				]
			},
			children: [
				{
					text: this.tooltipTextView.bindTemplate.to( 'text' )
				}
			]
		} );

		/**
		 * The instance of the balloon panel that renders and positions the tooltip.
		 *
		 * @readonly
		 * @member {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} #balloonPanelView
		 */
		this.balloonPanelView = new BalloonPanelView( editor.locale );
		this.balloonPanelView.class = BALLOON_CLASS;
		this.balloonPanelView.content.add( this.tooltipTextView );

		/**
		 * Stores the reference to the DOM element the tooltip is attached to. `null` when there's no tooltip
		 * in the UI.
		 *
		 * @private
		 * @readonly
		 * @member {HTMLElement|null} #_currentElementWithTooltip
		 */
		this._currentElementWithTooltip = null;

		/**
		 * Stores the current tooltip position. `null` when there's no tooltip in the UI.
		 *
		 * @private
		 * @readonly
		 * @member {String|null} #_currentTooltipPosition
		 */
		this._currentTooltipPosition = null;

		/**
		 * A debounced version of {@link #_pinTooltip}. Tooltips show with a delay to avoid flashing and
		 * to improve the UX.
		 *
		 * @private
		 * @readonly
		 * @member {Function} #_pinTooltipDebounced
		 */
		this._pinTooltipDebounced = debounce( this._pinTooltip, 600 );

		this.listenTo( global$2.document, 'mouseenter', this._onEnterOrFocus.bind( this ), { useCapture: true } );
		this.listenTo( global$2.document, 'mouseleave', this._onLeaveOrBlur.bind( this ), { useCapture: true } );

		this.listenTo( global$2.document, 'focus', this._onEnterOrFocus.bind( this ), { useCapture: true } );
		this.listenTo( global$2.document, 'blur', this._onLeaveOrBlur.bind( this ), { useCapture: true } );

		this.listenTo( global$2.document, 'scroll', this._onScroll.bind( this ), { useCapture: true } );

		// Because this class is a singleton, its only instance is shared across all editors and connects them through the reference.
		// This causes issues with the ContextWatchdog. When an error is thrown in one editor, the watchdog traverses the references
		// and (because of shared tooltip manager) figures that the error affects all editors and restarts them all.
		// This flag, excludes tooltip manager instance from the traversal and brings ContextWatchdog back to normal.
		// More in https://github.com/ckeditor/ckeditor5/issues/12292.
		this._watchdogExcluded = true;
	}

	/**
	 * Destroys the tooltip manager.
	 *
	 * **Note**: The manager singleton cannot be destroyed until all editors that use it are destroyed.
	 *
	 * @param {module:core/editor/editor~Editor} editor The editor the manager was created for.
	 */
	destroy( editor ) {
		TooltipManager._editors.delete( editor );
		this.stopListening( editor.ui );

		if ( !TooltipManager._editors.size ) {
			this._unpinTooltip();
			this.balloonPanelView.destroy();
			this.stopListening();

			TooltipManager._instance = null;
		}
	}

	/**
	 * Handles displaying tooltips on `mouseenter` and `focus` in DOM.
	 *
	 * @private
	 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
	 * @param {Event} domEvent The DOM event.
	 */
	_onEnterOrFocus( evt, { target } ) {
		const elementWithTooltipAttribute = getDescendantWithTooltip( target );

		// Abort when there's no descendant needing tooltip.
		if ( !elementWithTooltipAttribute ) {
			return;
		}

		// Abort to avoid flashing when, for instance:
		// * a tooltip is displayed for a focused element, then the same element gets mouseentered,
		// * a tooltip is displayed for an element via mouseenter, then the focus moves to the same element.
		if ( elementWithTooltipAttribute === this._currentElementWithTooltip ) {
			return;
		}

		this._unpinTooltip();

		this._pinTooltipDebounced( elementWithTooltipAttribute, getTooltipData( elementWithTooltipAttribute ) );
	}

	/**
	 * Handles hiding tooltips on `mouseleave` and `blur` in DOM.
	 *
	 * @private
	 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
	 * @param {Event} domEvent The DOM event.
	 */
	_onLeaveOrBlur( evt, { target, relatedTarget } ) {
		if ( evt.name === 'mouseleave' ) {
			// Don't act when the event does not concern a DOM element (e.g. a mouseleave out of an entire document),
			if ( !isElement( target ) ) {
				return;
			}

			// If a tooltip is currently visible, don't act for a targets other than the one it is attached to.
			// For instance, a random mouseleave far away in the page should not unpin the tooltip that was pinned because
			// of a previous focus. Only leaving the same element should hide the tooltip.
			if ( this._currentElementWithTooltip && target !== this._currentElementWithTooltip ) {
				return;
			}

			const descendantWithTooltip = getDescendantWithTooltip( target );
			const relatedDescendantWithTooltip = getDescendantWithTooltip( relatedTarget );

			// Unpin when the mouse was leaving element with a tooltip to a place which does not have or has a different tooltip.
			// Note that this should happen whether the tooltip is already visible or not, for instance, it could be invisible but queued
			// (debounced): it should get canceled.
			if ( descendantWithTooltip && descendantWithTooltip !== relatedDescendantWithTooltip ) {
				this._unpinTooltip();
			}
		}
		else {
			// If a tooltip is currently visible, don't act for a targets other than the one it is attached to.
			// For instance, a random blur in the web page should not unpin the tooltip that was pinned because of a previous mouseenter.
			if ( this._currentElementWithTooltip && target !== this._currentElementWithTooltip ) {
				return;
			}

			// Note that unpinning should happen whether the tooltip is already visible or not, for instance, it could be invisible but
			// queued (debounced): it should get canceled (e.g. quick focus then quick blur using the keyboard).
			this._unpinTooltip();
		}
	}

	/**
	 * Handles hiding tooltips on `scroll` in DOM.
	 *
	 * @private
	 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
	 * @param {Event} domEvent The DOM event.
	 */
	_onScroll( evt, { target } ) {
		// No tooltip, no reason to react on scroll.
		if ( !this._currentElementWithTooltip ) {
			return;
		}

		// When scrolling a container that has both the balloon and the current element (common ancestor), the balloon can remain
		// visible (e.g. scrolling ≤body>). Otherwise, to avoid glitches (clipping, lagging) better just hide the tooltip.
		// Also, don't do anything when scrolling an unrelated DOM element that has nothing to do with the current element and the balloon.
		if ( target.contains( this.balloonPanelView.element ) && target.contains( this._currentElementWithTooltip ) ) {
			return;
		}

		this._unpinTooltip();
	}

	/**
	 * Pins the tooltip to a specific DOM element.
	 *
	 * @private
	 * @param {Element} targetDomElement
	 * @param {Object} options
	 * @param {String} options.text Text of the tooltip to display.
	 * @param {String} options.position The position of the tooltip.
	 * @param {String} options.cssClass Additional CSS class of the balloon with the tooltip.
	 */
	_pinTooltip( targetDomElement, { text, position, cssClass } ) {
		// Use the body collection of the first editor.
		const bodyViewCollection = first( TooltipManager._editors.values() ).ui.view.body;

		if ( !bodyViewCollection.has( this.balloonPanelView ) ) {
			bodyViewCollection.add( this.balloonPanelView );
		}

		this.tooltipTextView.text = text;

		this.balloonPanelView.pin( {
			target: targetDomElement,
			positions: TooltipManager.getPositioningFunctions( position )
		} );

		this.balloonPanelView.class = [ BALLOON_CLASS, cssClass ]
			.filter( className => className )
			.join( ' ' );

		// Start responding to changes in editor UI or content layout. For instance, when collaborators change content
		// and a contextual toolbar attached to a content starts to move (and so should move the tooltip).
		// Note: Using low priority to let other listeners that position contextual toolbars etc. to react first.
		for ( const editor of TooltipManager._editors ) {
			this.listenTo( editor.ui, 'update', this._updateTooltipPosition.bind( this ), { priority: 'low' } );
		}

		this._currentElementWithTooltip = targetDomElement;
		this._currentTooltipPosition = position;
	}

	/**
	 * Unpins the tooltip and cancels all queued pinning.
	 *
	 * @private
	 */
	_unpinTooltip() {
		this._pinTooltipDebounced.cancel();

		this.balloonPanelView.unpin();

		for ( const editor of TooltipManager._editors ) {
			this.stopListening( editor.ui, 'update' );
		}

		this._currentElementWithTooltip = null;
		this._currentTooltipPosition = null;
	}

	/**
	 * Updates the position of the tooltip so it stays in sync with the element it is pinned to.
	 *
	 * Hides the tooltip when the element is no longer visible in DOM.
	 *
	 * @private
	 */
	_updateTooltipPosition() {
		// This could happen if the tooltip was attached somewhere in a contextual content toolbar and the toolbar
		// disappeared (e.g. removed an image).
		if ( !isVisible( this._currentElementWithTooltip ) ) {
			this._unpinTooltip();

			return;
		}

		this.balloonPanelView.pin( {
			target: this._currentElementWithTooltip,
			positions: TooltipManager.getPositioningFunctions( this._currentTooltipPosition )
		} );
	}

	/**
	 * Returns {@link #balloonPanelView} {@link module:utils/dom/position~PositioningFunction positioning functions} for a given position
	 * name.
	 *
	 * @static
	 * @param {String} position Name of the position (`s`, `se`, `sw`, `n`, `e`, or `w`).
	 * @returns {Array.<module:utils/dom/position~PositioningFunction>} Positioning functions to be used by the {@link #balloonPanelView}.
	 */
	static getPositioningFunctions( position ) {
		const defaultPositions = TooltipManager.defaultBalloonPositions;

		return {
			// South is most popular. We can use positioning heuristics to avoid clipping by the viewport with the sane fallback.
			s: [
				defaultPositions.southArrowNorth,
				defaultPositions.southArrowNorthEast,
				defaultPositions.southArrowNorthWest
			],
			n: [ defaultPositions.northArrowSouth ],
			e: [ defaultPositions.eastArrowWest ],
			w: [ defaultPositions.westArrowEast ],
			sw: [ defaultPositions.southArrowNorthEast ],
			se: [ defaultPositions.southArrowNorthWest ]
		}[ position ];
	}
}

mix( TooltipManager, DomEmitterMixin );

/**
 * A set of default {@link module:utils/dom/position~PositioningFunction positioning functions} used by the `TooltipManager`
 * to pin tooltips in different positions.
 *
 * @member {Object.<String,module:utils/dom/position~PositioningFunction>}
 * module:ui/tooltipmanager~TooltipManager.defaultBalloonPositions
 */
TooltipManager.defaultBalloonPositions = generatePositions( {
	heightOffset: 5,
	sideOffset: 13
} );

/**
 * A reference to the `TooltipManager` instance. The class is a singleton and as such,
 * successive attempts at creating instances should return this instance.
 *
 * @private
 * @member {module:ui/tooltipmanager~TooltipManager} module:ui/tooltipmanager~TooltipManager._instance
 */
TooltipManager._instance = null;

/**
 * An array of editors the single tooltip manager instance must listen to.
 * This is mostly to handle `EditorUI#update` listeners from individual editors.
 *
 * @private
 * @member {Set.<module:core/editor/editor~Editor>} module:ui/tooltipmanager~TooltipManager._editors
 */
TooltipManager._editors = new Set();

function getDescendantWithTooltip( element ) {
	if ( !isElement( element ) ) {
		return null;
	}

	return element.closest( '[data-cke-tooltip-text]:not([data-cke-tooltip-disabled])' );
}

function getTooltipData( element ) {
	return {
		text: element.dataset.ckeTooltipText,
		position: element.dataset.ckeTooltipPosition || 's',
		cssClass: element.dataset.ckeTooltipClass || ''
	};
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A class providing the minimal interface that is required to successfully bootstrap any editor UI.
 *
 * @mixes module:utils/emittermixin~EmitterMixin
 */
class EditorUI {
	/**
	 * Creates an instance of the editor UI class.
	 *
	 * @param {module:core/editor/editor~Editor} editor The editor instance.
	 */
	constructor( editor ) {
		/**
		 * The editor that the UI belongs to.
		 *
		 * @readonly
		 * @member {module:core/editor/editor~Editor} #editor
		 */
		this.editor = editor;

		/**
		 * An instance of the {@link module:ui/componentfactory~ComponentFactory}, a registry used by plugins
		 * to register factories of specific UI components.
		 *
		 * @readonly
		 * @member {module:ui/componentfactory~ComponentFactory} #componentFactory
		 */
		this.componentFactory = new ComponentFactory( editor );

		/**
		 * Stores the information about the editor UI focus and propagates it so various plugins and components
		 * are unified as a focus group.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker} #focusTracker
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * Manages the tooltips displayed on mouseover and focus across the UI.
		 *
		 * @readonly
		 * @member {module:ui/tooltipmanager~TooltipManager}
		 */
		this.tooltipManager = new TooltipManager( editor );

		/**
		 * Stores viewport offsets from every direction.
		 *
		 * Viewport offset can be used to constrain balloons or other UI elements into an element smaller than the viewport.
		 * This can be useful if there are any other absolutely positioned elements that may interfere with editor UI.
		 *
		 * Example `editor.ui.viewportOffset` returns:
		 *
		 * ```js
		 * {
		 * 	top: 50,
		 * 	right: 50,
		 * 	bottom: 50,
		 * 	left: 50
		 * }
		 * ```
		 *
		 * This property can be overriden after editor already being initialized:
		 *
		 * ```js
		 * editor.ui.viewportOffset = {
		 * 	top: 100,
		 * 	right: 0,
		 * 	bottom: 0,
		 * 	left: 0
		 * };
		 * ```
		 *
		 * @observable
		 * @member {Object} #viewportOffset
		 */
		this.set( 'viewportOffset', this._readViewportOffsetFromConfig() );

		/**
		 * Indicates the UI is ready. Set `true` after {@link #event:ready} event is fired.
		 *
		 * @readonly
		 * @default false
		 * @member {Boolean} #isReady
		 */
		this.isReady = false;
		this.once( 'ready', () => {
			this.isReady = true;
		} );

		/**
		 * Stores all editable elements used by the editor instance.
		 *
		 * @private
		 * @member {Map.<String,HTMLElement>}
		 */
		this._editableElementsMap = new Map();

		/**
		 * All available & focusable toolbars.
		 *
		 * @private
		 * @type {Array.<module:core/editor/editorui~FocusableToolbarDefinition>}
		 */
		this._focusableToolbarDefinitions = [];

		// Informs UI components that should be refreshed after layout change.
		this.listenTo( editor.editing.view.document, 'layoutChanged', () => this.update() );

		this._initFocusTracking();
	}

	/**
	 * The main (outermost) DOM element of the editor UI.
	 *
	 * For example, in {@link module:editor-classic/classiceditor~ClassicEditor} it is a `<div>` which
	 * wraps the editable element and the toolbar. In {@link module:editor-inline/inlineeditor~InlineEditor}
	 * it is the editable element itself (as there is no other wrapper). However, in
	 * {@link module:editor-decoupled/decouplededitor~DecoupledEditor} it is set to `null` because this editor does not
	 * come with a single "main" HTML element (its editable element and toolbar are separate).
	 *
	 * This property can be understood as a shorthand for retrieving the element that a specific editor integration
	 * considers to be its main DOM element.
	 *
	 * @readonly
	 * @member {HTMLElement|null} #element
	 */
	get element() {
		return null;
	}

	/**
	 * Fires the {@link module:core/editor/editorui~EditorUI#event:update `update`} event.
	 *
	 * This method should be called when the editor UI (e.g. positions of its balloons) needs to be updated due to
	 * some environmental change which CKEditor 5 is not aware of (e.g. resize of a container in which it is used).
	 */
	update() {
		this.fire( 'update' );
	}

	/**
	 * Destroys the UI.
	 */
	destroy() {
		this.stopListening();

		this.focusTracker.destroy();
		this.tooltipManager.destroy( this.editor );

		// Clean–up the references to the CKEditor instance stored in the native editable DOM elements.
		for ( const domElement of this._editableElementsMap.values() ) {
			domElement.ckeditorInstance = null;
		}

		this._editableElementsMap = new Map();
		this._focusableToolbarDefinitions = [];
	}

	/**
	 * Stores the native DOM editable element used by the editor under a unique name.
	 *
	 * Also, registers the element in the editor to maintain the accessibility of the UI. When the user is editing text in a focusable
	 * editable area, they can use the <kbd>Alt</kbd> + <kbd>F10</kbd> keystroke to navigate over editor toolbars. See {@link #addToolbar}.
	 *
	 * @param {String} rootName The unique name of the editable element.
	 * @param {HTMLElement} domElement The native DOM editable element.
	 */
	setEditableElement( rootName, domElement ) {
		this._editableElementsMap.set( rootName, domElement );

		// Put a reference to the CKEditor instance in the editable native DOM element.
		// It helps 3rd–party software (browser extensions, other libraries) access and recognize
		// CKEditor 5 instances (editing roots) and use their API (there is no global editor
		// instance registry).
		if ( !domElement.ckeditorInstance ) {
			domElement.ckeditorInstance = this.editor;
		}

		// Register the element so it becomes available for Alt+F10 and Esc navigation.
		this.focusTracker.add( domElement );

		const setUpKeystrokeHandler = () => {
			// The editing view of the editor is already listening to keystrokes from DOM roots (see: KeyObserver).
			// Do not duplicate listeners.
			if ( this.editor.editing.view.getDomRoot( rootName ) ) {
				return;
			}

			this.editor.keystrokes.listenTo( domElement );
		};

		// For editable elements set by features after EditorUI is ready (e.g. source editing).
		if ( this.isReady ) {
			setUpKeystrokeHandler();
		}
		// For editable elements set while the editor is being created (e.g. DOM roots).
		else {
			this.once( 'ready', setUpKeystrokeHandler );
		}
	}

	/**
	 * Returns the editable editor element with the given name or null if editable does not exist.
	 *
	 * @param {String} [rootName=main] The editable name.
	 * @returns {HTMLElement|undefined}
	 */
	getEditableElement( rootName = 'main' ) {
		return this._editableElementsMap.get( rootName );
	}

	/**
	 * Returns array of names of all editor editable elements.
	 *
	 * @returns {Iterable.<String>}
	 */
	getEditableElementsNames() {
		return this._editableElementsMap.keys();
	}

	/**
	 * Adds a toolbar to the editor UI. Used primarily to maintain the accessibility of the UI.
	 *
	 * Focusable toolbars can be accessed (focused) by users by pressing the <kbd>Alt</kbd> + <kbd>F10</kbd> keystroke.
	 * Successive keystroke presses navigate over available toolbars.
	 *
	 * @param {module:ui/toolbar/toolbarview~ToolbarView} toolbarView A instance of the toolbar to be registered.
	 * @param {Object} [options]
	 * @param {Boolean} [options.isContextual] Set `true` if the toolbar is attached to the content of the editor. Such toolbar takes
	 * a precedence over other toolbars when a user pressed <kbd>Alt</kbd> + <kbd>F10</kbd>.
	 * @param {Function} [options.beforeFocus] Specify a callback executed before the toolbar instance DOM element gains focus
	 * upon the <kbd>Alt</kbd> + <kbd>F10</kbd> keystroke.
	 * @param {Function} [options.afterBlur] Specify a callback executed after the toolbar instance DOM element loses focus upon
	 * <kbd>Esc</kbd> keystroke but before the focus goes back to the {@link #setEditableElement editable element}.
	 */
	addToolbar( toolbarView, options = {} ) {
		if ( toolbarView.isRendered ) {
			this.focusTracker.add( toolbarView.element );
			this.editor.keystrokes.listenTo( toolbarView.element );
		} else {
			toolbarView.once( 'render', () => {
				this.focusTracker.add( toolbarView.element );
				this.editor.keystrokes.listenTo( toolbarView.element );
			} );
		}

		this._focusableToolbarDefinitions.push( { toolbarView, options } );
	}

	/**
	 * Stores all editable elements used by the editor instance.
	 *
	 * @protected
	 * @deprecated
	 * @member {Map.<String,HTMLElement>}
	 */
	get _editableElements() {
		/**
		 * The {@link module:core/editor/editorui~EditorUI#_editableElements `EditorUI#_editableElements`} property has been
		 * deprecated and will be removed in the near future. Please use {@link #setEditableElement `setEditableElement()`} and
		 * {@link #getEditableElement `getEditableElement()`} methods instead.
		 *
		 * @error editor-ui-deprecated-editable-elements
		 * @param {module:core/editor/editorui~EditorUI} editorUI Editor UI instance the deprecated property belongs to.
		 */
		console.warn(
			'editor-ui-deprecated-editable-elements: ' +
			'The EditorUI#_editableElements property has been deprecated and will be removed in the near future.',
			{ editorUI: this } );

		return this._editableElementsMap;
	}

	/**
	 * Returns viewport offsets object:
	 *
	 * ```js
	 * {
	 * 	top: Number,
	 * 	right: Number,
	 * 	bottom: Number,
	 * 	left: Number
	 * }
	 * ```
	 *
	 * Only top property is currently supported.
	 *
	 * @private
	 * @return {Object}
	 */
	_readViewportOffsetFromConfig() {
		const editor = this.editor;
		const viewportOffsetConfig = editor.config.get( 'ui.viewportOffset' );

		if ( viewportOffsetConfig ) {
			return viewportOffsetConfig;
		}

		const legacyOffsetConfig = editor.config.get( 'toolbar.viewportTopOffset' );

		// Fall back to deprecated toolbar config.
		if ( legacyOffsetConfig ) {
			/**
			 * The {@link module:core/editor/editorconfig~EditorConfig#toolbar `EditorConfig#toolbar.viewportTopOffset`}
			 * property has been deprecated and will be removed in the near future. Please use
			 * {@link module:core/editor/editorconfig~EditorConfig#ui `EditorConfig#ui.viewportOffset`} instead.
			 *
			 * @error editor-ui-deprecated-viewport-offset-config
			 */
			console.warn(
				'editor-ui-deprecated-viewport-offset-config: ' +
				'The `toolbar.vieportTopOffset` configuration option is deprecated. ' +
				'It will be removed from future CKEditor versions. Use `ui.viewportOffset.top` instead.'
			);

			return { top: legacyOffsetConfig };
		}

		// More keys to come in the future.
		return { top: 0 };
	}

	/**
	 * Starts listening for <kbd>Alt</kbd> + <kbd>F10</kbd> and <kbd>Esc</kbd> keystrokes in the context of focusable
	 * {@link #setEditableElement editable elements} and {@link #addToolbar toolbars}
	 * to allow users navigate across the UI.
	 *
	 * @private
	 */
	_initFocusTracking() {
		const editor = this.editor;
		const editingView = editor.editing.view;

		let lastFocusedForeignElement;
		let candidateDefinitions;

		// Focus the next focusable toolbar on <kbd>Alt</kbd> + <kbd>F10</kbd>.
		editor.keystrokes.set( 'Alt+F10', ( data, cancel ) => {
			const focusedElement = this.focusTracker.focusedElement;

			// Focus moved out of a DOM element that
			// * is not a toolbar,
			// * does not belong to the editing view (e.g. source editing).
			if (
				Array.from( this._editableElementsMap.values() ).includes( focusedElement ) &&
				!Array.from( editingView.domRoots.values() ).includes( focusedElement )
			) {
				lastFocusedForeignElement = focusedElement;
			}

			const currentFocusedToolbarDefinition = this._getCurrentFocusedToolbarDefinition();

			// * When focusing a toolbar for the first time, set the array of definitions for successive presses of Alt+F10.
			// This ensures, the navigation works always the same and no pair of toolbars takes over
			// (e.g. image and table toolbars when a selected image is inside a cell).
			// * It could be that the focus went to the toolbar by clicking a toolbar item (e.g. a dropdown). In this case,
			// there were no candidates so they must be obtained (#12339).
			if ( !currentFocusedToolbarDefinition || !candidateDefinitions ) {
				candidateDefinitions = this._getFocusableCandidateToolbarDefinitions( currentFocusedToolbarDefinition );
			}

			// In a single Alt+F10 press, check all candidates but if none were focused, don't go any further.
			// This prevents an infinite loop.
			for ( let i = 0; i < candidateDefinitions.length; i++ ) {
				const candidateDefinition = candidateDefinitions.shift();

				// Put the first definition to the back of the array. This allows circular navigation over all toolbars
				// on successive presses of Alt+F10.
				candidateDefinitions.push( candidateDefinition );

				// Don't focus the same toolbar again. If you did, this would move focus from the nth focused toolbar item back to the
				// first item as per ToolbarView#focus() if the user navigated inside the toolbar.
				if (
					candidateDefinition !== currentFocusedToolbarDefinition &&
					this._focusFocusableCandidateToolbar( candidateDefinition )
				) {
					// Clean up after a current visible toolbar when switching to the next one.
					if ( currentFocusedToolbarDefinition && currentFocusedToolbarDefinition.options.afterBlur ) {
						currentFocusedToolbarDefinition.options.afterBlur();
					}

					break;
				}
			}

			cancel();
		} );

		// Blur the focused toolbar on <kbd>Esc</kbd> and bring the focus back to its origin.
		editor.keystrokes.set( 'Esc', ( data, cancel ) => {
			const focusedToolbarDef = this._getCurrentFocusedToolbarDefinition();

			if ( !focusedToolbarDef ) {
				return;
			}

			// Bring focus back to where it came from before focusing the toolbar:
			// 1. If it came from outside the engine view (e.g. source editing), move it there.
			if ( lastFocusedForeignElement ) {
				lastFocusedForeignElement.focus();
				lastFocusedForeignElement = null;
			}
			// 2. There are two possibilities left:
			//   2.1. It could be that the focus went from an editable element in the view (root or nested).
			//   2.2. It could be the focus went straight to the toolbar before even focusing the editing area.
			// In either case, just focus the view editing. The focus will land where it belongs.
			else {
				editor.editing.view.focus();
			}

			// Clean up after the toolbar if there is anything to do there.
			if ( focusedToolbarDef.options.afterBlur ) {
				focusedToolbarDef.options.afterBlur();
			}

			cancel();
		} );
	}

	/**
	 * Returns definitions of toolbars that could potentially be focused, sorted by their importance for the user.
	 *
	 * Focusable toolbars candidates are either:
	 * * already visible,
	 * * have `beforeFocus()` set in their {@link module:core/editor/editorui~FocusableToolbarDefinition definition} that suggests that
	 * they might show up when called. Keep in mind that determining whether a toolbar will show up (and become focusable) is impossible
	 * at this stage because it depends on its implementation, that in turn depends on the editing context (selection).
	 *
	 * **Note**: Contextual toolbars take precedence over regular toolbars.
	 *
	 * @private
	 * @returns {Array.<module:core/editor/editorui~FocusableToolbarDefinition>}
	 */
	_getFocusableCandidateToolbarDefinitions() {
		const definitions = [];

		for ( const toolbarDef of this._focusableToolbarDefinitions ) {
			const { toolbarView, options } = toolbarDef;

			if ( isVisible( toolbarView.element ) || options.beforeFocus ) {
				definitions.push( toolbarDef );
			}
		}

		// Contextual and already visible toolbars have higher priority. If both are true, the toolbar will always focus first.
		// For instance, a selected widget toolbar vs inline editor toolbar: both are visible but the widget toolbar is contextual.
		definitions.sort( ( defA, defB ) => getToolbarDefinitionWeight( defA ) - getToolbarDefinitionWeight( defB ) );

		return definitions;
	}

	/**
	 * Returns a definition of the toolbar that is currently visible and focused (one of its children has focus).
	 *
	 * `null` is returned when no toolbar is currently focused.
	 *
	 * @private
	 * @returns {module:core/editor/editorui~FocusableToolbarDefinition|null}
	 */
	_getCurrentFocusedToolbarDefinition() {
		for ( const definition of this._focusableToolbarDefinitions ) {
			if ( definition.toolbarView.element && definition.toolbarView.element.contains( this.focusTracker.focusedElement ) ) {
				return definition;
			}
		}

		return null;
	}

	/**
	 * Focuses a focusable toolbar candidate using its definition.
	 *
	 * @private
	 * @param {module:core/editor/editorui~FocusableToolbarDefinition} candidateToolbarDefinition A definition of the toolbar to focus.
	 * @returns {Boolean} `true` when the toolbar candidate was focused. `false` otherwise.
	 */
	_focusFocusableCandidateToolbar( candidateToolbarDefinition ) {
		const { toolbarView, options: { beforeFocus } } = candidateToolbarDefinition;

		if ( beforeFocus ) {
			beforeFocus();
		}

		// If it didn't show up after beforeFocus(), it's not focusable at all.
		if ( !isVisible( toolbarView.element ) ) {
			return false;
		}

		toolbarView.focus();

		return true;
	}

	/**
	 * Fired when the editor UI is ready.
	 *
	 * Fired before {@link module:engine/controller/datacontroller~DataController#event:ready}.
	 *
	 * @event ready
	 */

	/**
	 * Fired whenever the UI (all related components) should be refreshed.
	 *
	 * **Note:**: The event is fired after each {@link module:engine/view/document~Document#event:layoutChanged}.
	 * It can also be fired manually via the {@link module:core/editor/editorui~EditorUI#update} method.
	 *
	 * @event update
	 */
}

mix( EditorUI, ObservableMixin );

/**
 * A definition of a focusable toolbar. Used by {@link module:core/editor/editorui~EditorUI#addToolbar}.
 *
 * @private
 * @interface module:core/editor/editorui~FocusableToolbarDefinition
 */

/**
 * An instance of a focusable toolbar view.
 *
 * @member {module:ui/toolbar/toolbarview~ToolbarView} #toolbarView
 */

/**
 * Options of a focusable toolbar view:
 *
 * * `isContextual`: Marks the higher priority toolbar. For example when there are 2 visible toolbars,
 * it allows to distinguish which toolbar should be focused first after the `alt+f10` keystroke
 * * `beforeFocus`: A callback executed before the `ToolbarView` gains focus upon the `Alt+F10` keystroke.
 * * `afterBlur`: A callback executed after `ToolbarView` loses focus upon `Esc` keystroke but before the focus goes back to the `origin`.
 *
 * @member {Object} #options
 */

// Returns a number (weight) for a toolbar definition. Visible toolbars have a higher priority and so do
// contextual toolbars (displayed in the context of a content, for instance, an image toolbar).
//
// A standard invisible toolbar is the heaviest. A visible contextual toolbar is the lightest.
//
// @private
// @param {module:core/editor/editorui~FocusableToolbarDefinition} toolbarDef A toolbar definition to be weighted.
// @returns {Number}
function getToolbarDefinitionWeight( toolbarDef ) {
	const { toolbarView, options } = toolbarDef;
	let weight = 10;

	// Prioritize already visible toolbars. They should get focused first.
	if ( isVisible( toolbarView.element ) ) {
		weight--;
	}

	// Prioritize contextual toolbars. They are displayed at the selection.
	if ( options.isContextual ) {
		weight--;
	}

	return weight;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module core/editor/utils/attachtoform
 */

/**
 * Checks if the editor is initialized on a `<textarea>` element that belongs to a form. If yes, it updates the editor's element
 * content before submitting the form.
 *
 * This helper requires the {@link module:core/editor/utils/elementapimixin~ElementApi ElementApi interface}.
 *
 * @param {module:core/editor/editor~Editor} editor Editor instance.
 */
function attachToForm( editor ) {
	if ( !isFunction( editor.updateSourceElement ) ) {
		/**
		 * The editor passed to `attachToForm()` must implement the
		 * {@link module:core/editor/utils/elementapimixin~ElementApi} interface.
		 *
		 * @error attachtoform-missing-elementapi-interface
		 */
		throw new CKEditorError(
			'attachtoform-missing-elementapi-interface',
			editor
		);
	}

	const sourceElement = editor.sourceElement;

	// Only when replacing a textarea which is inside of a form element.
	if ( sourceElement && sourceElement.tagName.toLowerCase() === 'textarea' && sourceElement.form ) {
		let originalSubmit;
		const form = sourceElement.form;
		const onSubmit = () => editor.updateSourceElement();

		// Replace the original form#submit() to call a custom submit function first.
		// Check if #submit is a function because the form might have an input named "submit".
		if ( isFunction( form.submit ) ) {
			originalSubmit = form.submit;

			form.submit = () => {
				onSubmit();
				originalSubmit.apply( form );
			};
		}

		// Update the replaced textarea with data before each form#submit event.
		form.addEventListener( 'submit', onSubmit );

		// Remove the submit listener and revert the original submit method on
		// editor#destroy.
		editor.on( 'destroy', () => {
			form.removeEventListener( 'submit', onSubmit );

			if ( originalSubmit ) {
				form.submit = originalSubmit;
			}
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module core/editor/utils/dataapimixin
 */

/**
 * Implementation of the {@link module:core/editor/utils/dataapimixin~DataApi}.
 *
 * @mixin DataApiMixin
 * @implements module:core/editor/utils/dataapimixin~DataApi
 */
const DataApiMixin = {
	/**
	 * @inheritDoc
	 */
	setData( data ) {
		this.data.set( data );
	},

	/**
	 * @inheritDoc
	 */
	getData( options ) {
		return this.data.get( options );
	}
};

/**
 * Interface defining editor methods for setting and getting data to and from the editor's main root element
 * using the {@link module:core/editor/editor~Editor#data data pipeline}.
 *
 * This interface is not a part of the {@link module:core/editor/editor~Editor} class because one may want to implement
 * an editor with multiple root elements, in which case the methods for setting and getting data will need to be implemented
 * differently.
 *
 * @interface DataApi
 */

/**
 * Sets the data in the editor.
 *
 *		editor.setData( '<p>This is editor!</p>' );
 *
 * By default the editor accepts HTML. This can be controlled by injecting a different data processor.
 * See the {@glink features/markdown Markdown output} guide for more details.
 *
 * Note: Not only is the format of the data configurable, but the type of the `setData()`'s parameter does not
 * have to be a string either. You can e.g. accept an object or a DOM `DocumentFragment` if you consider this
 * the right format for you.
 *
 * @method #setData
 * @param {String} data Input data.
 */

/**
 * Gets the data from the editor.
 *
 *		editor.getData(); // -> '<p>This is editor!</p>'
 *
 * By default the editor outputs HTML. This can be controlled by injecting a different data processor.
 * See the {@glink features/markdown Markdown output} guide for more details.
 *
 * Note: Not only is the format of the data configurable, but the type of the `getData()`'s return value does not
 * have to be a string either. You can e.g. return an object or a DOM `DocumentFragment` if you consider this
 * the right format for you.
 *
 * @method #getData
 * @param {Object} [options] Additional configuration for the retrieved data.
 * Editor features may introduce more configuration options that can be set through this parameter.
 * @param {String} [options.rootName='main'] Root name.
 * @param {String} [options.trim='empty'] Whether returned data should be trimmed. This option is set to `'empty'` by default,
 * which means that whenever editor content is considered empty, an empty string is returned. To turn off trimming
 * use `'none'`. In such cases exact content will be returned (for example `'<p>&nbsp;</p>'` for an empty editor).
 * @returns {String} Output data.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module core/editor/utils/elementapimixin
 */

/**
 * Implementation of the {@link module:core/editor/utils/elementapimixin~ElementApi}.
 *
 * @mixin ElementApiMixin
 * @implements module:core/editor/utils/elementapimixin~ElementApi
 */
const ElementApiMixin = {
	/**
	 * @inheritDoc
	 */
	updateSourceElement( data = this.data.get() ) {
		if ( !this.sourceElement ) {
			/**
			 * Cannot update the source element of a detached editor.
			 *
			 * The {@link ~ElementApi#updateSourceElement `updateSourceElement()`} method cannot be called if you did not
			 * pass an element to `Editor.create()`.
			 *
			 * @error editor-missing-sourceelement
			 */
			throw new CKEditorError(
				'editor-missing-sourceelement',
				this
			);
		}

		const shouldUpdateSourceElement = this.config.get( 'updateSourceElementOnDestroy' );
		const isSourceElementTextArea = this.sourceElement instanceof HTMLTextAreaElement;

		// The data returned by the editor might be unsafe, so we want to prevent rendering
		// unsafe content inside the source element different than <textarea>, which is considered
		// secure. This behaviour could be changed by setting the `updateSourceElementOnDestroy`
		// configuration option to `true`.
		if ( !shouldUpdateSourceElement && !isSourceElementTextArea ) {
			setDataInElement( this.sourceElement, '' );

			return;
		}

		setDataInElement( this.sourceElement, data );
	}
};

/**
 * Interface describing an editor that replaced a DOM element (was "initialized on an element").
 *
 * Such an editor should provide a method to
 * {@link module:core/editor/utils/elementapimixin~ElementApi#updateSourceElement update the replaced element with the current data}.
 *
 * @interface ElementApi
 */

/**
 * The element on which the editor has been initialized.
 *
 * @readonly
 * @member {HTMLElement} #sourceElement
 */

/**
 * Updates the {@link #sourceElement editor source element}'s content with the data.
 *
 * @method #updateSourceElement
 * @param {String} data The data that should be used to update the source element.
 * By default, it is taken directly from the existing editor instance.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module core/editor/utils/securesourceelement
 */

/**
 * Marks the source element on which the editor was initialized. This prevents other editor instances from using this element.
 *
 * Running multiple editor instances on the same source element causes various issues and it is
 * crucial this helper is called as soon as the source element is known to prevent collisions.
 *
 * @param {module:core/editor/editor~Editor} editor Editor instance.
 */
function secureSourceElement( editor ) {
	const sourceElement = editor.sourceElement;

	// If the editor was initialized without specifying an element, we don't need to secure anything.
	if ( !sourceElement ) {
		return;
	}

	if ( sourceElement.ckeditorInstance ) {
		/**
		 * A DOM element used to create the editor (e.g.
		 * {@link module:editor-inline/inlineeditor~InlineEditor.create `InlineEditor.create()`})
		 * has already been used to create another editor instance. Make sure each editor is
		 * created with an unique DOM element.
		 *
		 * @error editor-source-element-already-used
		 * @param {HTMLElement} element DOM element that caused the collision.
		 */
		throw new CKEditorError(
			'editor-source-element-already-used',
			editor
		);
	}

	sourceElement.ckeditorInstance = editor;

	editor.once( 'destroy', () => {
		delete sourceElement.ckeditorInstance;
	} );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The list of pending editor actions.
 *
 * This plugin should be used to synchronise plugins that execute long-lasting actions
 * (e.g. file upload) with the editor integration. It gives the developer who integrates the editor
 * an easy way to check if there are any actions pending whenever such information is needed.
 * All plugins that register a pending action also provide a message about the action that is ongoing
 * which can be displayed to the user. This lets them decide if they want to interrupt the action or wait.
 *
 * Adding and updating a pending action:
 *
 * 		const pendingActions = editor.plugins.get( 'PendingActions' );
 * 		const action = pendingActions.add( 'Upload in progress: 0%.' );
 *
 *		// You can update the message:
 * 		action.message = 'Upload in progress: 10%.';
 *
 * Removing a pending action:
 *
 * 		const pendingActions = editor.plugins.get( 'PendingActions' );
 * 		const action = pendingActions.add( 'Unsaved changes.' );
 *
 * 		pendingActions.remove( action );
 *
 * Getting pending actions:
 *
 * 		const pendingActions = editor.plugins.get( 'PendingActions' );
 *
 * 		const action1 = pendingActions.add( 'Action 1' );
 * 		const action2 = pendingActions.add( 'Action 2' );
 *
 * 		pendingActions.first; // Returns action1
 * 		Array.from( pendingActions ); // Returns [ action1, action2 ]
 *
 * This plugin is used by features like {@link module:upload/filerepository~FileRepository} to register their ongoing actions
 * and by features like {@link module:autosave/autosave~Autosave} to detect whether there are any ongoing actions.
 * Read more about saving the data in the {@glink installation/advanced/saving-data Saving and getting data} guide.
 *
 * @extends module:core/contextplugin~ContextPlugin
 */
class PendingActions extends ContextPlugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'PendingActions';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		/**
		 * Defines whether there is any registered pending action.
		 *
		 * @readonly
		 * @observable
		 * @member {Boolean} #hasAny
		 */
		this.set( 'hasAny', false );

		/**
		 * A list of pending actions.
		 *
		 * @private
		 * @type {module:utils/collection~Collection}
		 */
		this._actions = new Collection( { idProperty: '_id' } );
		this._actions.delegate( 'add', 'remove' ).to( this );
	}

	/**
	 * Adds an action to the list of pending actions.
	 *
	 * This method returns an action object with an observable message property.
	 * The action object can be later used in the {@link #remove} method. It also allows you to change the message.
	 *
	 * @param {String} message The action message.
	 * @returns {Object} An observable object that represents a pending action.
	 */
	add( message ) {
		if ( typeof message !== 'string' ) {
			/**
			 * The message must be a string.
			 *
			 * @error pendingactions-add-invalid-message
			 */
			throw new CKEditorError( 'pendingactions-add-invalid-message', this );
		}

		const action = Object.create( ObservableMixin );

		action.set( 'message', message );
		this._actions.add( action );
		this.hasAny = true;

		return action;
	}

	/**
	 * Removes an action from the list of pending actions.
	 *
	 * @param {Object} action An action object.
	 */
	remove( action ) {
		this._actions.remove( action );
		this.hasAny = !!this._actions.length;
	}

	/**
	 * Returns the first action from the list or null when list is empty
	 *
	 * returns {Object|null} The pending action object.
	 */
	get first() {
		return this._actions.get( 0 );
	}

	/**
	 * Iterable interface.
	 *
	 * @returns {Iterable.<*>}
	 */
	[ Symbol.iterator ]() {
		return this._actions[ Symbol.iterator ]();
	}

	/**
	 * Fired when an action is added to the list.
	 *
	 * @event add
	 * @param {Object} action The added action.
	 */

	/**
	 * Fired when an action is removed from the list.
	 *
	 * @event remove
	 * @param {Object} action The removed action.
	 */
}var cancel = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m11.591 10.177 4.243 4.242a1 1 0 0 1-1.415 1.415l-4.242-4.243-4.243 4.243a1 1 0 0 1-1.414-1.415l4.243-4.242L4.52 5.934A1 1 0 0 1 5.934 4.52l4.243 4.243 4.242-4.243a1 1 0 1 1 1.415 1.414l-4.243 4.243z\"/></svg>";
var caption = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2 16h9a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2z\"/><path d=\"M17 1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h14zm0 1.5H3a.5.5 0 0 0-.492.41L2.5 3v9a.5.5 0 0 0 .41.492L3 12.5h14a.5.5 0 0 0 .492-.41L17.5 12V3a.5.5 0 0 0-.41-.492L17 2.5z\" fill-opacity=\".6\"/></svg>";
var check = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M6.972 16.615a.997.997 0 0 1-.744-.292l-4.596-4.596a1 1 0 1 1 1.414-1.414l3.926 3.926 9.937-9.937a1 1 0 0 1 1.414 1.415L7.717 16.323a.997.997 0 0 1-.745.292z\"/></svg>";
var cog = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m11.333 2 .19 2.263a5.899 5.899 0 0 1 1.458.604L14.714 3.4 16.6 5.286l-1.467 1.733c.263.452.468.942.605 1.46L18 8.666v2.666l-2.263.19a5.899 5.899 0 0 1-.604 1.458l1.467 1.733-1.886 1.886-1.733-1.467a5.899 5.899 0 0 1-1.46.605L11.334 18H8.667l-.19-2.263a5.899 5.899 0 0 1-1.458-.604L5.286 16.6 3.4 14.714l1.467-1.733a5.899 5.899 0 0 1-.604-1.458L2 11.333V8.667l2.262-.189a5.899 5.899 0 0 1 .605-1.459L3.4 5.286 5.286 3.4l1.733 1.467a5.899 5.899 0 0 1 1.46-.605L8.666 2h2.666zM10 6.267a3.733 3.733 0 1 0 0 7.466 3.733 3.733 0 0 0 0-7.466z\"/></svg>";
var eraser = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m8.636 9.531-2.758 3.94a.5.5 0 0 0 .122.696l3.224 2.284h1.314l2.636-3.736L8.636 9.53zm.288 8.451L5.14 15.396a2 2 0 0 1-.491-2.786l6.673-9.53a2 2 0 0 1 2.785-.49l3.742 2.62a2 2 0 0 1 .491 2.785l-7.269 10.053-2.147-.066z\"/><path d=\"M4 18h5.523v-1H4zm-2 0h1v-1H2z\"/></svg>";
var lowVision = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M5.085 6.22 2.943 4.078a.75.75 0 1 1 1.06-1.06l2.592 2.59A11.094 11.094 0 0 1 10 5.068c4.738 0 8.578 3.101 8.578 5.083 0 1.197-1.401 2.803-3.555 3.887l1.714 1.713a.75.75 0 0 1-.09 1.138.488.488 0 0 1-.15.084.75.75 0 0 1-.821-.16L6.17 7.304c-.258.11-.51.233-.757.365l6.239 6.24-.006.005.78.78c-.388.094-.78.166-1.174.215l-1.11-1.11h.011L4.55 8.197a7.2 7.2 0 0 0-.665.514l-.112.098 4.897 4.897-.005.006 1.276 1.276a10.164 10.164 0 0 1-1.477-.117l-.479-.479-.009.009-4.863-4.863-.022.031a2.563 2.563 0 0 0-.124.2c-.043.077-.08.158-.108.241a.534.534 0 0 0-.028.133.29.29 0 0 0 .008.072.927.927 0 0 0 .082.226c.067.133.145.26.234.379l3.242 3.365.025.01.59.623c-3.265-.918-5.59-3.155-5.59-4.668 0-1.194 1.448-2.838 3.663-3.93zm7.07.531a4.632 4.632 0 0 1 1.108 5.992l.345.344.046-.018a9.313 9.313 0 0 0 2-1.112c.256-.187.5-.392.727-.613.137-.134.27-.277.392-.431.072-.091.141-.185.203-.286.057-.093.107-.19.148-.292a.72.72 0 0 0 .036-.12.29.29 0 0 0 .008-.072.492.492 0 0 0-.028-.133.999.999 0 0 0-.036-.096 2.165 2.165 0 0 0-.071-.145 2.917 2.917 0 0 0-.125-.2 3.592 3.592 0 0 0-.263-.335 5.444 5.444 0 0 0-.53-.523 7.955 7.955 0 0 0-1.054-.768 9.766 9.766 0 0 0-1.879-.891c-.337-.118-.68-.219-1.027-.301zm-2.85.21-.069.002a.508.508 0 0 0-.254.097.496.496 0 0 0-.104.679.498.498 0 0 0 .326.199l.045.005c.091.003.181.003.272.012a2.45 2.45 0 0 1 2.017 1.513c.024.061.043.125.069.185a.494.494 0 0 0 .45.287h.008a.496.496 0 0 0 .35-.158.482.482 0 0 0 .13-.335.638.638 0 0 0-.048-.219 3.379 3.379 0 0 0-.36-.723 3.438 3.438 0 0 0-2.791-1.543l-.028-.001h-.013z\"/></svg>";
var image = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M6.91 10.54c.26-.23.64-.21.88.03l3.36 3.14 2.23-2.06a.64.64 0 0 1 .87 0l2.52 2.97V4.5H3.2v10.12l3.71-4.08zm10.27-7.51c.6 0 1.09.47 1.09 1.05v11.84c0 .59-.49 1.06-1.09 1.06H2.79c-.6 0-1.09-.47-1.09-1.06V4.08c0-.58.49-1.05 1.1-1.05h14.38zm-5.22 5.56a1.96 1.96 0 1 1 3.4-1.96 1.96 1.96 0 0 1-3.4 1.96z\"/></svg>";
var alignBottom = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m9.239 13.938-2.88-1.663a.75.75 0 0 1 .75-1.3L9 12.067V4.75a.75.75 0 1 1 1.5 0v7.318l1.89-1.093a.75.75 0 0 1 .75 1.3l-2.879 1.663a.752.752 0 0 1-.511.187.752.752 0 0 1-.511-.187zM4.25 17a.75.75 0 1 1 0-1.5h10.5a.75.75 0 0 1 0 1.5H4.25z\"/></svg>";
var alignMiddle = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M9.75 11.875a.752.752 0 0 1 .508.184l2.883 1.666a.75.75 0 0 1-.659 1.344l-.091-.044-1.892-1.093.001 4.318a.75.75 0 1 1-1.5 0v-4.317l-1.89 1.092a.75.75 0 0 1-.75-1.3l2.879-1.663a.752.752 0 0 1 .51-.187zM15.25 9a.75.75 0 1 1 0 1.5H4.75a.75.75 0 1 1 0-1.5h10.5zM9.75.375a.75.75 0 0 1 .75.75v4.318l1.89-1.093.092-.045a.75.75 0 0 1 .659 1.344l-2.883 1.667a.752.752 0 0 1-.508.184.752.752 0 0 1-.511-.187L6.359 5.65a.75.75 0 0 1 .75-1.299L9 5.442V1.125a.75.75 0 0 1 .75-.75z\"/></svg>";
var alignTop = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m10.261 7.062 2.88 1.663a.75.75 0 0 1-.75 1.3L10.5 8.933v7.317a.75.75 0 1 1-1.5 0V8.932l-1.89 1.093a.75.75 0 0 1-.75-1.3l2.879-1.663a.752.752 0 0 1 .511-.187.752.752 0 0 1 .511.187zM15.25 4a.75.75 0 1 1 0 1.5H4.75a.75.75 0 0 1 0-1.5h10.5z\"/></svg>";
var alignLeft = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2 3.75c0 .414.336.75.75.75h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75zm0 8c0 .414.336.75.75.75h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75zm0 4c0 .414.336.75.75.75h9.929a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75zm0-8c0 .414.336.75.75.75h9.929a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75z\"/></svg>";
var alignCenter = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2 3.75c0 .414.336.75.75.75h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75zm0 8c0 .414.336.75.75.75h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75zm2.286 4c0 .414.336.75.75.75h9.928a.75.75 0 1 0 0-1.5H5.036a.75.75 0 0 0-.75.75zm0-8c0 .414.336.75.75.75h9.928a.75.75 0 1 0 0-1.5H5.036a.75.75 0 0 0-.75.75z\"/></svg>";
var alignRight = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M18 3.75a.75.75 0 0 1-.75.75H2.75a.75.75 0 1 1 0-1.5h14.5a.75.75 0 0 1 .75.75zm0 8a.75.75 0 0 1-.75.75H2.75a.75.75 0 1 1 0-1.5h14.5a.75.75 0 0 1 .75.75zm0 4a.75.75 0 0 1-.75.75H7.321a.75.75 0 1 1 0-1.5h9.929a.75.75 0 0 1 .75.75zm0-8a.75.75 0 0 1-.75.75H7.321a.75.75 0 1 1 0-1.5h9.929a.75.75 0 0 1 .75.75z\"/></svg>";
var alignJustify = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2 3.75c0 .414.336.75.75.75h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75zm0 8c0 .414.336.75.75.75h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75zm0 4c0 .414.336.75.75.75h9.929a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75zm0-8c0 .414.336.75.75.75h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75z\"/></svg>";
var objectBlockLeft = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path opacity=\".5\" d=\"M2 3h16v1.5H2zm0 12h16v1.5H2z\"/><path d=\"M12.003 7v5.5a1 1 0 0 1-1 1H2.996a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h8.007a1 1 0 0 1 1 1zm-1.506.5H3.5V12h6.997V7.5z\"/></svg>";
var objectCenter = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path opacity=\".5\" d=\"M2 3h16v1.5H2zm0 12h16v1.5H2z\"/><path d=\"M15.003 7v5.5a1 1 0 0 1-1 1H5.996a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h8.007a1 1 0 0 1 1 1zm-1.506.5H6.5V12h6.997V7.5z\"/></svg>";
var objectBlockRight = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path opacity=\".5\" d=\"M2 3h16v1.5H2zm0 12h16v1.5H2z\"/><path d=\"M18.003 7v5.5a1 1 0 0 1-1 1H8.996a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h8.007a1 1 0 0 1 1 1zm-1.506.5H9.5V12h6.997V7.5z\"/></svg>";
var objectFullWidth = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path opacity=\".5\" d=\"M2 3h16v1.5H2zm0 12h16v1.5H2z\"/><path d=\"M18 7v5.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1zm-1.505.5H3.504V12h12.991V7.5z\"/></svg>";
var objectInline = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path opacity=\".5\" d=\"M2 3h16v1.5H2zm11.5 9H18v1.5h-4.5zM2 15h16v1.5H2z\"/><path d=\"M12.003 7v5.5a1 1 0 0 1-1 1H2.996a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h8.007a1 1 0 0 1 1 1zm-1.506.5H3.5V12h6.997V7.5z\"/></svg>";
var objectLeft = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path opacity=\".5\" d=\"M2 3h16v1.5H2zm11.5 9H18v1.5h-4.5zm0-3H18v1.5h-4.5zm0-3H18v1.5h-4.5zM2 15h16v1.5H2z\"/><path d=\"M12.003 7v5.5a1 1 0 0 1-1 1H2.996a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h8.007a1 1 0 0 1 1 1zm-1.506.5H3.5V12h6.997V7.5z\"/></svg>";
var objectRight = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path opacity=\".5\" d=\"M2 3h16v1.5H2zm0 12h16v1.5H2zm0-9h5v1.5H2zm0 3h5v1.5H2zm0 3h5v1.5H2z\"/><path d=\"M18.003 7v5.5a1 1 0 0 1-1 1H8.996a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h8.007a1 1 0 0 1 1 1zm-1.506.5H9.5V12h6.997V7.5z\"/></svg>";
var objectSizeFull = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\"><path d=\"M2.5 17v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zM1 15.5v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm0-2v1h-1v-1h1zm-19 0v1H0v-1h1zM14.5 2v1h-1V2h1zm2 0v1h-1V2h1zm2 0v1h-1V2h1zm-8 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm8 0v1h-1V2h1zm-10 0v1h-1V2h1z\"/><path d=\"M18.095 2H1.905C.853 2 0 2.895 0 4v12c0 1.105.853 2 1.905 2h16.19C19.147 18 20 17.105 20 16V4c0-1.105-.853-2-1.905-2zm0 1.5c.263 0 .476.224.476.5v12c0 .276-.213.5-.476.5H1.905a.489.489 0 0 1-.476-.5V4c0-.276.213-.5.476-.5h16.19z\"/></svg>";
var objectSizeLarge = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\"><path d=\"M2.5 17v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zM1 15.5v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm0-2v1h-1v-1h1zm-19 0v1H0v-1h1zM14.5 2v1h-1V2h1zm2 0v1h-1V2h1zm2 0v1h-1V2h1zm-8 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm8 0v1h-1V2h1zm-10 0v1h-1V2h1z\"/><path d=\"M13 6H2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2zm0 1.5a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5H2a.5.5 0 0 1-.5-.5V8a.5.5 0 0 1 .5-.5h11z\"/></svg>";
var objectSizeSmall = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\"><path d=\"M2.5 17v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zM1 15.5v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm0-2v1h-1v-1h1zm-19 0v1H0v-1h1zM14.5 2v1h-1V2h1zm2 0v1h-1V2h1zm2 0v1h-1V2h1zm-8 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm8 0v1h-1V2h1zm-10 0v1h-1V2h1z\"/><path d=\"M7 10H2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2zm0 1.5a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5H2a.5.5 0 0 1-.5-.5v-4a.5.5 0 0 1 .5-.5h5z\"/></svg>";
var objectSizeMedium = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\"><path d=\"M2.5 17v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zm2 0v1h-1v-1h1zM1 15.5v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm-19-2v1H0v-1h1zm19 0v1h-1v-1h1zm0-2v1h-1v-1h1zm-19 0v1H0v-1h1zM14.5 2v1h-1V2h1zm2 0v1h-1V2h1zm2 0v1h-1V2h1zm-8 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm-2 0v1h-1V2h1zm8 0v1h-1V2h1zm-10 0v1h-1V2h1z\"/><path d=\"M10 8H2a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2zm0 1.5a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-.5.5H2a.5.5 0 0 1-.5-.5v-6a.5.5 0 0 1 .5-.5h8z\"/></svg>";
var pencil = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m7.3 17.37-.061.088a1.518 1.518 0 0 1-.934.535l-4.178.663-.806-4.153a1.495 1.495 0 0 1 .187-1.058l.056-.086L8.77 2.639c.958-1.351 2.803-1.076 4.296-.03 1.497 1.047 2.387 2.693 1.433 4.055L7.3 17.37zM9.14 4.728l-5.545 8.346 3.277 2.294 5.544-8.346L9.14 4.728zM6.07 16.512l-3.276-2.295.53 2.73 2.746-.435zM9.994 3.506 13.271 5.8c.316-.452-.16-1.333-1.065-1.966-.905-.634-1.895-.78-2.212-.328zM8 18.5 9.375 17H19v1.5H8z\"/></svg>";
var pilcrow = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M6.999 2H15a1 1 0 0 1 0 2h-1.004v13a1 1 0 1 1-2 0V4H8.999v13a1 1 0 1 1-2 0v-7A4 4 0 0 1 3 6a4 4 0 0 1 3.999-4z\"/></svg>";
var quote = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M3 10.423a6.5 6.5 0 0 1 6.056-6.408l.038.67C6.448 5.423 5.354 7.663 5.22 10H9c.552 0 .5.432.5.986v4.511c0 .554-.448.503-1 .503h-5c-.552 0-.5-.449-.5-1.003v-4.574zm8 0a6.5 6.5 0 0 1 6.056-6.408l.038.67c-2.646.739-3.74 2.979-3.873 5.315H17c.552 0 .5.432.5.986v4.511c0 .554-.448.503-1 .503h-5c-.552 0-.5-.449-.5-1.003v-4.574z\"/></svg>";
var threeVerticalDots = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><circle cx=\"9.5\" cy=\"4.5\" r=\"1.5\"/><circle cx=\"9.5\" cy=\"10.5\" r=\"1.5\"/><circle cx=\"9.5\" cy=\"16.5\" r=\"1.5\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const icons = {
	cancel,
	caption,
	check,
	cog,
	eraser,
	lowVision,
	image,

	alignBottom,
	alignMiddle,
	alignTop,
	alignLeft,
	alignCenter,
	alignRight,
	alignJustify,

	objectLeft,
	objectCenter,
	objectRight,
	objectFullWidth,
	objectInline,
	objectBlockLeft,
	objectBlockRight,

	objectSizeFull,
	objectSizeLarge,
	objectSizeSmall,
	objectSizeMedium,

	pencil,
	pilcrow,
	quote,
	threeVerticalDots
};var index$7=/*#__PURE__*/Object.freeze({__proto__:null,icons:icons,Plugin:Plugin,Command:Command,MultiCommand:MultiCommand,Context:Context,ContextPlugin:ContextPlugin,Editor:Editor,EditorUI:EditorUI,attachToForm:attachToForm,DataApiMixin:DataApiMixin,ElementApiMixin:ElementApiMixin,secureSourceElement:secureSourceElement,PendingActions:PendingActions});/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module ui/bindings/clickoutsidehandler
 */

/* global document */

/**
 * Handles clicking **outside** of a specified set of elements, then fires an action.
 *
 * **Note**: Actually, the action is executed upon `mousedown`, not `click`. It prevents
 * certain issues when the user keeps holding the mouse button and the UI cannot react
 * properly.
 *
 * @param {Object} options Configuration options.
 * @param {module:utils/dom/emittermixin~Emitter} options.emitter The emitter to which this behavior
 * should be added.
 * @param {Function} options.activator Function returning a `Boolean`, to determine whether the handler is active.
 * @param {Array.<HTMLElement>} options.contextElements HTML elements that determine the scope of the
 * handler. Clicking any of them or their descendants will **not** fire the callback.
 * @param {Function} options.callback An action executed by the handler.
 */
function clickOutsideHandler( { emitter, activator, callback, contextElements } ) {
	emitter.listenTo( document, 'mousedown', ( evt, domEvt ) => {
		if ( !activator() ) {
			return;
		}

		// Check if `composedPath` is `undefined` in case the browser does not support native shadow DOM.
		// Can be removed when all supported browsers support native shadow DOM.
		const path = typeof domEvt.composedPath == 'function' ? domEvt.composedPath() : [];

		for ( const contextElement of contextElements ) {
			if ( contextElement.contains( domEvt.target ) || path.includes( contextElement ) ) {
				return;
			}
		}

		callback();
	} );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module ui/bindings/injectcsstransitiondisabler
 */

/**
 * A decorator that brings the possibility to temporarily disable CSS transitions using
 * {@link module:ui/view~View} methods. It is helpful when, for instance, the transitions should not happen
 * when the view is first displayed but they should work normal in other cases.
 *
 * The methods to control the CSS transitions are:
 * * `disableCssTransitions()` – Adds the `.ck-transitions-disabled` class to the
 * {@link module:ui/view~View#element view element}.
 * * `enableCssTransitions()` – Removes the `.ck-transitions-disabled` class from the
 * {@link module:ui/view~View#element view element}.
 *
 * **Note**: This helper extends the {@link module:ui/view~View#template template} and must be used **after**
 * {@link module:ui/view~View#setTemplate} is called:
 *
 *		import injectCssTransitionDisabler from '@ckeditor/ckeditor5-ui/src/bindings/injectcsstransitiondisabler';
 *
 *		class MyView extends View {
 *			constructor() {
 *				super();
 *
 *				// ...
 *
 *				this.setTemplate( { ... } );
 *
 *				// ...
 *
 *				injectCssTransitionDisabler( this );
 *
 *				// ...
 *			}
 *		}
 *
 * The usage comes down to:
 *
 *		const view = new MyView();
 *
 *		// ...
 *
 *		view.disableCssTransitions();
 *		view.show();
 *		view.enableCssTransitions();
 *
 * @param {module:ui/view~View} view View instance that should get this functionality.
 */
function injectCssTransitionDisabler( view ) {
	view.set( '_isCssTransitionsDisabled', false );

	view.disableCssTransitions = () => {
		view._isCssTransitionsDisabled = true;
	};

	view.enableCssTransitions = () => {
		view._isCssTransitionsDisabled = false;
	};

	view.extendTemplate( {
		attributes: {
			class: [
				view.bindTemplate.if( '_isCssTransitionsDisabled', 'ck-transitions-disabled' )
			]
		}
	} );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module ui/bindings/submithandler
 */

/**
 * A handler useful for {@link module:ui/view~View views} working as HTML forms. It intercepts a native DOM
 * `submit` event, prevents the default web browser behavior (navigation and page reload) and
 * fires the `submit` event on a view instead. Such a custom event can be then used by any
 * {@link module:utils/dom/emittermixin~Emitter emitter}, e.g. to serialize the form data.
 *
 *		import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler';
 *
 *		// ...
 *
 *		class AnyFormView extends View {
 *			constructor() {
 *				super();
 *
 *				// ...
 *
 *				submitHandler( {
 *					view: this
 *				} );
 *			}
 *		}
 *
 *		// ...
 *
 *		const view = new AnyFormView();
 *
 *		// A sample listener attached by an emitter working with the view.
 *		this.listenTo( view, 'submit', () => {
 *			saveTheFormData();
 *			hideTheForm();
 *		} );
 *
 * @param {Object} [options] Configuration options.
 * @param {module:ui/view~View} options.view The view which DOM `submit` events should be handled.
 */
function submitHandler( { view } ) {
	view.listenTo( view.element, 'submit', ( evt, domEvt ) => {
		domEvt.preventDefault();
		view.fire( 'submit' );
	}, { useCapture: true } );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module ui/bindings/addkeyboardhandlingforgrid
 */

/**
 * A helper that adds a keyboard navigation support (arrow up/down/left/right) for grids.
 *
 * @param {Object} options Configuration options.
 * @param {module:utils/keystrokehandler~KeystrokeHandler} options.keystrokeHandler Keystroke handler to register navigation with arrow
 * keys.
 * @param {module:utils/focustracker~FocusTracker} options.focusTracker A focus tracker for grid elements.
 * @param {module:ui/viewcollection~ViewCollection} options.gridItems A collection of grid items.
 * @param {Number|Function} options.numberOfColumns Number of columns in the grid. Can be specified as a function that returns
 * the number (e.g. for responsive grids).
 */
function addKeyboardHandlingForGrid( { keystrokeHandler, focusTracker, gridItems, numberOfColumns } ) {
	const getNumberOfColumns = typeof numberOfColumns === 'number' ? () => numberOfColumns : numberOfColumns;

	keystrokeHandler.set( 'arrowright', getGridItemFocuser( ( focusedElementIndex, gridItems ) => {
		if ( focusedElementIndex === gridItems.length - 1 ) {
			return 0;
		} else {
			return focusedElementIndex + 1;
		}
	} ) );

	keystrokeHandler.set( 'arrowleft', getGridItemFocuser( ( focusedElementIndex, gridItems ) => {
		if ( focusedElementIndex === 0 ) {
			return gridItems.length - 1;
		} else {
			return focusedElementIndex - 1;
		}
	} ) );

	keystrokeHandler.set( 'arrowup', getGridItemFocuser( ( focusedElementIndex, gridItems ) => {
		let nextIndex = focusedElementIndex - getNumberOfColumns();

		if ( nextIndex < 0 ) {
			nextIndex = focusedElementIndex + getNumberOfColumns() * Math.floor( gridItems.length / getNumberOfColumns() );

			if ( nextIndex > gridItems.length - 1 ) {
				nextIndex -= getNumberOfColumns();
			}
		}

		return nextIndex;
	} ) );

	keystrokeHandler.set( 'arrowdown', getGridItemFocuser( ( focusedElementIndex, gridItems ) => {
		let nextIndex = focusedElementIndex + getNumberOfColumns();

		if ( nextIndex > gridItems.length - 1 ) {
			nextIndex = focusedElementIndex % getNumberOfColumns();
		}

		return nextIndex;
	} ) );

	function getGridItemFocuser( getIndexToFocus ) {
		return evt => {
			const focusedElement = gridItems.find( item => item.element === focusTracker.focusedElement );
			const focusedElementIndex = gridItems.getIndex( focusedElement );
			const nextIndexToFocus = getIndexToFocus( focusedElementIndex, gridItems );

			gridItems.get( nextIndexToFocus ).focus();

			evt.stopPropagation();
			evt.preventDefault();
		};
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * This is a special {@link module:ui/viewcollection~ViewCollection} dedicated to elements that are detached
 * from the DOM structure of the editor, like panels, icons, etc.
 *
 * The body collection is available in the {@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`} property.
 * Any plugin can add a {@link module:ui/view~View view} to this collection.
 * These views will render in a container placed directly in the `<body>` element.
 * The editor will detach and destroy this collection when the editor will be {@link module:core/editor/editor~Editor#destroy destroyed}.
 *
 * If you need to control the life cycle of the body collection on your own, you can create your own instance of this class.
 *
 * A body collection will render itself automatically in the DOM body element as soon as you call {@link ~BodyCollection#attachToDom}.
 * If you create multiple body collections, this class will create a special wrapper element in the DOM to limit the number of
 * elements created directly in the body and remove it when the last body collection will be
 * {@link ~BodyCollection#detachFromDom detached}.
 *
 * @extends module:ui/viewcollection~ViewCollection
 */
class BodyCollection extends ViewCollection {
	/**
	 * Creates a new instance of the {@link module:ui/editorui/bodycollection~BodyCollection}.
	 *
	 * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor editor's locale} instance.
	 * @param {Iterable.<module:ui/view~View>} [initialItems] The initial items of the collection.
	 */
	constructor( locale, initialItems = [] ) {
		super( initialItems );

		/**
		 * The {@link module:core/editor/editor~Editor#locale editor's locale} instance.
		 * See the view {@link module:ui/view~View#locale locale} property.
		 *
		 * @member {module:utils/locale~Locale}
		 */
		this.locale = locale;
	}

	/**
	 * Attaches the body collection to the DOM body element. You need to execute this method to render the content of
	 * the body collection.
	 */
	attachToDom() {
		/**
		 * The element holding elements of the body region.
		 *
		 * @protected
		 * @member {HTMLElement} #_bodyCollectionContainer
		 */
		this._bodyCollectionContainer = new Template( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-reset_all',
					'ck-body',
					'ck-rounded-corners'
				],
				dir: this.locale.uiLanguageDirection
			},
			children: this
		} ).render();

		let wrapper = document.querySelector( '.ck-body-wrapper' );

		if ( !wrapper ) {
			wrapper = createElement( document, 'div', { class: 'ck-body-wrapper' } );
			document.body.appendChild( wrapper );
		}

		wrapper.appendChild( this._bodyCollectionContainer );
	}

	/**
	 * Detaches the collection from the DOM structure. Use this method when you do not need to use the body collection
	 * anymore to clean-up the DOM structure.
	 */
	detachFromDom() {
		super.destroy();

		if ( this._bodyCollectionContainer ) {
			this._bodyCollectionContainer.remove();
		}

		const wrapper = document.querySelector( '.ck-body-wrapper' );

		if ( wrapper && wrapper.childElementCount == 0 ) {
			wrapper.remove();
		}
	}
}var css_248z$S = ".ck.ck-icon{vertical-align:middle}:root{--ck-icon-size:calc(var(--ck-line-height-base)*var(--ck-font-size-normal))}.ck.ck-icon{font-size:.8333350694em;height:var(--ck-icon-size);width:var(--ck-icon-size);will-change:transform}.ck.ck-icon,.ck.ck-icon *{color:inherit;cursor:inherit}.ck.ck-icon :not([fill]){fill:currentColor}";
styleInject(css_248z$S);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The icon view class.
 *
 * @extends module:ui/view~View
 */
class IconView extends View {
	/**
	 * @inheritDoc
	 */
	constructor() {
		super();

		const bind = this.bindTemplate;

		/**
		 * The SVG source of the icon.
		 *
		 * @observable
		 * @member {String} #content
		 */
		this.set( 'content', '' );

		/**
		 * This attribute specifies the boundaries to which the
		 * icon content should stretch.
		 *
		 * @observable
		 * @default '0 0 20 20'
		 * @member {String} #viewBox
		 */
		this.set( 'viewBox', '0 0 20 20' );

		/**
		 * The fill color of the child `path.ck-icon__fill`.
		 *
		 * @observable
		 * @default ''
		 * @member {String} #fillColor
		 */
		this.set( 'fillColor', '' );

		this.setTemplate( {
			tag: 'svg',
			ns: 'http://www.w3.org/2000/svg',
			attributes: {
				class: [
					'ck',
					'ck-icon'
				],
				viewBox: bind.to( 'viewBox' )
			}
		} );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		this._updateXMLContent();
		this._colorFillPaths();

		// This is a hack for lack of innerHTML binding.
		// See: https://github.com/ckeditor/ckeditor5-ui/issues/99.
		this.on( 'change:content', () => {
			this._updateXMLContent();
			this._colorFillPaths();
		} );

		this.on( 'change:fillColor', () => {
			this._colorFillPaths();
		} );
	}

	/**
	 * Updates the {@link #element} with the value of {@link #content}.
	 *
	 * @private
	 */
	_updateXMLContent() {
		if ( this.content ) {
			const parsed = new DOMParser().parseFromString( this.content.trim(), 'image/svg+xml' );
			const svg = parsed.querySelector( 'svg' );
			const viewBox = svg.getAttribute( 'viewBox' );

			if ( viewBox ) {
				this.viewBox = viewBox;
			}

			while ( this.element.firstChild ) {
				this.element.removeChild( this.element.firstChild );
			}

			while ( svg.childNodes.length > 0 ) {
				this.element.appendChild( svg.childNodes[ 0 ] );
			}
		}
	}

	/**
	 * Fills all child `path.ck-icon__fill` with the `#fillColor`.
	 *
	 * @private
	 */
	_colorFillPaths() {
		if ( this.fillColor ) {
			this.element.querySelectorAll( '.ck-icon__fill' ).forEach( path => {
				path.style.fill = this.fillColor;
			} );
		}
	}
}var css_248z$R = ".ck.ck-button,a.ck.ck-button{align-items:center;display:inline-flex;justify-content:left;position:relative;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.ck.ck-button .ck-button__label,a.ck.ck-button .ck-button__label{display:none}.ck.ck-button.ck-button_with-text .ck-button__label,a.ck.ck-button.ck-button_with-text .ck-button__label{display:inline-block}.ck.ck-button:not(.ck-button_with-text),a.ck.ck-button:not(.ck-button_with-text){justify-content:center}.ck.ck-button,a.ck.ck-button{background:var(--ck-color-button-default-background)}.ck.ck-button:not(.ck-disabled):hover,a.ck.ck-button:not(.ck-disabled):hover{background:var(--ck-color-button-default-hover-background)}.ck.ck-button:not(.ck-disabled):active,a.ck.ck-button:not(.ck-disabled):active{background:var(--ck-color-button-default-active-background)}.ck.ck-button.ck-disabled,a.ck.ck-button.ck-disabled{background:var(--ck-color-button-default-disabled-background)}.ck.ck-button,a.ck.ck-button{border-radius:0}.ck-rounded-corners .ck.ck-button,.ck-rounded-corners a.ck.ck-button,.ck.ck-button.ck-rounded-corners,a.ck.ck-button.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-button,a.ck.ck-button{-webkit-appearance:none;border:1px solid transparent;cursor:default;font-size:inherit;line-height:1;min-height:var(--ck-ui-component-min-height);min-width:var(--ck-ui-component-min-height);padding:var(--ck-spacing-tiny);text-align:center;transition:box-shadow .2s ease-in-out,border .2s ease-in-out;vertical-align:middle;white-space:nowrap}.ck.ck-button:active,.ck.ck-button:focus,a.ck.ck-button:active,a.ck.ck-button:focus{border:var(--ck-focus-ring);box-shadow:var(--ck-focus-outer-shadow),0 0;outline:none}.ck.ck-button .ck-button__icon use,.ck.ck-button .ck-button__icon use *,a.ck.ck-button .ck-button__icon use,a.ck.ck-button .ck-button__icon use *{color:inherit}.ck.ck-button .ck-button__label,a.ck.ck-button .ck-button__label{color:inherit;cursor:inherit;font-size:inherit;font-weight:inherit;vertical-align:middle}[dir=ltr] .ck.ck-button .ck-button__label,[dir=ltr] a.ck.ck-button .ck-button__label{text-align:left}[dir=rtl] .ck.ck-button .ck-button__label,[dir=rtl] a.ck.ck-button .ck-button__label{text-align:right}.ck.ck-button .ck-button__keystroke,a.ck.ck-button .ck-button__keystroke{color:inherit}[dir=ltr] .ck.ck-button .ck-button__keystroke,[dir=ltr] a.ck.ck-button .ck-button__keystroke{margin-left:var(--ck-spacing-large)}[dir=rtl] .ck.ck-button .ck-button__keystroke,[dir=rtl] a.ck.ck-button .ck-button__keystroke{margin-right:var(--ck-spacing-large)}.ck.ck-button .ck-button__keystroke,a.ck.ck-button .ck-button__keystroke{font-weight:700;opacity:.7}.ck.ck-button.ck-disabled:active,.ck.ck-button.ck-disabled:focus,a.ck.ck-button.ck-disabled:active,a.ck.ck-button.ck-disabled:focus{box-shadow:var(--ck-focus-disabled-outer-shadow),0 0}.ck.ck-button.ck-disabled .ck-button__icon,.ck.ck-button.ck-disabled .ck-button__label,a.ck.ck-button.ck-disabled .ck-button__icon,a.ck.ck-button.ck-disabled .ck-button__label{opacity:var(--ck-disabled-opacity)}.ck.ck-button.ck-disabled .ck-button__keystroke,a.ck.ck-button.ck-disabled .ck-button__keystroke{opacity:.3}.ck.ck-button.ck-button_with-text,a.ck.ck-button.ck-button_with-text{padding:var(--ck-spacing-tiny) var(--ck-spacing-standard)}[dir=ltr] .ck.ck-button.ck-button_with-text .ck-button__icon,[dir=ltr] a.ck.ck-button.ck-button_with-text .ck-button__icon{margin-left:calc(var(--ck-spacing-small)*-1);margin-right:var(--ck-spacing-small)}[dir=rtl] .ck.ck-button.ck-button_with-text .ck-button__icon,[dir=rtl] a.ck.ck-button.ck-button_with-text .ck-button__icon{margin-left:var(--ck-spacing-small);margin-right:calc(var(--ck-spacing-small)*-1)}.ck.ck-button.ck-button_with-keystroke .ck-button__label,a.ck.ck-button.ck-button_with-keystroke .ck-button__label{flex-grow:1}.ck.ck-button.ck-on,a.ck.ck-button.ck-on{background:var(--ck-color-button-on-background)}.ck.ck-button.ck-on:not(.ck-disabled):hover,a.ck.ck-button.ck-on:not(.ck-disabled):hover{background:var(--ck-color-button-on-hover-background)}.ck.ck-button.ck-on:not(.ck-disabled):active,a.ck.ck-button.ck-on:not(.ck-disabled):active{background:var(--ck-color-button-on-active-background)}.ck.ck-button.ck-on.ck-disabled,a.ck.ck-button.ck-on.ck-disabled{background:var(--ck-color-button-on-disabled-background)}.ck.ck-button.ck-on,a.ck.ck-button.ck-on{color:var(--ck-color-button-on-color)}.ck.ck-button.ck-button-save,a.ck.ck-button.ck-button-save{color:var(--ck-color-button-save)}.ck.ck-button.ck-button-cancel,a.ck.ck-button.ck-button-cancel{color:var(--ck-color-button-cancel)}.ck.ck-button-action,a.ck.ck-button-action{background:var(--ck-color-button-action-background)}.ck.ck-button-action:not(.ck-disabled):hover,a.ck.ck-button-action:not(.ck-disabled):hover{background:var(--ck-color-button-action-hover-background)}.ck.ck-button-action:not(.ck-disabled):active,a.ck.ck-button-action:not(.ck-disabled):active{background:var(--ck-color-button-action-active-background)}.ck.ck-button-action.ck-disabled,a.ck.ck-button-action.ck-disabled{background:var(--ck-color-button-action-disabled-background)}.ck.ck-button-action,a.ck.ck-button-action{color:var(--ck-color-button-action-text)}.ck.ck-button-bold,a.ck.ck-button-bold{font-weight:700}";
styleInject(css_248z$R);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The button view class.
 *
 *		const view = new ButtonView();
 *
 *		view.set( {
 *			label: 'A button',
 *			keystroke: 'Ctrl+B',
 *			tooltip: true,
 *			withText: true
 *		} );
 *
 *		view.render();
 *
 *		document.body.append( view.element );
 *
 * @extends module:ui/view~View
 * @implements module:ui/button/button~Button
 */
class ButtonView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		const bind = this.bindTemplate;
		const ariaLabelUid = uid();

		// Implement the Button interface.
		this.set( 'class' );
		this.set( 'labelStyle' );
		this.set( 'icon' );
		this.set( 'isEnabled', true );
		this.set( 'isOn', false );
		this.set( 'isVisible', true );
		this.set( 'isToggleable', false );
		this.set( 'keystroke' );
		this.set( 'label' );
		this.set( 'tabindex', -1 );
		this.set( 'tooltip' );
		this.set( 'tooltipPosition', 's' );
		this.set( 'type', 'button' );
		this.set( 'withText', false );
		this.set( 'withKeystroke', false );

		/**
		 * Collection of the child views inside of the button {@link #element}.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.children = this.createCollection();

		/**
		 * Label of the button view. It is configurable using the {@link #label label attribute}.
		 *
		 * @readonly
		 * @member {module:ui/view~View} #labelView
		 */
		this.labelView = this._createLabelView( ariaLabelUid );

		/**
		 * The icon view of the button. Will be added to {@link #children} when the
		 * {@link #icon icon attribute} is defined.
		 *
		 * @readonly
		 * @member {module:ui/icon/iconview~IconView} #iconView
		 */
		this.iconView = new IconView();

		this.iconView.extendTemplate( {
			attributes: {
				class: 'ck-button__icon'
			}
		} );

		/**
		 * A view displaying the keystroke of the button next to the {@link #labelView label}.
		 * Added to {@link #children} when the {@link #withKeystroke `withKeystroke` attribute}
		 * is defined.
		 *
		 * @readonly
		 * @member {module:ui/view/view~View} #keystrokeView
		 */
		this.keystrokeView = this._createKeystrokeView();

		/**
		 * Tooltip of the button bound to the template.
		 *
		 * @see #tooltip
		 * @see #_getTooltipString
		 * @private
		 * @observable
		 * @member {String|undefined} #_tooltipString
		 */
		this.bind( '_tooltipString' ).to(
			this, 'tooltip',
			this, 'label',
			this, 'keystroke',
			this._getTooltipString.bind( this )
		);

		const template = {
			tag: 'button',

			attributes: {
				class: [
					'ck',
					'ck-button',
					bind.to( 'class' ),
					bind.if( 'isEnabled', 'ck-disabled', value => !value ),
					bind.if( 'isVisible', 'ck-hidden', value => !value ),
					bind.to( 'isOn', value => value ? 'ck-on' : 'ck-off' ),
					bind.if( 'withText', 'ck-button_with-text' ),
					bind.if( 'withKeystroke', 'ck-button_with-keystroke' )
				],
				type: bind.to( 'type', value => value ? value : 'button' ),
				tabindex: bind.to( 'tabindex' ),
				'aria-labelledby': `ck-editor__aria-label_${ ariaLabelUid }`,
				'aria-disabled': bind.if( 'isEnabled', true, value => !value ),
				'aria-pressed': bind.to( 'isOn', value => this.isToggleable ? String( !!value ) : false ),
				'data-cke-tooltip-text': bind.to( '_tooltipString' ),
				'data-cke-tooltip-position': bind.to( 'tooltipPosition' )
			},

			children: this.children,

			on: {
				click: bind.to( evt => {
					// We can't make the button disabled using the disabled attribute, because it won't be focusable.
					// Though, shouldn't this condition be moved to the button controller?
					if ( this.isEnabled ) {
						this.fire( 'execute' );
					} else {
						// Prevent the default when button is disabled, to block e.g.
						// automatic form submitting. See ckeditor/ckeditor5-link#74.
						evt.preventDefault();
					}
				} )
			}
		};

		// On Safari we have to force the focus on a button on click as it's the only browser
		// that doesn't do that automatically. See #12115.
		if ( env.isSafari ) {
			template.on.mousedown = bind.to( evt => {
				this.focus();
				evt.preventDefault();
			} );
		}

		this.setTemplate( template );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		if ( this.icon ) {
			this.iconView.bind( 'content' ).to( this, 'icon' );
			this.children.add( this.iconView );
		}

		this.children.add( this.labelView );

		if ( this.withKeystroke && this.keystroke ) {
			this.children.add( this.keystrokeView );
		}
	}

	/**
	 * Focuses the {@link #element} of the button.
	 */
	focus() {
		this.element.focus();
	}

	/**
	 * Creates a label view instance and binds it with button attributes.
	 *
	 * @private
	 * @param {String} ariaLabelUid The aria label UID.
	 * @returns {module:ui/view~View}
	 */
	_createLabelView( ariaLabelUid ) {
		const labelView = new View();
		const bind = this.bindTemplate;

		labelView.setTemplate( {
			tag: 'span',

			attributes: {
				class: [
					'ck',
					'ck-button__label'
				],
				style: bind.to( 'labelStyle' ),
				id: `ck-editor__aria-label_${ ariaLabelUid }`
			},

			children: [
				{
					text: this.bindTemplate.to( 'label' )
				}
			]
		} );

		return labelView;
	}

	/**
	 * Creates a view that displays a keystroke next to a {@link #labelView label }
	 * and binds it with button attributes.
	 *
	 * @private
	 * @returns {module:ui/view~View}
	 */
	_createKeystrokeView() {
		const keystrokeView = new View();

		keystrokeView.setTemplate( {
			tag: 'span',

			attributes: {
				class: [
					'ck',
					'ck-button__keystroke'
				]
			},

			children: [
				{
					text: this.bindTemplate.to( 'keystroke', text => getEnvKeystrokeText( text ) )
				}
			]
		} );

		return keystrokeView;
	}

	/**
	 * Gets the text for the tooltip from the combination of
	 * {@link #tooltip}, {@link #label} and {@link #keystroke} attributes.
	 *
	 * @private
	 * @see #tooltip
	 * @see #_tooltipString
	 * @param {Boolean|String|Function} tooltip Button tooltip.
	 * @param {String} label Button label.
	 * @param {String} keystroke Button keystroke.
	 * @returns {String}
	 */
	_getTooltipString( tooltip, label, keystroke ) {
		if ( tooltip ) {
			if ( typeof tooltip == 'string' ) {
				return tooltip;
			} else {
				if ( keystroke ) {
					keystroke = getEnvKeystrokeText( keystroke );
				}

				if ( tooltip instanceof Function ) {
					return tooltip( label, keystroke );
				} else {
					return `${ label }${ keystroke ? ` (${ keystroke })` : '' }`;
				}
			}
		}

		return '';
	}
}var css_248z$Q = ".ck.ck-button.ck-switchbutton .ck-button__toggle,.ck.ck-button.ck-switchbutton .ck-button__toggle .ck-button__toggle__inner{display:block}:root{--ck-switch-button-toggle-width:2.6153846154em;--ck-switch-button-toggle-inner-size:calc(1.07692em + 1px);--ck-switch-button-translation:calc(var(--ck-switch-button-toggle-width) - var(--ck-switch-button-toggle-inner-size) - 2px);--ck-switch-button-inner-hover-shadow:0 0 0 5px var(--ck-color-switch-button-inner-shadow)}[dir=ltr] .ck.ck-button.ck-switchbutton .ck-button__label{margin-right:calc(var(--ck-spacing-large)*2)}[dir=rtl] .ck.ck-button.ck-switchbutton .ck-button__label{margin-left:calc(var(--ck-spacing-large)*2)}.ck.ck-button.ck-switchbutton .ck-button__toggle{border-radius:0}.ck-rounded-corners .ck.ck-button.ck-switchbutton .ck-button__toggle,.ck.ck-button.ck-switchbutton .ck-button__toggle.ck-rounded-corners{border-radius:var(--ck-border-radius)}[dir=ltr] .ck.ck-button.ck-switchbutton .ck-button__toggle{margin-left:auto}[dir=rtl] .ck.ck-button.ck-switchbutton .ck-button__toggle{margin-right:auto}.ck.ck-button.ck-switchbutton .ck-button__toggle{background:var(--ck-color-switch-button-off-background);border:1px solid transparent;transition:background .4s ease,box-shadow .2s ease-in-out,outline .2s ease-in-out;width:var(--ck-switch-button-toggle-width)}.ck.ck-button.ck-switchbutton .ck-button__toggle .ck-button__toggle__inner{border-radius:0}.ck-rounded-corners .ck.ck-button.ck-switchbutton .ck-button__toggle .ck-button__toggle__inner,.ck.ck-button.ck-switchbutton .ck-button__toggle .ck-button__toggle__inner.ck-rounded-corners{border-radius:var(--ck-border-radius);border-radius:calc(var(--ck-border-radius)*.5)}.ck.ck-button.ck-switchbutton .ck-button__toggle .ck-button__toggle__inner{background:var(--ck-color-switch-button-inner-background);height:var(--ck-switch-button-toggle-inner-size);transition:all .3s ease;width:var(--ck-switch-button-toggle-inner-size)}.ck.ck-button.ck-switchbutton .ck-button__toggle:hover{background:var(--ck-color-switch-button-off-hover-background)}.ck.ck-button.ck-switchbutton .ck-button__toggle:hover .ck-button__toggle__inner{box-shadow:var(--ck-switch-button-inner-hover-shadow)}.ck.ck-button.ck-switchbutton.ck-disabled .ck-button__toggle{opacity:var(--ck-disabled-opacity)}.ck.ck-button.ck-switchbutton:focus{border-color:transparent;box-shadow:none;outline:none}.ck.ck-button.ck-switchbutton:focus .ck-button__toggle{box-shadow:0 0 0 1px var(--ck-color-base-background),0 0 0 5px var(--ck-color-focus-outer-shadow);outline:var(--ck-focus-ring);outline-offset:1px}.ck.ck-button.ck-switchbutton.ck-on .ck-button__toggle{background:var(--ck-color-switch-button-on-background)}.ck.ck-button.ck-switchbutton.ck-on .ck-button__toggle:hover{background:var(--ck-color-switch-button-on-hover-background)}[dir=ltr] .ck.ck-button.ck-switchbutton.ck-on .ck-button__toggle .ck-button__toggle__inner{transform:translateX(var( --ck-switch-button-translation ))}[dir=rtl] .ck.ck-button.ck-switchbutton.ck-on .ck-button__toggle .ck-button__toggle__inner{transform:translateX(calc(var( --ck-switch-button-translation )*-1))}.ck.ck-button.ck-switchbutton.ck-on:active,.ck.ck-button.ck-switchbutton:active{background:transparent}";
styleInject(css_248z$Q);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The switch button view class.
 *
 *		const view = new SwitchButtonView();
 *
 *		view.set( {
 *			withText: true,
 *			label: 'Switch me!'
 *		} );
 *
 *		view.render();
 *
 *		document.body.append( view.element );
 *
 * @extends module:ui/button/buttonview~ButtonView
 */
class SwitchButtonView extends ButtonView {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		this.isToggleable = true;

		/**
		 * The toggle switch of the button.
		 *
		 * @readonly
		 * @member {module:ui/view~View} #toggleSwitchView
		 */
		this.toggleSwitchView = this._createToggleView();

		this.extendTemplate( {
			attributes: {
				class: 'ck-switchbutton'
			}
		} );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		this.children.add( this.toggleSwitchView );
	}

	/**
	 * Creates a toggle child view.
	 *
	 * @private
	 * @returns {module:ui/view~View}
	 */
	_createToggleView() {
		const toggleSwitchView = new View();

		toggleSwitchView.setTemplate( {
			tag: 'span',

			attributes: {
				class: [
					'ck',
					'ck-button__toggle'
				]
			},

			children: [
				{
					tag: 'span',

					attributes: {
						class: [
							'ck',
							'ck-button__toggle__inner'
						]
					}
				}
			]
		} );

		return toggleSwitchView;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module ui/colorgrid/utils
 */

/**
 * Returns color configuration options as defined in `editor.config.(fontColor|fontBackgroundColor).colors` or
 * `editor.config.table.(tableProperties|tableCellProperties).(background|border).colors
 * but processed to account for editor localization in the correct language.
 *
 * Note: The reason behind this method is that there is no way to use {@link module:utils/locale~Locale#t}
 * when the user configuration is defined because the editor does not exist yet.
 *
 * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
 * @param {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>} options
 * @returns {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>}.
 */
function getLocalizedColorOptions( locale, options ) {
	const t = locale.t;
	const localizedColorNames = {
		Black: t( 'Black' ),
		'Dim grey': t( 'Dim grey' ),
		Grey: t( 'Grey' ),
		'Light grey': t( 'Light grey' ),
		White: t( 'White' ),
		Red: t( 'Red' ),
		Orange: t( 'Orange' ),
		Yellow: t( 'Yellow' ),
		'Light green': t( 'Light green' ),
		Green: t( 'Green' ),
		Aquamarine: t( 'Aquamarine' ),
		Turquoise: t( 'Turquoise' ),
		'Light blue': t( 'Light blue' ),
		Blue: t( 'Blue' ),
		Purple: t( 'Purple' )
	};

	return options.map( colorOption => {
		const label = localizedColorNames[ colorOption.label ];

		if ( label && label != colorOption.label ) {
			colorOption.label = label;
		}

		return colorOption;
	} );
}

/**
 * Creates a unified color definition object from color configuration options.
 * The object contains the information necessary to both render the UI and initialize the conversion.
 *
 * @param {module:ui/colorgrid/colorgrid~ColorDefinition} options
 * @returns {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>}
 */
function normalizeColorOptions( options ) {
	return options
		.map( normalizeSingleColorDefinition )
		.filter( option => !!option );
}

// Creates a normalized color definition from the user-defined configuration.
// The "normalization" means it will create full
// {@link module:ui/colorgrid/colorgrid~ColorDefinition `ColorDefinition-like`}
// object for string values, and add a `view` property, for each definition.
//
// @param {String|module:ui/colorgrid/colorgrid~ColorDefinition}
// @returns {module:ui/colorgrid/colorgrid~ColorDefinition}
function normalizeSingleColorDefinition( color ) {
	if ( typeof color === 'string' ) {
		return {
			model: color,
			label: color,
			hasBorder: false,
			view: {
				name: 'span',
				styles: {
					color
				}
			}
		};
	} else {
		return {
			model: color.color,
			label: color.label || color.color,
			hasBorder: color.hasBorder === undefined ? false : color.hasBorder,
			view: {
				name: 'span',
				styles: {
					color: `${ color.color }`
				}
			}
		};
	}
}var checkIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path class=\"ck-icon__fill\" d=\"M16.935 5.328a2 2 0 0 1 0 2.829l-7.778 7.778a2 2 0 0 1-2.829 0L3.5 13.107a1.999 1.999 0 1 1 2.828-2.829l.707.707a1 1 0 0 0 1.414 0l5.658-5.657a2 2 0 0 1 2.828 0z\"/><path d=\"M14.814 6.035 8.448 12.4a1 1 0 0 1-1.414 0l-1.413-1.415A1 1 0 1 0 4.207 12.4l2.829 2.829a1 1 0 0 0 1.414 0l7.778-7.778a1 1 0 1 0-1.414-1.415z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * This class represents a single color tile in the {@link module:ui/colorgrid/colorgrid~ColorGridView}.
 *
 * @extends module:ui/button/buttonview~ButtonView
 */
class ColorTileView extends ButtonView {
	constructor( locale ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * String representing a color shown as tile's background.
		 *
		 * @type {String}
		 */
		this.set( 'color' );

		/**
		 * A flag that toggles a special CSS class responsible for displaying
		 * a border around the button.
		 *
		 * @type {Boolean}
		 */
		this.set( 'hasBorder' );

		this.icon = checkIcon;

		this.extendTemplate( {
			attributes: {
				style: {
					backgroundColor: bind.to( 'color' )
				},
				class: [
					'ck',
					'ck-color-grid__tile',
					bind.if( 'hasBorder', 'ck-color-table__color-tile_bordered' )
				]
			}
		} );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		this.iconView.fillColor = 'hsl(0, 0%, 100%)';
	}
}var css_248z$P = ".ck.ck-color-grid{display:grid}:root{--ck-color-grid-tile-size:24px;--ck-color-color-grid-check-icon:#166fd4}.ck.ck-color-grid{grid-gap:5px;padding:8px}.ck.ck-color-grid__tile{border:0;height:var(--ck-color-grid-tile-size);min-height:var(--ck-color-grid-tile-size);min-width:var(--ck-color-grid-tile-size);padding:0;transition:box-shadow .2s ease;width:var(--ck-color-grid-tile-size)}.ck.ck-color-grid__tile.ck-disabled{cursor:unset;transition:unset}.ck.ck-color-grid__tile.ck-color-table__color-tile_bordered{box-shadow:0 0 0 1px var(--ck-color-base-border)}.ck.ck-color-grid__tile .ck.ck-icon{color:var(--ck-color-color-grid-check-icon);display:none}.ck.ck-color-grid__tile.ck-on{box-shadow:inset 0 0 0 1px var(--ck-color-base-background),0 0 0 2px var(--ck-color-base-text)}.ck.ck-color-grid__tile.ck-on .ck.ck-icon{display:block}.ck.ck-color-grid__tile.ck-on,.ck.ck-color-grid__tile:focus:not(.ck-disabled),.ck.ck-color-grid__tile:hover:not(.ck-disabled){border:0}.ck.ck-color-grid__tile:focus:not(.ck-disabled),.ck.ck-color-grid__tile:hover:not(.ck-disabled){box-shadow:inset 0 0 0 1px var(--ck-color-base-background),0 0 0 2px var(--ck-color-focus-border)}.ck.ck-color-grid__label{padding:0 var(--ck-spacing-standard)}";
styleInject(css_248z$P);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A grid of {@link module:ui/colorgrid/colortile~ColorTileView color tiles}.
 *
 * @extends module:ui/view~View
 */
class ColorGridView extends View {
	/**
	 * Creates an instance of a color grid containing {@link module:ui/colorgrid/colortile~ColorTileView tiles}.
	 *
	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
	 * @param {Object} options Component configuration
	 * @param {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>} [options.colorDefinitions] Array with definitions
	 * required to create the {@link module:ui/colorgrid/colortile~ColorTileView tiles}.
	 * @param {Number} [options.columns=5] A number of columns to display the tiles.
	 */
	constructor( locale, options ) {
		super( locale );

		const colorDefinitions = options && options.colorDefinitions || [];
		const viewStyleAttribute = {};

		/**
		 * A number of columns for the tiles grid.
		 *
		 * @readonly
		 * @member {Number}
		 */
		this.columns = options && options.columns ? options.columns : 5;

		viewStyleAttribute.gridTemplateColumns = `repeat( ${ this.columns }, 1fr)`;

		/**
		 * The color of the currently selected color tile in {@link #items}.
		 *
		 * @observable
		 * @type {String}
		 */
		this.set( 'selectedColor' );

		/**
		 * Collection of the child tile views.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.items = this.createCollection();

		/**
		 * Tracks information about DOM focus in the grid.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * Instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
		 *
		 * @readonly
		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
		 */
		this.keystrokes = new KeystrokeHandler();

		this.items.on( 'add', ( evt, colorTile ) => {
			colorTile.isOn = colorTile.color === this.selectedColor;
		} );

		colorDefinitions.forEach( color => {
			const colorTile = new ColorTileView();

			colorTile.set( {
				color: color.color,
				label: color.label,
				tooltip: true,
				hasBorder: color.options.hasBorder
			} );

			colorTile.on( 'execute', () => {
				this.fire( 'execute', {
					value: color.color,
					hasBorder: color.options.hasBorder,
					label: color.label
				} );
			} );

			this.items.add( colorTile );
		} );

		this.setTemplate( {
			tag: 'div',
			children: this.items,
			attributes: {
				class: [
					'ck',
					'ck-color-grid'
				],
				style: viewStyleAttribute
			}
		} );

		this.on( 'change:selectedColor', ( evt, name, selectedColor ) => {
			for ( const item of this.items ) {
				item.isOn = item.color === selectedColor;
			}
		} );
	}

	/**
	 * Focuses the first focusable in {@link #items}.
	 */
	focus() {
		if ( this.items.length ) {
			this.items.first.focus();
		}
	}

	/**
	 * Focuses the last focusable in {@link #items}.
	 */
	focusLast() {
		if ( this.items.length ) {
			this.items.last.focus();
		}
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		// Items added before rendering should be known to the #focusTracker.
		for ( const item of this.items ) {
			this.focusTracker.add( item.element );
		}

		this.items.on( 'add', ( evt, item ) => {
			this.focusTracker.add( item.element );
		} );

		this.items.on( 'remove', ( evt, item ) => {
			this.focusTracker.remove( item.element );
		} );

		// Start listening for the keystrokes coming from #element.
		this.keystrokes.listenTo( this.element );

		addKeyboardHandlingForGrid( {
			keystrokeHandler: this.keystrokes,
			focusTracker: this.focusTracker,
			gridItems: this.items,
			numberOfColumns: this.columns
		} );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}

	/**
	 * Fired when the `ColorTileView` for the picked item is executed.
	 *
	 * @event execute
	 * @param {Object} data Additional information about the event.
	 * @param {String} data.value The value of the selected color
	 * ({@link module:ui/colorgrid/colorgrid~ColorDefinition#color `color.color`}).
	 * @param {Boolean} data.hasBorder The `hasBorder` property of the selected color
	 * ({@link module:ui/colorgrid/colorgrid~ColorDefinition#options `color.options.hasBorder`}).
	 * @param {String} data.Label The label of the selected color
	 * ({@link module:ui/colorgrid/colorgrid~ColorDefinition#label `color.label`})
	 */
}

/**
 * A color definition used to create a {@link module:ui/colorgrid/colortile~ColorTileView}.
 *
 *		{
 *			color: 'hsl(0, 0%, 75%)',
 *			label: 'Light Grey',
 *			options: {
 *				hasBorder: true
 *			}
 *		}
 *
 * @typedef {Object} module:ui/colorgrid/colorgrid~ColorDefinition
 * @type Object
 *
 * @property {String} color String representing a color.
 * It is used as value of background-color style in {@link module:ui/colorgrid/colortile~ColorTileView}.
 * @property {String} label String used as label for {@link module:ui/colorgrid/colortile~ColorTileView}.
 * @property {Object} options Additional options passed to create a {@link module:ui/colorgrid/colortile~ColorTileView}.
 * @property {Boolean} options.hasBorder A flag that indicates if special a CSS class should be added
 * to {@link module:ui/colorgrid/colortile~ColorTileView}, which renders a border around it.
 */var dropdownArrowIcon = "<svg viewBox=\"0 0 10 10\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M.941 4.523a.75.75 0 1 1 1.06-1.06l3.006 3.005 3.005-3.005a.75.75 0 1 1 1.06 1.06l-3.549 3.55a.75.75 0 0 1-1.168-.136L.941 4.523z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The default dropdown button view class.
 *
 *		const view = new DropdownButtonView();
 *
 *		view.set( {
 *			label: 'A button',
 *			keystroke: 'Ctrl+B',
 *			tooltip: true
 *		} );
 *
 *		view.render();
 *
 *		document.body.append( view.element );
 *
 * Also see the {@link module:ui/dropdown/utils~createDropdown `createDropdown()` util}.
 *
 * @implements module:ui/dropdown/button/dropdownbutton~DropdownButton
 * @extends module:ui/button/buttonview~ButtonView
 */
class DropdownButtonView extends ButtonView {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		/**
		 * An icon that displays arrow to indicate a dropdown button.
		 *
		 * @readonly
		 * @member {module:ui/icon/iconview~IconView}
		 */
		this.arrowView = this._createArrowView();

		this.extendTemplate( {
			attributes: {
				'aria-haspopup': true,
				'aria-expanded': this.bindTemplate.to( 'isOn', value => String( value ) )
			}
		} );

		// The DropdownButton interface expects the open event upon which will open the dropdown.
		this.delegate( 'execute' ).to( this, 'open' );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		this.children.add( this.arrowView );
	}

	/**
	 * Creates a {@link module:ui/icon/iconview~IconView} instance as {@link #arrowView}.
	 *
	 * @private
	 * @returns {module:ui/icon/iconview~IconView}
	 */
	_createArrowView() {
		const arrowView = new IconView();

		arrowView.content = dropdownArrowIcon;

		arrowView.extendTemplate( {
			attributes: {
				class: 'ck-dropdown__arrow'
			}
		} );

		return arrowView;
	}
}var css_248z$O = ".ck.ck-splitbutton{font-size:inherit}.ck.ck-splitbutton .ck-splitbutton__action:focus{z-index:calc(var(--ck-z-default) + 1)}:root{--ck-color-split-button-hover-background:#ebebeb;--ck-color-split-button-hover-border:#b3b3b3}[dir=ltr] .ck.ck-splitbutton.ck-splitbutton_open>.ck-splitbutton__action,[dir=ltr] .ck.ck-splitbutton:hover>.ck-splitbutton__action{border-bottom-right-radius:unset;border-top-right-radius:unset}[dir=rtl] .ck.ck-splitbutton.ck-splitbutton_open>.ck-splitbutton__action,[dir=rtl] .ck.ck-splitbutton:hover>.ck-splitbutton__action{border-bottom-left-radius:unset;border-top-left-radius:unset}.ck.ck-splitbutton>.ck-splitbutton__arrow{min-width:unset}[dir=ltr] .ck.ck-splitbutton>.ck-splitbutton__arrow{border-bottom-left-radius:unset;border-top-left-radius:unset}[dir=rtl] .ck.ck-splitbutton>.ck-splitbutton__arrow{border-bottom-right-radius:unset;border-top-right-radius:unset}.ck.ck-splitbutton>.ck-splitbutton__arrow svg{width:var(--ck-dropdown-arrow-size)}.ck.ck-splitbutton.ck-splitbutton_open>.ck-button:not(.ck-on):not(.ck-disabled):not(:hover),.ck.ck-splitbutton:hover>.ck-button:not(.ck-on):not(.ck-disabled):not(:hover){background:var(--ck-color-split-button-hover-background)}.ck.ck-splitbutton.ck-splitbutton_open>.ck-splitbutton__arrow:not(.ck-disabled):after,.ck.ck-splitbutton:hover>.ck-splitbutton__arrow:not(.ck-disabled):after{background-color:var(--ck-color-split-button-hover-border);content:\"\";height:100%;position:absolute;width:1px}[dir=ltr] .ck.ck-splitbutton.ck-splitbutton_open>.ck-splitbutton__arrow:not(.ck-disabled):after,[dir=ltr] .ck.ck-splitbutton:hover>.ck-splitbutton__arrow:not(.ck-disabled):after{left:-1px}[dir=rtl] .ck.ck-splitbutton.ck-splitbutton_open>.ck-splitbutton__arrow:not(.ck-disabled):after,[dir=rtl] .ck.ck-splitbutton:hover>.ck-splitbutton__arrow:not(.ck-disabled):after{right:-1px}.ck.ck-splitbutton.ck-splitbutton_open{border-radius:0}.ck-rounded-corners .ck.ck-splitbutton.ck-splitbutton_open,.ck.ck-splitbutton.ck-splitbutton_open.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck-rounded-corners .ck.ck-splitbutton.ck-splitbutton_open>.ck-splitbutton__action,.ck.ck-splitbutton.ck-splitbutton_open.ck-rounded-corners>.ck-splitbutton__action{border-bottom-left-radius:0}.ck-rounded-corners .ck.ck-splitbutton.ck-splitbutton_open>.ck-splitbutton__arrow,.ck.ck-splitbutton.ck-splitbutton_open.ck-rounded-corners>.ck-splitbutton__arrow{border-bottom-right-radius:0}";
styleInject(css_248z$O);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The split button view class.
 *
 *		const view = new SplitButtonView();
 *
 *		view.set( {
 *			label: 'A button',
 *			keystroke: 'Ctrl+B',
 *			tooltip: true
 *		} );
 *
 *		view.render();
 *
 *		document.body.append( view.element );
 *
 * Also see the {@link module:ui/dropdown/utils~createDropdown `createDropdown()` util}.
 *
 * @implements module:ui/dropdown/button/dropdownbutton~DropdownButton
 * @extends module:ui/view~View
 */
class SplitButtonView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		const bind = this.bindTemplate;

		// Implement the Button interface.
		this.set( 'class' );
		this.set( 'icon' );
		this.set( 'isEnabled', true );
		this.set( 'isOn', false );
		this.set( 'isToggleable', false );
		this.set( 'isVisible', true );
		this.set( 'keystroke' );
		this.set( 'label' );
		this.set( 'tabindex', -1 );
		this.set( 'tooltip' );
		this.set( 'tooltipPosition', 's' );
		this.set( 'type', 'button' );
		this.set( 'withText', false );

		/**
		 * Collection of the child views inside of the split button {@link #element}.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.children = this.createCollection();

		/**
		 * A main button of split button.
		 *
		 * @readonly
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this.actionView = this._createActionView();

		/**
		 * A secondary button of split button that opens dropdown.
		 *
		 * @readonly
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this.arrowView = this._createArrowView();

		/**
		 * Instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}. It manages
		 * keystrokes of the split button:
		 *
		 * * <kbd>▶</kbd> moves focus to arrow view when action view is focused,
		 * * <kbd>◀</kbd> moves focus to action view when arrow view is focused.
		 *
		 * @readonly
		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
		 */
		this.keystrokes = new KeystrokeHandler();

		/**
		 * Tracks information about DOM focus in the dropdown.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		this.setTemplate( {
			tag: 'div',

			attributes: {
				class: [
					'ck',
					'ck-splitbutton',
					bind.to( 'class' ),
					bind.if( 'isVisible', 'ck-hidden', value => !value ),
					this.arrowView.bindTemplate.if( 'isOn', 'ck-splitbutton_open' )
				]
			},

			children: this.children
		} );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		this.children.add( this.actionView );
		this.children.add( this.arrowView );

		this.focusTracker.add( this.actionView.element );
		this.focusTracker.add( this.arrowView.element );

		this.keystrokes.listenTo( this.element );

		// Overrides toolbar focus cycling behavior.
		this.keystrokes.set( 'arrowright', ( evt, cancel ) => {
			if ( this.focusTracker.focusedElement === this.actionView.element ) {
				this.arrowView.focus();

				cancel();
			}
		} );

		// Overrides toolbar focus cycling behavior.
		this.keystrokes.set( 'arrowleft', ( evt, cancel ) => {
			if ( this.focusTracker.focusedElement === this.arrowView.element ) {
				this.actionView.focus();

				cancel();
			}
		} );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}

	/**
	 * Focuses the {@link #actionView#element} of the action part of split button.
	 */
	focus() {
		this.actionView.focus();
	}

	/**
	 * Creates a {@link module:ui/button/buttonview~ButtonView} instance as {@link #actionView} and binds it with main split button
	 * attributes.
	 *
	 * @private
	 * @returns {module:ui/button/buttonview~ButtonView}
	 */
	_createActionView() {
		const actionView = new ButtonView();

		actionView.bind(
			'icon',
			'isEnabled',
			'isOn',
			'isToggleable',
			'keystroke',
			'label',
			'tabindex',
			'tooltip',
			'tooltipPosition',
			'type',
			'withText'
		).to( this );

		actionView.extendTemplate( {
			attributes: {
				class: 'ck-splitbutton__action'
			}
		} );

		actionView.delegate( 'execute' ).to( this );

		return actionView;
	}

	/**
	 * Creates a {@link module:ui/button/buttonview~ButtonView} instance as {@link #arrowView} and binds it with main split button
	 * attributes.
	 *
	 * @private
	 * @returns {module:ui/button/buttonview~ButtonView}
	 */
	_createArrowView() {
		const arrowView = new ButtonView();
		const bind = arrowView.bindTemplate;

		arrowView.icon = dropdownArrowIcon;

		arrowView.extendTemplate( {
			attributes: {
				class: [
					'ck-splitbutton__arrow'
				],
				'data-cke-tooltip-disabled': bind.to( 'isOn' ),
				'aria-haspopup': true,
				'aria-expanded': bind.to( 'isOn', value => String( value ) )
			}
		} );

		arrowView.bind( 'isEnabled' ).to( this );
		arrowView.bind( 'label' ).to( this );
		arrowView.bind( 'tooltip' ).to( this );

		arrowView.delegate( 'execute' ).to( this, 'open' );

		return arrowView;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The dropdown panel view class.
 *
 * See {@link module:ui/dropdown/dropdownview~DropdownView} to learn about the common usage.
 *
 * @extends module:ui/view~View
 */
class DropdownPanelView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * Controls whether the panel is visible.
		 *
		 * @observable
		 * @member {Boolean} #isVisible
		 */
		this.set( 'isVisible', false );

		/**
		 * The position of the panel, relative to the parent.
		 *
		 * This property is reflected in the CSS class set to {@link #element} that controls
		 * the position of the panel.
		 *
		 * @observable
		 * @default 'se'
		 * @member {'s'|'se'|'sw'|'sme'|'smw'|'n'|'ne'|'nw'|'nme'|'nmw'} #position
		 */
		this.set( 'position', 'se' );

		/**
		 * Collection of the child views in this panel.
		 *
		 * A common child type is the {@link module:ui/list/listview~ListView} and {@link module:ui/toolbar/toolbarview~ToolbarView}.
		 * See {@link module:ui/dropdown/utils~addListToDropdown} and
		 * {@link module:ui/dropdown/utils~addToolbarToDropdown} to learn more about child views of dropdowns.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.children = this.createCollection();

		this.setTemplate( {
			tag: 'div',

			attributes: {
				class: [
					'ck',
					'ck-reset',
					'ck-dropdown__panel',
					bind.to( 'position', value => `ck-dropdown__panel_${ value }` ),
					bind.if( 'isVisible', 'ck-dropdown__panel-visible' )
				]
			},

			children: this.children,

			on: {
				// Drag and drop in the panel should not break the selection in the editor.
				// https://github.com/ckeditor/ckeditor5-ui/issues/228
				selectstart: bind.to( evt => evt.preventDefault() )
			}
		} );
	}

	/**
	 * Focuses the first view in the {@link #children} collection.
	 *
	 * See also {@link module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable}.
	 */
	focus() {
		if ( this.children.length ) {
			if ( typeof this.children.first.focus === 'function' ) {
				this.children.first.focus();
			} else {
				/**
				 * The child view of a dropdown could not be focused because it is missing the `focus()` method.
				 *
				 * This warning appears when a dropdown {@link module:ui/dropdown/dropdownview~DropdownView#isOpen gets open} and it
				 * attempts to focus the {@link module:ui/dropdown/dropdownpanelview~DropdownPanelView#children first child} of its panel
				 * but the child does not implement the
				 * {@link module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable focusable interface}.
				 *
				 * Focusing the content of a dropdown on open greatly improves the accessibility. Please make sure the view instance
				 * provides the `focus()` method for the best user experience.
				 *
				 * @error ui-dropdown-panel-focus-child-missing-focus
				 * @param {module:ui/view~View} childView
				 * @param {module:ui/dropdown/dropdownpanelview~DropdownPanelView} dropdownPanel
				 */
				logWarning( 'ui-dropdown-panel-focus-child-missing-focus', { childView: this.children.first, dropdownPanel: this } );
			}
		}
	}

	/**
	 * Focuses the view element or last item in view collection on opening dropdown's panel.
	 *
	 * See also {@link module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable}.
	 */
	focusLast() {
		if ( this.children.length ) {
			const lastChild = this.children.last;

			if ( typeof lastChild.focusLast === 'function' ) {
				lastChild.focusLast();
			} else {
				lastChild.focus();
			}
		}
	}
}var css_248z$N = ":root{--ck-dropdown-max-width:75vw}.ck.ck-dropdown{display:inline-block;position:relative}.ck.ck-dropdown .ck-dropdown__arrow{pointer-events:none;z-index:var(--ck-z-default)}.ck.ck-dropdown .ck-button.ck-dropdown__button{width:100%}.ck.ck-dropdown .ck-dropdown__panel{display:none;max-width:var(--ck-dropdown-max-width);position:absolute;z-index:var(--ck-z-modal)}.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel-visible{display:inline-block}.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_n,.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_ne,.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_nme,.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_nmw,.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_nw{bottom:100%}.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_s,.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_se,.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sme,.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_smw,.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sw{bottom:auto;top:100%}.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_ne,.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_se{left:0}.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_nw,.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sw{right:0}.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_n,.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_s{left:50%;transform:translateX(-50%)}.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_nmw,.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_smw{left:75%;transform:translateX(-75%)}.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_nme,.ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sme{left:25%;transform:translateX(-25%)}.ck.ck-toolbar .ck-dropdown__panel{z-index:calc(var(--ck-z-modal) + 1)}:root{--ck-dropdown-arrow-size:calc(var(--ck-icon-size)*0.5)}.ck.ck-dropdown{font-size:inherit}.ck.ck-dropdown .ck-dropdown__arrow{width:var(--ck-dropdown-arrow-size)}[dir=ltr] .ck.ck-dropdown .ck-dropdown__arrow{margin-left:var(--ck-spacing-standard);right:var(--ck-spacing-standard)}[dir=rtl] .ck.ck-dropdown .ck-dropdown__arrow{left:var(--ck-spacing-standard);margin-right:var(--ck-spacing-small)}.ck.ck-dropdown.ck-disabled .ck-dropdown__arrow{opacity:var(--ck-disabled-opacity)}[dir=ltr] .ck.ck-dropdown .ck-button.ck-dropdown__button:not(.ck-button_with-text){padding-left:var(--ck-spacing-small)}[dir=rtl] .ck.ck-dropdown .ck-button.ck-dropdown__button:not(.ck-button_with-text){padding-right:var(--ck-spacing-small)}.ck.ck-dropdown .ck-button.ck-dropdown__button .ck-button__label{overflow:hidden;text-overflow:ellipsis;width:7em}.ck.ck-dropdown .ck-button.ck-dropdown__button.ck-disabled .ck-button__label{opacity:var(--ck-disabled-opacity)}.ck.ck-dropdown .ck-button.ck-dropdown__button.ck-on{border-bottom-left-radius:0;border-bottom-right-radius:0}.ck.ck-dropdown .ck-button.ck-dropdown__button.ck-dropdown__button_label-width_auto .ck-button__label{width:auto}.ck.ck-dropdown .ck-button.ck-dropdown__button.ck-off:active,.ck.ck-dropdown .ck-button.ck-dropdown__button.ck-on:active{box-shadow:none}.ck.ck-dropdown .ck-button.ck-dropdown__button.ck-off:active:focus,.ck.ck-dropdown .ck-button.ck-dropdown__button.ck-on:active:focus{box-shadow:var(--ck-focus-outer-shadow),0 0}.ck.ck-dropdown__panel{border-radius:0}.ck-rounded-corners .ck.ck-dropdown__panel,.ck.ck-dropdown__panel.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-dropdown__panel{background:var(--ck-color-dropdown-panel-background);border:1px solid var(--ck-color-dropdown-panel-border);bottom:0;box-shadow:var(--ck-drop-shadow),0 0;min-width:100%}.ck.ck-dropdown__panel.ck-dropdown__panel_se{border-top-left-radius:0}.ck.ck-dropdown__panel.ck-dropdown__panel_sw{border-top-right-radius:0}.ck.ck-dropdown__panel.ck-dropdown__panel_ne{border-bottom-left-radius:0}.ck.ck-dropdown__panel.ck-dropdown__panel_nw{border-bottom-right-radius:0}";
styleInject(css_248z$N);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The dropdown view class. It manages the dropdown button and dropdown panel.
 *
 * In most cases, the easiest way to create a dropdown is by using the {@link module:ui/dropdown/utils~createDropdown}
 * util:
 *
 *		const dropdown = createDropdown( locale );
 *
 *		// Configure dropdown's button properties:
 *		dropdown.buttonView.set( {
 *			label: 'A dropdown',
 *			withText: true
 *		} );
 *
 *		dropdown.render();
 *
 *		dropdown.panelView.element.textContent = 'Content of the panel';
 *
 *		// Will render a dropdown with a panel containing a "Content of the panel" text.
 *		document.body.appendChild( dropdown.element );
 *
 * If you want to add a richer content to the dropdown panel, you can use the {@link module:ui/dropdown/utils~addListToDropdown}
 * and {@link module:ui/dropdown/utils~addToolbarToDropdown} helpers. See more examples in
 * {@link module:ui/dropdown/utils~createDropdown} documentation.
 *
 * If you want to create a completely custom dropdown, then you can compose it manually:
 *
 *		const button = new DropdownButtonView( locale );
 *		const panel = new DropdownPanelView( locale );
 *		const dropdown = new DropdownView( locale, button, panel );
 *
 *		button.set( {
 *			label: 'A dropdown',
 *			withText: true
 *		} );
 *
 *		dropdown.render();
 *
 *		panel.element.textContent = 'Content of the panel';
 *
 *		// Will render a dropdown with a panel containing a "Content of the panel" text.
 *		document.body.appendChild( dropdown.element );
 *
 * However, dropdown created this way will contain little behavior. You will need to implement handlers for actions
 * such as {@link module:ui/bindings/clickoutsidehandler~clickOutsideHandler clicking outside an open dropdown}
 * (which should close it) and support for arrow keys inside the panel. Therefore, unless you really know what
 * you do and you really need to do it, it is recommended to use the {@link module:ui/dropdown/utils~createDropdown} helper.
 *
 * @extends module:ui/view~View
 */
class DropdownView extends View {
	/**
	 * Creates an instance of the dropdown.
	 *
	 * Also see {@link #render}.
	 *
	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
	 * @param {module:ui/dropdown/button/dropdownbutton~DropdownButton} buttonView
	 * @param {module:ui/dropdown/dropdownpanelview~DropdownPanelView} panelView
	 */
	constructor( locale, buttonView, panelView ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * Button of the dropdown view. Clicking the button opens the {@link #panelView}.
		 *
		 * @readonly
		 * @member {module:ui/button/buttonview~ButtonView} #buttonView
		 */
		this.buttonView = buttonView;

		/**
		 * Panel of the dropdown. It opens when the {@link #buttonView} is
		 * {@link module:ui/button/buttonview~ButtonView#event:execute executed} (i.e. clicked).
		 *
		 * Child views can be added to the panel's `children` collection:
		 *
		 *		dropdown.panelView.children.add( childView );
		 *
		 * See {@link module:ui/dropdown/dropdownpanelview~DropdownPanelView#children} and
		 * {@link module:ui/viewcollection~ViewCollection#add}.
		 *
		 * @readonly
		 * @member {module:ui/dropdown/dropdownpanelview~DropdownPanelView} #panelView
		 */
		this.panelView = panelView;

		/**
		 * Controls whether the dropdown view is open, i.e. shows or hides the {@link #panelView panel}.
		 *
		 * **Note**: When the dropdown gets open, it will attempt to call `focus()` on the first child of its {@link #panelView}.
		 * See {@link module:ui/dropdown/utils~addToolbarToDropdown}, {@link module:ui/dropdown/utils~addListToDropdown}, and
		 * {@link module:ui/dropdown/utils~focusChildOnDropdownOpen} to learn more about focus management in dropdowns.
		 *
		 * @observable
		 * @member {Boolean} #isOpen
		 */
		this.set( 'isOpen', false );

		/**
		 * Controls whether the dropdown is enabled, i.e. it can be clicked and execute an action.
		 *
		 * See {@link module:ui/button/buttonview~ButtonView#isEnabled}.
		 *
		 * @observable
		 * @member {Boolean} #isEnabled
		 */
		this.set( 'isEnabled', true );

		/**
		 * (Optional) The additional CSS class set on the dropdown {@link #element}.
		 *
		 * @observable
		 * @member {String} #class
		 */
		this.set( 'class' );

		/**
		 * (Optional) The `id` attribute of the dropdown (i.e. to pair with a `<label>` element).
		 *
		 * @observable
		 * @member {String} #id
		 */
		this.set( 'id' );

		/**
		 * The position of the panel, relative to the dropdown.
		 *
		 * **Note**: When `'auto'`, the panel will use one of the remaining positions to stay
		 * in the viewport, visible to the user. The positions correspond directly to
		 * {@link module:ui/dropdown/dropdownview~DropdownView.defaultPanelPositions default panel positions}.
		 *
		 * **Note**: This value has an impact on the
		 * {@link module:ui/dropdown/dropdownpanelview~DropdownPanelView#position} property
		 * each time the panel becomes {@link #isOpen open}.
		 *
		 * @observable
		 * @default 'auto'
		 * @member {'auto'|'s'|'se'|'sw'|'sme'|'smw'|'n'|'ne'|'nw'|'nme'|'nmw'} #panelPosition
		 */
		this.set( 'panelPosition', 'auto' );

		/**
		 * Instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}. It manages
		 * keystrokes of the dropdown:
		 *
		 * * <kbd>▼</kbd> opens the dropdown,
		 * * <kbd>◀</kbd> and <kbd>Esc</kbd> closes the dropdown.
		 *
		 * @readonly
		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
		 */
		this.keystrokes = new KeystrokeHandler();

		this.setTemplate( {
			tag: 'div',

			attributes: {
				class: [
					'ck',
					'ck-dropdown',
					bind.to( 'class' ),
					bind.if( 'isEnabled', 'ck-disabled', value => !value )
				],
				id: bind.to( 'id' ),
				'aria-describedby': bind.to( 'ariaDescribedById' )
			},

			children: [
				buttonView,
				panelView
			]
		} );

		buttonView.extendTemplate( {
			attributes: {
				class: [
					'ck-dropdown__button'
				],
				'data-cke-tooltip-disabled': bind.to( 'isOpen' )
			}
		} );

		/**
		 * A child {@link module:ui/list/listview~ListView list view} of the dropdown located
		 * in its {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel}.
		 *
		 * **Note**: Only supported when dropdown has list view added using {@link module:ui/dropdown/utils~addListToDropdown}.
		 *
		 * @readonly
		 * @member {module:ui/list/listview~ListView} #listView
		 */

		/**
		 * A child toolbar of the dropdown located in the
		 * {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel}.
		 *
		 * **Note**: Only supported when dropdown has list view added using {@link module:ui/dropdown/utils~addToolbarToDropdown}.
		 *
		 * @readonly
		 * @member {module:ui/toolbar/toolbarview~ToolbarView} #toolbarView
		 */

		/**
		 * Fired when the toolbar button or list item is executed.
		 *
		 * For {@link #listView} It fires when a child of some {@link module:ui/list/listitemview~ListItemView}
		 * fired `execute`.
		 *
		 * For {@link #toolbarView} It fires when one of the buttons has been
		 * {@link module:ui/button/buttonview~ButtonView#event:execute executed}.
		 *
		 * **Note**: Only supported when dropdown has list view added using {@link module:ui/dropdown/utils~addListToDropdown}
		 * or {@link module:ui/dropdown/utils~addToolbarToDropdown}.
		 *
		 * @event execute
		 */
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		// Toggle the dropdown when its button has been clicked.
		this.listenTo( this.buttonView, 'open', () => {
			this.isOpen = !this.isOpen;
		} );

		// Toggle the visibility of the panel when the dropdown becomes open.
		this.panelView.bind( 'isVisible' ).to( this, 'isOpen' );

		// Let the dropdown control the position of the panel. The position must
		// be updated every time the dropdown is open.
		this.on( 'change:isOpen', () => {
			if ( !this.isOpen ) {
				// If the dropdown was closed, move the focus back to the button (#12125).
				this.focus();

				return;
			}

			// If "auto", find the best position of the panel to fit into the viewport.
			// Otherwise, simply assign the static position.
			if ( this.panelPosition === 'auto' ) {
				this.panelView.position = DropdownView._getOptimalPosition( {
					element: this.panelView.element,
					target: this.buttonView.element,
					fitInViewport: true,
					positions: this._panelPositions
				} ).name;
			} else {
				this.panelView.position = this.panelPosition;
			}

			// Focus the first item in the dropdown when the dropdown opened
			this.panelView.focus();
		} );

		// Listen for keystrokes coming from within #element.
		this.keystrokes.listenTo( this.element );

		const closeDropdown = ( data, cancel ) => {
			if ( this.isOpen ) {
				this.isOpen = false;
				cancel();
			}
		};

		// Open the dropdown panel using the arrow down key, just like with return or space.
		this.keystrokes.set( 'arrowdown', ( data, cancel ) => {
			// Don't open if the dropdown is disabled or already open.
			if ( this.buttonView.isEnabled && !this.isOpen ) {
				this.isOpen = true;
				cancel();
			}
		} );

		// Block the right arrow key (until nested dropdowns are implemented).
		this.keystrokes.set( 'arrowright', ( data, cancel ) => {
			if ( this.isOpen ) {
				cancel();
			}
		} );

		// Close the dropdown using the arrow left/escape key.
		this.keystrokes.set( 'arrowleft', closeDropdown );
		this.keystrokes.set( 'esc', closeDropdown );
	}

	/**
	 * Focuses the {@link #buttonView}.
	 */
	focus() {
		this.buttonView.focus();
	}

	/**
	 * Returns {@link #panelView panel} positions to be used by the
	 * {@link module:utils/dom/position~getOptimalPosition `getOptimalPosition()`}
	 * utility considering the direction of the language the UI of the editor is displayed in.
	 *
	 * @type {module:utils/dom/position~Options#positions}
	 * @private
	 */
	get _panelPositions() {
		const {
			south, north,
			southEast, southWest,
			northEast, northWest,
			southMiddleEast, southMiddleWest,
			northMiddleEast, northMiddleWest
		} = DropdownView.defaultPanelPositions;

		if ( this.locale.uiLanguageDirection !== 'rtl' ) {
			return [
				southEast, southWest, southMiddleEast, southMiddleWest, south,
				northEast, northWest, northMiddleEast, northMiddleWest, north
			];
		} else {
			return [
				southWest, southEast, southMiddleWest, southMiddleEast, south,
				northWest, northEast, northMiddleWest, northMiddleEast, north
			];
		}
	}
}

/**
 * A set of positioning functions used by the dropdown view to determine
 * the optimal position (i.e. fitting into the browser viewport) of its
 * {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel} when
 * {@link module:ui/dropdown/dropdownview~DropdownView#panelPosition} is set to 'auto'`.
 *
 * The available positioning functions are as follow:
 *
 * **South**
 *
 * * `south`
 *
 *			[ Button ]
 *		+-----------------+
 *		|      Panel      |
 *		+-----------------+
 *
 * * `southEast`
 *
 *		[ Button ]
 *		+-----------------+
 *		|      Panel      |
 *		+-----------------+
 *
 * * `southWest`
 *
 *		         [ Button ]
 *		+-----------------+
 *		|      Panel      |
 *		+-----------------+
 *
 * * `southMiddleEast`
 *
 *		  [ Button ]
 *		+-----------------+
 *		|      Panel      |
 *		+-----------------+
 *
 * * `southMiddleWest`
 *
 *		       [ Button ]
 *		+-----------------+
 *		|      Panel      |
 *		+-----------------+
 *
 * **North**
 *
 * * `north`
 *
 *		+-----------------+
 *		|      Panel      |
 *		+-----------------+
 *		    [ Button ]
 *
 * * `northEast`
 *
 *		+-----------------+
 *		|      Panel      |
 *		+-----------------+
 *		[ Button ]
 *
 * * `northWest`
 *
 *		+-----------------+
 *		|      Panel      |
 *		+-----------------+
 *		         [ Button ]
 *
 * * `northMiddleEast`
 *
 *		+-----------------+
 *		|      Panel      |
 *		+-----------------+
 *		  [ Button ]
 *
 * * `northMiddleWest`
 *
 *		+-----------------+
 *		|      Panel      |
 *		+-----------------+
 *		       [ Button ]
 *
 * Positioning functions are compatible with {@link module:utils/dom/position~Position}.
 *
 * The name that position function returns will be reflected in dropdown panel's class that
 * controls its placement. See {@link module:ui/dropdown/dropdownview~DropdownView#panelPosition}
 * to learn more.
 *
 * @member {Object} module:ui/dropdown/dropdownview~DropdownView.defaultPanelPositions
 */
DropdownView.defaultPanelPositions = {
	south: ( buttonRect, panelRect ) => {
		return {
			top: buttonRect.bottom,
			left: buttonRect.left - ( panelRect.width - buttonRect.width ) / 2,
			name: 's'
		};
	},
	southEast: buttonRect => {
		return {
			top: buttonRect.bottom,
			left: buttonRect.left,
			name: 'se'
		};
	},
	southWest: ( buttonRect, panelRect ) => {
		return {
			top: buttonRect.bottom,
			left: buttonRect.left - panelRect.width + buttonRect.width,
			name: 'sw'
		};
	},
	southMiddleEast: ( buttonRect, panelRect ) => {
		return {
			top: buttonRect.bottom,
			left: buttonRect.left - ( panelRect.width - buttonRect.width ) / 4,
			name: 'sme'
		};
	},
	southMiddleWest: ( buttonRect, panelRect ) => {
		return {
			top: buttonRect.bottom,
			left: buttonRect.left - ( panelRect.width - buttonRect.width ) * 3 / 4,
			name: 'smw'
		};
	},
	north: ( buttonRect, panelRect ) => {
		return {
			top: buttonRect.top - panelRect.height,
			left: buttonRect.left - ( panelRect.width - buttonRect.width ) / 2,
			name: 'n'
		};
	},
	northEast: ( buttonRect, panelRect ) => {
		return {
			top: buttonRect.top - panelRect.height,
			left: buttonRect.left,
			name: 'ne'
		};
	},
	northWest: ( buttonRect, panelRect ) => {
		return {
			top: buttonRect.top - panelRect.height,
			left: buttonRect.left - panelRect.width + buttonRect.width,
			name: 'nw'
		};
	},
	northMiddleEast: ( buttonRect, panelRect ) => {
		return {
			top: buttonRect.top - panelRect.height,
			left: buttonRect.left - ( panelRect.width - buttonRect.width ) / 4,
			name: 'nme'
		};
	},
	northMiddleWest: ( buttonRect, panelRect ) => {
		return {
			top: buttonRect.top - panelRect.height,
			left: buttonRect.left - ( panelRect.width - buttonRect.width ) * 3 / 4,
			name: 'nmw'
		};
	}
};

/**
 * A function used to calculate the optimal position for the dropdown panel.
 *
 * @protected
 * @member {Function} module:ui/dropdown/dropdownview~DropdownView._getOptimalPosition
 */
DropdownView._getOptimalPosition = getOptimalPosition;/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A utility class that helps cycling over focusable {@link module:ui/view~View views} in a
 * {@link module:ui/viewcollection~ViewCollection} when the focus is tracked by the
 * {@link module:utils/focustracker~FocusTracker} instance. It helps implementing keyboard
 * navigation in HTML forms, toolbars, lists and the like.
 *
 * To work properly it requires:
 * * a collection of focusable (HTML `tabindex` attribute) views that implement the `focus()` method,
 * * an associated focus tracker to determine which view is focused.
 *
 * A simple cycler setup can look like this:
 *
 *		const focusables = new ViewCollection();
 *		const focusTracker = new FocusTracker();
 *
 *		// Add focusable views to the focus tracker.
 *		focusTracker.add( ... );
 *
 * Then, the cycler can be used manually:
 *
 *		const cycler = new FocusCycler( { focusables, focusTracker } );
 *
 *		// Will focus the first focusable view in #focusables.
 *		cycler.focusFirst();
 *
 *		// Will log the next focusable item in #focusables.
 *		console.log( cycler.next );
 *
 * Alternatively, it can work side by side with the {@link module:utils/keystrokehandler~KeystrokeHandler}:
 *
 *		const keystrokeHandler = new KeystrokeHandler();
 *
 *		// Activate the keystroke handler.
 *		keystrokeHandler.listenTo( sourceOfEvents );
 *
 *		const cycler = new FocusCycler( {
 *			focusables, focusTracker, keystrokeHandler,
 *			actions: {
 *				// When arrowup of arrowleft is detected by the #keystrokeHandler,
 *				// focusPrevious() will be called on the cycler.
 *				focusPrevious: [ 'arrowup', 'arrowleft' ],
 *			}
 *		} );
 *
 * Check out the {@glink framework/guides/deep-dive/ui/focus-tracking "Deep dive into focus tracking" guide} to learn more.
 */
class FocusCycler {
	/**
	 * Creates an instance of the focus cycler utility.
	 *
	 * @param {Object} options Configuration options.
	 * @param {module:utils/collection~Collection|Object} options.focusables
	 * @param {module:utils/focustracker~FocusTracker} options.focusTracker
	 * @param {module:utils/keystrokehandler~KeystrokeHandler} [options.keystrokeHandler]
	 * @param {Object} [options.actions]
	 */
	constructor( options ) {
		Object.assign( this, options );

		/**
		 * A {@link module:ui/view~View view} collection that the cycler operates on.
		 *
		 * @readonly
		 * @member {module:utils/collection~Collection} #focusables
		 */

		/**
		 * A focus tracker instance that the cycler uses to determine the current focus
		 * state in {@link #focusables}.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker} #focusTracker
		 */

		/**
		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}
		 * which can respond to certain keystrokes and cycle the focus.
		 *
		 * @readonly
		 * @member {module:utils/keystrokehandler~KeystrokeHandler} #keystrokeHandler
		 */

		/**
		 * Actions that the cycler can take when a keystroke is pressed. Requires
		 * `options.keystrokeHandler` to be passed and working. When an action is
		 * performed, `preventDefault` and `stopPropagation` will be called on the event
		 * the keystroke fired in the DOM.
		 *
		 *		actions: {
		 *			// Will call #focusPrevious() when arrowleft or arrowup is pressed.
		 *			focusPrevious: [ 'arrowleft', 'arrowup' ],
		 *
		 *			// Will call #focusNext() when arrowdown is pressed.
		 *			focusNext: 'arrowdown'
		 *		}
		 *
		 * @readonly
		 * @member {Object} #actions
		 */

		if ( options.actions && options.keystrokeHandler ) {
			for ( const methodName in options.actions ) {
				let actions = options.actions[ methodName ];

				if ( typeof actions == 'string' ) {
					actions = [ actions ];
				}

				for ( const keystroke of actions ) {
					options.keystrokeHandler.set( keystroke, ( data, cancel ) => {
						this[ methodName ]();
						cancel();
					} );
				}
			}
		}
	}

	/**
	 * Returns the first focusable view in {@link #focusables}.
	 * Returns `null` if there is none.
	 *
	 * **Note**: Hidden views (e.g. with `display: none`) are ignored.
	 *
	 * @readonly
	 * @member {module:ui/view~View|null} #first
	 */
	get first() {
		return this.focusables.find( isFocusable ) || null;
	}

	/**
	 * Returns the last focusable view in {@link #focusables}.
	 * Returns `null` if there is none.
	 *
	 * **Note**: Hidden views (e.g. with `display: none`) are ignored.
	 *
	 * @readonly
	 * @member {module:ui/view~View|null} #last
	 */
	get last() {
		return this.focusables.filter( isFocusable ).slice( -1 )[ 0 ] || null;
	}

	/**
	 * Returns the next focusable view in {@link #focusables} based on {@link #current}.
	 * Returns `null` if there is none.
	 *
	 * **Note**: Hidden views (e.g. with `display: none`) are ignored.
	 *
	 * @readonly
	 * @member {module:ui/view~View|null} #next
	 */
	get next() {
		return this._getFocusableItem( 1 );
	}

	/**
	 * Returns the previous focusable view in {@link #focusables} based on {@link #current}.
	 * Returns `null` if there is none.
	 *
	 * **Note**: Hidden views (e.g. with `display: none`) are ignored.
	 *
	 * @readonly
	 * @member {module:ui/view~View|null} #previous
	 */
	get previous() {
		return this._getFocusableItem( -1 );
	}

	/**
	 * An index of the view in the {@link #focusables} which is focused according
	 * to {@link #focusTracker}. Returns `null` when there is no such view.
	 *
	 * @readonly
	 * @member {Number|null} #current
	 */
	get current() {
		let index = null;

		// There's no focused view in the focusables.
		if ( this.focusTracker.focusedElement === null ) {
			return null;
		}

		this.focusables.find( ( view, viewIndex ) => {
			const focused = view.element === this.focusTracker.focusedElement;

			if ( focused ) {
				index = viewIndex;
			}

			return focused;
		} );

		return index;
	}

	/**
	 * Focuses the {@link #first} item in {@link #focusables}.
	 *
	 * **Note**: Hidden views (e.g. with `display: none`) are ignored.
	 */
	focusFirst() {
		this._focus( this.first );
	}

	/**
	 * Focuses the {@link #last} item in {@link #focusables}.
	 *
	 * **Note**: Hidden views (e.g. with `display: none`) are ignored.
	 */
	focusLast() {
		this._focus( this.last );
	}

	/**
	 * Focuses the {@link #next} item in {@link #focusables}.
	 *
	 * **Note**: Hidden views (e.g. with `display: none`) are ignored.
	 */
	focusNext() {
		this._focus( this.next );
	}

	/**
	 * Focuses the {@link #previous} item in {@link #focusables}.
	 *
	 * **Note**: Hidden views (e.g. with `display: none`) are ignored.
	 */
	focusPrevious() {
		this._focus( this.previous );
	}

	/**
	 * Focuses the given view if it exists.
	 *
	 * @protected
	 * @param {module:ui/view~View} view
	 */
	_focus( view ) {
		if ( view ) {
			view.focus();
		}
	}

	/**
	 * Returns the next or previous focusable view in {@link #focusables} with respect
	 * to {@link #current}.
	 *
	 * @protected
	 * @param {Number} step Either `1` for checking forward from {@link #current} or
	 * `-1` for checking backwards.
	 * @returns {module:ui/view~View|null}
	 */
	_getFocusableItem( step ) {
		// Cache for speed.
		const current = this.current;
		const collectionLength = this.focusables.length;

		if ( !collectionLength ) {
			return null;
		}

		// Start from the beginning if no view is focused.
		// https://github.com/ckeditor/ckeditor5-ui/issues/206
		if ( current === null ) {
			return this[ step === 1 ? 'first' : 'last' ];
		}

		// Cycle in both directions.
		let index = ( current + collectionLength + step ) % collectionLength;

		do {
			const view = this.focusables.get( index );

			if ( isFocusable( view ) ) {
				return view;
			}

			// Cycle in both directions.
			index = ( index + collectionLength + step ) % collectionLength;
		} while ( index !== current );

		return null;
	}
}

// Checks whether a view is focusable.
//
// @private
// @param {module:ui/view~View} view A view to be checked.
// @returns {Boolean}
function isFocusable( view ) {
	return !!( view.focus && isVisible( view.element ) );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The toolbar separator view class.
 *
 * @extends module:ui/view~View
 */
class ToolbarSeparatorView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		this.setTemplate( {
			tag: 'span',
			attributes: {
				class: [
					'ck',
					'ck-toolbar__separator'
				]
			}
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The toolbar line break view class.
 *
 * @extends module:ui/view~View
 */
class ToolbarLineBreakView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		this.setTemplate( {
			tag: 'span',
			attributes: {
				class: [
					'ck',
					'ck-toolbar__line-break'
				]
			}
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module ui/bindings/preventdefault
 */

/**
 * A helper which executes a native `Event.preventDefault()` if the target of an event equals the
 * {@link module:ui/view~View#element element of the view}. It shortens the definition of a
 * {@link module:ui/view~View#template template}.
 *
 *		// In a class extending View.
 *		import preventDefault from '@ckeditor/ckeditor5-ui/src/bindings/preventdefault';
 *
 *		// ...
 *
 *		this.setTemplate( {
 *			tag: 'div',
 *
 *			on: {
 *				// Prevent the default mousedown action on this view.
 *				mousedown: preventDefault( this )
 *			}
 *		} );
 *
 * @param {module:ui/view~View} view View instance that defines the template.
 * @returns {module:ui/template~TemplateToBinding}
 */
function preventDefault( view ) {
	return view.bindTemplate.to( evt => {
		if ( evt.target === view.element ) {
			evt.preventDefault();
		}
	} );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module ui/toolbar/normalizetoolbarconfig
 */

/**
 * Normalizes the toolbar configuration (`config.toolbar`), which:
 *
 * * may be defined as an `Array`:
 *
 * 		toolbar: [ 'heading', 'bold', 'italic', 'link', ... ]
 *
 * * or an `Object`:
 *
 *		toolbar: {
 *			items: [ 'heading', 'bold', 'italic', 'link', ... ],
 *			removeItems: [ 'bold' ],
 *			...
 *		}
 *
 * * or may not be defined at all (`undefined`)
 *
 * and returns it in the object form.
 *
 * @param {Array|Object|undefined} config The value of `config.toolbar`.
 * @returns {Object} A normalized toolbar config object.
 */
function normalizeToolbarConfig( config ) {
	if ( Array.isArray( config ) ) {
		return {
			items: config,
			removeItems: []
		};
	}

	if ( !config ) {
		return {
			items: [],
			removeItems: []
		};
	}

	return Object.assign( {
		items: [],
		removeItems: []
	}, config );
}var css_248z$M = ".ck.ck-toolbar{align-items:center;display:flex;flex-flow:row nowrap;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.ck.ck-toolbar>.ck-toolbar__items{align-items:center;display:flex;flex-flow:row wrap;flex-grow:1}.ck.ck-toolbar .ck.ck-toolbar__separator{display:inline-block}.ck.ck-toolbar .ck.ck-toolbar__separator:first-child,.ck.ck-toolbar .ck.ck-toolbar__separator:last-child{display:none}.ck.ck-toolbar .ck-toolbar__line-break{flex-basis:100%}.ck.ck-toolbar.ck-toolbar_grouping>.ck-toolbar__items{flex-wrap:nowrap}.ck.ck-toolbar.ck-toolbar_vertical>.ck-toolbar__items{flex-direction:column}.ck.ck-toolbar.ck-toolbar_floating>.ck-toolbar__items{flex-wrap:nowrap}.ck.ck-toolbar>.ck.ck-toolbar__grouped-dropdown>.ck-dropdown__button .ck-dropdown__arrow{display:none}.ck.ck-toolbar{border-radius:0}.ck-rounded-corners .ck.ck-toolbar,.ck.ck-toolbar.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-toolbar{background:var(--ck-color-toolbar-background);border:1px solid var(--ck-color-toolbar-border);padding:0 var(--ck-spacing-small)}.ck.ck-toolbar .ck.ck-toolbar__separator{align-self:stretch;background:var(--ck-color-toolbar-border);margin-bottom:var(--ck-spacing-small);margin-top:var(--ck-spacing-small);min-width:1px;width:1px}.ck.ck-toolbar .ck-toolbar__line-break{height:0}.ck.ck-toolbar>.ck-toolbar__items>:not(.ck-toolbar__line-break){margin-right:var(--ck-spacing-small)}.ck.ck-toolbar>.ck-toolbar__items:empty+.ck.ck-toolbar__separator{display:none}.ck.ck-toolbar>.ck-toolbar__items>:not(.ck-toolbar__line-break),.ck.ck-toolbar>.ck.ck-toolbar__grouped-dropdown{margin-bottom:var(--ck-spacing-small);margin-top:var(--ck-spacing-small)}.ck.ck-toolbar.ck-toolbar_vertical{padding:0}.ck.ck-toolbar.ck-toolbar_vertical>.ck-toolbar__items>.ck{border-radius:0;margin:0;width:100%}.ck.ck-toolbar.ck-toolbar_compact{padding:0}.ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>*{margin:0}.ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>:not(:first-child):not(:last-child){border-radius:0}.ck.ck-toolbar>.ck.ck-toolbar__grouped-dropdown>.ck.ck-button.ck-dropdown__button{padding-left:var(--ck-spacing-tiny)}.ck-toolbar-container .ck.ck-toolbar{border:0}.ck.ck-toolbar[dir=rtl]>.ck-toolbar__items>.ck,[dir=rtl] .ck.ck-toolbar>.ck-toolbar__items>.ck{margin-right:0}.ck.ck-toolbar[dir=rtl]:not(.ck-toolbar_compact)>.ck-toolbar__items>.ck,[dir=rtl] .ck.ck-toolbar:not(.ck-toolbar_compact)>.ck-toolbar__items>.ck{margin-left:var(--ck-spacing-small)}.ck.ck-toolbar[dir=rtl]>.ck-toolbar__items>.ck:last-child,[dir=rtl] .ck.ck-toolbar>.ck-toolbar__items>.ck:last-child{margin-left:0}.ck.ck-toolbar.ck-toolbar_compact[dir=rtl]>.ck-toolbar__items>.ck:first-child,[dir=rtl] .ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>.ck:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.ck.ck-toolbar.ck-toolbar_compact[dir=rtl]>.ck-toolbar__items>.ck:last-child,[dir=rtl] .ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>.ck:last-child{border-bottom-right-radius:0;border-top-right-radius:0}.ck.ck-toolbar.ck-toolbar_grouping[dir=rtl]>.ck-toolbar__items:not(:empty):not(:only-child),.ck.ck-toolbar[dir=rtl]>.ck.ck-toolbar__separator,[dir=rtl] .ck.ck-toolbar.ck-toolbar_grouping>.ck-toolbar__items:not(:empty):not(:only-child),[dir=rtl] .ck.ck-toolbar>.ck.ck-toolbar__separator{margin-left:var(--ck-spacing-small)}.ck.ck-toolbar[dir=ltr]>.ck-toolbar__items>.ck:last-child,[dir=ltr] .ck.ck-toolbar>.ck-toolbar__items>.ck:last-child{margin-right:0}.ck.ck-toolbar.ck-toolbar_compact[dir=ltr]>.ck-toolbar__items>.ck:first-child,[dir=ltr] .ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>.ck:first-child{border-bottom-right-radius:0;border-top-right-radius:0}.ck.ck-toolbar.ck-toolbar_compact[dir=ltr]>.ck-toolbar__items>.ck:last-child,[dir=ltr] .ck.ck-toolbar.ck-toolbar_compact>.ck-toolbar__items>.ck:last-child{border-bottom-left-radius:0;border-top-left-radius:0}.ck.ck-toolbar.ck-toolbar_grouping[dir=ltr]>.ck-toolbar__items:not(:empty):not(:only-child),.ck.ck-toolbar[dir=ltr]>.ck.ck-toolbar__separator,[dir=ltr] .ck.ck-toolbar.ck-toolbar_grouping>.ck-toolbar__items:not(:empty):not(:only-child),[dir=ltr] .ck.ck-toolbar>.ck.ck-toolbar__separator{margin-right:var(--ck-spacing-small)}";
styleInject(css_248z$M);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The toolbar view class.
 *
 * @extends module:ui/view~View
 * @implements module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable
 */
class ToolbarView extends View {
	/**
	 * Creates an instance of the {@link module:ui/toolbar/toolbarview~ToolbarView} class.
	 *
	 * Also see {@link #render}.
	 *
	 * @param {module:utils/locale~Locale} locale The localization services instance.
	 * @param {module:ui/toolbar/toolbarview~ToolbarOptions} [options] Configuration options of the toolbar.
	 */
	constructor( locale, options ) {
		super( locale );

		const bind = this.bindTemplate;
		const t = this.t;

		/**
		 * A reference to the options object passed to the constructor.
		 *
		 * @readonly
		 * @member {module:ui/toolbar/toolbarview~ToolbarOptions}
		 */
		this.options = options || {};

		/**
		 * Label used by assistive technologies to describe this toolbar element.
		 *
		 * @default 'Editor toolbar'
		 * @member {String} #ariaLabel
		 */
		this.set( 'ariaLabel', t( 'Editor toolbar' ) );

		/**
		 * The maximum width of the toolbar element.
		 *
		 * **Note**: When set to a specific value (e.g. `'200px'`), the value will affect the behavior of the
		 * {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull}
		 * option by changing the number of {@link #items} that will be displayed in the toolbar at a time.
		 *
		 * @observable
		 * @default 'auto'
		 * @member {String} #maxWidth
		 */
		this.set( 'maxWidth', 'auto' );

		/**
		 * A collection of toolbar items (buttons, dropdowns, etc.).
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.items = this.createCollection();

		/**
		 * Tracks information about the DOM focus in the toolbar.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}
		 * to handle keyboard navigation in the toolbar.
		 *
		 * @readonly
		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
		 */
		this.keystrokes = new KeystrokeHandler();

		/**
		 * An additional CSS class added to the {@link #element}.
		 *
		 * @observable
		 * @member {String} #class
		 */
		this.set( 'class' );

		/**
		 * When set true, makes the toolbar look compact with {@link #element}.
		 *
		 * @observable
		 * @default false
		 * @member {String} #isCompact
		 */
		this.set( 'isCompact', false );

		/**
		 * A (child) view containing {@link #items toolbar items}.
		 *
		 * @readonly
		 * @member {module:ui/toolbar/toolbarview~ItemsView}
		 */
		this.itemsView = new ItemsView( locale );

		/**
		 * A top–level collection aggregating building blocks of the toolbar.
		 *
		 *	┌───────────────── ToolbarView ─────────────────┐
		 *	| ┌──────────────── #children ────────────────┐ |
		 *	| |   ┌──────────── #itemsView ───────────┐   | |
		 *	| |   | [ item1 ] [ item2 ] ... [ itemN ] |   | |
		 *	| |   └──────────────────────────────────-┘   | |
		 *	| └───────────────────────────────────────────┘ |
		 *	└───────────────────────────────────────────────┘
		 *
		 * By default, it contains the {@link #itemsView} but it can be extended with additional
		 * UI elements when necessary.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.children = this.createCollection();
		this.children.add( this.itemsView );

		/**
		 * A collection of {@link #items} that take part in the focus cycling
		 * (i.e. navigation using the keyboard). Usually, it contains a subset of {@link #items} with
		 * some optional UI elements that also belong to the toolbar and should be focusable
		 * by the user.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.focusables = this.createCollection();

		/**
		 * Controls the orientation of toolbar items. Only available when
		 * {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull dynamic items grouping}
		 * is **disabled**.
		 *
		 * @observable
		 * @member {Boolean} #isVertical
		 */

		/**
		 * Helps cycling over {@link #focusables focusable items} in the toolbar.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/focuscycler~FocusCycler}
		 */

		const isRtl = locale.uiLanguageDirection === 'rtl';

		this._focusCycler = new FocusCycler( {
			focusables: this.focusables,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				// Navigate toolbar items backwards using the arrow[left,up] keys.
				focusPrevious: [ isRtl ? 'arrowright' : 'arrowleft', 'arrowup' ],

				// Navigate toolbar items forwards using the arrow[right,down] keys.
				focusNext: [ isRtl ? 'arrowleft' : 'arrowright', 'arrowdown' ]
			}
		} );

		const classes = [
			'ck',
			'ck-toolbar',
			bind.to( 'class' ),
			bind.if( 'isCompact', 'ck-toolbar_compact' )
		];

		if ( this.options.shouldGroupWhenFull && this.options.isFloating ) {
			classes.push( 'ck-toolbar_floating' );
		}

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: classes,
				role: 'toolbar',
				'aria-label': bind.to( 'ariaLabel' ),
				style: {
					maxWidth: bind.to( 'maxWidth' )
				}
			},

			children: this.children,

			on: {
				// https://github.com/ckeditor/ckeditor5-ui/issues/206
				mousedown: preventDefault( this )
			}
		} );

		/**
		 * An instance of the active toolbar behavior that shapes its look and functionality.
		 *
		 * See {@link module:ui/toolbar/toolbarview~ToolbarBehavior} to learn more.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/toolbar/toolbarview~ToolbarBehavior}
		 */
		this._behavior = this.options.shouldGroupWhenFull ? new DynamicGrouping( this ) : new StaticLayout( this );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		// Children added before rendering should be known to the #focusTracker.
		for ( const item of this.items ) {
			this.focusTracker.add( item.element );
		}

		this.items.on( 'add', ( evt, item ) => {
			this.focusTracker.add( item.element );
		} );

		this.items.on( 'remove', ( evt, item ) => {
			this.focusTracker.remove( item.element );
		} );

		// Start listening for the keystrokes coming from #element.
		this.keystrokes.listenTo( this.element );

		this._behavior.render( this );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		this._behavior.destroy();
		this.focusTracker.destroy();
		this.keystrokes.destroy();

		return super.destroy();
	}

	/**
	 * Focuses the first focusable in {@link #focusables}.
	 */
	focus() {
		this._focusCycler.focusFirst();
	}

	/**
	 * Focuses the last focusable in {@link #focusables}.
	 */
	focusLast() {
		this._focusCycler.focusLast();
	}

	/**
	 * A utility that expands the plain toolbar configuration into
	 * {@link module:ui/toolbar/toolbarview~ToolbarView#items} using a given component factory.
	 *
	 * @param {Array.<String>|Object} itemsOrConfig The toolbar items or the entire toolbar configuration object.
	 * @param {module:ui/componentfactory~ComponentFactory} factory A factory producing toolbar items.
	 */
	fillFromConfig( itemsOrConfig, factory ) {
		const config = normalizeToolbarConfig( itemsOrConfig );

		const itemsToClean = config.items
			.filter( ( name, idx, items ) => {
				if ( name === '|' ) {
					return true;
				}

				// Items listed in `config.removeItems` should not be added to the toolbar.
				if ( config.removeItems.indexOf( name ) !== -1 ) {
					return false;
				}

				if ( name === '-' ) {
					// The toolbar line breaks must not be rendered when toolbar grouping is enabled.
					// (https://github.com/ckeditor/ckeditor5/issues/8582)
					if ( this.options.shouldGroupWhenFull ) {
						/**
						 * The toolbar multiline breaks (`-` items) only work when the automatic button grouping
						 * is disabled in the toolbar configuration.
						 * To do this, set the `shouldNotGroupWhenFull` option to `true` in the editor configuration:
						 *
						 *		const config = {
						 *			toolbar: {
						 *				items: [ ... ],
						 *				shouldNotGroupWhenFull: true
						 *			}
						 *		}
						 *
						 * Learn more about {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar configuration}.
						 *
						 * @error toolbarview-line-break-ignored-when-grouping-items
						 */
						logWarning( 'toolbarview-line-break-ignored-when-grouping-items', items );

						return false;
					}

					return true;
				}

				// For the items that cannot be instantiated we are sending warning message. We also filter them out.
				if ( !factory.has( name ) ) {
					/**
					 * There was a problem processing the configuration of the toolbar. The item with the given
					 * name does not exist so it was omitted when rendering the toolbar.
					 *
					 * This warning usually shows up when the {@link module:core/plugin~Plugin} which is supposed
					 * to provide a toolbar item has not been loaded or there is a typo in the configuration.
					 *
					 * Make sure the plugin responsible for this toolbar item is loaded and the toolbar configuration
					 * is correct, e.g. {@link module:basic-styles/bold~Bold} is loaded for the `'bold'` toolbar item.
					 *
					 * You can use the following snippet to retrieve all available toolbar items:
					 *
					 *		Array.from( editor.ui.componentFactory.names() );
					 *
					 * @error toolbarview-item-unavailable
					 * @param {String} name The name of the component.
					 */
					logWarning( 'toolbarview-item-unavailable', { name } );

					return false;
				}

				return true;
			} );

		const itemsToAdd = this._cleanSeparators( itemsToClean )
			// Instantiate toolbar items.
			.map( name => {
				if ( name === '|' ) {
					return new ToolbarSeparatorView();
				} else if ( name === '-' ) {
					return new ToolbarLineBreakView();
				}

				return factory.create( name );
			} );

		this.items.addMany( itemsToAdd );
	}

	/**
	 * Remove leading, trailing, and duplicated separators (`-` and `|`).
	 *
	 * @private
	 * @param {Array.<String>} items
	 */
	_cleanSeparators( items ) {
		const nonSeparatorPredicate = item => ( item !== '-' && item !== '|' );
		const count = items.length;

		// Find an index of the first item that is not a separator.
		const firstCommandItem = items.findIndex( nonSeparatorPredicate );

		// Search from the end of the list, then convert found index back to the original direction.
		const lastCommandItem = count - items
			.slice()
			.reverse()
			.findIndex( nonSeparatorPredicate );

		return items
			// Return items without the leading and trailing separators.
			.slice( firstCommandItem, lastCommandItem )
			// Remove duplicated separators.
			.filter( ( name, idx, items ) => {
				// Filter only separators.
				if ( nonSeparatorPredicate( name ) ) {
					return true;
				}
				const isDuplicated = idx > 0 && items[ idx - 1 ] === name;

				return !isDuplicated;
			} );
	}

	/**
	 * Fired when some toolbar {@link #items} were grouped or ungrouped as a result of some change
	 * in the toolbar geometry.
	 *
	 * **Note**: This event is always fired **once** regardless of the number of items that were be
	 * grouped or ungrouped at a time.
	 *
	 * **Note**: This event is fired only if the items grouping functionality was enabled in
	 * the first place (see {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull}).
	 *
	 * @event groupedItemsUpdate
	 */
}

/**
 * An inner block of the {@link module:ui/toolbar/toolbarview~ToolbarView} hosting its
 * {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
 *
 * @private
 * @extends module:ui/view~View
 */
class ItemsView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		/**
		 * A collection of items (buttons, dropdowns, etc.).
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.children = this.createCollection();

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-toolbar__items'
				]
			},
			children: this.children
		} );
	}
}

/**
 * A toolbar behavior that makes it static and unresponsive to the changes of the environment.
 * At the same time, it also makes it possible to display a toolbar with a vertical layout
 * using the {@link module:ui/toolbar/toolbarview~ToolbarView#isVertical} property.
 *
 * @private
 * @implements module:ui/toolbar/toolbarview~ToolbarBehavior
 */
class StaticLayout {
	/**
	 * Creates an instance of the {@link module:ui/toolbar/toolbarview~StaticLayout} toolbar
	 * behavior.
	 *
	 * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
	 * is added to.
	 */
	constructor( view ) {
		const bind = view.bindTemplate;

		// Static toolbar can be vertical when needed.
		view.set( 'isVertical', false );

		// 1:1 pass–through binding, all ToolbarView#items are visible.
		view.itemsView.children.bindTo( view.items ).using( item => item );

		// 1:1 pass–through binding, all ToolbarView#items are focusable.
		view.focusables.bindTo( view.items ).using( item => item );

		view.extendTemplate( {
			attributes: {
				class: [
					// When vertical, the toolbar has an additional CSS class.
					bind.if( 'isVertical', 'ck-toolbar_vertical' )
				]
			}
		} );
	}

	/**
	 * @inheritDoc
	 */
	render() {}

	/**
	 * @inheritDoc
	 */
	destroy() {}
}

/**
 * A toolbar behavior that makes the items respond to changes in the geometry.
 *
 * In a nutshell, it groups {@link module:ui/toolbar/toolbarview~ToolbarView#items}
 * that do not fit visually into a single row of the toolbar (due to limited space).
 * Items that do not fit are aggregated in a dropdown displayed at the end of the toolbar.
 *
 *	┌──────────────────────────────────────── ToolbarView ──────────────────────────────────────────┐
 *	| ┌─────────────────────────────────────── #children ─────────────────────────────────────────┐ |
 *	| |   ┌─────── #itemsView ────────┐ ┌──────────────────────┐ ┌── #groupedItemsDropdown ───┐   | |
 *	| |   |       #ungroupedItems     | | ToolbarSeparatorView | |        #groupedItems       |   | |
 *	| |   └──────────────────────────-┘ └──────────────────────┘ └────────────────────────────┘   | |
 *	| |                                  \---------- only when toolbar items overflow --------/    | |
 *	| └───────────────────────────────────────────────────────────────────────────────────────────┘ |
 *	└───────────────────────────────────────────────────────────────────────────────────────────────┘
 *
 * @private
 * @implements module:ui/toolbar/toolbarview~ToolbarBehavior
 */
class DynamicGrouping {
	/**
	 * Creates an instance of the {@link module:ui/toolbar/toolbarview~DynamicGrouping} toolbar
	 * behavior.
	 *
	 * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
	 * is added to.
	 */
	constructor( view ) {
		/**
		 * A toolbar view this behavior belongs to.
		 *
		 * @readonly
		 * @member {module:ui/toolbar~ToolbarView}
		 */
		this.view = view;

		/**
		 * A collection of toolbar children.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.viewChildren = view.children;

		/**
		 * A collection of focusable toolbar elements.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.viewFocusables = view.focusables;

		/**
		 * A view containing toolbar items.
		 *
		 * @readonly
		 * @member {module:ui/toolbar/toolbarview~ItemsView}
		 */
		this.viewItemsView = view.itemsView;

		/**
		 * Toolbar focus tracker.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this.viewFocusTracker = view.focusTracker;

		/**
		 * Toolbar locale.
		 *
		 * @readonly
		 * @member {module:utils/locale~Locale}
		 */
		this.viewLocale = view.locale;

		/**
		 * Toolbar element.
		 *
		 * @readonly
		 * @member {HTMLElement} #viewElement
		 */

		/**
		 * A subset of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
		 * Aggregates items that fit into a single row of the toolbar and were not {@link #groupedItems grouped}
		 * into a {@link #groupedItemsDropdown dropdown}. Items of this collection are displayed in the
		 * {@link module:ui/toolbar/toolbarview~ToolbarView#itemsView}.
		 *
		 * When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped, it
		 * matches the {@link module:ui/toolbar/toolbarview~ToolbarView#items} collection in size and order.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.ungroupedItems = view.createCollection();

		/**
		 * A subset of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
		 * A collection of the toolbar items that do not fit into a single row of the toolbar.
		 * Grouped items are displayed in a dedicated {@link #groupedItemsDropdown dropdown}.
		 *
		 * When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped,
		 * this collection is empty.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.groupedItems = view.createCollection();

		/**
		 * The dropdown that aggregates {@link #groupedItems grouped items} that do not fit into a single
		 * row of the toolbar. It is displayed on demand as the last of
		 * {@link module:ui/toolbar/toolbarview~ToolbarView#children toolbar children} and offers another
		 * (nested) toolbar which displays items that would normally overflow.
		 *
		 * @readonly
		 * @member {module:ui/dropdown/dropdownview~DropdownView}
		 */
		this.groupedItemsDropdown = this._createGroupedItemsDropdown();

		/**
		 * An instance of the resize observer that helps dynamically determine the geometry of the toolbar
		 * and manage items that do not fit into a single row.
		 *
		 * **Note:** Created in {@link #_enableGroupingOnResize}.
		 *
		 * @readonly
		 * @member {module:utils/dom/resizeobserver~ResizeObserver}
		 */
		this.resizeObserver = null;

		/**
		 * A cached value of the horizontal padding style used by {@link #_updateGrouping}
		 * to manage the {@link module:ui/toolbar/toolbarview~ToolbarView#items} that do not fit into
		 * a single toolbar line. This value can be reused between updates because it is unlikely that
		 * the padding will change and re–using `Window.getComputedStyle()` is expensive.
		 *
		 * @readonly
		 * @member {Number}
		 */
		this.cachedPadding = null;

		/**
		 * A flag indicating that an items grouping update has been queued (e.g. due to the toolbar being visible)
		 * and should be executed immediately the next time the toolbar shows up.
		 *
		 * @readonly
		 * @member {Boolean}
		 */
		this.shouldUpdateGroupingOnNextResize = false;

		// Only those items that were not grouped are visible to the user.
		view.itemsView.children.bindTo( this.ungroupedItems ).using( item => item );

		// Make sure all #items visible in the main space of the toolbar are "focuscycleable".
		this.ungroupedItems.on( 'add', this._updateFocusCycleableItems.bind( this ) );
		this.ungroupedItems.on( 'remove', this._updateFocusCycleableItems.bind( this ) );

		// Make sure the #groupedItemsDropdown is also included in cycling when it appears.
		view.children.on( 'add', this._updateFocusCycleableItems.bind( this ) );
		view.children.on( 'remove', this._updateFocusCycleableItems.bind( this ) );

		// ToolbarView#items is dynamic. When an item is added or removed, it should be automatically
		// represented in either grouped or ungrouped items at the right index.
		// In other words #items == concat( #ungroupedItems, #groupedItems )
		// (in length and order).
		view.items.on( 'change', ( evt, changeData ) => {
			const index = changeData.index;

			// Removing.
			for ( const removedItem of changeData.removed ) {
				if ( index >= this.ungroupedItems.length ) {
					this.groupedItems.remove( removedItem );
				} else {
					this.ungroupedItems.remove( removedItem );
				}
			}

			// Adding.
			for ( let currentIndex = index; currentIndex < index + changeData.added.length; currentIndex++ ) {
				const addedItem = changeData.added[ currentIndex - index ];

				if ( currentIndex > this.ungroupedItems.length ) {
					this.groupedItems.add( addedItem, currentIndex - this.ungroupedItems.length );
				} else {
					this.ungroupedItems.add( addedItem, currentIndex );
				}
			}

			// When new ungrouped items join in and land in #ungroupedItems, there's a chance it causes
			// the toolbar to overflow.
			// Consequently if removed from grouped or ungrouped items, there is a chance
			// some new space is available and we could do some ungrouping.
			this._updateGrouping();
		} );

		view.extendTemplate( {
			attributes: {
				class: [
					// To group items dynamically, the toolbar needs a dedicated CSS class.
					'ck-toolbar_grouping'
				]
			}
		} );
	}

	/**
	 * Enables dynamic items grouping based on the dimensions of the toolbar.
	 *
	 * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
	 * is added to.
	 */
	render( view ) {
		this.viewElement = view.element;

		this._enableGroupingOnResize();
		this._enableGroupingOnMaxWidthChange( view );
	}

	/**
	 * Cleans up the internals used by this behavior.
	 */
	destroy() {
		// The dropdown may not be in ToolbarView#children at the moment of toolbar destruction
		// so let's make sure it's actually destroyed along with the toolbar.
		this.groupedItemsDropdown.destroy();

		this.resizeObserver.destroy();
	}

	/**
	 * When called, it will check if any of the {@link #ungroupedItems} do not fit into a single row of the toolbar,
	 * and it will move them to the {@link #groupedItems} when it happens.
	 *
	 * At the same time, it will also check if there is enough space in the toolbar for the first of the
	 * {@link #groupedItems} to be returned back to {@link #ungroupedItems} and still fit into a single row
	 * without the toolbar wrapping.
	 *
	 * @protected
	 */
	_updateGrouping() {
		// Do no grouping–related geometry analysis when the toolbar is detached from visible DOM,
		// for instance before #render(), or after render but without a parent or a parent detached
		// from DOM. DOMRects won't work anyway and there will be tons of warning in the console and
		// nothing else. This happens, for instance, when the toolbar is detached from DOM and
		// some logic adds or removes its #items.
		if ( !this.viewElement.ownerDocument.body.contains( this.viewElement ) ) {
			return;
		}

		// Do not update grouping when the element is invisible. Such toolbar has DOMRect filled with zeros
		// and that would cause all items to be grouped. Instead, queue the grouping so it runs next time
		// the toolbar is visible (the next ResizeObserver callback execution). This is handy because
		// the grouping could be caused by increasing the #maxWidth when the toolbar was invisible and the next
		// time it shows up, some items could actually be ungrouped (https://github.com/ckeditor/ckeditor5/issues/6575).
		if ( !isVisible( this.viewElement ) ) {
			this.shouldUpdateGroupingOnNextResize = true;

			return;
		}

		// Remember how many items were initially grouped so at the it is possible to figure out if the number
		// of grouped items has changed. If the number has changed, geometry of the toolbar has also changed.
		const initialGroupedItemsCount = this.groupedItems.length;
		let wereItemsGrouped;

		// Group #items as long as some wrap to the next row. This will happen, for instance,
		// when the toolbar is getting narrow and there is not enough space to display all items in
		// a single row.
		while ( this._areItemsOverflowing ) {
			this._groupLastItem();

			wereItemsGrouped = true;
		}

		// If none were grouped now but there were some items already grouped before,
		// then, what the hell, maybe let's see if some of them can be ungrouped. This happens when,
		// for instance, the toolbar is stretching and there's more space in it than before.
		if ( !wereItemsGrouped && this.groupedItems.length ) {
			// Ungroup items as long as none are overflowing or there are none to ungroup left.
			while ( this.groupedItems.length && !this._areItemsOverflowing ) {
				this._ungroupFirstItem();
			}

			// If the ungrouping ended up with some item wrapping to the next row,
			// put it back to the group toolbar ("undo the last ungroup"). We don't know whether
			// an item will wrap or not until we ungroup it (that's a DOM/CSS thing) so this
			// clean–up is vital for the algorithm.
			if ( this._areItemsOverflowing ) {
				this._groupLastItem();
			}
		}

		if ( this.groupedItems.length !== initialGroupedItemsCount ) {
			this.view.fire( 'groupedItemsUpdate' );
		}
	}

	/**
	 * Returns `true` when {@link module:ui/toolbar/toolbarview~ToolbarView#element} children visually overflow,
	 * for instance if the toolbar is narrower than its members. Returns `false` otherwise.
	 *
	 * @private
	 * @type {Boolean}
	 */
	get _areItemsOverflowing() {
		// An empty toolbar cannot overflow.
		if ( !this.ungroupedItems.length ) {
			return false;
		}

		const element = this.viewElement;
		const uiLanguageDirection = this.viewLocale.uiLanguageDirection;
		const lastChildRect = new Rect( element.lastChild );
		const toolbarRect = new Rect( element );

		if ( !this.cachedPadding ) {
			const computedStyle = global$2.window.getComputedStyle( element );
			const paddingProperty = uiLanguageDirection === 'ltr' ? 'paddingRight' : 'paddingLeft';

			// parseInt() is essential because of quirky floating point numbers logic and DOM.
			// If the padding turned out too big because of that, the grouped items dropdown would
			// always look (from the Rect perspective) like it overflows (while it's not).
			this.cachedPadding = Number.parseInt( computedStyle[ paddingProperty ] );
		}

		if ( uiLanguageDirection === 'ltr' ) {
			return lastChildRect.right > toolbarRect.right - this.cachedPadding;
		} else {
			return lastChildRect.left < toolbarRect.left + this.cachedPadding;
		}
	}

	/**
	 * Enables the functionality that prevents {@link #ungroupedItems} from overflowing (wrapping to the next row)
	 * upon resize when there is little space available. Instead, the toolbar items are moved to the
	 * {@link #groupedItems} collection and displayed in a dropdown at the end of the row (which has its own nested toolbar).
	 *
	 * When called, the toolbar will automatically analyze the location of its {@link #ungroupedItems} and "group"
	 * them in the dropdown if necessary. It will also observe the browser window for size changes in
	 * the future and respond to them by grouping more items or reverting already grouped back, depending
	 * on the visual space available.
	 *
	 * @private
	 */
	_enableGroupingOnResize() {
		let previousWidth;

		// TODO: Consider debounce.
		this.resizeObserver = new ResizeObserver( this.viewElement, entry => {
			if ( !previousWidth || previousWidth !== entry.contentRect.width || this.shouldUpdateGroupingOnNextResize ) {
				this.shouldUpdateGroupingOnNextResize = false;

				this._updateGrouping();

				previousWidth = entry.contentRect.width;
			}
		} );

		this._updateGrouping();
	}

	/**
	 * Enables the grouping functionality, just like {@link #_enableGroupingOnResize} but the difference is that
	 * it listens to the changes of {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth} instead.
	 *
	 * @private
	 */
	_enableGroupingOnMaxWidthChange( view ) {
		view.on( 'change:maxWidth', () => {
			this._updateGrouping();
		} );
	}

	/**
	 * When called, it will remove the last item from {@link #ungroupedItems} and move it back
	 * to the {@link #groupedItems} collection.
	 *
	 * The opposite of {@link #_ungroupFirstItem}.
	 *
	 * @private
	 */
	_groupLastItem() {
		if ( !this.groupedItems.length ) {
			this.viewChildren.add( new ToolbarSeparatorView() );
			this.viewChildren.add( this.groupedItemsDropdown );
			this.viewFocusTracker.add( this.groupedItemsDropdown.element );
		}

		this.groupedItems.add( this.ungroupedItems.remove( this.ungroupedItems.last ), 0 );
	}

	/**
	 * Moves the very first item belonging to {@link #groupedItems} back
	 * to the {@link #ungroupedItems} collection.
	 *
	 * The opposite of {@link #_groupLastItem}.
	 *
	 * @private
	 */
	_ungroupFirstItem() {
		this.ungroupedItems.add( this.groupedItems.remove( this.groupedItems.first ) );

		if ( !this.groupedItems.length ) {
			this.viewChildren.remove( this.groupedItemsDropdown );
			this.viewChildren.remove( this.viewChildren.last );
			this.viewFocusTracker.remove( this.groupedItemsDropdown.element );
		}
	}

	/**
	 * Creates the {@link #groupedItemsDropdown} that hosts the members of the {@link #groupedItems}
	 * collection when there is not enough space in the toolbar to display all items in a single row.
	 *
	 * @private
	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
	 */
	_createGroupedItemsDropdown() {
		const locale = this.viewLocale;
		const t = locale.t;
		const dropdown = createDropdown( locale );

		dropdown.class = 'ck-toolbar__grouped-dropdown';

		// Make sure the dropdown never sticks out to the left/right. It should be under the main toolbar.
		// (https://github.com/ckeditor/ckeditor5/issues/5608)
		dropdown.panelPosition = locale.uiLanguageDirection === 'ltr' ? 'sw' : 'se';

		addToolbarToDropdown( dropdown, [] );

		dropdown.buttonView.set( {
			label: t( 'Show more items' ),
			tooltip: true,
			tooltipPosition: locale.uiLanguageDirection === 'rtl' ? 'se' : 'sw',
			icon: threeVerticalDots
		} );

		// 1:1 pass–through binding.
		dropdown.toolbarView.items.bindTo( this.groupedItems ).using( item => item );

		return dropdown;
	}

	/**
	 * Updates the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables focus–cycleable items}
	 * collection so it represents the up–to–date state of the UI from the perspective of the user.
	 *
	 * For instance, the {@link #groupedItemsDropdown} can show up and hide but when it is visible,
	 * it must be subject to focus cycling in the toolbar.
	 *
	 * See the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables collection} documentation
	 * to learn more about the purpose of this method.
	 *
	 * @private
	 */
	_updateFocusCycleableItems() {
		this.viewFocusables.clear();

		this.ungroupedItems.map( item => {
			this.viewFocusables.add( item );
		} );

		if ( this.groupedItems.length ) {
			this.viewFocusables.add( this.groupedItemsDropdown );
		}
	}
}

/**
 * Options passed to the {@link module:ui/toolbar/toolbarview~ToolbarView#constructor} of the toolbar.
 *
 * @interface module:ui/toolbar/toolbarview~ToolbarOptions
 */

/**
 * When set to `true`, the toolbar will automatically group {@link module:ui/toolbar/toolbarview~ToolbarView#items} that
 * would normally wrap to the next line when there is not enough space to display them in a single row, for
 * instance, if the parent container of the toolbar is narrow. For toolbars in absolutely positioned containers
 * without width restrictions also the {@link module:ui/toolbar/toolbarview~ToolbarOptions#isFloating} option is required to be `true`.
 *
 * See also: {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth}.
 *
 * @member {Boolean} module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull
 */

/**
 * This option should be enabled for toolbars in absolutely positioned containers without width restrictions
 * to enable automatic {@link module:ui/toolbar/toolbarview~ToolbarView#items} grouping.
 * When this option is set to `true`, the items will stop wrapping to the next line
 * and together with {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull},
 * this will allow grouping them when there is not enough space in a single row.
 *
 * @member {Boolean} module:ui/toolbar/toolbarview~ToolbarOptions#isFloating
 */

/**
 * A class interface defining the behavior of the {@link module:ui/toolbar/toolbarview~ToolbarView}.
 *
 * Toolbar behaviors extend its look and functionality and have an impact on the
 * {@link module:ui/toolbar/toolbarview~ToolbarView#element} template or
 * {@link module:ui/toolbar/toolbarview~ToolbarView#render rendering}. They can be enabled
 * conditionally, e.g. depending on the configuration of the toolbar.
 *
 * @private
 * @interface module:ui/toolbar/toolbarview~ToolbarBehavior
 */

/**
 * Creates a new toolbar behavior instance.
 *
 * The instance is created in the {@link module:ui/toolbar/toolbarview~ToolbarView#constructor} of the toolbar.
 * This is the right place to extend the {@link module:ui/toolbar/toolbarview~ToolbarView#template} of
 * the toolbar, define extra toolbar properties, etc.
 *
 * @method #constructor
 * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior is added to.
 */

/**
 * A method called after the toolbar has been {@link module:ui/toolbar/toolbarview~ToolbarView#render rendered}.
 * It can be used to, for example, customize the behavior of the toolbar when its {@link module:ui/toolbar/toolbarview~ToolbarView#element}
 * is available.
 *
 * @readonly
 * @member {Function} #render
 * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar being rendered.
 */

/**
 * A method called after the toolbar has been {@link module:ui/toolbar/toolbarview~ToolbarView#destroy destroyed}.
 * It allows cleaning up after the toolbar behavior, for instance, this is the right place to detach
 * event listeners, free up references, etc.
 *
 * @readonly
 * @member {Function} #destroy
 */var css_248z$L = ".ck.ck-list{display:flex;flex-direction:column;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.ck.ck-list .ck-list__item,.ck.ck-list .ck-list__separator{display:block}.ck.ck-list .ck-list__item>:focus{position:relative;z-index:var(--ck-z-default)}.ck.ck-list{border-radius:0}.ck-rounded-corners .ck.ck-list,.ck.ck-list.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-list{background:var(--ck-color-list-background);list-style-type:none}.ck.ck-list__item{cursor:default;min-width:12em}.ck.ck-list__item .ck-button{border-radius:0;min-height:unset;padding:calc(var(--ck-line-height-base)*.2*var(--ck-font-size-base)) calc(var(--ck-line-height-base)*.4*var(--ck-font-size-base));text-align:left;width:100%}.ck.ck-list__item .ck-button .ck-button__label{line-height:calc(var(--ck-line-height-base)*1.2*var(--ck-font-size-base))}.ck.ck-list__item .ck-button:active{box-shadow:none}.ck.ck-list__item .ck-button.ck-on{background:var(--ck-color-list-button-on-background);color:var(--ck-color-list-button-on-text)}.ck.ck-list__item .ck-button.ck-on:active{box-shadow:none}.ck.ck-list__item .ck-button.ck-on:hover:not(.ck-disabled){background:var(--ck-color-list-button-on-background-focus)}.ck.ck-list__item .ck-button.ck-on:focus:not(.ck-switchbutton):not(.ck-disabled){border-color:var(--ck-color-base-background)}.ck.ck-list__item .ck-button:hover:not(.ck-disabled){background:var(--ck-color-list-button-hover-background)}.ck.ck-list__item .ck-switchbutton.ck-on{background:var(--ck-color-list-background);color:inherit}.ck.ck-list__item .ck-switchbutton.ck-on:hover:not(.ck-disabled){background:var(--ck-color-list-button-hover-background);color:inherit}.ck.ck-list__separator{background:var(--ck-color-base-border);height:1px;width:100%}";
styleInject(css_248z$L);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The list view class.
 *
 * @extends module:ui/view~View
 * @implements module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable
 */
class ListView extends View {
	/**
	 * @inheritDoc
	 */
	constructor() {
		super();

		/**
		 * Collection of the child list views.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.items = this.createCollection();

		/**
		 * Tracks information about DOM focus in the list.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * Instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
		 *
		 * @readonly
		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
		 */
		this.keystrokes = new KeystrokeHandler();

		/**
		 * Helps cycling over focusable {@link #items} in the list.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/focuscycler~FocusCycler}
		 */
		this._focusCycler = new FocusCycler( {
			focusables: this.items,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				// Navigate list items backwards using the arrowup key.
				focusPrevious: 'arrowup',

				// Navigate toolbar items forwards using the arrowdown key.
				focusNext: 'arrowdown'
			}
		} );

		this.setTemplate( {
			tag: 'ul',

			attributes: {
				class: [
					'ck',
					'ck-reset',
					'ck-list'
				]
			},

			children: this.items
		} );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		// Items added before rendering should be known to the #focusTracker.
		for ( const item of this.items ) {
			this.focusTracker.add( item.element );
		}

		this.items.on( 'add', ( evt, item ) => {
			this.focusTracker.add( item.element );
		} );

		this.items.on( 'remove', ( evt, item ) => {
			this.focusTracker.remove( item.element );
		} );

		// Start listening for the keystrokes coming from #element.
		this.keystrokes.listenTo( this.element );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}

	/**
	 * Focuses the first focusable in {@link #items}.
	 */
	focus() {
		this._focusCycler.focusFirst();
	}

	/**
	 * Focuses the last focusable in {@link #items}.
	 */
	focusLast() {
		this._focusCycler.focusLast();
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The list item view class.
 *
 * @extends module:ui/view~View
 */
class ListItemView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * Controls whether the item view is visible. Visible by default, list items are hidden
		 * using a CSS class.
		 *
		 * @observable
		 * @default true
		 * @member {Boolean} #isVisible
		 */
		this.set( 'isVisible', true );

		/**
		 * Collection of the child views inside of the list item {@link #element}.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.children = this.createCollection();

		this.setTemplate( {
			tag: 'li',

			attributes: {
				class: [
					'ck',
					'ck-list__item',
					bind.if( 'isVisible', 'ck-hidden', value => !value )
				]
			},

			children: this.children
		} );
	}

	/**
	 * Focuses the list item.
	 */
	focus() {
		this.children.first.focus();
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The list separator view class.
 *
 * @extends module:ui/view~View
 */
class ListSeparatorView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		this.setTemplate( {
			tag: 'li',
			attributes: {
				class: [
					'ck',
					'ck-list__separator'
				]
			}
		} );
	}
}var css_248z$K = ":root{--ck-toolbar-dropdown-max-width:60vw}.ck.ck-toolbar-dropdown>.ck-dropdown__panel{max-width:var(--ck-toolbar-dropdown-max-width);width:max-content}.ck.ck-toolbar-dropdown>.ck-dropdown__panel .ck-button:focus{z-index:calc(var(--ck-z-default) + 1)}.ck.ck-toolbar-dropdown .ck-toolbar{border:0}";
styleInject(css_248z$K);var css_248z$J = ".ck.ck-dropdown .ck-dropdown__panel .ck-list{border-radius:0}.ck-rounded-corners .ck.ck-dropdown .ck-dropdown__panel .ck-list,.ck.ck-dropdown .ck-dropdown__panel .ck-list.ck-rounded-corners{border-radius:var(--ck-border-radius);border-top-left-radius:0}.ck.ck-dropdown .ck-dropdown__panel .ck-list .ck-list__item:first-child .ck-button{border-radius:0}.ck-rounded-corners .ck.ck-dropdown .ck-dropdown__panel .ck-list .ck-list__item:first-child .ck-button,.ck.ck-dropdown .ck-dropdown__panel .ck-list .ck-list__item:first-child .ck-button.ck-rounded-corners{border-radius:var(--ck-border-radius);border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:0}.ck.ck-dropdown .ck-dropdown__panel .ck-list .ck-list__item:last-child .ck-button{border-radius:0}.ck-rounded-corners .ck.ck-dropdown .ck-dropdown__panel .ck-list .ck-list__item:last-child .ck-button,.ck.ck-dropdown .ck-dropdown__panel .ck-list .ck-list__item:last-child .ck-button.ck-rounded-corners{border-radius:var(--ck-border-radius);border-top-left-radius:0;border-top-right-radius:0}";
styleInject(css_248z$J);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A helper for creating dropdowns. It creates an instance of a {@link module:ui/dropdown/dropdownview~DropdownView dropdown},
 * with a {@link module:ui/dropdown/button/dropdownbutton~DropdownButton button},
 * {@link module:ui/dropdown/dropdownpanelview~DropdownPanelView panel} and all standard dropdown's behaviors.
 *
 * # Creating dropdowns
 *
 * By default, the default {@link module:ui/dropdown/button/dropdownbuttonview~DropdownButtonView} class is used as
 * definition of the button:
 *
 *		const dropdown = createDropdown( model );
 *
 *		// Configure dropdown's button properties:
 *		dropdown.buttonView.set( {
 *			label: 'A dropdown',
 *			withText: true
 *		} );
 *
 *		dropdown.render();
 *
 *		// Will render a dropdown labeled "A dropdown" with an empty panel.
 *		document.body.appendChild( dropdown.element );
 *
 * You can also provide other button views (they need to implement the
 * {@link module:ui/dropdown/button/dropdownbutton~DropdownButton} interface). For instance, you can use
 * {@link module:ui/dropdown/button/splitbuttonview~SplitButtonView} to create a dropdown with a split button.
 *
 *		const dropdown = createDropdown( locale, SplitButtonView );
 *
 *		// Configure dropdown's button properties:
 *		dropdown.buttonView.set( {
 *			label: 'A dropdown',
 *			withText: true
 *		} );
 *
 *		dropdown.buttonView.on( 'execute', () => {
 *			// Add the behavior of the "action part" of the split button.
 *			// Split button consists of the "action part" and "arrow part".
 *			// The arrow opens the dropdown while the action part can have some other behavior.
 * 		} );
 *
 *		dropdown.render();
 *
 *		// Will render a dropdown labeled "A dropdown" with an empty panel.
 *		document.body.appendChild( dropdown.element );
 *
 * # Adding content to the dropdown's panel
 *
 * The content of the panel can be inserted directly into the `dropdown.panelView.element`:
 *
 *		dropdown.panelView.element.textContent = 'Content of the panel';
 *
 * However, most of the time you will want to add there either a {@link module:ui/list/listview~ListView list of options}
 * or a list of buttons (i.e. a {@link module:ui/toolbar/toolbarview~ToolbarView toolbar}).
 * To simplify the task, you can use, respectively, {@link module:ui/dropdown/utils~addListToDropdown} or
 * {@link module:ui/dropdown/utils~addToolbarToDropdown} utils.
 *
 * @param {module:utils/locale~Locale} locale The locale instance.
 * @param {Function} ButtonClass The dropdown button view class. Needs to implement the
 * {@link module:ui/dropdown/button/dropdownbutton~DropdownButton} interface.
 * @returns {module:ui/dropdown/dropdownview~DropdownView} The dropdown view instance.
 */
function createDropdown( locale, ButtonClass = DropdownButtonView ) {
	const buttonView = new ButtonClass( locale );

	const panelView = new DropdownPanelView( locale );
	const dropdownView = new DropdownView( locale, buttonView, panelView );

	buttonView.bind( 'isEnabled' ).to( dropdownView );

	if ( buttonView instanceof DropdownButtonView ) {
		buttonView.bind( 'isOn' ).to( dropdownView, 'isOpen' );
	} else {
		buttonView.arrowView.bind( 'isOn' ).to( dropdownView, 'isOpen' );
	}

	addDefaultBehavior( dropdownView );

	return dropdownView;
}

/**
 * Adds an instance of {@link module:ui/toolbar/toolbarview~ToolbarView} to a dropdown.
 *
 *		const buttons = [];
 *
 *		// Either create a new ButtonView instance or create existing.
 *		buttons.push( new ButtonView() );
 *		buttons.push( editor.ui.componentFactory.create( 'someButton' ) );
 *
 *		const dropdown = createDropdown( locale );
 *
 *		addToolbarToDropdown( dropdown, buttons );
 *
 *		dropdown.toolbarView.isVertical = true;
 *
 *		// Will render a vertical button dropdown labeled "A button dropdown"
 *		// with a button group in the panel containing two buttons.
 *		dropdown.render()
 *		document.body.appendChild( dropdown.element );
 *
 * **Note:** To improve the accessibility, you can tell the dropdown to focus the first active button of the toolbar when the dropdown
 * {@link module:ui/dropdown/dropdownview~DropdownView#isOpen gets open}. See the documentation of `options` to learn more.
 *
 * See {@link module:ui/dropdown/utils~createDropdown} and {@link module:ui/toolbar/toolbarview~ToolbarView}.
 *
 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView A dropdown instance to which `ToolbarView` will be added.
 * @param {Iterable.<module:ui/button/buttonview~ButtonView>} buttons
 * @param {Object} [options]
 * @param {Boolean} [options.enableActiveItemFocusOnDropdownOpen=false] When set `true`, the focus will automatically move to the first
 * active {@link module:ui/toolbar/toolbarview~ToolbarView#items item} of the toolbar upon
 * {@link module:ui/dropdown/dropdownview~DropdownView#isOpen opening} the dropdown. Active items are those with the `isOn` property set
 * `true` (for instance {@link module:ui/button/buttonview~ButtonView buttons}). If no active items is found, the toolbar will be focused
 * as a whole resulting in the focus moving to its first focusable item (default behavior of
 * {@link module:ui/dropdown/dropdownview~DropdownView}).
 */
function addToolbarToDropdown( dropdownView, buttons, options = {} ) {
	const locale = dropdownView.locale;
	const t = locale.t;
	const toolbarView = dropdownView.toolbarView = new ToolbarView( locale );

	toolbarView.set( 'ariaLabel', t( 'Dropdown toolbar' ) );

	dropdownView.extendTemplate( {
		attributes: {
			class: [ 'ck-toolbar-dropdown' ]
		}
	} );

	buttons.map( view => toolbarView.items.add( view ) );

	if ( options.enableActiveItemFocusOnDropdownOpen ) {
		// Accessibility: Focus the first active button in the toolbar when the dropdown gets open.
		focusChildOnDropdownOpen( dropdownView, () => toolbarView.items.find( item => item.isOn ) );
	}

	dropdownView.panelView.children.add( toolbarView );
	toolbarView.items.delegate( 'execute' ).to( dropdownView );
}

/**
 * Adds an instance of {@link module:ui/list/listview~ListView} to a dropdown.
 *
 *		const items = new Collection();
 *
 *		items.add( {
 *			type: 'button',
 *			model: new Model( {
 *				withText: true,
 *				label: 'First item',
 *				labelStyle: 'color: red'
 *			} )
 *		} );
 *
 *		items.add( {
 *			 type: 'button',
 *			 model: new Model( {
 *				withText: true,
 *				label: 'Second item',
 *				labelStyle: 'color: green',
 *				class: 'foo'
 *			} )
 *		} );
 *
 *		const dropdown = createDropdown( locale );
 *
 *		addListToDropdown( dropdown, items );
 *
 *		// Will render a dropdown with a list in the panel containing two items.
 *		dropdown.render()
 *		document.body.appendChild( dropdown.element );
 *
 * The `items` collection passed to this methods controls the presence and attributes of respective
 * {@link module:ui/list/listitemview~ListItemView list items}.
 *
 * **Note:** To improve the accessibility, when a list is added to the dropdown using this helper the dropdown will automatically attempt
 * to focus the first active item (a host to a {@link module:ui/button/buttonview~ButtonView} with
 * {@link module:ui/button/buttonview~ButtonView#isOn} set `true`) or the very first item when none are active.
 *
 * See {@link module:ui/dropdown/utils~createDropdown} and {@link module:list/list~List}.
 *
 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView A dropdown instance to which `ListVIew` will be added.
 * @param {Iterable.<module:ui/dropdown/utils~ListDropdownItemDefinition>} items
 * A collection of the list item definitions to populate the list.
 */
function addListToDropdown( dropdownView, items ) {
	const locale = dropdownView.locale;
	const listView = dropdownView.listView = new ListView( locale );

	listView.items.bindTo( items ).using( ( { type, model } ) => {
		if ( type === 'separator' ) {
			return new ListSeparatorView( locale );
		} else if ( type === 'button' || type === 'switchbutton' ) {
			const listItemView = new ListItemView( locale );
			let buttonView;

			if ( type === 'button' ) {
				buttonView = new ButtonView( locale );
			} else {
				buttonView = new SwitchButtonView( locale );
			}

			// Bind all model properties to the button view.
			buttonView.bind( ...Object.keys( model ) ).to( model );
			buttonView.delegate( 'execute' ).to( listItemView );

			listItemView.children.add( buttonView );

			return listItemView;
		}
	} );

	dropdownView.panelView.children.add( listView );

	listView.items.delegate( 'execute' ).to( dropdownView );

	// Accessibility: Focus the first active button in the list when the dropdown gets open.
	focusChildOnDropdownOpen( dropdownView, () => listView.items.find( item => {
		if ( item instanceof ListItemView ) {
			return item.children.first.isOn;
		}

		return false;
	} ) );
}

/**
 * A helper to be used on an existing {@link module:ui/dropdown/dropdownview~DropdownView} that focuses
 * a specific child in DOM when the dropdown {@link module:ui/dropdown/dropdownview~DropdownView#isOpen gets open}.
 *
 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView A dropdown instance to which the focus behavior will be added.
 * @param {Function} childSelectorCallback A callback executed when the dropdown gets open. It should return a {@link module:ui/view~View}
 * instance (child of {@link module:ui/dropdown/dropdownview~DropdownView#panelView}) that will get focused or a falsy value.
 * If falsy value is returned, a default behavior of the dropdown will engage focusing the first focusable child in
 * the {@link module:ui/dropdown/dropdownview~DropdownView#panelView}.
 */
function focusChildOnDropdownOpen( dropdownView, childSelectorCallback ) {
	dropdownView.on( 'change:isOpen', () => {
		if ( !dropdownView.isOpen ) {
			return;
		}

		const childToFocus = childSelectorCallback();

		if ( !childToFocus ) {
			return;
		}

		if ( typeof childToFocus.focus === 'function' ) {
			childToFocus.focus();
		} else {
			/**
			 * The child view of a {@link module:ui/dropdown/dropdownview~DropdownView dropdown} is missing the `focus()` method
			 * and could not be focused when the dropdown got {@link module:ui/dropdown/dropdownview~DropdownView#isOpen open}.
			 *
			 * Making the content of a dropdown focusable in this case greatly improves the accessibility. Please make the view instance
			 * implements the {@link module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable focusable interface} for the best user
			 * experience.
			 *
			 * @error ui-dropdown-focus-child-on-open-child-missing-focus
			 * @param {module:ui/view~View} view
			 */
			logWarning( 'ui-dropdown-focus-child-on-open-child-missing-focus', { view: childToFocus } );
		}
	}, { priority: 'low' } );
}

// Add a set of default behaviors to dropdown view.
//
// @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
function addDefaultBehavior( dropdownView ) {
	closeDropdownOnBlur( dropdownView );
	closeDropdownOnExecute( dropdownView );
	focusDropdownContentsOnArrows( dropdownView );
}

// Adds a behavior to a dropdownView that closes opened dropdown when user clicks outside the dropdown.
//
// @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
function closeDropdownOnBlur( dropdownView ) {
	dropdownView.on( 'render', () => {
		clickOutsideHandler( {
			emitter: dropdownView,
			activator: () => dropdownView.isOpen,
			callback: () => {
				dropdownView.isOpen = false;
			},
			contextElements: [ dropdownView.element ]
		} );
	} );
}

// Adds a behavior to a dropdownView that closes the dropdown view on "execute" event.
//
// @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
function closeDropdownOnExecute( dropdownView ) {
	// Close the dropdown when one of the list items has been executed.
	dropdownView.on( 'execute', evt => {
		// Toggling a switch button view should not close the dropdown.
		if ( evt.source instanceof SwitchButtonView ) {
			return;
		}

		dropdownView.isOpen = false;
	} );
}

// Adds a behavior to a dropdownView that focuses the dropdown's panel view contents on keystrokes.
//
// @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
function focusDropdownContentsOnArrows( dropdownView ) {
	// If the dropdown panel is already open, the arrow down key should focus the first child of the #panelView.
	dropdownView.keystrokes.set( 'arrowdown', ( data, cancel ) => {
		if ( dropdownView.isOpen ) {
			dropdownView.panelView.focus();
			cancel();
		}
	} );

	// If the dropdown panel is already open, the arrow up key should focus the last child of the #panelView.
	dropdownView.keystrokes.set( 'arrowup', ( data, cancel ) => {
		if ( dropdownView.isOpen ) {
			dropdownView.panelView.focusLast();
			cancel();
		}
	} );
}

/**
 * A definition of the list item used by the {@link module:ui/dropdown/utils~addListToDropdown}
 * utility.
 *
 * @typedef {Object} module:ui/dropdown/utils~ListDropdownItemDefinition
 *
 * @property {String} type Either `'separator'`, `'button'` or `'switchbutton'`.
 * @property {module:ui/model~Model} [model] Model of the item (when **not** `'separator'`).
 * Its properties fuel the newly created list item (or its children, depending on the `type`).
 */var css_248z$I = ":root{--ck-color-editable-blur-selection:#d9d9d9}.ck.ck-editor__editable:not(.ck-editor__nested-editable){border-radius:0}.ck-rounded-corners .ck.ck-editor__editable:not(.ck-editor__nested-editable),.ck.ck-editor__editable.ck-rounded-corners:not(.ck-editor__nested-editable){border-radius:var(--ck-border-radius)}.ck.ck-editor__editable.ck-focused:not(.ck-editor__nested-editable){border:var(--ck-focus-ring);box-shadow:var(--ck-inner-shadow),0 0;outline:none}.ck.ck-editor__editable_inline{border:1px solid transparent;overflow:auto;padding:0 var(--ck-spacing-standard)}.ck.ck-editor__editable_inline[dir=ltr]{text-align:left}.ck.ck-editor__editable_inline[dir=rtl]{text-align:right}.ck.ck-editor__editable_inline>:first-child{margin-top:var(--ck-spacing-large)}.ck.ck-editor__editable_inline>:last-child{margin-bottom:var(--ck-spacing-large)}.ck.ck-editor__editable_inline.ck-blurred ::selection{background:var(--ck-color-editable-blur-selection)}.ck.ck-balloon-panel.ck-toolbar-container[class*=arrow_n]:after{border-bottom-color:var(--ck-color-base-foreground)}.ck.ck-balloon-panel.ck-toolbar-container[class*=arrow_s]:after{border-top-color:var(--ck-color-base-foreground)}";
styleInject(css_248z$I);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The editor UI view class. Base class for the editor main views.
 *
 * @extends module:ui/view~View
 */
class EditorUIView extends View {
	/**
	 * Creates an instance of the editor UI view class.
	 *
	 * @param {module:utils/locale~Locale} [locale] The locale instance.
	 */
	constructor( locale ) {
		super( locale );

		/**
		 * Collection of the child views, detached from the DOM
		 * structure of the editor, like panels, icons etc.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection} #body
		 */
		this.body = new BodyCollection( locale );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		this.body.attachToDom();
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		this.body.detachFromDom();

		return super.destroy();
	}
}var css_248z$H = ".ck.ck-label{display:block}.ck.ck-voice-label{display:none}.ck.ck-label{font-weight:700}";
styleInject(css_248z$H);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The label view class.
 *
 * @extends module:ui/view~View
 */
class LabelView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		/**
		 * The text of the label.
		 *
		 * @observable
		 * @member {String} #text
		 */
		this.set( 'text' );

		/**
		 * The `for` attribute of the label (i.e. to pair with an `<input>` element).
		 *
		 * @observable
		 * @member {String} #for
		 */
		this.set( 'for' );

		/**
		 * An unique id of the label. It can be used by other UI components to reference
		 * the label, for instance, using the `aria-describedby` DOM attribute.
		 *
		 * @member {String} #id
		 */
		this.id = `ck-editor__label_${ uid() }`;

		const bind = this.bindTemplate;

		this.setTemplate( {
			tag: 'label',
			attributes: {
				class: [
					'ck',
					'ck-label'
				],
				id: this.id,
				for: bind.to( 'for' )
			},
			children: [
				{
					text: bind.to( 'text' )
				}
			]
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The boxed editor UI view class. This class represents an editor interface
 * consisting of a toolbar and an editable area, enclosed within a box.
 *
 * @extends module:ui/editorui/editoruiview~EditorUIView
 */
class BoxedEditorUIView extends EditorUIView {
	/**
	 * Creates an instance of the boxed editor UI view class.
	 *
	 * @param {module:utils/locale~Locale} locale The locale instance..
	 */
	constructor( locale ) {
		super( locale );

		/**
		 * Collection of the child views located in the top (`.ck-editor__top`)
		 * area of the UI.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.top = this.createCollection();

		/**
		 * Collection of the child views located in the main (`.ck-editor__main`)
		 * area of the UI.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.main = this.createCollection();

		/**
		 * Voice label of the UI.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/view~View} #_voiceLabelView
		 */
		this._voiceLabelView = this._createVoiceLabel();

		this.setTemplate( {
			tag: 'div',

			attributes: {
				class: [
					'ck',
					'ck-reset',
					'ck-editor',
					'ck-rounded-corners'
				],
				role: 'application',
				dir: locale.uiLanguageDirection,
				lang: locale.uiLanguage,
				'aria-labelledby': this._voiceLabelView.id
			},

			children: [
				this._voiceLabelView,
				{
					tag: 'div',
					attributes: {
						class: [
							'ck',
							'ck-editor__top',
							'ck-reset_all'
						],
						role: 'presentation'
					},
					children: this.top
				},
				{
					tag: 'div',
					attributes: {
						class: [
							'ck',
							'ck-editor__main'
						],
						role: 'presentation'
					},
					children: this.main
				}
			]
		} );
	}

	/**
	 * Creates a voice label view instance.
	 *
	 * @private
	 * @returns {module:ui/label/labelview~LabelView}
	 */
	_createVoiceLabel() {
		const t = this.t;
		const voiceLabel = new LabelView();

		voiceLabel.text = t( 'Rich Text Editor' );

		voiceLabel.extendTemplate( {
			attributes: {
				class: 'ck-voice-label'
			}
		} );

		return voiceLabel;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The editable UI view class.
 *
 * @extends module:ui/view~View
 */
class EditableUIView extends View {
	/**
	 * Creates an instance of EditableUIView class.
	 *
	 * @param {module:utils/locale~Locale} [locale] The locale instance.
	 * @param {module:engine/view/view~View} editingView The editing view instance the editable is related to.
	 * @param {HTMLElement} [editableElement] The editable element. If not specified, this view
	 * should create it. Otherwise, the existing element should be used.
	 */
	constructor( locale, editingView, editableElement ) {
		super( locale );

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-content',
					'ck-editor__editable',
					'ck-rounded-corners'
				],
				lang: locale.contentLanguage,
				dir: locale.contentLanguageDirection
			}
		} );

		/**
		 * The name of the editable UI view.
		 *
		 * @member {String} #name
		 */
		this.name = null;

		/**
		 * Controls whether the editable is focused, i.e. the user is typing in it.
		 *
		 * @observable
		 * @member {Boolean} #isFocused
		 */
		this.set( 'isFocused', false );

		/**
		 * The element which is the main editable element (usually the one with `contentEditable="true"`).
		 *
		 * @private
		 * @type {HTMLElement}
		 */
		this._editableElement = editableElement;

		/**
		 * Whether an external {@link #_editableElement} was passed into the constructor, which also means
		 * the view will not render its {@link #template}.
		 *
		 * @private
		 * @type {Boolean}
		 */
		this._hasExternalElement = !!this._editableElement;

		/**
		 * The editing view instance the editable is related to. Editable uses the editing
		 * view to dynamically modify its certain DOM attributes after {@link #render rendering}.
		 *
		 * **Note**: The DOM attributes are performed by the editing view and not UI
		 * {@link module:ui/view~View#bindTemplate template bindings} because once rendered,
		 * the editable DOM element must remain under the full control of the engine to work properly.
		 *
		 * @protected
		 * @type {module:engine/view/view~View}
		 */
		this._editingView = editingView;
	}

	/**
	 * Renders the view by either applying the {@link #template} to the existing
	 * {@link #_editableElement} or assigning {@link #element} as {@link #_editableElement}.
	 */
	render() {
		super.render();

		if ( this._hasExternalElement ) {
			this.template.apply( this.element = this._editableElement );
		} else {
			this._editableElement = this.element;
		}

		this.on( 'change:isFocused', () => this._updateIsFocusedClasses() );
		this._updateIsFocusedClasses();
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		if ( this._hasExternalElement ) {
			this.template.revert( this._editableElement );
		}

		super.destroy();
	}

	/**
	 * Updates the `ck-focused` and `ck-blurred` CSS classes on the {@link #element} according to
	 * the {@link #isFocused} property value using the {@link #_editingView editing view} API.
	 *
	 * @private
	 */
	_updateIsFocusedClasses() {
		const editingView = this._editingView;

		if ( editingView.isRenderingInProgress ) {
			updateAfterRender( this );
		} else {
			update( this );
		}

		function update( view ) {
			editingView.change( writer => {
				const viewRoot = editingView.document.getRoot( view.name );

				writer.addClass( view.isFocused ? 'ck-focused' : 'ck-blurred', viewRoot );
				writer.removeClass( view.isFocused ? 'ck-blurred' : 'ck-focused', viewRoot );
			} );
		}

		// In a case of a multi-root editor, a callback will be attached more than once (one callback for each root).
		// While executing one callback the `isRenderingInProgress` observable is changing what causes executing another
		// callback and render is called inside the already pending render.
		// We need to be sure that callback is executed only when the value has changed from `true` to `false`.
		// See https://github.com/ckeditor/ckeditor5/issues/1676.
		function updateAfterRender( view ) {
			editingView.once( 'change:isRenderingInProgress', ( evt, name, value ) => {
				if ( !value ) {
					update( view );
				} else {
					updateAfterRender( view );
				}
			} );
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The inline editable UI class implementing an inline {@link module:ui/editableui/editableuiview~EditableUIView}.
 *
 * @extends module:ui/editableui/editableuiview~EditableUIView
 */
class InlineEditableUIView extends EditableUIView {
	/**
	 * Creates an instance of the InlineEditableUIView class.
	 *
	 * @param {module:utils/locale~Locale} [locale] The locale instance.
	 * @param {module:engine/view/view~View} editingView The editing view instance the editable is related to.
	 * @param {HTMLElement} [editableElement] The editable element. If not specified, the
	 * {@link module:ui/editableui/editableuiview~EditableUIView}
	 * will create it. Otherwise, the existing element will be used.
	 * @param {Object} [options] Additional configuration of the view.
	 * @param {Function} [options.label] A function that gets called with the instance of this view as an argument
	 * and should return a string that represents the label of the editable for assistive technologies. If not provided,
	 * a default label generator is used.
	 */
	constructor( locale, editingView, editableElement, options = {} ) {
		super( locale, editingView, editableElement );

		const t = locale.t;

		this.extendTemplate( {
			attributes: {
				role: 'textbox',
				class: 'ck-editor__editable_inline'
			}
		} );

		/**
		 * A function that gets called with the instance of this view as an argument and should return a string that
		 * represents the label of the editable for assistive technologies.
		 *
		 * @private
		 * @readonly
		 * @param {Function}
		 */
		this._generateLabel = options.label || ( () => t( 'Editor editing area: %0', this.name ) );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		const editingView = this._editingView;

		editingView.change( writer => {
			const viewRoot = editingView.document.getRoot( this.name );

			writer.setAttribute( 'aria-label', this._generateLabel( this ), viewRoot );
		} );
	}
}var css_248z$G = ".ck.ck-form__header{align-items:center;display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-between}:root{--ck-form-header-height:38px}.ck.ck-form__header{border-bottom:1px solid var(--ck-color-base-border);height:var(--ck-form-header-height);line-height:var(--ck-form-header-height);padding:var(--ck-spacing-small) var(--ck-spacing-large)}.ck.ck-form__header .ck-form__header__label{font-weight:700}";
styleInject(css_248z$G);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The class component representing a form header view. It should be used in more advanced forms to
 * describe the main purpose of the form.
 *
 * By default the component contains a bolded label view that has to be set. The label is usually a short (at most 3-word) string.
 * The component can also be extended by any other elements, like: icons, dropdowns, etc.
 *
 * It is used i.a.
 * by {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView}
 * and {@link module:special-characters/ui/specialcharactersnavigationview~SpecialCharactersNavigationView}.
 *
 * The latter is an example, where the component has been extended by {@link module:ui/dropdown/dropdownview~DropdownView} view.
 *
 * @extends module:ui/view~View
 */
class FormHeaderView extends View {
	/**
	 * Creates an instance of the form header class.
	 *
	 * @param {module:utils/locale~Locale} locale The locale instance.
	 * @param {Object} options
	 * @param {String} options.label A label.
	 * @param {String} [options.class] An additional class.
	 */
	constructor( locale, options = {} ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * The label of the header.
		 *
		 * @observable
		 * @member {String} #label
		 */
		this.set( 'label', options.label || '' );

		/**
		 * An additional CSS class added to the {@link #element}.
		 *
		 * @observable
		 * @member {String} #class
		 */
		this.set( 'class', options.class || null );

		/**
		 * A collection of header items.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.children = this.createCollection();

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-form__header',
					bind.to( 'class' )
				]
			},
			children: this.children
		} );

		const label = new View( locale );

		label.setTemplate( {
			tag: 'span',
			attributes: {
				class: [
					'ck',
					'ck-form__header__label'
				]
			},
			children: [
				{ text: bind.to( 'label' ) }
			]
		} );

		this.children.add( label );
	}
}var css_248z$F = ":root{--ck-input-width:18em;--ck-input-text-width:var(--ck-input-width)}.ck.ck-input{border-radius:0}.ck-rounded-corners .ck.ck-input,.ck.ck-input.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-input{background:var(--ck-color-input-background);border:1px solid var(--ck-color-input-border);min-height:var(--ck-ui-component-min-height);min-width:var(--ck-input-width);padding:var(--ck-spacing-extra-tiny) var(--ck-spacing-medium);transition:box-shadow .1s ease-in-out,border .1s ease-in-out}.ck.ck-input:focus{border:var(--ck-focus-ring);box-shadow:var(--ck-focus-outer-shadow),0 0;outline:none}.ck.ck-input[readonly]{background:var(--ck-color-input-disabled-background);border:1px solid var(--ck-color-input-disabled-border);color:var(--ck-color-input-disabled-text)}.ck.ck-input[readonly]:focus{box-shadow:var(--ck-focus-disabled-outer-shadow),0 0}.ck.ck-input.ck-error{animation:ck-input-shake .3s ease both;border-color:var(--ck-color-input-error-border)}.ck.ck-input.ck-error:focus{box-shadow:var(--ck-focus-error-outer-shadow),0 0}@keyframes ck-input-shake{20%{transform:translateX(-2px)}40%{transform:translateX(2px)}60%{transform:translateX(-1px)}80%{transform:translateX(1px)}}";
styleInject(css_248z$F);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The base input view class.
 *
 * @extends module:ui/view~View
 */
class InputView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		/**
		 * The value of the input.
		 *
		 * @observable
		 * @member {String} #value
		 */
		this.set( 'value' );

		/**
		 * The `id` attribute of the input (i.e. to pair with a `<label>` element).
		 *
		 * @observable
		 * @member {String} #id
		 */
		this.set( 'id' );

		/**
		 * The `placeholder` attribute of the input.
		 *
		 * @observable
		 * @member {String} #placeholder
		 */
		this.set( 'placeholder' );

		/**
		 * Controls whether the input view is in read-only mode.
		 *
		 * @observable
		 * @member {Boolean} #isReadOnly
		 */
		this.set( 'isReadOnly', false );

		/**
		 * Set to `true` when the field has some error. Usually controlled via
		 * {@link module:ui/labeledinput/labeledinputview~LabeledInputView#errorText}.
		 *
		 * @observable
		 * @member {Boolean} #hasError
		 */
		this.set( 'hasError', false );

		/**
		 * The `id` of the element describing this field, e.g. when it has
		 * some error; it helps screen readers read the error text.
		 *
		 * @observable
		 * @member {Boolean} #ariaDescribedById
		 */
		this.set( 'ariaDescribedById' );

		/**
		 * Stores information about the editor UI focus and propagates it so various plugins and components
		 * are unified as a focus group.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker} #focusTracker
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * An observable flag set to `true` when the input is currently focused by the user.
		 * Set to `false` otherwise.
		 *
		 * @readonly
		 * @observable
		 * @member {Boolean} #isFocused
		 * @default false
		 */
		this.bind( 'isFocused' ).to( this.focusTracker );

		/**
		 * An observable flag set to `true` when the input contains no text, i.e.
		 * when {@link #value} is `''`, `null`, or `false`.
		 *
		 * @readonly
		 * @observable
		 * @member {Boolean} #isEmpty
		 * @default true
		 */
		this.set( 'isEmpty', true );

		/**
		 * Corresponds to the `inputmode` DOM attribute. Can be `text`, `numeric`, `decimal`, etc.
		 *
		 * @observable
		 * @member {Boolean} #inputMode
		 * @default 'text'
		 */
		this.set( 'inputMode', 'text' );

		const bind = this.bindTemplate;

		this.setTemplate( {
			tag: 'input',
			attributes: {
				class: [
					'ck',
					'ck-input',
					bind.if( 'isFocused', 'ck-input_focused' ),
					bind.if( 'isEmpty', 'ck-input-text_empty' ),
					bind.if( 'hasError', 'ck-error' )
				],
				id: bind.to( 'id' ),
				placeholder: bind.to( 'placeholder' ),
				readonly: bind.to( 'isReadOnly' ),
				inputmode: bind.to( 'inputMode' ),
				'aria-invalid': bind.if( 'hasError', true ),
				'aria-describedby': bind.to( 'ariaDescribedById' )
			},
			on: {
				input: bind.to( ( ...args ) => {
					this.fire( 'input', ...args );
					this._updateIsEmpty();
				} ),
				change: bind.to( this._updateIsEmpty.bind( this ) )
			}
		} );

		/**
		 * Fired when the user types in the input. Corresponds to the native
		 * DOM `input` event.
		 *
		 * @event input
		 */
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		this.focusTracker.add( this.element );

		this._setDomElementValue( this.value );
		this._updateIsEmpty();

		// Bind `this.value` to the DOM element's value.
		// We cannot use `value` DOM attribute because removing it on Edge does not clear the DOM element's value property.
		this.on( 'change:value', ( evt, name, value ) => {
			this._setDomElementValue( value );
			this._updateIsEmpty();
		} );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this.focusTracker.destroy();
	}

	/**
	 * Moves the focus to the input and selects the value.
	 */
	select() {
		this.element.select();
	}

	/**
	 * Focuses the input.
	 */
	focus() {
		this.element.focus();
	}

	/**
	 * Updates the {@link #isEmpty} property value on demand.
	 *
	 * @private
	 */
	_updateIsEmpty() {
		this.isEmpty = isInputElementEmpty( this.element );
	}

	/**
	 * Sets the `value` property of the {@link #element DOM element} on demand.
	 *
	 * @private
	 */
	_setDomElementValue( value ) {
		this.element.value = ( !value && value !== 0 ) ? '' : value;
	}
}

function isInputElementEmpty( domElement ) {
	return !domElement.value;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The text input view class.
 *
 * @extends module:ui/input/inputview~InputView
 */
class InputTextView extends InputView {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		this.extendTemplate( {
			attributes: {
				type: 'text',
				class: [
					'ck-input-text'
				]
			}
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The number input view class.
 *
 * @extends module:ui/input/inputview~InputView
 */
class InputNumberView extends InputView {
	/**
	 * Creates an instance of the input number view.
	 *
	 * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
	 * @param {Object} [options] The options of the input.
	 * @param {Number} [options.min] The value of the `min` DOM attribute (the lowest accepted value).
	 * @param {Number} [options.max] The value of the `max` DOM attribute (the highest accepted value).
	 * @param {Number} [options.step] The value of the `step` DOM attribute.
	 */
	constructor( locale, { min, max, step } = {} ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * The value of the `min` DOM attribute (the lowest accepted value) set on the {@link #element}.
		 *
		 * @observable
		 * @default undefined
		 * @member {Number} #min
		 */
		this.set( 'min', min );

		/**
		 * The value of the `max` DOM attribute (the highest accepted value) set on the {@link #element}.
		 *
		 * @observable
		 * @default undefined
		 * @member {Number} #max
		 */
		this.set( 'max', max );

		/**
		 * The value of the `step` DOM attribute set on the {@link #element}.
		 *
		 * @observable
		 * @default undefined
		 * @member {Number} #step
		 */
		this.set( 'step', step );

		this.extendTemplate( {
			attributes: {
				type: 'number',
				class: [
					'ck-input-number'
				],
				min: bind.to( 'min' ),
				max: bind.to( 'max' ),
				step: bind.to( 'step' )
			}
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The iframe view class.
 *
 * 		const iframe = new IframeView();
 *
 *		iframe.render();
 *		document.body.appendChild( iframe.element );
 *
 * 		iframe.on( 'loaded', () => {
 *			console.log( 'The iframe has loaded', iframe.element.contentWindow );
 *		} );
 *
 * 		iframe.element.src = 'https://ckeditor.com';
 *
 * @extends module:ui/view~View
 */
class IframeView extends View {
	/**
	 * Creates a new instance of the iframe view.
	 *
	 * @param {module:utils/locale~Locale} [locale] The locale instance.
	 */
	constructor( locale ) {
		super( locale );

		const bind = this.bindTemplate;

		this.setTemplate( {
			tag: 'iframe',
			attributes: {
				class: [
					'ck',
					'ck-reset_all'
				],
				// It seems that we need to allow scripts in order to be able to listen to events.
				// TODO: Research that. Perhaps the src must be set?
				sandbox: 'allow-same-origin allow-scripts'
			},
			on: {
				load: bind.to( 'loaded' )
			}
		} );
	}

	/**
	 * Renders the iframe's {@link #element} and returns a `Promise` for asynchronous
	 * child `contentDocument` loading process.
	 *
	 * @returns {Promise} A promise which resolves once the iframe `contentDocument` has
	 * been {@link #event:loaded}.
	 */
	render() {
		return new Promise( resolve => {
			this.on( 'loaded', resolve );
			super.render();
		} );
	}
}

/**
 * Fired when the DOM iframe's `contentDocument` finished loading.
 *
 * @event loaded
 */var css_248z$E = ".ck.ck-labeled-field-view>.ck.ck-labeled-field-view__input-wrapper{display:flex;position:relative}.ck.ck-labeled-field-view .ck.ck-label{display:block;position:absolute}:root{--ck-labeled-field-view-transition:.1s cubic-bezier(0,0,0.24,0.95);--ck-labeled-field-empty-unfocused-max-width:100% - 2 * var(--ck-spacing-medium);--ck-color-labeled-field-label-background:var(--ck-color-base-background)}.ck.ck-labeled-field-view{border-radius:0}.ck-rounded-corners .ck.ck-labeled-field-view,.ck.ck-labeled-field-view.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-labeled-field-view>.ck.ck-labeled-field-view__input-wrapper{width:100%}.ck.ck-labeled-field-view>.ck.ck-labeled-field-view__input-wrapper>.ck.ck-label{top:0}[dir=ltr] .ck.ck-labeled-field-view>.ck.ck-labeled-field-view__input-wrapper>.ck.ck-label{left:0}[dir=rtl] .ck.ck-labeled-field-view>.ck.ck-labeled-field-view__input-wrapper>.ck.ck-label{right:0}.ck.ck-labeled-field-view>.ck.ck-labeled-field-view__input-wrapper>.ck.ck-label{background:var(--ck-color-labeled-field-label-background);font-weight:400;line-height:normal;max-width:100%;overflow:hidden;padding:0 calc(var(--ck-font-size-tiny)*.5);pointer-events:none;text-overflow:ellipsis;transform:translate(var(--ck-spacing-medium),-6px) scale(.75);transform-origin:0 0;transition:transform var(--ck-labeled-field-view-transition),padding var(--ck-labeled-field-view-transition),background var(--ck-labeled-field-view-transition)}.ck.ck-labeled-field-view.ck-error .ck-input:not([readonly])+.ck.ck-label,.ck.ck-labeled-field-view.ck-error>.ck.ck-labeled-field-view__input-wrapper>.ck.ck-label{color:var(--ck-color-base-error)}.ck.ck-labeled-field-view .ck-labeled-field-view__status{font-size:var(--ck-font-size-small);margin-top:var(--ck-spacing-small);white-space:normal}.ck.ck-labeled-field-view .ck-labeled-field-view__status.ck-labeled-field-view__status_error{color:var(--ck-color-base-error)}.ck.ck-labeled-field-view.ck-disabled>.ck.ck-labeled-field-view__input-wrapper>.ck.ck-label,.ck.ck-labeled-field-view.ck-labeled-field-view_empty:not(.ck-labeled-field-view_focused)>.ck.ck-labeled-field-view__input-wrapper>.ck.ck-label{color:var(--ck-color-input-disabled-text)}[dir=ltr] .ck.ck-labeled-field-view.ck-disabled.ck-labeled-field-view_empty>.ck.ck-labeled-field-view__input-wrapper>.ck.ck-label,[dir=ltr] .ck.ck-labeled-field-view.ck-labeled-field-view_empty:not(.ck-labeled-field-view_focused):not(.ck-labeled-field-view_placeholder)>.ck.ck-labeled-field-view__input-wrapper>.ck.ck-label{transform:translate(var(--ck-spacing-medium),calc(var(--ck-font-size-base)*.6)) scale(1)}[dir=rtl] .ck.ck-labeled-field-view.ck-disabled.ck-labeled-field-view_empty>.ck.ck-labeled-field-view__input-wrapper>.ck.ck-label,[dir=rtl] .ck.ck-labeled-field-view.ck-labeled-field-view_empty:not(.ck-labeled-field-view_focused):not(.ck-labeled-field-view_placeholder)>.ck.ck-labeled-field-view__input-wrapper>.ck.ck-label{transform:translate(calc(var(--ck-spacing-medium)*-1),calc(var(--ck-font-size-base)*.6)) scale(1)}.ck.ck-labeled-field-view.ck-disabled.ck-labeled-field-view_empty>.ck.ck-labeled-field-view__input-wrapper>.ck.ck-label,.ck.ck-labeled-field-view.ck-labeled-field-view_empty:not(.ck-labeled-field-view_focused):not(.ck-labeled-field-view_placeholder)>.ck.ck-labeled-field-view__input-wrapper>.ck.ck-label{background:transparent;max-width:calc(var(--ck-labeled-field-empty-unfocused-max-width));padding:0}.ck.ck-labeled-field-view>.ck.ck-labeled-field-view__input-wrapper>.ck-dropdown>.ck.ck-button{background:transparent}.ck.ck-labeled-field-view.ck-labeled-field-view_empty>.ck.ck-labeled-field-view__input-wrapper>.ck-dropdown>.ck-button>.ck-button__label{opacity:0}.ck.ck-labeled-field-view.ck-labeled-field-view_empty:not(.ck-labeled-field-view_focused):not(.ck-labeled-field-view_placeholder)>.ck.ck-labeled-field-view__input-wrapper>.ck-dropdown+.ck-label{max-width:calc(var(--ck-labeled-field-empty-unfocused-max-width) - var(--ck-dropdown-arrow-size) - var(--ck-spacing-standard))}";
styleInject(css_248z$E);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The labeled field view class. It can be used to enhance any view with the following features:
 *
 * * a label,
 * * (optional) an error message,
 * * (optional) an info (status) text,
 *
 * all bound logically by proper DOM attributes for UX and accessibility.  It also provides an interface
 * (e.g. observable properties) that allows controlling those additional features.
 *
 * The constructor of this class requires a callback that returns a view to be labeled. The callback
 * is called with unique ids that allow binding of DOM properties:
 *
 *		const labeledInputView = new LabeledFieldView( locale, ( labeledFieldView, viewUid, statusUid ) => {
 *			const inputView = new InputTextView( labeledFieldView.locale );
 *
 *			inputView.set( {
 *				id: viewUid,
 *				ariaDescribedById: statusUid
 *			} );
 *
 *			inputView.bind( 'isReadOnly' ).to( labeledFieldView, 'isEnabled', value => !value );
 *			inputView.bind( 'hasError' ).to( labeledFieldView, 'errorText', value => !!value );
 *
 *			return inputView;
 *		} );
 *
 *		labeledInputView.label = 'User name';
 *		labeledInputView.infoText = 'Full name like for instance, John Doe.';
 *		labeledInputView.render();
 *
 *		document.body.append( labeledInputView.element );
 *
 * See {@link module:ui/labeledfield/utils} to discover ready–to–use labeled input helpers for common
 * UI components.
 *
 * @extends module:ui/view~View
 */
class LabeledFieldView extends View {
	/**
	 * Creates an instance of the labeled field view class using a provided creator function
	 * that provides the view to be labeled.
	 *
	 * @param {module:utils/locale~Locale} locale The locale instance.
	 * @param {Function} viewCreator A function that returns a {@link module:ui/view~View}
	 * that will be labeled. The following arguments are passed to the creator function:
	 *
	 * * an instance of the `LabeledFieldView` to allow binding observable properties,
	 * * an UID string that connects the {@link #labelView label} and the labeled field view in DOM,
	 * * an UID string that connects the {@link #statusView status} and the labeled field view in DOM.
	 */
	constructor( locale, viewCreator ) {
		super( locale );

		const viewUid = `ck-labeled-field-view-${ uid() }`;
		const statusUid = `ck-labeled-field-view-status-${ uid() }`;

		/**
		 * The field view that gets labeled.
		 *
		 * @member {module:ui/view~View} #fieldView
		 */
		this.fieldView = viewCreator( this, viewUid, statusUid );

		/**
		 * The text of the label.
		 *
		 * @observable
		 * @member {String} #label
		 */
		this.set( 'label' );

		/**
		 * Controls whether the component is in read-only mode.
		 *
		 * @observable
		 * @member {Boolean} #isEnabled
		 */
		this.set( 'isEnabled', true );

		/**
		 * An observable flag set to `true` when {@link #fieldView} is empty (`false` otherwise).
		 *
		 * @readonly
		 * @observable
		 * @member {Boolean} #isEmpty
		 * @default true
		 */
		this.set( 'isEmpty', true );

		/**
		 * An observable flag set to `true` when {@link #fieldView} is currently focused by
		 * the user (`false` otherwise).
		 *
		 * @readonly
		 * @observable
		 * @member {Boolean} #isFocused
		 * @default false
		 */
		this.set( 'isFocused', false );

		/**
		 * The validation error text. When set, it will be displayed
		 * next to the {@link #fieldView} as a typical validation error message.
		 * Set it to `null` to hide the message.
		 *
		 * **Note:** Setting this property to anything but `null` will automatically
		 * make the `hasError` of the {@link #fieldView} `true`.
		 *
		 * @observable
		 * @member {String|null} #errorText
		 */
		this.set( 'errorText', null );

		/**
		 * The additional information text displayed next to the {@link #fieldView} which can
		 * be used to inform the user about its purpose, provide help or hints.
		 *
		 * Set it to `null` to hide the message.
		 *
		 * **Note:** This text will be displayed in the same place as {@link #errorText} but the
		 * latter always takes precedence: if the {@link #errorText} is set, it replaces
		 * {@link #infoText}.
		 *
		 * @observable
		 * @member {String|null} #infoText
		 * @default null
		 */
		this.set( 'infoText', null );

		/**
		 * (Optional) The additional CSS class set on the dropdown {@link #element}.
		 *
		 * @observable
		 * @member {String} #class
		 */
		this.set( 'class' );

		/**
		 * The content of the `placeholder` attribute of the {@link #fieldView}.
		 *
		 * @observable
		 * @member {String} #placeholder
		 */
		this.set( 'placeholder' );

		/**
		 * The label view instance that describes the entire view.
		 *
		 * @member {module:ui/label/labelview~LabelView} #labelView
		 */
		this.labelView = this._createLabelView( viewUid );

		/**
		 * The status view for the {@link #fieldView}. It displays {@link #errorText} and
		 * {@link #infoText}.
		 *
		 * @member {module:ui/view~View} #statusView
		 */
		this.statusView = this._createStatusView( statusUid );

		/**
		 * The combined status text made of {@link #errorText} and {@link #infoText}.
		 * Note that when present, {@link #errorText} always takes precedence in the
		 * status.
		 *
		 * @see #errorText
		 * @see #infoText
		 * @see #statusView
		 * @private
		 * @observable
		 * @member {String|null} #_statusText
		 */
		this.bind( '_statusText' ).to(
			this, 'errorText',
			this, 'infoText',
			( errorText, infoText ) => errorText || infoText
		);

		const bind = this.bindTemplate;

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-labeled-field-view',
					bind.to( 'class' ),
					bind.if( 'isEnabled', 'ck-disabled', value => !value ),
					bind.if( 'isEmpty', 'ck-labeled-field-view_empty' ),
					bind.if( 'isFocused', 'ck-labeled-field-view_focused' ),
					bind.if( 'placeholder', 'ck-labeled-field-view_placeholder' ),
					bind.if( 'errorText', 'ck-error' )
				]
			},
			children: [
				{
					tag: 'div',
					attributes: {
						class: [
							'ck',
							'ck-labeled-field-view__input-wrapper'
						]
					},
					children: [
						this.fieldView,
						this.labelView
					]
				},
				this.statusView
			]
		} );
	}

	/**
	 * Creates label view class instance and bind with view.
	 *
	 * @private
	 * @param {String} id Unique id to set as labelView#for attribute.
	 * @returns {module:ui/label/labelview~LabelView}
	 */
	_createLabelView( id ) {
		const labelView = new LabelView( this.locale );

		labelView.for = id;
		labelView.bind( 'text' ).to( this, 'label' );

		return labelView;
	}

	/**
	 * Creates the status view instance. It displays {@link #errorText} and {@link #infoText}
	 * next to the {@link #fieldView}. See {@link #_statusText}.
	 *
	 * @private
	 * @param {String} statusUid Unique id of the status, shared with the {@link #fieldView view's}
	 * `aria-describedby` attribute.
	 * @returns {module:ui/view~View}
	 */
	_createStatusView( statusUid ) {
		const statusView = new View( this.locale );
		const bind = this.bindTemplate;

		statusView.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-labeled-field-view__status',
					bind.if( 'errorText', 'ck-labeled-field-view__status_error' ),
					bind.if( '_statusText', 'ck-hidden', value => !value )
				],
				id: statusUid,
				role: bind.if( 'errorText', 'alert' )
			},
			children: [
				{
					text: bind.to( '_statusText' )
				}
			]
		} );

		return statusView;
	}

	/**
	 * Focuses the {@link #fieldView}.
	 */
	focus() {
		this.fieldView.focus();
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A helper for creating labeled inputs.
 *
 * It creates an instance of a {@link module:ui/inputtext/inputtextview~InputTextView input text} that is
 * logically related to a {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView labeled view} in DOM.
 *
 * The helper does the following:
 *
 * * It sets input's `id` and `ariaDescribedById` attributes.
 * * It binds input's `isReadOnly` to the labeled view.
 * * It binds input's `hasError` to the labeled view.
 * * It enables a logic that cleans up the error when user starts typing in the input.
 *
 * Usage:
 *
 *		const labeledInputView = new LabeledFieldView( locale, createLabeledInputText );
 *		console.log( labeledInputView.fieldView ); // A text input instance.
 *
 * @param {module:ui/labeledfield/labeledfieldview~LabeledFieldView} labeledFieldView The instance of the labeled field view.
 * @param {String} viewUid An UID string that allows DOM logical connection between the
 * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#labelView labeled view's label} and the input.
 * @param {String} statusUid An UID string that allows DOM logical connection between the
 * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#statusView labeled view's status} and the input.
 * @returns {module:ui/inputtext/inputtextview~InputTextView} The input text view instance.
 */
function createLabeledInputText( labeledFieldView, viewUid, statusUid ) {
	const inputView = new InputTextView( labeledFieldView.locale );

	inputView.set( {
		id: viewUid,
		ariaDescribedById: statusUid
	} );

	inputView.bind( 'isReadOnly' ).to( labeledFieldView, 'isEnabled', value => !value );
	inputView.bind( 'hasError' ).to( labeledFieldView, 'errorText', value => !!value );

	inputView.on( 'input', () => {
		// UX: Make the error text disappear and disable the error indicator as the user
		// starts fixing the errors.
		labeledFieldView.errorText = null;
	} );

	labeledFieldView.bind( 'isEmpty', 'isFocused', 'placeholder' ).to( inputView );

	return inputView;
}

/**
 * A helper for creating labeled number inputs.
 *
 * It creates an instance of a {@link module:ui/inputnumber/inputnumberview~InputNumberView input number} that is
 * logically related to a {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView labeled view} in DOM.
 *
 * The helper does the following:
 *
 * * It sets input's `id` and `ariaDescribedById` attributes.
 * * It binds input's `isReadOnly` to the labeled view.
 * * It binds input's `hasError` to the labeled view.
 * * It enables a logic that cleans up the error when user starts typing in the input.
 *
 * Usage:
 *
 *		const labeledInputView = new LabeledFieldView( locale, createLabeledInputNumber );
 *		console.log( labeledInputView.fieldView ); // A number input instance.
 *
 * @param {module:ui/labeledfield/labeledfieldview~LabeledFieldView} labeledFieldView The instance of the labeled field view.
 * @param {String} viewUid An UID string that allows DOM logical connection between the
 * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#labelView labeled view's label} and the input.
 * @param {String} statusUid An UID string that allows DOM logical connection between the
 * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#statusView labeled view's status} and the input.
 * @returns {module:ui/inputnumber/inputnumberview~InputNumberView} The input number view instance.
 */
function createLabeledInputNumber( labeledFieldView, viewUid, statusUid ) {
	const inputView = new InputNumberView( labeledFieldView.locale );

	inputView.set( {
		id: viewUid,
		ariaDescribedById: statusUid,
		inputMode: 'numeric'
	} );

	inputView.bind( 'isReadOnly' ).to( labeledFieldView, 'isEnabled', value => !value );
	inputView.bind( 'hasError' ).to( labeledFieldView, 'errorText', value => !!value );

	inputView.on( 'input', () => {
		// UX: Make the error text disappear and disable the error indicator as the user
		// starts fixing the errors.
		labeledFieldView.errorText = null;
	} );

	labeledFieldView.bind( 'isEmpty', 'isFocused', 'placeholder' ).to( inputView );

	return inputView;
}

/**
 * A helper for creating labeled dropdowns.
 *
 * It creates an instance of a {@link module:ui/dropdown/dropdownview~DropdownView dropdown} that is
 * logically related to a {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView labeled field view}.
 *
 * The helper does the following:
 *
 * * It sets dropdown's `id` and `ariaDescribedById` attributes.
 * * It binds input's `isEnabled` to the labeled view.
 *
 * Usage:
 *
 *		const labeledInputView = new LabeledFieldView( locale, createLabeledDropdown );
 *		console.log( labeledInputView.fieldView ); // A dropdown instance.
 *
 * @param {module:ui/labeledfield/labeledfieldview~LabeledFieldView} labeledFieldView The instance of the labeled field view.
 * @param {String} viewUid An UID string that allows DOM logical connection between the
 * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#labelView labeled view label} and the dropdown.
 * @param {String} statusUid An UID string that allows DOM logical connection between the
 * {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView#statusView labeled view status} and the dropdown.
 * @returns {module:ui/dropdown/dropdownview~DropdownView} The dropdown view instance.
 */
function createLabeledDropdown( labeledFieldView, viewUid, statusUid ) {
	const dropdownView = createDropdown( labeledFieldView.locale );

	dropdownView.set( {
		id: viewUid,
		ariaDescribedById: statusUid
	} );

	dropdownView.bind( 'isEnabled' ).to( labeledFieldView );

	return dropdownView;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The Notification plugin.
 *
 * This plugin sends a few types of notifications: `success`, `info` and `warning`. The notifications need to be
 * handled and displayed by a plugin responsible for showing the UI of the notifications. Using this plugin for dispatching
 * notifications makes it possible to switch the notifications UI.
 *
 * Note that every unhandled and not stopped `warning` notification will be displayed as a system alert.
 * See {@link module:ui/notification/notification~Notification#showWarning}.
 *
 * @extends module:core/contextplugin~ContextPlugin
 */
class Notification extends ContextPlugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Notification';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		// Each unhandled and not stopped `show:warning` event is displayed as a system alert.
		this.on( 'show:warning', ( evt, data ) => {
			window.alert( data.message ); // eslint-disable-line no-alert
		}, { priority: 'lowest' } );
	}

	/**
	 * Shows a success notification.
	 *
	 * By default, it fires the {@link #event:show:success `show:success` event} with the given `data`. The event namespace can be extended
	 * using the `data.namespace` option. For example:
	 *
	 * 		showSuccess( 'Image is uploaded.', {
	 * 			namespace: 'upload:image'
	 * 		} );
	 *
	 * will fire the `show:success:upload:image` event.
	 *
	 * You can provide the title of the notification:
	 *
	 *		showSuccess( 'Image is uploaded.', {
	 *			title: 'Image upload success'
	 *		} );
	 *
	 * @param {String} message The content of the notification.
	 * @param {Object} [data={}] Additional data.
	 * @param {String} [data.namespace] Additional event namespace.
	 * @param {String} [data.title] The title of the notification.
	 */
	showSuccess( message, data = {} ) {
		this._showNotification( {
			message,
			type: 'success',
			namespace: data.namespace,
			title: data.title
		} );
	}

	/**
	 * Shows an information notification.
	 *
	 * By default, it fires the {@link #event:show:info `show:info` event} with the given `data`. The event namespace can be extended
	 * using the `data.namespace` option. For example:
	 *
	 * 		showInfo( 'Editor is offline.', {
	 * 			namespace: 'editor:status'
	 * 		} );
	 *
	 * will fire the `show:info:editor:status` event.
	 *
	 * You can provide the title of the notification:
	 *
	 *		showInfo( 'Editor is offline.', {
	 *			title: 'Network information'
	 *		} );
	 *
	 * @param {String} message The content of the notification.
	 * @param {Object} [data={}] Additional data.
	 * @param {String} [data.namespace] Additional event namespace.
	 * @param {String} [data.title] The title of the notification.
	 */
	showInfo( message, data = {} ) {
		this._showNotification( {
			message,
			type: 'info',
			namespace: data.namespace,
			title: data.title
		} );
	}

	/**
	 * Shows a warning notification.
	 *
	 * By default, it fires the {@link #event:show:warning `show:warning` event}
	 * with the given `data`. The event namespace can be extended using the `data.namespace` option. For example:
	 *
	 * 		showWarning( 'Image upload error.', {
	 * 			namespace: 'upload:image'
	 * 		} );
	 *
	 * will fire the `show:warning:upload:image` event.
	 *
	 * You can provide the title of the notification:
	 *
	 *		showWarning( 'Image upload error.', {
	 *			title: 'Upload failed'
	 *		} );
	 *
	 * Note that each unhandled and not stopped `warning` notification will be displayed as a system alert.
	 * The plugin responsible for displaying warnings should `stop()` the event to prevent displaying it as an alert:
	 *
	 * 		notifications.on( 'show:warning', ( evt, data ) => {
	 * 			// Do something with the data.
	 *
	 * 			// Stop this event to prevent displaying it as an alert.
	 * 			evt.stop();
	 * 		} );
	 *
	 * You can attach many listeners to the same event and `stop()` this event in a listener with a low priority:
	 *
	 * 		notifications.on( 'show:warning', ( evt, data ) => {
	 * 			// Show the warning in the UI, but do not stop it.
	 * 		} );
	 *
	 * 		notifications.on( 'show:warning', ( evt, data ) => {
	 * 			// Log the warning to some error tracker.
	 *
	 * 			// Stop this event to prevent displaying it as an alert.
	 * 			evt.stop();
	 * 		}, { priority: 'low' } );
	 *
	 * @param {String} message The content of the notification.
	 * @param {Object} [data={}] Additional data.
	 * @param {String} [data.namespace] Additional event namespace.
	 * @param {String} [data.title] The title of the notification.
	 */
	showWarning( message, data = {} ) {
		this._showNotification( {
			message,
			type: 'warning',
			namespace: data.namespace,
			title: data.title
		} );
	}

	/**
	 * Fires the `show` event with the specified type, namespace and message.
	 *
	 * @private
	 * @param {Object} data The message data.
	 * @param {String} data.message The content of the notification.
	 * @param {'success'|'info'|'warning'} data.type The type of the message.
	 * @param {String} [data.namespace] Additional event namespace.
	 * @param {String} [data.title=''] The title of the notification.
	 */
	_showNotification( data ) {
		const event = `show:${ data.type }` + ( data.namespace ? `:${ data.namespace }` : '' );

		this.fire( event, {
			message: data.message,
			type: data.type,
			title: data.title || ''
		} );
	}

	/**
	 * Fired when one of the `showSuccess()`, `showInfo()`, `showWarning()` methods is called.
	 *
	 * @event show
	 * @param {Object} data The notification data.
	 * @param {String} data.message The content of the notification.
	 * @param {String} data.title The title of the notification.
	 * @param {'success'|'info'|'warning'} data.type The type of the notification.
	 */

	/**
	 * Fired when the `showSuccess()` method is called.
	 *
	 * @event show:success
	 * @param {Object} data The notification data.
	 * @param {String} data.message The content of the notification.
	 * @param {String} data.title The title of the notification.
	 * @param {'success'} data.type The type of the notification.
	 */

	/**
	 * Fired when the `showInfo()` method is called.
	 *
	 * @event show:info
	 * @param {Object} data The notification data.
	 * @param {String} data.message The content of the notification.
	 * @param {String} data.title The title of the notification.
	 * @param {'info'} data.type The type of the notification.
	 */

	/**
	 * Fired when the `showWarning()` method is called.
	 *
	 * When this event is not handled or stopped by `event.stop()`, the `data.message` of this event will
	 * be automatically displayed as a system alert.
	 *
	 * @event show:warning
	 * @param {Object} data The notification data.
	 * @param {String} data.message The content of the notification.
	 * @param {String} data.title The title of the notification.
	 * @param {'warning'} data.type The type of the notification.
	 */
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The base MVC model class.
 *
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class Model {
	/**
	 * Creates a new Model instance.
	 *
	 * @param {Object} [attributes] The model state attributes to be defined during the instance creation.
	 * @param {Object} [properties] The (out of state) properties to be appended to the instance during creation.
	 */
	constructor( attributes, properties ) {
		// Extend this instance with the additional (out of state) properties.
		if ( properties ) {
			extend( this, properties );
		}

		// Initialize the attributes.
		if ( attributes ) {
			this.set( attributes );
		}
	}
}

mix( Model, ObservableMixin );var previousArrow = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M11.463 5.187a.888.888 0 1 1 1.254 1.255L9.16 10l3.557 3.557a.888.888 0 1 1-1.254 1.255L7.26 10.61a.888.888 0 0 1 .16-1.382l4.043-4.042z\"/></svg>";
var nextIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M8.537 14.813a.888.888 0 1 1-1.254-1.255L10.84 10 7.283 6.442a.888.888 0 1 1 1.254-1.255L12.74 9.39a.888.888 0 0 1-.16 1.382l-4.043 4.042z\"/></svg>";
var css_248z$D = ".ck .ck-balloon-rotator__navigation{align-items:center;display:flex;justify-content:center}.ck .ck-balloon-rotator__content .ck-toolbar{justify-content:center}.ck .ck-balloon-rotator__navigation{background:var(--ck-color-toolbar-background);border-bottom:1px solid var(--ck-color-toolbar-border);padding:0 var(--ck-spacing-small)}.ck .ck-balloon-rotator__navigation>*{margin-bottom:var(--ck-spacing-small);margin-right:var(--ck-spacing-small);margin-top:var(--ck-spacing-small)}.ck .ck-balloon-rotator__navigation .ck-balloon-rotator__counter{margin-left:var(--ck-spacing-small);margin-right:var(--ck-spacing-standard)}.ck .ck-balloon-rotator__content .ck.ck-annotation-wrapper{box-shadow:none}";
styleInject(css_248z$D);var css_248z$C = ".ck .ck-fake-panel{position:absolute;z-index:calc(var(--ck-z-modal) - 1)}.ck .ck-fake-panel div{position:absolute}.ck .ck-fake-panel div:first-child{z-index:2}.ck .ck-fake-panel div:nth-child(2){z-index:1}:root{--ck-balloon-fake-panel-offset-horizontal:6px;--ck-balloon-fake-panel-offset-vertical:6px}.ck .ck-fake-panel div{background:var(--ck-color-panel-background);border:1px solid var(--ck-color-panel-border);border-radius:var(--ck-border-radius);box-shadow:var(--ck-drop-shadow),0 0;height:100%;min-height:15px;width:100%}.ck .ck-fake-panel div:first-child{margin-left:var(--ck-balloon-fake-panel-offset-horizontal);margin-top:var(--ck-balloon-fake-panel-offset-vertical)}.ck .ck-fake-panel div:nth-child(2){margin-left:calc(var(--ck-balloon-fake-panel-offset-horizontal)*2);margin-top:calc(var(--ck-balloon-fake-panel-offset-vertical)*2)}.ck .ck-fake-panel div:nth-child(3){margin-left:calc(var(--ck-balloon-fake-panel-offset-horizontal)*3);margin-top:calc(var(--ck-balloon-fake-panel-offset-vertical)*3)}.ck .ck-balloon-panel_arrow_s+.ck-fake-panel,.ck .ck-balloon-panel_arrow_se+.ck-fake-panel,.ck .ck-balloon-panel_arrow_sw+.ck-fake-panel{--ck-balloon-fake-panel-offset-vertical:-6px}";
styleInject(css_248z$C);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const toPx$4 = toUnit( 'px' );

/**
 * Provides the common contextual balloon for the editor.
 *
 * The role of this plugin is to unify the contextual balloons logic, simplify views management and help
 * avoid the unnecessary complexity of handling multiple {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
 * instances in the editor.
 *
 * This plugin allows for creating single or multiple panel stacks.
 *
 * Each stack may have multiple views, with the one on the top being visible. When the visible view is removed from the stack,
 * the previous view becomes visible.
 *
 * It might be useful to implement nested navigation in a balloon. For instance, a toolbar view may contain a link button.
 * When you click it, a link view (which lets you set the URL) is created and put on top of the toolbar view, so the link panel
 * is displayed. When you finish editing the link and close (remove) the link view, the toolbar view is visible again.
 *
 * However, there are cases when there are multiple independent balloons to be displayed, for instance, if the selection
 * is inside two inline comments at the same time. For such cases, you can create two independent panel stacks.
 * The contextual balloon plugin will create a navigation bar to let the users switch between these panel stacks using the "Next"
 * and "Previous" buttons.
 *
 * If there are no views in the current stack, the balloon panel will try to switch to the next stack. If there are no
 * panels in any stack, the balloon panel will be hidden.
 *
 * **Note**: To force the balloon panel to show only one view, even if there are other stacks, use the `singleViewMode=true` option
 * when {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon#add adding} a view to a panel.
 *
 * From the implementation point of view, the contextual ballon plugin is reusing a single
 * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView} instance to display multiple contextual balloon
 * panels in the editor. It also creates a special {@link module:ui/panel/balloon/contextualballoon~RotatorView rotator view},
 * used to manage multiple panel stacks. Rotator view is a child of the balloon panel view and the parent of the specific
 * view you want to display. If there is more than one panel stack to be displayed, the rotator view will add a
 * navigation bar. If there is only one stack, the rotator view is transparent (it does not add any UI elements).
 *
 * @extends module:core/plugin~Plugin
 */
class ContextualBalloon extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'ContextualBalloon';
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		/**
		 * The {@link module:utils/dom/position~Options#limiter position limiter}
		 * for the {@link #view balloon}, used when no `limiter` has been passed into {@link #add}
		 * or {@link #updatePosition}.
		 *
		 * By default, a function that obtains the farthest DOM
		 * {@link module:engine/view/rooteditableelement~RootEditableElement}
		 * of the {@link module:engine/view/document~Document#selection}.
		 *
		 * @member {module:utils/dom/position~Options#limiter} #positionLimiter
		 */
		this.positionLimiter = () => {
			const view = this.editor.editing.view;
			const viewDocument = view.document;
			const editableElement = viewDocument.selection.editableElement;

			if ( editableElement ) {
				return view.domConverter.mapViewToDom( editableElement.root );
			}

			return null;
		};

		/**
		 * The currently visible view or `null` when there are no views in any stack.
		 *
		 * @readonly
		 * @observable
		 * @member {module:ui/view~View|null} #visibleView
		 */
		this.set( 'visibleView', null );

		/**
		 * The common balloon panel view.
		 *
		 * @readonly
		 * @member {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} #view
		 */
		this.view = new BalloonPanelView( editor.locale );
		editor.ui.view.body.add( this.view );
		editor.ui.focusTracker.add( this.view.element );

		/**
		 * The map of views and their stacks.
		 *
		 * @private
		 * @type {Map.<module:ui/view~View,Set>}
		 */
		this._viewToStack = new Map();

		/**
		 * The map of IDs and stacks.
		 *
		 * @private
		 * @type {Map.<String,Set>}
		 */
		this._idToStack = new Map();

		/**
		 * A total number of all stacks in the balloon.
		 *
		 * @private
		 * @readonly
		 * @observable
		 * @member {Number} #_numberOfStacks
		 */
		this.set( '_numberOfStacks', 0 );

		/**
		 * A flag that controls the single view mode.
		 *
		 * @private
		 * @readonly
		 * @observable
		 * @member {Boolean} #_singleViewMode
		 */
		this.set( '_singleViewMode', false );

		/**
		 * Rotator view embedded in the contextual balloon.
		 * Displays the currently visible view in the balloon and provides navigation for switching stacks.
		 *
		 * @private
		 * @type {module:ui/panel/balloon/contextualballoon~RotatorView}
		 */
		this._rotatorView = this._createRotatorView();

		/**
		 * Displays fake panels under the balloon panel view when multiple stacks are added to the balloon.
		 *
		 * @private
		 * @type {module:ui/view~View}
		 */
		this._fakePanelsView = this._createFakePanelsView();
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this.view.destroy();
		this._rotatorView.destroy();
		this._fakePanelsView.destroy();
	}

	/**
	 * Returns `true` when the given view is in one of the stacks. Otherwise returns `false`.
	 *
	 * @param {module:ui/view~View} view
	 * @returns {Boolean}
	 */
	hasView( view ) {
		return Array.from( this._viewToStack.keys() ).includes( view );
	}

	/**
	 * Adds a new view to the stack and makes it visible if the current stack is visible
	 * or it is the first view in the balloon.
	 *
	 * @param {Object} data The configuration of the view.
	 * @param {String} [data.stackId='main'] The ID of the stack that the view is added to.
	 * @param {module:ui/view~View} [data.view] The content of the balloon.
	 * @param {module:utils/dom/position~Options} [data.position] Positioning options.
	 * @param {String} [data.balloonClassName] An additional CSS class added to the {@link #view balloon} when visible.
	 * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
	 * @param {Boolean} [data.singleViewMode=false] Whether the view should be the only visible view even if other stacks were added.
	 */
	add( data ) {
		if ( this.hasView( data.view ) ) {
			/**
			 * Trying to add configuration of the same view more than once.
			 *
			 * @error contextualballoon-add-view-exist
			 */
			throw new CKEditorError(
				'contextualballoon-add-view-exist',
				[ this, data ]
			);
		}

		const stackId = data.stackId || 'main';

		// If new stack is added, creates it and show view from this stack.
		if ( !this._idToStack.has( stackId ) ) {
			this._idToStack.set( stackId, new Map( [ [ data.view, data ] ] ) );
			this._viewToStack.set( data.view, this._idToStack.get( stackId ) );
			this._numberOfStacks = this._idToStack.size;

			if ( !this._visibleStack || data.singleViewMode ) {
				this.showStack( stackId );
			}

			return;
		}

		const stack = this._idToStack.get( stackId );

		if ( data.singleViewMode ) {
			this.showStack( stackId );
		}

		// Add new view to the stack.
		stack.set( data.view, data );
		this._viewToStack.set( data.view, stack );

		// And display it if is added to the currently visible stack.
		if ( stack === this._visibleStack ) {
			this._showView( data );
		}
	}

	/**
	 * Removes the given view from the stack. If the removed view was visible,
	 * the view preceding it in the stack will become visible instead.
	 * When there is no view in the stack, the next stack will be displayed.
	 * When there are no more stacks, the balloon will hide.
	 *
	 * @param {module:ui/view~View} view A view to be removed from the balloon.
	 */
	remove( view ) {
		if ( !this.hasView( view ) ) {
			/**
			 * Trying to remove the configuration of the view not defined in the stack.
			 *
			 * @error contextualballoon-remove-view-not-exist
			 */
			throw new CKEditorError(
				'contextualballoon-remove-view-not-exist',
				[ this, view ]
			);
		}

		const stack = this._viewToStack.get( view );

		if ( this._singleViewMode && this.visibleView === view ) {
			this._singleViewMode = false;
		}

		// When visible view will be removed we need to show a preceding view or next stack
		// if a view is the only view in the stack.
		if ( this.visibleView === view ) {
			if ( stack.size === 1 ) {
				if ( this._idToStack.size > 1 ) {
					this._showNextStack();
				} else {
					this.view.hide();
					this.visibleView = null;
					this._rotatorView.hideView();
				}
			} else {
				this._showView( Array.from( stack.values() )[ stack.size - 2 ] );
			}
		}

		if ( stack.size === 1 ) {
			this._idToStack.delete( this._getStackId( stack ) );
			this._numberOfStacks = this._idToStack.size;
		} else {
			stack.delete( view );
		}

		this._viewToStack.delete( view );
	}

	/**
	 * Updates the position of the balloon using the position data of the first visible view in the stack.
	 * When new position data is given, the position data of the currently visible view will be updated.
	 *
	 * @param {module:utils/dom/position~Options} [position] position options.
	 */
	updatePosition( position ) {
		if ( position ) {
			this._visibleStack.get( this.visibleView ).position = position;
		}

		this.view.pin( this._getBalloonPosition() );
		this._fakePanelsView.updatePosition();
	}

	/**
	 * Shows the last view from the stack of a given ID.
	 *
	 * @param {String} id
	 */
	showStack( id ) {
		this.visibleStack = id;
		const stack = this._idToStack.get( id );

		if ( !stack ) {
			/**
			 * Trying to show a stack that does not exist.
			 *
			 * @error contextualballoon-showstack-stack-not-exist
			 */
			throw new CKEditorError(
				'contextualballoon-showstack-stack-not-exist',
				this
			);
		}

		if ( this._visibleStack === stack ) {
			return;
		}

		this._showView( Array.from( stack.values() ).pop() );
	}

	/**
	 * Returns the stack of the currently visible view.
	 *
	 * @private
	 * @type {Set}
	 */
	get _visibleStack() {
		return this._viewToStack.get( this.visibleView );
	}

	/**
	 * Returns the ID of the given stack.
	 *
	 * @private
	 * @param {Set} stack
	 * @returns {String}
	 */
	_getStackId( stack ) {
		const entry = Array.from( this._idToStack.entries() ).find( entry => entry[ 1 ] === stack );

		return entry[ 0 ];
	}

	/**
	 * Shows the last view from the next stack.
	 *
	 * @private
	 */
	_showNextStack() {
		const stacks = Array.from( this._idToStack.values() );

		let nextIndex = stacks.indexOf( this._visibleStack ) + 1;

		if ( !stacks[ nextIndex ] ) {
			nextIndex = 0;
		}

		this.showStack( this._getStackId( stacks[ nextIndex ] ) );
	}

	/**
	 * Shows the last view from the previous stack.
	 *
	 * @private
	 */
	_showPrevStack() {
		const stacks = Array.from( this._idToStack.values() );

		let nextIndex = stacks.indexOf( this._visibleStack ) - 1;

		if ( !stacks[ nextIndex ] ) {
			nextIndex = stacks.length - 1;
		}

		this.showStack( this._getStackId( stacks[ nextIndex ] ) );
	}

	/**
	 * Creates a rotator view.
	 *
	 * @private
	 * @returns {module:ui/panel/balloon/contextualballoon~RotatorView}
	 */
	_createRotatorView() {
		const view = new RotatorView( this.editor.locale );
		const t = this.editor.locale.t;

		this.view.content.add( view );

		// Hide navigation when there is only a one stack & not in single view mode.
		view.bind( 'isNavigationVisible' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( value, isSingleViewMode ) => {
			return !isSingleViewMode && value > 1;
		} );

		// Update balloon position after toggling navigation.
		view.on( 'change:isNavigationVisible', () => ( this.updatePosition() ), { priority: 'low' } );

		// Update stacks counter value.
		view.bind( 'counter' ).to( this, 'visibleView', this, '_numberOfStacks', ( visibleView, numberOfStacks ) => {
			if ( numberOfStacks < 2 ) {
				return '';
			}

			const current = Array.from( this._idToStack.values() ).indexOf( this._visibleStack ) + 1;

			return t( '%0 of %1', [ current, numberOfStacks ] );
		} );

		view.buttonNextView.on( 'execute', () => {
			// When current view has a focus then move focus to the editable before removing it,
			// otherwise editor will lost focus.
			if ( view.focusTracker.isFocused ) {
				this.editor.editing.view.focus();
			}

			this._showNextStack();
		} );

		view.buttonPrevView.on( 'execute', () => {
			// When current view has a focus then move focus to the editable before removing it,
			// otherwise editor will lost focus.
			if ( view.focusTracker.isFocused ) {
				this.editor.editing.view.focus();
			}

			this._showPrevStack();
		} );

		return view;
	}

	/**
	 * @private
	 * @returns {module:ui/view~View}
	 */
	_createFakePanelsView() {
		const view = new FakePanelsView( this.editor.locale, this.view );

		view.bind( 'numberOfPanels' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( number, isSingleViewMode ) => {
			const showPanels = !isSingleViewMode && number >= 2;

			return showPanels ? Math.min( number - 1, 2 ) : 0;
		} );

		view.listenTo( this.view, 'change:top', () => view.updatePosition() );
		view.listenTo( this.view, 'change:left', () => view.updatePosition() );

		this.editor.ui.view.body.add( view );

		return view;
	}

	/**
	 * Sets the view as the content of the balloon and attaches the balloon using position
	 * options of the first view.
	 *
	 * @private
	 * @param {Object} data Configuration.
	 * @param {module:ui/view~View} [data.view] The view to show in the balloon.
	 * @param {String} [data.balloonClassName=''] Additional class name which will be added to the {@link #view balloon}.
	 * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
	 */
	_showView( { view, balloonClassName = '', withArrow = true, singleViewMode = false } ) {
		this.view.class = balloonClassName;
		this.view.withArrow = withArrow;

		this._rotatorView.showView( view );
		this.visibleView = view;
		this.view.pin( this._getBalloonPosition() );
		this._fakePanelsView.updatePosition();

		if ( singleViewMode ) {
			this._singleViewMode = true;
		}
	}

	/**
	 * Returns position options of the last view in the stack.
	 * This keeps the balloon in the same position when the view is changed.
	 *
	 * @private
	 * @returns {module:utils/dom/position~Options}
	 */
	_getBalloonPosition() {
		let position = Array.from( this._visibleStack.values() ).pop().position;

		if ( position ) {
			// Use the default limiter if none has been specified.
			if ( !position.limiter ) {
				// Don't modify the original options object.
				position = Object.assign( {}, position, {
					limiter: this.positionLimiter
				} );
			}

			// Don't modify the original options object.
			position = Object.assign( {}, position, {
				viewportOffsetConfig: this.editor.ui.viewportOffset
			} );
		}

		return position;
	}
}

/**
 * Rotator view is a helper class for the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon ContextualBalloon}.
 * It is used for displaying the last view from the current stack and providing navigation buttons for switching stacks.
 * See the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon ContextualBalloon} documentation to learn more.
 *
 * @extends module:ui/view~View
 */
class RotatorView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		const t = locale.t;
		const bind = this.bindTemplate;

		/**
		 * Defines whether navigation is visible or not.
		 *
		 * @member {Boolean} #isNavigationVisible
		 */
		this.set( 'isNavigationVisible', true );

		/**
		 * Used for checking if a view is focused or not.
		 *
		 * @type {module:utils/focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * Navigation button for switching the stack to the previous one.
		 *
		 * @type {module:ui/button/buttonview~ButtonView}
		 */
		this.buttonPrevView = this._createButtonView( t( 'Previous' ), previousArrow );

		/**
		 * Navigation button for switching the stack to the next one.
		 *
		 * @type {module:ui/button/buttonview~ButtonView}
		 */
		this.buttonNextView = this._createButtonView( t( 'Next' ), nextIcon );

		/**
		 * A collection of the child views that creates the rotator content.
		 *
		 * @readonly
		 * @type {module:ui/viewcollection~ViewCollection}
		 */
		this.content = this.createCollection();

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-balloon-rotator'
				],
				'z-index': '-1'
			},
			children: [
				{
					tag: 'div',
					attributes: {
						class: [
							'ck-balloon-rotator__navigation',
							bind.to( 'isNavigationVisible', value => value ? '' : 'ck-hidden' )
						]
					},
					children: [
						this.buttonPrevView,
						{
							tag: 'span',

							attributes: {
								class: [
									'ck-balloon-rotator__counter'
								]
							},

							children: [
								{
									text: bind.to( 'counter' )
								}
							]
						},
						this.buttonNextView
					]
				},
				{
					tag: 'div',
					attributes: {
						class: 'ck-balloon-rotator__content'
					},
					children: this.content
				}
			]
		} );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		this.focusTracker.add( this.element );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this.focusTracker.destroy();
	}

	/**
	 * Shows a given view.
	 *
	 * @param {module:ui/view~View} view The view to show.
	 */
	showView( view ) {
		this.hideView();
		this.content.add( view );
	}

	/**
	 * Hides the currently displayed view.
	 */
	hideView() {
		this.content.clear();
	}

	/**
	 * Creates a navigation button view.
	 *
	 * @private
	 * @param {String} label The button label.
	 * @param {String} icon The button icon.
	 * @returns {module:ui/button/buttonview~ButtonView}
	 */
	_createButtonView( label, icon ) {
		const view = new ButtonView( this.locale );

		view.set( {
			label,
			icon,
			tooltip: true
		} );

		return view;
	}
}

// Displays additional layers under the balloon when multiple stacks are added to the balloon.
//
// @private
// @extends module:ui/view~View
class FakePanelsView extends View {
	// @inheritDoc
	constructor( locale, balloonPanelView ) {
		super( locale );

		const bind = this.bindTemplate;

		// Fake panels top offset.
		//
		// @observable
		// @member {Number} #top
		this.set( 'top', 0 );

		// Fake panels left offset.
		//
		// @observable
		// @member {Number} #left
		this.set( 'left', 0 );

		// Fake panels height.
		//
		// @observable
		// @member {Number} #height
		this.set( 'height', 0 );

		// Fake panels width.
		//
		// @observable
		// @member {Number} #width
		this.set( 'width', 0 );

		// Number of rendered fake panels.
		//
		// @observable
		// @member {Number} #numberOfPanels
		this.set( 'numberOfPanels', 0 );

		// Collection of the child views which creates fake panel content.
		//
		// @readonly
		// @type {module:ui/viewcollection~ViewCollection}
		this.content = this.createCollection();

		// Context.
		//
		// @private
		// @type {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
		this._balloonPanelView = balloonPanelView;

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck-fake-panel',
					bind.to( 'numberOfPanels', number => number ? '' : 'ck-hidden' )
				],
				style: {
					top: bind.to( 'top', toPx$4 ),
					left: bind.to( 'left', toPx$4 ),
					width: bind.to( 'width', toPx$4 ),
					height: bind.to( 'height', toPx$4 )
				}
			},
			children: this.content
		} );

		this.on( 'change:numberOfPanels', ( evt, name, next, prev ) => {
			if ( next > prev ) {
				this._addPanels( next - prev );
			} else {
				this._removePanels( prev - next );
			}

			this.updatePosition();
		} );
	}

	// @private
	// @param {Number} number
	_addPanels( number ) {
		while ( number-- ) {
			const view = new View();

			view.setTemplate( { tag: 'div' } );

			this.content.add( view );
			this.registerChild( view );
		}
	}

	// @private
	// @param {Number} number
	_removePanels( number ) {
		while ( number-- ) {
			const view = this.content.last;

			this.content.remove( view );
			this.deregisterChild( view );
			view.destroy();
		}
	}

	// Updates coordinates of fake panels.
	updatePosition() {
		if ( this.numberOfPanels ) {
			const { top, left } = this._balloonPanelView;
			const { width, height } = new Rect( this._balloonPanelView.element );

			Object.assign( this, { top, left, width, height } );
		}
	}
}var css_248z$B = ".ck.ck-sticky-panel .ck-sticky-panel__content_sticky{position:fixed;top:0;z-index:var(--ck-z-modal)}.ck.ck-sticky-panel .ck-sticky-panel__content_sticky_bottom-limit{position:absolute;top:auto}.ck.ck-sticky-panel .ck-sticky-panel__content_sticky{border-top-left-radius:0;border-top-right-radius:0;border-width:0 1px 1px;box-shadow:var(--ck-drop-shadow),0 0}";
styleInject(css_248z$B);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const toPx$3 = toUnit( 'px' );

/**
 * The sticky panel view class.
 */
class StickyPanelView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * Controls whether the sticky panel should be active.
		 *
		 * @readonly
		 * @observable
		 * @member {Boolean} #isActive
		 */
		this.set( 'isActive', false );

		/**
		 * Controls whether the sticky panel is in the "sticky" state.
		 *
		 * @readonly
		 * @observable
		 * @member {Boolean} #isSticky
		 */
		this.set( 'isSticky', false );

		/**
		 * The limiter element for the sticky panel instance. Its bounding rect limits
		 * the "stickyness" of the panel, i.e. when the panel reaches the bottom
		 * edge of the limiter, it becomes sticky to that edge and does not float
		 * off the limiter. It is mandatory for the panel to work properly and once
		 * set, it cannot be changed.
		 *
		 * @readonly
		 * @observable
		 * @member {HTMLElement} #limiterElement
		 */
		this.set( 'limiterElement', null );

		/**
		 * The offset from the bottom edge of {@link #limiterElement}
		 * which stops the panel from stickying any further to prevent limiter's content
		 * from being completely covered.
		 *
		 * @readonly
		 * @observable
		 * @default 50
		 * @member {Number} #limiterBottomOffset
		 */
		this.set( 'limiterBottomOffset', 50 );

		/**
		 * The offset from the top edge of the web browser's viewport which makes the
		 * panel become sticky. The default value is `0`, which means the panel becomes
		 * sticky when it's upper edge touches the top of the page viewport.
		 *
		 * This attribute is useful when the web page has UI elements positioned to the top
		 * either using `position: fixed` or `position: sticky`, which would cover the
		 * sticky panel or vice–versa (depending on the `z-index` hierarchy).
		 *
		 * Bound to {@link module:core/editor/editorui~EditorUI#viewportOffset `EditorUI#viewportOffset`}.
		 *
		 * If {@link module:core/editor/editorconfig~EditorConfig#ui `EditorConfig#ui.viewportOffset.top`} is defined, then
		 * it will override the default value.
		 *
		 * @observable
		 * @default 0
		 * @member {Number} #viewportTopOffset
		 */
		this.set( 'viewportTopOffset', 0 );

		/**
		 * Controls the `margin-left` CSS style of the panel.
		 *
		 * @protected
		 * @readonly
		 * @observable
		 * @member {String} #_marginLeft
		 */
		this.set( '_marginLeft', null );

		/**
		 * Set `true` if the sticky panel reached the bottom edge of the
		 * {@link #limiterElement}.
		 *
		 * @protected
		 * @readonly
		 * @observable
		 * @member {Boolean} #_isStickyToTheLimiter
		 */
		this.set( '_isStickyToTheLimiter', false );

		/**
		 * Set `true` if the sticky panel uses the {@link #viewportTopOffset},
		 * i.e. not {@link #_isStickyToTheLimiter} and the {@link #viewportTopOffset}
		 * is not `0`.
		 *
		 * @protected
		 * @readonly
		 * @observable
		 * @member {Boolean} #_hasViewportTopOffset
		 */
		this.set( '_hasViewportTopOffset', false );

		/**
		 * Collection of the child views which creates balloon panel contents.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.content = this.createCollection();

		/**
		 * The DOM bounding client rect of the {@link module:ui/view~View#element} of the panel.
		 *
		 * @protected
		 * @member {Object} #_panelRect
		 */

		/**
		 * The DOM bounding client rect of the {@link #limiterElement}
		 * of the panel.
		 *
		 * @protected
		 * @member {Object} #_limiterRect
		 */

		/**
		 * A dummy element which visually fills the space as long as the
		 * actual panel is sticky. It prevents flickering of the UI.
		 *
		 * @protected
		 * @property {HTMLElement}
		 */
		this._contentPanelPlaceholder = new Template( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-sticky-panel__placeholder'
				],
				style: {
					display: bind.to( 'isSticky', isSticky => isSticky ? 'block' : 'none' ),
					height: bind.to( 'isSticky', isSticky => {
						return isSticky ? toPx$3( this._panelRect.height ) : null;
					} )
				}
			}
		} ).render();

		/**
		 * The panel which accepts children into {@link #content} collection.
		 * Also an element which is positioned when {@link #isSticky}.
		 *
		 * @protected
		 * @property {HTMLElement}
		 */
		this._contentPanel = new Template( {
			tag: 'div',

			attributes: {
				class: [
					'ck',
					'ck-sticky-panel__content',
					// Toggle class of the panel when "sticky" state changes in the view.
					bind.if( 'isSticky', 'ck-sticky-panel__content_sticky' ),
					bind.if( '_isStickyToTheLimiter', 'ck-sticky-panel__content_sticky_bottom-limit' )
				],
				style: {
					width: bind.to( 'isSticky', isSticky => {
						return isSticky ? toPx$3( this._contentPanelPlaceholder.getBoundingClientRect().width ) : null;
					} ),

					top: bind.to( '_hasViewportTopOffset', _hasViewportTopOffset => {
						return _hasViewportTopOffset ? toPx$3( this.viewportTopOffset ) : null;
					} ),

					bottom: bind.to( '_isStickyToTheLimiter', _isStickyToTheLimiter => {
						return _isStickyToTheLimiter ? toPx$3( this.limiterBottomOffset ) : null;
					} ),

					marginLeft: bind.to( '_marginLeft' )
				}
			},

			children: this.content
		} ).render();

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-sticky-panel'
				]
			},
			children: [
				this._contentPanelPlaceholder,
				this._contentPanel
			]
		} );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		// Check if the panel should go into the sticky state immediately.
		this._checkIfShouldBeSticky();

		// Update sticky state of the panel as the window is being scrolled.
		this.listenTo( global$2.window, 'scroll', () => {
			this._checkIfShouldBeSticky();
		} );

		// Synchronize with `model.isActive` because sticking an inactive panel is pointless.
		this.listenTo( this, 'change:isActive', () => {
			this._checkIfShouldBeSticky();
		} );
	}

	/**
	 * Analyzes the environment to decide whether the panel should
	 * be sticky or not.
	 *
	 * @protected
	 */
	_checkIfShouldBeSticky() {
		const panelRect = this._panelRect = this._contentPanel.getBoundingClientRect();
		let limiterRect;

		if ( !this.limiterElement ) {
			this.isSticky = false;
		} else {
			limiterRect = this._limiterRect = this.limiterElement.getBoundingClientRect();

			// The panel must be active to become sticky.
			this.isSticky = this.isActive &&
				// The limiter's top edge must be beyond the upper edge of the visible viewport (+the viewportTopOffset).
				limiterRect.top < this.viewportTopOffset &&
				// The model#limiterElement's height mustn't be smaller than the panel's height and model#limiterBottomOffset.
				// There's no point in entering the sticky mode if the model#limiterElement is very, very small, because
				// it would immediately set model#_isStickyToTheLimiter true and, given model#limiterBottomOffset, the panel
				// would be positioned before the model#limiterElement.
				this._panelRect.height + this.limiterBottomOffset < limiterRect.height;
		}

		// Stick the panel to the top edge of the viewport simulating CSS position:sticky.
		// TODO: Possibly replaced by CSS in the future http://caniuse.com/#feat=css-sticky
		if ( this.isSticky ) {
			this._isStickyToTheLimiter =
				limiterRect.bottom < panelRect.height + this.limiterBottomOffset + this.viewportTopOffset;
			this._hasViewportTopOffset = !this._isStickyToTheLimiter && !!this.viewportTopOffset;
			this._marginLeft = this._isStickyToTheLimiter ? null : toPx$3( -global$2.window.scrollX );
		}
		// Detach the panel from the top edge of the viewport.
		else {
			this._isStickyToTheLimiter = false;
			this._hasViewportTopOffset = false;
			this._marginLeft = null;
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const toPx$2 = toUnit( 'px' );

/**
 * The contextual toolbar.
 *
 * It uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
 *
 * @extends module:core/plugin~Plugin
 */
class BalloonToolbar extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'BalloonToolbar';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ ContextualBalloon ];
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		/**
		 * A cached and normalized `config.balloonToolbar` object.
		 *
		 * @type {module:core/editor/editorconfig~EditorConfig#balloonToolbar}
		 * @private
		 */
		this._balloonConfig = normalizeToolbarConfig( editor.config.get( 'balloonToolbar' ) );

		/**
		 * The toolbar view displayed in the balloon.
		 *
		 * @type {module:ui/toolbar/toolbarview~ToolbarView}
		 */
		this.toolbarView = this._createToolbarView();

		/**
		 * Tracks the focus of the {@link module:core/editor/editorui~EditorUI#getEditableElement editable element}
		 * and the {@link #toolbarView}. When both are blurred then the toolbar should hide.
		 *
		 * @readonly
		 * @type {module:utils:focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		// Wait for the EditorUI#init. EditableElement is not available before.
		editor.ui.once( 'ready', () => {
			this.focusTracker.add( editor.ui.getEditableElement() );
			this.focusTracker.add( this.toolbarView.element );
		} );

		// Register the toolbar so it becomes available for Alt+F10 and Esc navigation.
		editor.ui.addToolbar( this.toolbarView, {
			beforeFocus: () => this.show( true ),
			afterBlur: () => this.hide(),
			isContextual: true
		} );

		/**
		 * An instance of the resize observer that allows to respond to changes in editable's geometry
		 * so the toolbar can stay within its boundaries (and group toolbar items that do not fit).
		 *
		 * **Note**: Used only when `shouldNotGroupWhenFull` was **not** set in the
		 * {@link module:core/editor/editorconfig~EditorConfig#balloonToolbar configuration}.
		 *
		 * **Note:** Created in {@link #init}.
		 *
		 * @protected
		 * @member {module:utils/dom/resizeobserver~ResizeObserver}
		 */
		this._resizeObserver = null;

		/**
		 * The contextual balloon plugin instance.
		 *
		 * @private
		 * @type {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
		 */
		this._balloon = editor.plugins.get( ContextualBalloon );

		/**
		 * Fires {@link #event:_selectionChangeDebounced} event using `lodash#debounce`.
		 *
		 * This function is stored as a plugin property to make possible to cancel
		 * trailing debounced invocation on destroy.
		 *
		 * @private
		 * @type {Function}
		 */
		this._fireSelectionChangeDebounced = debounce( () => this.fire( '_selectionChangeDebounced' ), 200 );

		// The appearance of the BalloonToolbar method is event–driven.
		// It is possible to stop the #show event and this prevent the toolbar from showing up.
		this.decorate( 'show' );
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const selection = editor.model.document.selection;

		// Show/hide the toolbar on editable focus/blur.
		this.listenTo( this.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
			const isToolbarVisible = this._balloon.visibleView === this.toolbarView;

			if ( !isFocused && isToolbarVisible ) {
				this.hide();
			} else if ( isFocused ) {
				this.show();
			}
		} );

		// Hide the toolbar when the selection is changed by a direct change or has changed to collapsed.
		this.listenTo( selection, 'change:range', ( evt, data ) => {
			if ( data.directChange || selection.isCollapsed ) {
				this.hide();
			}

			// Fire internal `_selectionChangeDebounced` event to use it for showing
			// the toolbar after the selection stops changing.
			this._fireSelectionChangeDebounced();
		} );

		// Show the toolbar when the selection stops changing.
		this.listenTo( this, '_selectionChangeDebounced', () => {
			if ( this.editor.editing.view.document.isFocused ) {
				this.show();
			}
		} );

		if ( !this._balloonConfig.shouldNotGroupWhenFull ) {
			this.listenTo( editor, 'ready', () => {
				const editableElement = editor.ui.view.editable.element;

				// Set #toolbarView's max-width on the initialization and update it on the editable resize.
				this._resizeObserver = new ResizeObserver( editableElement, () => {
					// The max-width equals 90% of the editable's width for the best user experience.
					// The value keeps the balloon very close to the boundaries of the editable and limits the cases
					// when the balloon juts out from the editable element it belongs to.
					this.toolbarView.maxWidth = toPx$2( new Rect( editableElement ).width * .9 );
				} );
			} );
		}

		// Listen to the toolbar view and whenever it changes its geometry due to some items being
		// grouped or ungrouped, update the position of the balloon because a shorter/longer toolbar
		// means the balloon could be pointing at the wrong place. Once updated, the balloon will point
		// at the right selection in the content again.
		// https://github.com/ckeditor/ckeditor5/issues/6444
		this.listenTo( this.toolbarView, 'groupedItemsUpdate', () => {
			this._updatePosition();
		} );
	}

	/**
	 * Creates toolbar components based on given configuration.
	 * This needs to be done when all plugins are ready.
	 *
	 * @inheritDoc
	 */
	afterInit() {
		const factory = this.editor.ui.componentFactory;

		this.toolbarView.fillFromConfig( this._balloonConfig, factory );
	}

	/**
	 * Creates the toolbar view instance.
	 *
	 * @private
	 * @returns {module:ui/toolbar/toolbarview~ToolbarView}
	 */
	_createToolbarView() {
		const t = this.editor.locale.t;
		const shouldGroupWhenFull = !this._balloonConfig.shouldNotGroupWhenFull;
		const toolbarView = new ToolbarView( this.editor.locale, {
			shouldGroupWhenFull,
			isFloating: true
		} );

		toolbarView.ariaLabel = t( 'Editor contextual toolbar' );
		toolbarView.render();

		return toolbarView;
	}

	/**
	 * Shows the toolbar and attaches it to the selection.
	 *
	 * Fires {@link #event:show} event which can be stopped to prevent the toolbar from showing up.
	 *
	 * @param {Boolean} [showForCollapsedSelection=false] When set `true`, the toolbar will show despite collapsed selection in the
	 * editing view.
	 */
	show( showForCollapsedSelection = false ) {
		const editor = this.editor;
		const selection = editor.model.document.selection;
		const schema = editor.model.schema;

		// Do not add the toolbar to the balloon stack twice.
		if ( this._balloon.hasView( this.toolbarView ) ) {
			return;
		}

		// Do not show the toolbar when the selection is collapsed.
		if ( selection.isCollapsed && !showForCollapsedSelection ) {
			return;
		}

		// Do not show the toolbar when there is more than one range in the selection and they fully contain selectable elements.
		// See https://github.com/ckeditor/ckeditor5/issues/6443.
		if ( selectionContainsOnlyMultipleSelectables( selection, schema ) ) {
			return;
		}

		// Don not show the toolbar when all components inside are disabled
		// see https://github.com/ckeditor/ckeditor5-ui/issues/269.
		if ( Array.from( this.toolbarView.items ).every( item => item.isEnabled !== undefined && !item.isEnabled ) ) {
			return;
		}

		// Update the toolbar position when the editor ui should be refreshed.
		this.listenTo( this.editor.ui, 'update', () => {
			this._updatePosition();
		} );

		// Add the toolbar to the common editor contextual balloon.
		this._balloon.add( {
			view: this.toolbarView,
			position: this._getBalloonPositionData(),
			balloonClassName: 'ck-toolbar-container'
		} );
	}

	/**
	 * Hides the toolbar.
	 */
	hide() {
		if ( this._balloon.hasView( this.toolbarView ) ) {
			this.stopListening( this.editor.ui, 'update' );
			this._balloon.remove( this.toolbarView );
		}
	}

	/**
	 * Returns positioning options for the {@link #_balloon}. They control the way balloon is attached
	 * to the selection.
	 *
	 * @private
	 * @returns {module:utils/dom/position~Options}
	 */
	_getBalloonPositionData() {
		const editor = this.editor;
		const view = editor.editing.view;
		const viewDocument = view.document;
		const viewSelection = viewDocument.selection;

		// Get direction of the selection.
		const isBackward = viewDocument.selection.isBackward;

		return {
			// Because the target for BalloonPanelView is a Rect (not DOMRange), it's geometry will stay fixed
			// as the window scrolls. To let the BalloonPanelView follow such Rect, is must be continuously
			// computed and hence, the target is defined as a function instead of a static value.
			// https://github.com/ckeditor/ckeditor5-ui/issues/195
			target: () => {
				const range = isBackward ? viewSelection.getFirstRange() : viewSelection.getLastRange();
				const rangeRects = Rect.getDomRangeRects( view.domConverter.viewRangeToDom( range ) );

				// Select the proper range rect depending on the direction of the selection.
				if ( isBackward ) {
					return rangeRects[ 0 ];
				} else {
					// Ditch the zero-width "orphan" rect in the next line for the forward selection if there's
					// another one preceding it. It is not rendered as a selection by the web browser anyway.
					// https://github.com/ckeditor/ckeditor5-ui/issues/308
					if ( rangeRects.length > 1 && rangeRects[ rangeRects.length - 1 ].width === 0 ) {
						rangeRects.pop();
					}

					return rangeRects[ rangeRects.length - 1 ];
				}
			},
			positions: this._getBalloonPositions( isBackward )
		};
	}

	/**
	 * Updates the position of the {@link #_balloon} to make up for changes:
	 *
	 * * in the geometry of the selection it is attached to (e.g. the selection moved in the viewport or expanded or shrunk),
	 * * or the geometry of the balloon toolbar itself (e.g. the toolbar has grouped or ungrouped some items and it is shorter or longer).
	 *
	 * @private
	 */
	_updatePosition() {
		this._balloon.updatePosition( this._getBalloonPositionData() );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this.stopListening();
		this._fireSelectionChangeDebounced.cancel();
		this.toolbarView.destroy();
		this.focusTracker.destroy();

		if ( this._resizeObserver ) {
			this._resizeObserver.destroy();
		}
	}

	/**
	 * This event is fired just before the toolbar shows up. Stopping this event will prevent this.
	 *
	 * @event show
	 */

	/**
	 * This is internal plugin event which is fired 200 ms after model selection last change.
	 * This is to makes easy test debounced action without need to use `setTimeout`.
	 *
	 * @protected
	 * @event _selectionChangeDebounced
	 */

	/**
	 * Returns toolbar positions for the given direction of the selection.
	 *
	 * @private
	 * @param {Boolean} isBackward
	 * @returns {Array.<module:utils/dom/position~Position>}
	 */
	_getBalloonPositions( isBackward ) {
		const isSafariIniOS = env.isSafari && env.isiOS;

		// https://github.com/ckeditor/ckeditor5/issues/7707
		const positions = isSafariIniOS ? generatePositions( {
			// 20px when zoomed out. Less then 20px when zoomed in; the "radius" of the native selection handle gets
			// smaller as the user zooms in. No less than the default v-offset, though.
			heightOffset: Math.max(
				BalloonPanelView.arrowHeightOffset,
				Math.round( 20 / global$2.window.visualViewport.scale )
			)
		} ) : BalloonPanelView.defaultPositions;

		return isBackward ? [
			positions.northWestArrowSouth,
			positions.northWestArrowSouthWest,
			positions.northWestArrowSouthEast,
			positions.northWestArrowSouthMiddleEast,
			positions.northWestArrowSouthMiddleWest,
			positions.southWestArrowNorth,
			positions.southWestArrowNorthWest,
			positions.southWestArrowNorthEast,
			positions.southWestArrowNorthMiddleWest,
			positions.southWestArrowNorthMiddleEast
		] : [
			positions.southEastArrowNorth,
			positions.southEastArrowNorthEast,
			positions.southEastArrowNorthWest,
			positions.southEastArrowNorthMiddleEast,
			positions.southEastArrowNorthMiddleWest,
			positions.northEastArrowSouth,
			positions.northEastArrowSouthEast,
			positions.northEastArrowSouthWest,
			positions.northEastArrowSouthMiddleEast,
			positions.northEastArrowSouthMiddleWest
		];
	}
}

// Returns "true" when the selection has multiple ranges and each range contains a selectable element
// and nothing else.
//
// @private
// @param {module:engine/model/selection~Selection} selection
// @param {module:engine/model/schema~Schema} schema
// @returns {Boolean}
function selectionContainsOnlyMultipleSelectables( selection, schema ) {
	// It doesn't contain multiple objects if there is only one range.
	if ( selection.rangeCount === 1 ) {
		return false;
	}

	return [ ...selection.getRanges() ].every( range => {
		const element = range.getContainedElement();

		return element && schema.isSelectable( element );
	} );
}

/**
 * Contextual toolbar configuration. Used by the {@link module:ui/toolbar/balloon/balloontoolbar~BalloonToolbar}
 * feature.
 *
 * ## Configuring toolbar items
 *
 *		const config = {
 *			balloonToolbar: [ 'bold', 'italic', 'undo', 'redo' ]
 *		};
 *
 * You can also use `'|'` to create a separator between groups of items:
 *
 *		const config = {
 *			balloonToolbar: [ 'bold', 'italic', '|', 'undo', 'redo' ]
 *		};
 *
 * Read also about configuring the main editor toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
 *
 * ## Configuring items grouping
 *
 * You can prevent automatic items grouping by setting the `shouldNotGroupWhenFull` option:
 *
 *		const config = {
 *			balloonToolbar: {
 *				items: [ 'bold', 'italic', 'undo', 'redo' ],
 *				shouldNotGroupWhenFull: true
 *			},
 *		};
 *
 * @member {Array.<String>|Object} module:core/editor/editorconfig~EditorConfig#balloonToolbar
 */var css_248z$A = ".ck.ck-block-toolbar-button{position:absolute;z-index:var(--ck-z-default)}:root{--ck-color-block-toolbar-button:var(--ck-color-text);--ck-block-toolbar-button-size:var(--ck-font-size-normal)}.ck.ck-block-toolbar-button{color:var(--ck-color-block-toolbar-button);font-size:var(--ck-block-toolbar-size)}";
styleInject(css_248z$A);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const toPx$1 = toUnit( 'px' );

/**
 * The block button view class.
 *
 * This view represents a button attached next to block element where the selection is anchored.
 *
 * See {@link module:ui/toolbar/block/blocktoolbar~BlockToolbar}.
 *
 * @extends {module:ui/button/buttonview~ButtonView}
 */
class BlockButtonView extends ButtonView {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		const bind = this.bindTemplate;

		// Hide button on init.
		this.isVisible = false;

		this.isToggleable = true;

		/**
		 * Top offset.
		 *
		 * @member {Number} #top
		 */
		this.set( 'top', 0 );

		/**
		 * Left offset.
		 *
		 * @member {Number} #left
		 */
		this.set( 'left', 0 );

		this.extendTemplate( {
			attributes: {
				class: 'ck-block-toolbar-button',
				style: {
					top: bind.to( 'top', val => toPx$1( val ) ),
					left: bind.to( 'left', val => toPx$1( val ) )
				}
			}
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const toPx = toUnit( 'px' );

/**
 * The block toolbar plugin.
 *
 * This plugin provides a button positioned next to the block of content where the selection is anchored.
 * Upon clicking the button, a dropdown providing access to editor features shows up, as configured in
 * {@link module:core/editor/editorconfig~EditorConfig#blockToolbar}.
 *
 * By default, the button is displayed next to all elements marked in {@link module:engine/model/schema~Schema}
 * as `$block` for which the toolbar provides at least one option.
 *
 * By default, the button is attached so its right boundary is touching the
 * {@link module:engine/view/editableelement~EditableElement}:
 *
 * 		 __ |
 * 		|  ||  This is a block of content that the
 * 		 ¯¯ |  button is attached to. This is a
 * 		    |  block of content that the button is
 * 		    |  attached to.
 *
 * The position of the button can be adjusted using the CSS `transform` property:
 *
 * 		.ck-block-toolbar-button {
 * 			transform: translateX( -10px );
 * 		}
 *
 * 		 __   |
 * 		|  |  |  This is a block of content that the
 * 		 ¯¯   |  button is attached to. This is a
 * 		      |  block of content that the button is
 * 		      |  attached to.
 *
 * **Note**: If you plan to run the editor in a right–to–left (RTL) language, keep in mind the button
 * will be attached to the **right** boundary of the editable area. In that case, make sure the
 * CSS position adjustment works properly by adding the following styles:
 *
 * 		.ck[dir="rtl"] .ck-block-toolbar-button {
 * 			transform: translateX( 10px );
 * 		}
 *
 * @extends module:core/plugin~Plugin
 */
class BlockToolbar extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'BlockToolbar';
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		/**
		 * A cached and normalized `config.blockToolbar` object.
		 *
		 * @type {module:core/editor/editorconfig~EditorConfig#blockToolbar}
		 * @private
		 */
		this._blockToolbarConfig = normalizeToolbarConfig( this.editor.config.get( 'blockToolbar' ) );

		/**
		 * The toolbar view.
		 *
		 * @type {module:ui/toolbar/toolbarview~ToolbarView}
		 */
		this.toolbarView = this._createToolbarView();

		/**
		 * The balloon panel view, containing the {@link #toolbarView}.
		 *
		 * @type {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
		 */
		this.panelView = this._createPanelView();

		/**
		 * The button view that opens the {@link #toolbarView}.
		 *
		 * @type {module:ui/toolbar/block/blockbuttonview~BlockButtonView}
		 */
		this.buttonView = this._createButtonView();

		/**
		 * An instance of the resize observer that allows to respond to changes in editable's geometry
		 * so the toolbar can stay within its boundaries (and group toolbar items that do not fit).
		 *
		 * **Note**: Used only when `shouldNotGroupWhenFull` was **not** set in the
		 * {@link module:core/editor/editorconfig~EditorConfig#blockToolbar configuration}.
		 *
		 * **Note:** Created in {@link #afterInit}.
		 *
		 * @protected
		 * @member {module:utils/dom/resizeobserver~ResizeObserver}
		 */
		this._resizeObserver = null;

		// Close the #panelView upon clicking outside of the plugin UI.
		clickOutsideHandler( {
			emitter: this.panelView,
			contextElements: [ this.panelView.element, this.buttonView.element ],
			activator: () => this.panelView.isVisible,
			callback: () => this._hidePanel()
		} );
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		// Hides panel on a direct selection change.
		this.listenTo( editor.model.document.selection, 'change:range', ( evt, data ) => {
			if ( data.directChange ) {
				this._hidePanel();
			}
		} );

		this.listenTo( editor.ui, 'update', () => this._updateButton() );
		// `low` priority is used because of https://github.com/ckeditor/ckeditor5-core/issues/133.
		this.listenTo( editor, 'change:isReadOnly', () => this._updateButton(), { priority: 'low' } );
		this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => this._updateButton() );

		// Reposition button on resize.
		this.listenTo( this.buttonView, 'change:isVisible', ( evt, name, isVisible ) => {
			if ( isVisible ) {
				// Keep correct position of button and panel on window#resize.
				this.buttonView.listenTo( window, 'resize', () => this._updateButton() );
			} else {
				// Stop repositioning button when is hidden.
				this.buttonView.stopListening( window, 'resize' );

				// Hide the panel when the button disappears.
				this._hidePanel();
			}
		} );

		// Register the toolbar so it becomes available for Alt+F10 and Esc navigation.
		editor.ui.addToolbar( this.toolbarView, {
			beforeFocus: () => this._showPanel(),
			afterBlur: () => this._hidePanel()
		} );
	}

	/**
	 * Fills the toolbar with its items based on the configuration.
	 *
	 * **Note:** This needs to be done after all plugins are ready.
	 *
	 * @inheritDoc
	 */
	afterInit() {
		const factory = this.editor.ui.componentFactory;
		const config = this._blockToolbarConfig;

		this.toolbarView.fillFromConfig( config, factory );

		// Hide panel before executing each button in the panel.
		for ( const item of this.toolbarView.items ) {
			item.on( 'execute', () => this._hidePanel( true ), { priority: 'high' } );
		}

		if ( !config.shouldNotGroupWhenFull ) {
			this.listenTo( this.editor, 'ready', () => {
				const editableElement = this.editor.ui.view.editable.element;

				// Set #toolbarView's max-width just after the initialization and update it on the editable resize.
				this._resizeObserver = new ResizeObserver( editableElement, () => {
					this.toolbarView.maxWidth = this._getToolbarMaxWidth();
				} );
			} );
		}
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		// Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
		this.panelView.destroy();
		this.buttonView.destroy();
		this.toolbarView.destroy();

		if ( this._resizeObserver ) {
			this._resizeObserver.destroy();
		}
	}

	/**
	 * Creates the {@link #toolbarView}.
	 *
	 * @private
	 * @returns {module:ui/toolbar/toolbarview~ToolbarView}
	 */
	_createToolbarView() {
		const t = this.editor.locale.t;
		const shouldGroupWhenFull = !this._blockToolbarConfig.shouldNotGroupWhenFull;
		const toolbarView = new ToolbarView( this.editor.locale, {
			shouldGroupWhenFull,
			isFloating: true
		} );

		toolbarView.ariaLabel = t( 'Editor block content toolbar' );

		// When toolbar lost focus then panel should hide.
		toolbarView.focusTracker.on( 'change:isFocused', ( evt, name, is ) => {
			if ( !is ) {
				this._hidePanel();
			}
		} );

		return toolbarView;
	}

	/**
	 * Creates the {@link #panelView}.
	 *
	 * @private
	 * @returns {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
	 */
	_createPanelView() {
		const editor = this.editor;
		const panelView = new BalloonPanelView( editor.locale );

		panelView.content.add( this.toolbarView );
		panelView.class = 'ck-toolbar-container';
		editor.ui.view.body.add( panelView );
		editor.ui.focusTracker.add( panelView.element );

		// Close #panelView on `Esc` press.
		this.toolbarView.keystrokes.set( 'Esc', ( evt, cancel ) => {
			this._hidePanel( true );
			cancel();
		} );

		return panelView;
	}

	/**
	 * Creates the {@link #buttonView}.
	 *
	 * @private
	 * @returns {module:ui/toolbar/block/blockbuttonview~BlockButtonView}
	 */
	_createButtonView() {
		const editor = this.editor;
		const t = editor.t;
		const buttonView = new BlockButtonView( editor.locale );

		buttonView.set( {
			label: t( 'Edit block' ),
			icon: pilcrow,
			withText: false
		} );

		// Bind the panelView observable properties to the buttonView.
		buttonView.bind( 'isOn' ).to( this.panelView, 'isVisible' );
		buttonView.bind( 'tooltip' ).to( this.panelView, 'isVisible', isVisible => !isVisible );

		// Toggle the panelView upon buttonView#execute.
		this.listenTo( buttonView, 'execute', () => {
			if ( !this.panelView.isVisible ) {
				this._showPanel();
			} else {
				this._hidePanel( true );
			}
		} );

		editor.ui.view.body.add( buttonView );
		editor.ui.focusTracker.add( buttonView.element );

		return buttonView;
	}

	/**
	 * Shows or hides the button.
	 * When all the conditions for displaying the button are matched, it shows the button. Hides otherwise.
	 *
	 * @private
	 */
	_updateButton() {
		const editor = this.editor;
		const model = editor.model;
		const view = editor.editing.view;

		// Hides the button when the editor is not focused.
		if ( !editor.ui.focusTracker.isFocused ) {
			this._hideButton();

			return;
		}

		// Hides the button when the editor switches to the read-only mode.
		if ( editor.isReadOnly ) {
			this._hideButton();

			return;
		}

		// Get the first selected block, button will be attached to this element.
		const modelTarget = Array.from( model.document.selection.getSelectedBlocks() )[ 0 ];

		// Hides the button when there is no enabled item in toolbar for the current block element.
		if ( !modelTarget || Array.from( this.toolbarView.items ).every( item => !item.isEnabled ) ) {
			this._hideButton();

			return;
		}

		// Get DOM target element.
		const domTarget = view.domConverter.mapViewToDom( editor.editing.mapper.toViewElement( modelTarget ) );

		// Show block button.
		this.buttonView.isVisible = true;

		// Attach block button to target DOM element.
		this._attachButtonToElement( domTarget );

		// When panel is opened then refresh it position to be properly aligned with block button.
		if ( this.panelView.isVisible ) {
			this._showPanel();
		}
	}

	/**
	 * Hides the button.
	 *
	 * @private
	 */
	_hideButton() {
		this.buttonView.isVisible = false;
	}

	/**
	 * Shows the {@link #toolbarView} attached to the {@link #buttonView}.
	 * If the toolbar is already visible, then it simply repositions it.
	 *
	 * @private
	 */
	_showPanel() {
		// Usually, the only way to show the toolbar is by pressing the block button. It makes it impossible for
		// the toolbar to show up when the button is invisible (feature does not make sense for the selection then).
		// The toolbar navigation using Alt+F10 does not access the button but shows the panel directly using this method.
		// So we need to check whether this is possible first.
		if ( !this.buttonView.isVisible ) {
			return;
		}

		const wasVisible = this.panelView.isVisible;

		// So here's the thing: If there was no initial panelView#show() or these two were in different order, the toolbar
		// positioning will break in RTL editors. Weird, right? What you show know is that the toolbar
		// grouping works thanks to:
		//
		// * the ResizeObserver, which kicks in as soon as the toolbar shows up in DOM (becomes visible again).
		// * the observable ToolbarView#maxWidth, which triggers re-grouping when changed.
		//
		// Here are the possible scenarios:
		//
		// 1. (WRONG ❌) If the #maxWidth is set when the toolbar is invisible, it won't affect item grouping (no DOMRects, no grouping).
		//    Then, when panelView.pin() is called, the position of the toolbar will be calculated for the old
		//    items grouping state, and when finally ResizeObserver kicks in (hey, the toolbar is visible now, right?)
		//    it will group/ungroup some items and the length of the toolbar will change. But since in RTL the toolbar
		//    is attached on the right side and the positioning uses CSS "left", it will result in the toolbar shifting
		//    to the left and being displayed in the wrong place.
		// 2. (WRONG ❌) If the panelView.pin() is called first and #maxWidth set next, then basically the story repeats. The balloon
		//    calculates the position for the old toolbar grouping state, then the toolbar re-groups items and because
		//    it is positioned using CSS "left" it will move.
		// 3. (RIGHT ✅) We show the panel first (the toolbar does re-grouping but it does not matter), then the #maxWidth
		//    is set allowing the toolbar to re-group again and finally panelView.pin() does the positioning when the
		//    items grouping state is stable and final.
		//
		// https://github.com/ckeditor/ckeditor5/issues/6449, https://github.com/ckeditor/ckeditor5/issues/6575
		this.panelView.show();
		this.toolbarView.maxWidth = this._getToolbarMaxWidth();

		this.panelView.pin( {
			target: this.buttonView.element,
			limiter: this.editor.ui.getEditableElement()
		} );

		if ( !wasVisible ) {
			this.toolbarView.items.get( 0 ).focus();
		}
	}

	/**
	 * Hides the {@link #toolbarView}.
	 *
	 * @private
	 * @param {Boolean} [focusEditable=false] When `true`, the editable will be focused after hiding the panel.
	 */
	_hidePanel( focusEditable ) {
		this.panelView.isVisible = false;

		if ( focusEditable ) {
			this.editor.editing.view.focus();
		}
	}

	/**
	 * Attaches the {@link #buttonView} to the target block of content.
	 *
	 * @protected
	 * @param {HTMLElement} targetElement Target element.
	 */
	_attachButtonToElement( targetElement ) {
		const contentStyles = window.getComputedStyle( targetElement );

		const editableRect = new Rect( this.editor.ui.getEditableElement() );
		const contentPaddingTop = parseInt( contentStyles.paddingTop, 10 );
		// When line height is not an integer then thread it as "normal".
		// MDN says that 'normal' == ~1.2 on desktop browsers.
		const contentLineHeight = parseInt( contentStyles.lineHeight, 10 ) || parseInt( contentStyles.fontSize, 10 ) * 1.2;

		const position = getOptimalPosition( {
			element: this.buttonView.element,
			target: targetElement,
			positions: [
				( contentRect, buttonRect ) => {
					let left;

					if ( this.editor.locale.uiLanguageDirection === 'ltr' ) {
						left = editableRect.left - buttonRect.width;
					} else {
						left = editableRect.right;
					}

					return {
						top: contentRect.top + contentPaddingTop + ( contentLineHeight - buttonRect.height ) / 2,
						left
					};
				}
			]
		} );

		this.buttonView.top = position.top;
		this.buttonView.left = position.left;
	}

	/**
	 * Gets the {@link #toolbarView} max-width, based on
	 * editable width plus distance between farthest edge of the {@link #buttonView} and the editable.
	 *
	 * @private
	 * @returns {String} maxWidth A maximum width that toolbar can have, in pixels.
	 */
	_getToolbarMaxWidth() {
		const editableElement = this.editor.ui.view.editable.element;
		const editableRect = new Rect( editableElement );
		const buttonRect = new Rect( this.buttonView.element );
		const isRTL = this.editor.locale.uiLanguageDirection === 'rtl';
		const offset = isRTL ? ( buttonRect.left - editableRect.right ) + buttonRect.width : editableRect.left - buttonRect.left;

		return toPx( editableRect.width + offset );
	}
}

/**
 * The block toolbar configuration. Used by the {@link module:ui/toolbar/block/blocktoolbar~BlockToolbar}
 * feature.
 *
 *		const config = {
 *			blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'bulletedList', 'numberedList' ]
 *		};
 *
 * You can also use `'|'` to create a separator between groups of items:
 *
 *		const config = {
 *			blockToolbar: [ 'paragraph', 'heading1', 'heading2', '|', 'bulletedList', 'numberedList' ]
 *		};
 *
 * ## Configuring items grouping
 *
 * You can prevent automatic items grouping by setting the `shouldNotGroupWhenFull` option:
 *
 *		const config = {
 *			blockToolbar: {
 *				items: [ 'paragraph', 'heading1', 'heading2', '|', 'bulletedList', 'numberedList' ],
 *				shouldNotGroupWhenFull: true
 *			},
 *		};
 *
 * Read more about configuring the main editor toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
 *
 * @member {Array.<String>|Object} module:core/editor/editorconfig~EditorConfig#blockToolbar
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */var index$6=/*#__PURE__*/Object.freeze({__proto__:null,clickOutsideHandler:clickOutsideHandler,injectCssTransitionDisabler:injectCssTransitionDisabler,submitHandler:submitHandler,addKeyboardHandlingForGrid:addKeyboardHandlingForGrid,BodyCollection:BodyCollection,ButtonView:ButtonView,SwitchButtonView:SwitchButtonView,ColorGridView:ColorGridView,ColorTileView:ColorTileView,DropdownButtonView:DropdownButtonView,SplitButtonView:SplitButtonView,EditorUIView:EditorUIView,BoxedEditorUIView:BoxedEditorUIView,InlineEditableUIView:InlineEditableUIView,FormHeaderView:FormHeaderView,FocusCycler:FocusCycler,IconView:IconView,InputView:InputView,InputTextView:InputTextView,InputNumberView:InputNumberView,IframeView:IframeView,LabelView:LabelView,LabeledFieldView:LabeledFieldView,ListItemView:ListItemView,ListView:ListView,Notification:Notification,Model:Model,BalloonPanelView:BalloonPanelView,ContextualBalloon:ContextualBalloon,StickyPanelView:StickyPanelView,TooltipManager:TooltipManager,Template:Template,ToolbarView:ToolbarView,ToolbarSeparatorView:ToolbarSeparatorView,normalizeToolbarConfig:normalizeToolbarConfig,BalloonToolbar:BalloonToolbar,BlockToolbar:BlockToolbar,View:View,ViewCollection:ViewCollection,getLocalizedColorOptions:getLocalizedColorOptions,normalizeColorOptions:normalizeColorOptions,normalizeSingleColorDefinition:normalizeSingleColorDefinition,createDropdown:createDropdown,addToolbarToDropdown:addToolbarToDropdown,addListToDropdown:addListToDropdown,focusChildOnDropdownOpen:focusChildOnDropdownOpen,createLabeledInputText:createLabeledInputText,createLabeledInputNumber:createLabeledInputNumber,createLabeledDropdown:createLabeledDropdown});var css_248z$z = ".ck .ck-placeholder,.ck.ck-placeholder{position:relative}.ck .ck-placeholder:before,.ck.ck-placeholder:before{content:attr(data-placeholder);left:0;pointer-events:none;position:absolute;right:0}.ck.ck-read-only .ck-placeholder:before{display:none}.ck.ck-reset_all .ck-placeholder{position:relative}.ck .ck-placeholder:before,.ck.ck-placeholder:before{color:var(--ck-color-engine-placeholder-text);cursor:text}";
styleInject(css_248z$z);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
// Each document stores information about its placeholder elements and check functions.
const documentPlaceholders = new WeakMap();
/**
 * A helper that enables a placeholder on the provided view element (also updates its visibility).
 * The placeholder is a CSS pseudo–element (with a text content) attached to the element.
 *
 * To change the placeholder text, simply call this method again with new options.
 *
 * To disable the placeholder, use {@link module:engine/view/placeholder~disablePlaceholder `disablePlaceholder()`} helper.
 *
 * @param {Object} [options] Configuration options of the placeholder.
 * @param {module:engine/view/view~View} options.view Editing view instance.
 * @param {module:engine/view/element~Element} options.element Element that will gain a placeholder.
 * See `options.isDirectHost` to learn more.
 * @param {String} options.text Placeholder text.
 * @param {Boolean} [options.isDirectHost=true] If set `false`, the placeholder will not be enabled directly
 * in the passed `element` but in one of its children (selected automatically, i.e. a first empty child element).
 * Useful when attaching placeholders to elements that can host other elements (not just text), for instance,
 * editable root elements.
 * @param {Boolean} [options.keepOnFocus=false] If set `true`, the placeholder stay visible when the host element is focused.
 */
function enablePlaceholder(options) {
    const { view, element, text, isDirectHost = true, keepOnFocus = false } = options;
    const doc = view.document;
    // Use a single a single post fixer per—document to update all placeholders.
    if (!documentPlaceholders.has(doc)) {
        documentPlaceholders.set(doc, new Map());
        // If a post-fixer callback makes a change, it should return `true` so other post–fixers
        // can re–evaluate the document again.
        doc.registerPostFixer(writer => updateDocumentPlaceholders(doc, writer));
    }
    // Store information about the element placeholder under its document.
    documentPlaceholders.get(doc).set(element, {
        text,
        isDirectHost,
        keepOnFocus,
        hostElement: isDirectHost ? element : null
    });
    // Update the placeholders right away.
    view.change(writer => updateDocumentPlaceholders(doc, writer));
}
/**
 * Disables the placeholder functionality from a given element.
 *
 * See {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} to learn more.
 *
 * @param {module:engine/view/view~View} view
 * @param {module:engine/view/element~Element} element
 */
function disablePlaceholder(view, element) {
    const doc = element.document;
    view.change(writer => {
        if (!documentPlaceholders.has(doc)) {
            return;
        }
        const placeholders = documentPlaceholders.get(doc);
        const config = placeholders.get(element);
        writer.removeAttribute('data-placeholder', config.hostElement);
        hidePlaceholder(writer, config.hostElement);
        placeholders.delete(element);
    });
}
/**
 * Shows a placeholder in the provided element by changing related attributes and CSS classes.
 *
 * **Note**: This helper will not update the placeholder visibility nor manage the
 * it in any way in the future. What it does is a one–time state change of an element. Use
 * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} and
 * {@link module:engine/view/placeholder~disablePlaceholder `disablePlaceholder()`} for full
 * placeholder functionality.
 *
 * **Note**: This helper will blindly show the placeholder directly in the root editable element if
 * one is passed, which could result in a visual clash if the editable element has some children
 * (for instance, an empty paragraph). Use {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`}
 * in that case or make sure the correct element is passed to the helper.
 *
 * @param {module:engine/view/downcastwriter~DowncastWriter} writer
 * @param {module:engine/view/element~Element} element
 * @returns {Boolean} `true`, if any changes were made to the `element`.
 */
function showPlaceholder(writer, element) {
    if (!element.hasClass('ck-placeholder')) {
        writer.addClass('ck-placeholder', element);
        return true;
    }
    return false;
}
/**
 * Hides a placeholder in the element by changing related attributes and CSS classes.
 *
 * **Note**: This helper will not update the placeholder visibility nor manage the
 * it in any way in the future. What it does is a one–time state change of an element. Use
 * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} and
 * {@link module:engine/view/placeholder~disablePlaceholder `disablePlaceholder()`} for full
 * placeholder functionality.
 *
 * @param {module:engine/view/downcastwriter~DowncastWriter} writer
 * @param {module:engine/view/element~Element} element
 * @returns {Boolean} `true`, if any changes were made to the `element`.
 */
function hidePlaceholder(writer, element) {
    if (element.hasClass('ck-placeholder')) {
        writer.removeClass('ck-placeholder', element);
        return true;
    }
    return false;
}
/**
 * Checks if a placeholder should be displayed in the element.
 *
 * **Note**: This helper will blindly check the possibility of showing a placeholder directly in the
 * root editable element if one is passed, which may not be the expected result. If an element can
 * host other elements (not just text), most likely one of its children should be checked instead
 * because it will be the final host for the placeholder. Use
 * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} in that case or make
 * sure the correct element is passed to the helper.
 *
 * @param {module:engine/view/element~Element} element Element that holds the placeholder.
 * @param {Boolean} keepOnFocus Focusing the element will keep the placeholder visible.
 * @returns {Boolean}
 */
function needsPlaceholder(element, keepOnFocus) {
    if (!element.isAttached()) {
        return false;
    }
    // Anything but uiElement(s) counts as content.
    const hasContent = Array.from(element.getChildren())
        .some(element => !element.is('uiElement'));
    if (hasContent) {
        return false;
    }
    // Skip the focus check and make the placeholder visible already regardless of document focus state.
    if (keepOnFocus) {
        return true;
    }
    const doc = element.document;
    // If the document is blurred.
    if (!doc.isFocused) {
        return true;
    }
    const viewSelection = doc.selection;
    const selectionAnchor = viewSelection.anchor;
    // If document is focused and the element is empty but the selection is not anchored inside it.
    return !!selectionAnchor && selectionAnchor.parent !== element;
}
// Updates all placeholders associated with a document in a post–fixer callback.
//
// @private
// @param { module:engine/view/document~Document} doc
// @param {module:engine/view/downcastwriter~DowncastWriter} writer
// @returns {Boolean} True if any changes were made to the view document.
function updateDocumentPlaceholders(doc, writer) {
    const placeholders = documentPlaceholders.get(doc);
    const directHostElements = [];
    let wasViewModified = false;
    // First set placeholders on the direct hosts.
    for (const [element, config] of placeholders) {
        if (config.isDirectHost) {
            directHostElements.push(element);
            if (updatePlaceholder(writer, element, config)) {
                wasViewModified = true;
            }
        }
    }
    // Then set placeholders on the indirect hosts but only on those that does not already have an direct host placeholder.
    for (const [element, config] of placeholders) {
        if (config.isDirectHost) {
            continue;
        }
        const hostElement = getChildPlaceholderHostSubstitute(element);
        // When not a direct host, it could happen that there is no child element
        // capable of displaying a placeholder.
        if (!hostElement) {
            continue;
        }
        // Don't override placeholder if the host element already has some direct placeholder.
        if (directHostElements.includes(hostElement)) {
            continue;
        }
        // Update the host element (used for setting and removing the placeholder).
        config.hostElement = hostElement;
        if (updatePlaceholder(writer, element, config)) {
            wasViewModified = true;
        }
    }
    return wasViewModified;
}
// Updates a single placeholder in a post–fixer callback.
//
// @private
// @param {module:engine/view/downcastwriter~DowncastWriter} writer
// @param {module:engine/view/element~Element} element
// @param {Object} config Configuration of the placeholder
// @param {String} config.text
// @param {Boolean} config.isDirectHost
// @returns {Boolean} True if any changes were made to the view document.
function updatePlaceholder(writer, element, config) {
    const { text, isDirectHost, hostElement } = config;
    let wasViewModified = false;
    // This may be necessary when updating the placeholder text to something else.
    if (hostElement.getAttribute('data-placeholder') !== text) {
        writer.setAttribute('data-placeholder', text, hostElement);
        wasViewModified = true;
    }
    // If the host element is not a direct host then placeholder is needed only when there is only one element.
    const isOnlyChild = isDirectHost || element.childCount == 1;
    if (isOnlyChild && needsPlaceholder(hostElement, config.keepOnFocus)) {
        if (showPlaceholder(writer, hostElement)) {
            wasViewModified = true;
        }
    }
    else if (hidePlaceholder(writer, hostElement)) {
        wasViewModified = true;
    }
    return wasViewModified;
}
// Gets a child element capable of displaying a placeholder if a parent element can host more
// than just text (for instance, when it is a root editable element). The child element
// can then be used in other placeholder helpers as a substitute of its parent.
//
// @private
// @param {module:engine/view/element~Element} parent
// @returns {module:engine/view/element~Element|null}
function getChildPlaceholderHostSubstitute(parent) {
    if (parent.childCount) {
        const firstChild = parent.getChild(0);
        if (firstChild.is('element') && !firstChild.is('uiElement') && !firstChild.is('attributeElement')) {
            return firstChild;
        }
    }
    return null;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const transformations = new Map();
/**
 * @module engine/model/operation/transform
 */
/**
 * Sets a transformation function to be be used to transform instances of class `OperationA` by instances of class `OperationB`.
 *
 * The `transformationFunction` is passed three parameters:
 *
 * * `a` - operation to be transformed, an instance of `OperationA`,
 * * `b` - operation to be transformed by, an instance of `OperationB`,
 * * {@link module:engine/model/operation/transform~TransformationContext `context`} - object with additional information about
 * transformation context.
 *
 * The `transformationFunction` should return transformation result, which is an array with one or multiple
 * {@link module:engine/model/operation/operation~Operation operation} instances.
 *
 * @protected
 * @param {Function} OperationA
 * @param {Function} OperationB
 * @param {Function} transformationFunction Function to use for transforming.
 */
function setTransformation(OperationA, OperationB, transformationFunction) {
    let aGroup = transformations.get(OperationA);
    if (!aGroup) {
        aGroup = new Map();
        transformations.set(OperationA, aGroup);
    }
    aGroup.set(OperationB, transformationFunction);
}
/**
 * Returns a previously set transformation function for transforming an instance of `OperationA` by an instance of `OperationB`.
 *
 * If no transformation was set for given pair of operations, {@link module:engine/model/operation/transform~noUpdateTransformation}
 * is returned. This means that if no transformation was set, the `OperationA` instance will not change when transformed
 * by the `OperationB` instance.
 *
 * @private
 * @param {Function} OperationA
 * @param {Function} OperationB
 * @returns {Function} Function set to transform an instance of `OperationA` by an instance of `OperationB`.
 */
function getTransformation(OperationA, OperationB) {
    const aGroup = transformations.get(OperationA);
    if (aGroup && aGroup.has(OperationB)) {
        return aGroup.get(OperationB);
    }
    return noUpdateTransformation;
}
/**
 * A transformation function that only clones operation to transform, without changing it.
 *
 * @private
 * @param {module:engine/model/operation/operation~Operation} a Operation to transform.
 * @returns {Array.<module:engine/model/operation/operation~Operation>}
 */
function noUpdateTransformation(a) {
    return [a];
}
/**
 * Transforms operation `a` by operation `b`.
 *
 * @param {module:engine/model/operation/operation~Operation} a Operation to be transformed.
 * @param {module:engine/model/operation/operation~Operation} b Operation to transform by.
 * @param {module:engine/model/operation/transform~TransformationContext} [context] Transformation context for this transformation.
 * @returns {Array.<module:engine/model/operation/operation~Operation>} Transformation result.
 */
function transform(a, b, context = {}) {
    const transformationFunction = getTransformation(a.constructor, b.constructor);
    /* eslint-disable no-useless-catch */
    try {
        a = a.clone();
        return transformationFunction(a, b, context);
    }
    catch (e) {
        // @if CK_DEBUG // console.warn( 'Error during operation transformation!', e.message );
        // @if CK_DEBUG // console.warn( 'Transformed operation', a );
        // @if CK_DEBUG // console.warn( 'Operation transformed by', b );
        // @if CK_DEBUG // console.warn( 'context.aIsStrong', context.aIsStrong );
        // @if CK_DEBUG // console.warn( 'context.aWasUndone', context.aWasUndone );
        // @if CK_DEBUG // console.warn( 'context.bWasUndone', context.bWasUndone );
        // @if CK_DEBUG // console.warn( 'context.abRelation', context.abRelation );
        // @if CK_DEBUG // console.warn( 'context.baRelation', context.baRelation );
        throw e;
    }
    /* eslint-enable no-useless-catch */
}
/**
 * Performs a transformation of two sets of operations - `operationsA` and `operationsB`. The transformation is two-way -
 * both transformed `operationsA` and transformed `operationsB` are returned.
 *
 * Note, that the first operation in each set should base on the same document state (
 * {@link module:engine/model/document~Document#version document version}).
 *
 * It is assumed that `operationsA` are "more important" during conflict resolution between two operations.
 *
 * New copies of both passed arrays and operations inside them are returned. Passed arguments are not altered.
 *
 * Base versions of the transformed operations sets are updated accordingly. For example, assume that base versions are `4`
 * and there are `3` operations in `operationsA` and `5` operations in `operationsB`. Then:
 *
 * * transformed `operationsA` will start from base version `9` (`4` base version + `5` operations B),
 * * transformed `operationsB` will start from base version `7` (`4` base version + `3` operations A).
 *
 * If no operation was broken into two during transformation, then both sets will end up with an operation that bases on version `11`:
 *
 * * transformed `operationsA` start from `9` and there are `3` of them, so the last will have `baseVersion` equal to `11`,
 * * transformed `operationsB` start from `7` and there are `5` of them, so the last will have `baseVersion` equal to `11`.
 *
 * @param {Array.<module:engine/model/operation/operation~Operation>} operationsA
 * @param {Array.<module:engine/model/operation/operation~Operation>} operationsB
 * @param {Object} options Additional transformation options.
 * @param {module:engine/model/document~Document} options.document Document which the operations change.
 * @param {Boolean} [options.useRelations=false] Whether during transformation relations should be used (used during undo for
 * better conflict resolution).
 * @param {Boolean} [options.padWithNoOps=false] Whether additional {@link module:engine/model/operation/nooperation~NoOperation}s
 * should be added to the transformation results to force the same last base version for both transformed sets (in case
 * if some operations got broken into multiple operations during transformation).
 * @param {Boolean} [options.forceWeakRemove] If set to `false`, remove operation will be always stronger than move operation,
 * so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
 * @returns {Object} Transformation result.
 * @returns {Array.<module:engine/model/operation/operation~Operation>} return.operationsA Transformed `operationsA`.
 * @returns {Array.<module:engine/model/operation/operation~Operation>} return.operationsB Transformed `operationsB`.
 * @returns {Map} return.originalOperations A map that links transformed operations to original operations. The keys are the transformed
 * operations and the values are the original operations from the input (`operationsA` and `operationsB`).
 */
function transformSets(operationsA, operationsB, options) {
    // Create new arrays so the originally passed arguments are not changed.
    // No need to clone operations, they are cloned as they are transformed.
    operationsA = operationsA.slice();
    operationsB = operationsB.slice();
    const contextFactory = new ContextFactory(options.document, options.useRelations, options.forceWeakRemove);
    contextFactory.setOriginalOperations(operationsA);
    contextFactory.setOriginalOperations(operationsB);
    const originalOperations = contextFactory.originalOperations;
    // If one of sets is empty there is simply nothing to transform, so return sets as they are.
    if (operationsA.length == 0 || operationsB.length == 0) {
        return { operationsA, operationsB, originalOperations };
    }
    //
    // Following is a description of transformation process:
    //
    // There are `operationsA` and `operationsB` to be transformed, both by both.
    //
    // So, suppose we have sets of two operations each: `operationsA` = `[ a1, a2 ]`, `operationsB` = `[ b1, b2 ]`.
    //
    // Remember, that we can only transform operations that base on the same context. We assert that `a1` and `b1` base on
    // the same context and we transform them. Then, we get `a1'` and `b1'`. `a2` bases on a context with `a1` -- `a2`
    // is an operation that followed `a1`. Similarly, `b2` bases on a context with `b1`.
    //
    // However, since `a1'` is a result of transformation by `b1`, `a1'` now also has a context with `b1`. This means that
    // we can safely transform `a1'` by `b2`. As we finish transforming `a1`, we also transformed all `operationsB`.
    // All `operationsB` also have context including `a1`. Now, we can properly transform `a2` by those operations.
    //
    // The transformation process can be visualized on a transformation diagram ("diamond diagram"):
    //
    //          [the initial state]
    //         [common for a1 and b1]
    //
    //                   *
    //                  / \
    //                 /   \
    //               b1     a1
    //               /       \
    //              /         \
    //             *           *
    //            / \         / \
    //           /   \       /   \
    //         b2    a1'   b1'    a2
    //         /       \   /       \
    //        /         \ /         \
    //       *           *           *
    //        \         / \         /
    //         \       /   \       /
    //        a1''   b2'   a2'   b1''
    //           \   /       \   /
    //            \ /         \ /
    //             *           *
    //              \         /
    //               \       /
    //              a2''   b2''
    //                 \   /
    //                  \ /
    //                   *
    //
    //           [the final state]
    //
    // The final state can be reached from the initial state by applying `a1`, `a2`, `b1''` and `b2''`, as well as by
    // applying `b1`, `b2`, `a1''`, `a2''`. Note how the operations get to a proper common state before each pair is
    // transformed.
    //
    // Another thing to consider is that an operation during transformation can be broken into multiple operations.
    // Suppose that `a1` * `b1` = `[ a11', a12' ]` (instead of `a1'` that we considered previously).
    //
    // In that case, we leave `a12'` for later and we continue transforming `a11'` until it is transformed by all `operationsB`
    // (in our case it is just `b2`). At this point, `b1` is transformed by "whole" `a1`, while `b2` is only transformed
    // by `a11'`. Similarly, `a12'` is only transformed by `b1`. This leads to a conclusion that we need to start transforming `a12'`
    // from the moment just after it was broken. So, `a12'` is transformed by `b2`. Now, "the whole" `a1` is transformed
    // by `operationsB`, while all `operationsB` are transformed by "the whole" `a1`. This means that we can continue with
    // following `operationsA` (in our case it is just `a2`).
    //
    // Of course, also `operationsB` can be broken. However, since we focus on transforming operation `a` to the end,
    // the only thing to do is to store both pieces of operation `b`, so that the next transformed operation `a` will
    // be transformed by both of them.
    //
    //                       *
    //                      / \
    //                     /   \
    //                    /     \
    //                  b1       a1
    //                  /         \
    //                 /           \
    //                /             \
    //               *               *
    //              / \             / \
    //             /  a11'         /   \
    //            /     \         /     \
    //          b2       *      b1'      a2
    //          /       / \     /         \
    //         /       /  a12' /           \
    //        /       /     \ /             \
    //       *       b2'     *               *
    //        \     /       / \             /
    //       a11'' /     b21'' \           /
    //          \ /       /     \         /
    //           *       *      a2'     b1''
    //            \     / \       \     /
    //          a12'' b22''\       \   /
    //              \ /     \       \ /
    //               *      a2''     *
    //                \       \     /
    //                 \       \  b21'''
    //                  \       \ /
    //                a2'''      *
    //                    \     /
    //                     \  b22'''
    //                      \ /
    //                       *
    //
    // Note, how `a1` is broken and transformed into `a11'` and `a12'`, while `b2'` got broken and transformed into `b21''` and `b22''`.
    //
    // Having all that on mind, here is an outline for the transformation process algorithm:
    //
    // 1. We have `operationsA` and `operationsB` array, which we dynamically update as the transformation process goes.
    //
    // 2. We take next (or first) operation from `operationsA` and check from which operation `b` we need to start transforming it.
    // All original `operationsA` are set to be transformed starting from the first operation `b`.
    //
    // 3. We take operations from `operationsB`, one by one, starting from the correct one, and transform operation `a`
    // by operation `b` (and vice versa). We update `operationsA` and `operationsB` by replacing the original operations
    // with the transformation results.
    //
    // 4. If operation is broken into multiple operations, we save all the new operations in the place of the
    // original operation.
    //
    // 5. Additionally, if operation `a` was broken, for the "new" operation, we remember from which operation `b` it should
    // be transformed by.
    //
    // 6. We continue transforming "current" operation `a` until it is transformed by all `operationsB`. Then, go to 2.
    // unless the last operation `a` was transformed.
    //
    // The actual implementation of the above algorithm is slightly different, as only one loop (while) is used.
    // The difference is that we have "current" `a` operation to transform and we store the index of the next `b` operation
    // to transform by. Each loop operates on two indexes then: index pointing to currently processed `a` operation and
    // index pointing to next `b` operation. Each loop is just one `a * b` + `b * a` transformation. After each loop
    // operation `b` index is updated. If all `b` operations were visited for the current `a` operation, we change
    // current `a` operation index to the next one.
    //
    // For each operation `a`, keeps information what is the index in `operationsB` from which the transformation should start.
    const nextTransformIndex = new WeakMap();
    // For all the original `operationsA`, set that they should be transformed starting from the first of `operationsB`.
    for (const op of operationsA) {
        nextTransformIndex.set(op, 0);
    }
    // Additional data that is used for some postprocessing after the main transformation process is done.
    const data = {
        nextBaseVersionA: operationsA[operationsA.length - 1].baseVersion + 1,
        nextBaseVersionB: operationsB[operationsB.length - 1].baseVersion + 1,
        originalOperationsACount: operationsA.length,
        originalOperationsBCount: operationsB.length
    };
    // Index of currently transformed operation `a`.
    let i = 0;
    // While not all `operationsA` are transformed...
    while (i < operationsA.length) {
        // Get "current" operation `a`.
        const opA = operationsA[i];
        // For the "current" operation `a`, get the index of the next operation `b` to transform by.
        const indexB = nextTransformIndex.get(opA);
        // If operation `a` was already transformed by every operation `b`, change "current" operation `a` to the next one.
        if (indexB == operationsB.length) {
            i++;
            continue;
        }
        const opB = operationsB[indexB];
        // Transform `a` by `b` and `b` by `a`.
        const newOpsA = transform(opA, opB, contextFactory.getContext(opA, opB, true));
        const newOpsB = transform(opB, opA, contextFactory.getContext(opB, opA, false));
        // As a result we get one or more `newOpsA` and one or more `newOpsB` operations.
        // Update contextual information about operations.
        contextFactory.updateRelation(opA, opB);
        contextFactory.setOriginalOperations(newOpsA, opA);
        contextFactory.setOriginalOperations(newOpsB, opB);
        // For new `a` operations, update their index of the next operation `b` to transform them by.
        //
        // This is needed even if there was only one result (`a` was not broken) because that information is used
        // at the beginning of this loop every time.
        for (const newOpA of newOpsA) {
            // Acknowledge, that operation `b` also might be broken into multiple operations.
            //
            // This is why we raise `indexB` not just by 1. If `newOpsB` are multiple operations, they will be
            // spliced in the place of `opB`. So we need to change `transformBy` accordingly, so that an operation won't
            // be transformed by the same operation (part of it) again.
            nextTransformIndex.set(newOpA, indexB + newOpsB.length);
        }
        // Update `operationsA` and `operationsB` with the transformed versions.
        operationsA.splice(i, 1, ...newOpsA);
        operationsB.splice(indexB, 1, ...newOpsB);
    }
    if (options.padWithNoOps) {
        // If no-operations padding is enabled, count how many extra `a` and `b` operations were generated.
        const brokenOperationsACount = operationsA.length - data.originalOperationsACount;
        const brokenOperationsBCount = operationsB.length - data.originalOperationsBCount;
        // Then, if that number is not the same, pad `operationsA` or `operationsB` with correct number of no-ops so
        // that the base versions are equalled.
        //
        // Note that only one array will be updated, as only one of those subtractions can be greater than zero.
        padWithNoOps(operationsA, brokenOperationsBCount - brokenOperationsACount);
        padWithNoOps(operationsB, brokenOperationsACount - brokenOperationsBCount);
    }
    // Finally, update base versions of transformed operations.
    updateBaseVersions(operationsA, data.nextBaseVersionB);
    updateBaseVersions(operationsB, data.nextBaseVersionA);
    return { operationsA, operationsB, originalOperations };
}
// Gathers additional data about operations processed during transformation. Can be used to obtain contextual information
// about two operations that are about to be transformed. This contextual information can be used for better conflict resolution.
class ContextFactory {
    // Creates `ContextFactory` instance.
    //
    // @param {module:engine/model/document~Document} document Document which the operations change.
    // @param {Boolean} useRelations Whether during transformation relations should be used (used during undo for
    // better conflict resolution).
    // @param {Boolean} [forceWeakRemove=false] If set to `false`, remove operation will be always stronger than move operation,
    // so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
    constructor(document, useRelations, forceWeakRemove = false) {
        // For each operation that is created during transformation process, we keep a reference to the original operation
        // which it comes from. The original operation works as a kind of "identifier". Every contextual information
        // gathered during transformation that we want to save for given operation, is actually saved for the original operation.
        // This way no matter if operation `a` is cloned, then transformed, even breaks, we still have access to the previously
        // gathered data through original operation reference.
        this.originalOperations = new Map();
        // `model.History` instance which information about undone operations will be taken from.
        this._history = document.history;
        // Whether additional context should be used.
        this._useRelations = useRelations;
        this._forceWeakRemove = !!forceWeakRemove;
        // Relations is a double-map structure (maps in map) where for two operations we store how those operations were related
        // to each other. Those relations are evaluated during transformation process. For every transformated pair of operations
        // we keep relations between them.
        this._relations = new Map();
    }
    // Sets "original operation" for given operations.
    //
    // During transformation process, operations are cloned, then changed, then processed again, sometimes broken into two
    // or multiple operations. When gathering additional data it is important that all operations can be somehow linked
    // so a cloned and transformed "version" still kept track of the data assigned earlier to it.
    //
    // The original operation object will be used as such an universal linking id. Throughout the transformation process
    // all cloned operations will refer to "the original operation" when storing and reading additional data.
    //
    // If `takeFrom` is not set, each operation from `operations` array will be assigned itself as "the original operation".
    // This should be used as an initialization step.
    //
    // If `takeFrom` is set, each operation from `operations` will be assigned the same original operation as assigned
    // for `takeFrom` operation. This should be used to update original operations. It should be used in a way that
    // `operations` are the result of `takeFrom` transformation to ensure proper "original operation propagation".
    //
    // @param {Array.<module:engine/model/operation/operation~Operation>} operations
    // @param {module:engine/model/operation/operation~Operation|null} [takeFrom=null]
    setOriginalOperations(operations, takeFrom = null) {
        const originalOperation = takeFrom ? this.originalOperations.get(takeFrom) : null;
        for (const operation of operations) {
            this.originalOperations.set(operation, originalOperation || operation);
        }
    }
    // Saves a relation between operations `opA` and `opB`.
    //
    // Relations are then later used to help solve conflicts when operations are transformed.
    //
    // @param {module:engine/model/operation/operation~Operation} opA
    // @param {module:engine/model/operation/operation~Operation} opB
    updateRelation(opA, opB) {
        // The use of relations is described in a bigger detail in transformation functions.
        //
        // In brief, this function, for specified pairs of operation types, checks how positions defined in those operations relate.
        // Then those relations are saved. For example, for two move operations, it is saved if one of those operations target
        // position is before the other operation source position. This kind of information gives contextual information when
        // transformation is used during undo. Similar checks are done for other pairs of operations.
        //
        if (opA instanceof MoveOperation) {
            if (opB instanceof MergeOperation) {
                if (opA.targetPosition.isEqual(opB.sourcePosition) || opB.movedRange.containsPosition(opA.targetPosition)) {
                    this._setRelation(opA, opB, 'insertAtSource');
                }
                else if (opA.targetPosition.isEqual(opB.deletionPosition)) {
                    this._setRelation(opA, opB, 'insertBetween');
                }
                else if (opA.targetPosition.isAfter(opB.sourcePosition)) {
                    this._setRelation(opA, opB, 'moveTargetAfter');
                }
            }
            else if (opB instanceof MoveOperation) {
                if (opA.targetPosition.isEqual(opB.sourcePosition) || opA.targetPosition.isBefore(opB.sourcePosition)) {
                    this._setRelation(opA, opB, 'insertBefore');
                }
                else {
                    this._setRelation(opA, opB, 'insertAfter');
                }
            }
        }
        else if (opA instanceof SplitOperation) {
            if (opB instanceof MergeOperation) {
                if (opA.splitPosition.isBefore(opB.sourcePosition)) {
                    this._setRelation(opA, opB, 'splitBefore');
                }
            }
            else if (opB instanceof MoveOperation) {
                if (opA.splitPosition.isEqual(opB.sourcePosition) || opA.splitPosition.isBefore(opB.sourcePosition)) {
                    this._setRelation(opA, opB, 'splitBefore');
                }
                else {
                    const range = Range._createFromPositionAndShift(opB.sourcePosition, opB.howMany);
                    if (opA.splitPosition.hasSameParentAs(opB.sourcePosition) && range.containsPosition(opA.splitPosition)) {
                        const howMany = range.end.offset - opA.splitPosition.offset;
                        const offset = opA.splitPosition.offset - range.start.offset;
                        this._setRelation(opA, opB, { howMany, offset });
                    }
                }
            }
        }
        else if (opA instanceof MergeOperation) {
            if (opB instanceof MergeOperation) {
                if (!opA.targetPosition.isEqual(opB.sourcePosition)) {
                    this._setRelation(opA, opB, 'mergeTargetNotMoved');
                }
                if (opA.sourcePosition.isEqual(opB.targetPosition)) {
                    this._setRelation(opA, opB, 'mergeSourceNotMoved');
                }
                if (opA.sourcePosition.isEqual(opB.sourcePosition)) {
                    this._setRelation(opA, opB, 'mergeSameElement');
                }
            }
            else if (opB instanceof SplitOperation) {
                if (opA.sourcePosition.isEqual(opB.splitPosition)) {
                    this._setRelation(opA, opB, 'splitAtSource');
                }
            }
        }
        else if (opA instanceof MarkerOperation) {
            const markerRange = opA.newRange;
            if (!markerRange) {
                return;
            }
            if (opB instanceof MoveOperation) {
                const movedRange = Range._createFromPositionAndShift(opB.sourcePosition, opB.howMany);
                const affectedLeft = movedRange.containsPosition(markerRange.start) ||
                    movedRange.start.isEqual(markerRange.start);
                const affectedRight = movedRange.containsPosition(markerRange.end) ||
                    movedRange.end.isEqual(markerRange.end);
                if ((affectedLeft || affectedRight) && !movedRange.containsRange(markerRange)) {
                    this._setRelation(opA, opB, {
                        side: affectedLeft ? 'left' : 'right',
                        path: affectedLeft ? markerRange.start.path.slice() : markerRange.end.path.slice()
                    });
                }
            }
            else if (opB instanceof MergeOperation) {
                const wasInLeftElement = markerRange.start.isEqual(opB.targetPosition);
                const wasStartBeforeMergedElement = markerRange.start.isEqual(opB.deletionPosition);
                const wasEndBeforeMergedElement = markerRange.end.isEqual(opB.deletionPosition);
                const wasInRightElement = markerRange.end.isEqual(opB.sourcePosition);
                if (wasInLeftElement || wasStartBeforeMergedElement || wasEndBeforeMergedElement || wasInRightElement) {
                    this._setRelation(opA, opB, {
                        wasInLeftElement,
                        wasStartBeforeMergedElement,
                        wasEndBeforeMergedElement,
                        wasInRightElement
                    });
                }
            }
        }
    }
    // Evaluates and returns contextual information about two given operations `opA` and `opB` which are about to be transformed.
    //
    // @param {module:engine/model/operation/operation~Operation} opA
    // @param {module:engine/model/operation/operation~Operation} opB
    // @returns {module:engine/model/operation/transform~TransformationContext}
    getContext(opA, opB, aIsStrong) {
        return {
            aIsStrong,
            aWasUndone: this._wasUndone(opA),
            bWasUndone: this._wasUndone(opB),
            abRelation: this._useRelations ? this._getRelation(opA, opB) : null,
            baRelation: this._useRelations ? this._getRelation(opB, opA) : null,
            forceWeakRemove: this._forceWeakRemove
        };
    }
    // Returns whether given operation `op` has already been undone.
    //
    // Information whether an operation was undone gives more context when making a decision when two operations are in conflict.
    //
    // @param {module:engine/model/operation/operation~Operation} op
    // @returns {Boolean}
    _wasUndone(op) {
        // For `op`, get its original operation. After all, if `op` is a clone (or even transformed clone) of another
        // operation, literally `op` couldn't be undone. It was just generated. If anything, it was the operation it origins
        // from which was undone. So get that original operation.
        const originalOp = this.originalOperations.get(op);
        // And check with the document if the original operation was undone.
        return originalOp.wasUndone || this._history.isUndoneOperation(originalOp);
    }
    // Returns a relation between `opA` and an operation which is undone by `opB`. This can be `String` value if a relation
    // was set earlier or `null` if there was no relation between those operations.
    //
    // This is a little tricky to understand, so let's compare it to `ContextFactory#_wasUndone`.
    //
    // When `wasUndone( opB )` is used, we check if the `opB` has already been undone. It is obvious, that the
    // undoing operation must happen after the undone operation. So, essentially, we have `opB`, we take document history,
    // we look forward in the future and ask if in that future `opB` was undone.
    //
    // Relations is a backward process to `wasUndone()`.
    //
    // Long story short - using relations is asking what happened in the past. Looking back. This time we have an undoing
    // operation `opB` which has undone some other operation. When there is a transformation `opA` x `opB` and there is
    // a conflict to solve and `opB` is an undoing operation, we can look back in the history and see what was a relation
    // between `opA` and the operation which `opB` undone. Basing on that relation from the past, we can now make
    // a better decision when resolving a conflict between two operations, because we know more about the context of
    // those two operations.
    //
    // This is why this function does not return a relation directly between `opA` and `opB` because we need to look
    // back to search for a meaningful contextual information.
    //
    // @param {module:engine/model/operation/operation~Operation} opA
    // @param {module:engine/model/operation/operation~Operation} opB
    // @returns {String|null}
    _getRelation(opA, opB) {
        // Get the original operation. Similarly as in `wasUndone()` it is used as an universal identifier for stored data.
        const origB = this.originalOperations.get(opB);
        const undoneB = this._history.getUndoneOperation(origB);
        // If `opB` is not undoing any operation, there is no relation.
        if (!undoneB) {
            return null;
        }
        const origA = this.originalOperations.get(opA);
        const relationsA = this._relations.get(origA);
        // Get all relations for `opA`, and check if there is a relation with `opB`-undone-counterpart. If so, return it.
        if (relationsA) {
            return relationsA.get(undoneB) || null;
        }
        return null;
    }
    // Helper function for `ContextFactory#updateRelations`.
    //
    // @private
    // @param {module:engine/model/operation/operation~Operation} opA
    // @param {module:engine/model/operation/operation~Operation} opB
    // @param {String} relation
    _setRelation(opA, opB, relation) {
        // As always, setting is for original operations, not the clones/transformed operations.
        const origA = this.originalOperations.get(opA);
        const origB = this.originalOperations.get(opB);
        let relationsA = this._relations.get(origA);
        if (!relationsA) {
            relationsA = new Map();
            this._relations.set(origA, relationsA);
        }
        relationsA.set(origB, relation);
    }
}
/**
 * An utility function that updates {@link module:engine/model/operation/operation~Operation#baseVersion base versions}
 * of passed operations.
 *
 * The function simply sets `baseVersion` as a base version of the first passed operation and then increments it for
 * each following operation in `operations`.
 *
 * @private
 * @param {Array.<module:engine/model/operation/operation~Operation>} operations Operations to update.
 * @param {Number} baseVersion Base version to set for the first operation in `operations`.
 */
function updateBaseVersions(operations, baseVersion) {
    for (const operation of operations) {
        operation.baseVersion = baseVersion++;
    }
}
/**
 * Adds `howMany` instances of {@link module:engine/model/operation/nooperation~NoOperation} to `operations` set.
 *
 * @private
 * @param {Array.<module:engine/model/operation/operation~Operation>} operations
 * @param {Number} howMany
 */
function padWithNoOps(operations, howMany) {
    for (let i = 0; i < howMany; i++) {
        operations.push(new NoOperation(0));
    }
}
// -----------------------
setTransformation(AttributeOperation, AttributeOperation, (a, b, context) => {
    // If operations in conflict, check if their ranges intersect and manage them properly.
    //
    // Operations can be in conflict only if:
    //
    // * their key is the same (they change the same attribute), and
    // * they are in the same parent (operations for ranges [ 1 ] - [ 3 ] and [ 2, 0 ] - [ 2, 5 ] change different
    // elements and can't be in conflict).
    if (a.key === b.key && a.range.start.hasSameParentAs(b.range.start)) {
        // 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(range, a.key, a.oldValue, a.newValue, 0);
        });
        // 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.aIsStrong) {
                operations.push(new AttributeOperation(common, b.key, b.newValue, a.newValue, 0));
            }
        }
        if (operations.length == 0) {
            return [new NoOperation(0)];
        }
        return operations;
    }
    else {
        // If operations don't conflict, simply return an array containing just a clone of this operation.
        return [a];
    }
});
setTransformation(AttributeOperation, InsertOperation, (a, b) => {
    // Case 1:
    //
    // The attribute operation range includes the position where nodes were inserted.
    // There are two possible scenarios: the inserted nodes were text and they should receive attributes or
    // the inserted nodes were elements and they should not receive attributes.
    //
    if (a.range.start.hasSameParentAs(b.position) && a.range.containsPosition(b.position)) {
        // If new nodes should not receive attributes, two separated ranges will be returned.
        // Otherwise, one expanded range will be returned.
        const range = a.range._getTransformedByInsertion(b.position, b.howMany, !b.shouldReceiveAttributes);
        const result = range.map(r => {
            return new AttributeOperation(r, a.key, a.oldValue, a.newValue, a.baseVersion);
        });
        if (b.shouldReceiveAttributes) {
            // `AttributeOperation#range` includes some newly inserted text.
            // The operation should also change the attribute of that text. An example:
            //
            // Bold should be applied on the following range:
            // <p>Fo[zb]ar</p>
            //
            // In meantime, new text is typed:
            // <p>Fozxxbar</p>
            //
            // Bold should be applied also on the new text:
            // <p>Fo[zxxb]ar</p>
            // <p>Fo<$text bold="true">zxxb</$text>ar</p>
            //
            // There is a special case to consider here to consider.
            //
            // Consider setting an attribute with multiple possible values, for example `highlight`. The inserted text might
            // have already an attribute value applied and the `oldValue` property of the attribute operation might be wrong:
            //
            // Attribute `highlight="yellow"` should be applied on the following range:
            // <p>Fo[zb]ar<p>
            //
            // In meantime, character `x` with `highlight="red"` is typed:
            // <p>Fo[z<$text highlight="red">x</$text>b]ar</p>
            //
            // In this case we cannot simply apply operation changing the attribute value from `null` to `"yellow"` for the whole range
            // because that would lead to an exception (`oldValue` is incorrect for `x`).
            //
            // We also cannot break the original range as this would mess up a scenario when there are multiple following
            // insert operations, because then only the first inserted character is included in those ranges:
            // <p>Fo[z][x][b]ar</p>   -->   <p>Fo[z][x]x[b]ar</p>   -->   <p>Fo[z][x]xx[b]ar</p>
            //
            // So, the attribute range needs be expanded, no matter what attributes are set on the inserted nodes:
            //
            // <p>Fo[z<$text highlight="red">x</$text>b]ar</p>      <--- Change from `null` to `yellow`, throwing an exception.
            //
            // But before that operation would be applied, we will add an additional attribute operation that will change
            // attributes on the inserted nodes in a way which would make the original operation correct:
            //
            // <p>Fo[z{<$text highlight="red">}x</$text>b]ar</p>    <--- Change range `{}` from `red` to `null`.
            // <p>Fo[zxb]ar</p>                                     <--- Now change from `null` to `yellow` is completely fine.
            //
            // Generate complementary attribute operation. Be sure to add it before the original operation.
            const op = _getComplementaryAttributeOperations(b, a.key, a.oldValue);
            if (op) {
                result.unshift(op);
            }
        }
        // If nodes should not receive new attribute, we are done here.
        return result;
    }
    // If insert operation is not expanding the attribute operation range, simply transform the range.
    a.range = a.range._getTransformedByInsertion(b.position, b.howMany, false)[0];
    return [a];
});
/**
 * Helper function for `AttributeOperation` x `InsertOperation` (and reverse) transformation.
 *
 * For given `insertOperation` it checks the inserted node if it has an attribute `key` set to a value different
 * than `newValue`. If so, it generates an `AttributeOperation` which changes the value of `key` attribute to `newValue`.
 *
 * @private
 * @param {module:engine/model/operation/insertoperation~InsertOperation} insertOperation
 * @param {String} key
 * @param {*} newValue
 * @returns {module:engine/model/operation/attributeoperation~AttributeOperation|null}
 */
function _getComplementaryAttributeOperations(insertOperation, key, newValue) {
    const nodes = insertOperation.nodes;
    // At the beginning we store the attribute value from the first node.
    const insertValue = nodes.getNode(0).getAttribute(key);
    if (insertValue == newValue) {
        return null;
    }
    const range = new Range(insertOperation.position, insertOperation.position.getShiftedBy(insertOperation.howMany));
    return new AttributeOperation(range, key, insertValue, newValue, 0);
}
setTransformation(AttributeOperation, MergeOperation, (a, b) => {
    const ranges = [];
    // Case 1:
    //
    // Attribute change on the merged element. In this case, the merged element was moved to the graveyard.
    // An additional attribute operation that will change the (re)moved element needs to be generated.
    //
    if (a.range.start.hasSameParentAs(b.deletionPosition)) {
        if (a.range.containsPosition(b.deletionPosition) || a.range.start.isEqual(b.deletionPosition)) {
            ranges.push(Range._createFromPositionAndShift(b.graveyardPosition, 1));
        }
    }
    const range = a.range._getTransformedByMergeOperation(b);
    // Do not add empty (collapsed) ranges to the result. `range` may be collapsed if it contained only the merged element.
    if (!range.isCollapsed) {
        ranges.push(range);
    }
    // Create `AttributeOperation`s out of the ranges.
    return ranges.map(range => {
        return new AttributeOperation(range, a.key, a.oldValue, a.newValue, a.baseVersion);
    });
});
setTransformation(AttributeOperation, MoveOperation, (a, b) => {
    const ranges = _breakRangeByMoveOperation(a.range, b);
    // Create `AttributeOperation`s out of the ranges.
    return ranges.map(range => new AttributeOperation(range, a.key, a.oldValue, a.newValue, a.baseVersion));
});
// Helper function for `AttributeOperation` x `MoveOperation` transformation.
//
// Takes the passed `range` and transforms it by move operation `moveOp` in a specific way. Only top-level nodes of `range`
// are considered to be in the range. If move operation moves nodes deep from inside of the range, those nodes won't
// be included in the result. In other words, top-level nodes of the ranges from the result are exactly the same as
// top-level nodes of the original `range`.
//
// This is important for `AttributeOperation` because, for its range, it changes only the top-level nodes. So we need to
// track only how those nodes have been affected by `MoveOperation`.
//
// @private
// @param {module:engine/model/range~Range} range
// @param {module:engine/model/operation/moveoperation~MoveOperation} moveOp
// @returns {Array.<module:engine/model/range~Range>}
function _breakRangeByMoveOperation(range, moveOp) {
    const moveRange = Range._createFromPositionAndShift(moveOp.sourcePosition, moveOp.howMany);
    // We are transforming `range` (original range) by `moveRange` (range moved by move operation). As usual when it comes to
    // transforming a ranges, we may have a common part of the ranges and we may have a difference part (zero to two ranges).
    let common = null;
    let difference = [];
    // Let's compare the ranges.
    if (moveRange.containsRange(range, true)) {
        // If the whole original range is moved, treat it whole as a common part. There's also no difference part.
        common = range;
    }
    else if (range.start.hasSameParentAs(moveRange.start)) {
        // If the ranges are "on the same level" (in the same parent) then move operation may move exactly those nodes
        // that are changed by the attribute operation. In this case we get common part and difference part in the usual way.
        difference = range.getDifference(moveRange);
        common = range.getIntersection(moveRange);
    }
    else {
        // In any other situation we assume that original range is different than move range, that is that move operation
        // moves other nodes that attribute operation change. Even if the moved range is deep inside in the original range.
        //
        // Note that this is different than in `.getIntersection` (we would get a common part in that case) and different
        // than `.getDifference` (we would get two ranges).
        difference = [range];
    }
    const result = [];
    // The default behaviour of `_getTransformedByMove` might get wrong results for difference part, though, so
    // we do it by hand.
    for (let diff of difference) {
        // First, transform the range by removing moved nodes. Since this is a difference, this is safe, `null` won't be returned
        // as the range is different than the moved range.
        diff = diff._getTransformedByDeletion(moveOp.sourcePosition, moveOp.howMany);
        // Transform also `targetPosition`.
        const targetPosition = moveOp.getMovedRangeStart();
        // Spread the range only if moved nodes are inserted only between the top-level nodes of the `diff` range.
        const spread = diff.start.hasSameParentAs(targetPosition);
        // Transform by insertion of moved nodes.
        const diffs = diff._getTransformedByInsertion(targetPosition, moveOp.howMany, spread);
        result.push(...diffs);
    }
    // Common part can be simply transformed by the move operation. This is because move operation will not target to
    // that common part (the operation would have to target inside its own moved range).
    if (common) {
        result.push(common._getTransformedByMove(moveOp.sourcePosition, moveOp.targetPosition, moveOp.howMany, false)[0]);
    }
    return result;
}
setTransformation(AttributeOperation, SplitOperation, (a, b) => {
    // Case 1:
    //
    // Split node is the last node in `AttributeOperation#range`.
    // `AttributeOperation#range` needs to be expanded to include the new (split) node.
    //
    // Attribute `type` to be changed to `numbered` but the `listItem` is split.
    // <listItem type="bulleted">foobar</listItem>
    //
    // After split:
    // <listItem type="bulleted">foo</listItem><listItem type="bulleted">bar</listItem>
    //
    // After attribute change:
    // <listItem type="numbered">foo</listItem><listItem type="numbered">foo</listItem>
    //
    if (a.range.end.isEqual(b.insertionPosition)) {
        if (!b.graveyardPosition) {
            a.range.end.offset++;
        }
        return [a];
    }
    // Case 2:
    //
    // Split position is inside `AttributeOperation#range`, at the same level, so the nodes to change are
    // not going to make a flat range.
    //
    // Content with range-to-change and split position:
    // <p>Fo[zb^a]r</p>
    //
    // After split:
    // <p>Fozb</p><p>ar</p>
    //
    // Make two separate ranges containing all nodes to change:
    // <p>Fo[zb]</p><p>[a]r</p>
    //
    if (a.range.start.hasSameParentAs(b.splitPosition) && a.range.containsPosition(b.splitPosition)) {
        const secondPart = a.clone();
        secondPart.range = new Range(b.moveTargetPosition.clone(), a.range.end._getCombined(b.splitPosition, b.moveTargetPosition));
        a.range.end = b.splitPosition.clone();
        a.range.end.stickiness = 'toPrevious';
        return [a, secondPart];
    }
    // The default case.
    //
    a.range = a.range._getTransformedBySplitOperation(b);
    return [a];
});
setTransformation(InsertOperation, AttributeOperation, (a, b) => {
    const result = [a];
    // Case 1:
    //
    // The attribute operation range includes the position where nodes were inserted.
    // There are two possible scenarios: the inserted nodes were text and they should receive attributes or
    // the inserted nodes were elements and they should not receive attributes.
    //
    // This is a mirror scenario to the one described in `AttributeOperation` x `InsertOperation` transformation,
    // although this case is a little less complicated. In this case we simply need to change attributes of the
    // inserted nodes and that's it.
    //
    if (a.shouldReceiveAttributes && a.position.hasSameParentAs(b.range.start) && b.range.containsPosition(a.position)) {
        const op = _getComplementaryAttributeOperations(a, b.key, b.newValue);
        if (op) {
            result.push(op);
        }
    }
    // The default case is: do nothing.
    // `AttributeOperation` does not change the model tree structure so `InsertOperation` does not need to be changed.
    //
    return result;
});
setTransformation(InsertOperation, InsertOperation, (a, b, context) => {
    // Case 1:
    //
    // Two insert operations insert nodes at the same position. Since they are the same, it needs to be decided
    // what will be the order of inserted nodes. However, there is no additional information to help in that
    // decision. Also, when `b` will be transformed by `a`, the same order must be maintained.
    //
    // To achieve that, we will check if the operation is strong.
    // If it is, it won't get transformed. If it is not, it will be moved.
    //
    if (a.position.isEqual(b.position) && context.aIsStrong) {
        return [a];
    }
    // The default case.
    //
    a.position = a.position._getTransformedByInsertOperation(b);
    return [a];
});
setTransformation(InsertOperation, MoveOperation, (a, b) => {
    // The default case.
    //
    a.position = a.position._getTransformedByMoveOperation(b);
    return [a];
});
setTransformation(InsertOperation, SplitOperation, (a, b) => {
    // The default case.
    //
    a.position = a.position._getTransformedBySplitOperation(b);
    return [a];
});
setTransformation(InsertOperation, MergeOperation, (a, b) => {
    a.position = a.position._getTransformedByMergeOperation(b);
    return [a];
});
// -----------------------
setTransformation(MarkerOperation, InsertOperation, (a, b) => {
    if (a.oldRange) {
        a.oldRange = a.oldRange._getTransformedByInsertOperation(b)[0];
    }
    if (a.newRange) {
        a.newRange = a.newRange._getTransformedByInsertOperation(b)[0];
    }
    return [a];
});
setTransformation(MarkerOperation, MarkerOperation, (a, b, context) => {
    if (a.name == b.name) {
        if (context.aIsStrong) {
            a.oldRange = b.newRange ? b.newRange.clone() : null;
        }
        else {
            return [new NoOperation(0)];
        }
    }
    return [a];
});
setTransformation(MarkerOperation, MergeOperation, (a, b) => {
    if (a.oldRange) {
        a.oldRange = a.oldRange._getTransformedByMergeOperation(b);
    }
    if (a.newRange) {
        a.newRange = a.newRange._getTransformedByMergeOperation(b);
    }
    return [a];
});
setTransformation(MarkerOperation, MoveOperation, (a, b, context) => {
    if (a.oldRange) {
        a.oldRange = Range._createFromRanges(a.oldRange._getTransformedByMoveOperation(b));
    }
    if (a.newRange) {
        if (context.abRelation) {
            const aNewRange = Range._createFromRanges(a.newRange._getTransformedByMoveOperation(b));
            if (context.abRelation.side == 'left' && b.targetPosition.isEqual(a.newRange.start)) {
                a.newRange.end = aNewRange.end;
                a.newRange.start.path = context.abRelation.path;
                return [a];
            }
            else if (context.abRelation.side == 'right' && b.targetPosition.isEqual(a.newRange.end)) {
                a.newRange.start = aNewRange.start;
                a.newRange.end.path = context.abRelation.path;
                return [a];
            }
        }
        a.newRange = Range._createFromRanges(a.newRange._getTransformedByMoveOperation(b));
    }
    return [a];
});
setTransformation(MarkerOperation, SplitOperation, (a, b, context) => {
    if (a.oldRange) {
        a.oldRange = a.oldRange._getTransformedBySplitOperation(b);
    }
    if (a.newRange) {
        if (context.abRelation) {
            const aNewRange = a.newRange._getTransformedBySplitOperation(b);
            if (a.newRange.start.isEqual(b.splitPosition) && context.abRelation.wasStartBeforeMergedElement) {
                a.newRange.start = Position._createAt(b.insertionPosition);
            }
            else if (a.newRange.start.isEqual(b.splitPosition) && !context.abRelation.wasInLeftElement) {
                a.newRange.start = Position._createAt(b.moveTargetPosition);
            }
            if (a.newRange.end.isEqual(b.splitPosition) && context.abRelation.wasInRightElement) {
                a.newRange.end = Position._createAt(b.moveTargetPosition);
            }
            else if (a.newRange.end.isEqual(b.splitPosition) && context.abRelation.wasEndBeforeMergedElement) {
                a.newRange.end = Position._createAt(b.insertionPosition);
            }
            else {
                a.newRange.end = aNewRange.end;
            }
            return [a];
        }
        a.newRange = a.newRange._getTransformedBySplitOperation(b);
    }
    return [a];
});
// -----------------------
setTransformation(MergeOperation, InsertOperation, (a, b) => {
    if (a.sourcePosition.hasSameParentAs(b.position)) {
        a.howMany += b.howMany;
    }
    a.sourcePosition = a.sourcePosition._getTransformedByInsertOperation(b);
    a.targetPosition = a.targetPosition._getTransformedByInsertOperation(b);
    return [a];
});
setTransformation(MergeOperation, MergeOperation, (a, b, context) => {
    // Case 1:
    //
    // Same merge operations.
    //
    // Both operations have same source and target positions. So the element already got merged and there is
    // theoretically nothing to do.
    //
    if (a.sourcePosition.isEqual(b.sourcePosition) && a.targetPosition.isEqual(b.targetPosition)) {
        // There are two ways that we can provide a do-nothing operation.
        //
        // First is simply a NoOperation instance. We will use it if `b` operation was not undone.
        //
        // Second is a merge operation that has the source operation in the merged element - in the graveyard -
        // same target position and `howMany` equal to `0`. So it is basically merging an empty element from graveyard
        // which is almost the same as NoOperation.
        //
        // This way the merge operation can be later transformed by split operation
        // to provide correct undo. This will be used if `b` operation was undone (only then it is correct).
        //
        if (!context.bWasUndone) {
            return [new NoOperation(0)];
        }
        else {
            const path = b.graveyardPosition.path.slice();
            path.push(0);
            a.sourcePosition = new Position(b.graveyardPosition.root, path);
            a.howMany = 0;
            return [a];
        }
    }
    // Case 2:
    //
    // Same merge source position but different target position.
    //
    // This can happen during collaboration. For example, if one client merged a paragraph to the previous paragraph
    // and the other person removed that paragraph and merged the same paragraph to something before:
    //
    // Client A:
    // <p>Foo</p><p>Bar</p><p>[]Xyz</p>
    // <p>Foo</p><p>BarXyz</p>
    //
    // Client B:
    // <p>Foo</p>[<p>Bar</p>]<p>Xyz</p>
    // <p>Foo</p><p>[]Xyz</p>
    // <p>FooXyz</p>
    //
    // In this case we need to decide where finally "Xyz" will land:
    //
    // <p>FooXyz</p>               graveyard: <p>Bar</p>
    // <p>Foo</p>                  graveyard: <p>BarXyz</p>
    //
    // Let's move it in a way so that a merge operation that does not target to graveyard is more important so that
    // nodes does not end up in the graveyard. It makes sense. Both for Client A and for Client B "Xyz" finally did not
    // end up in the graveyard (see above).
    //
    // If neither or both operations point to graveyard, then let `aIsStrong` decide.
    //
    if (a.sourcePosition.isEqual(b.sourcePosition) && !a.targetPosition.isEqual(b.targetPosition) &&
        !context.bWasUndone && context.abRelation != 'splitAtSource') {
        const aToGraveyard = a.targetPosition.root.rootName == '$graveyard';
        const bToGraveyard = b.targetPosition.root.rootName == '$graveyard';
        // If `aIsWeak` it means that `a` points to graveyard while `b` doesn't. Don't move nodes then.
        const aIsWeak = aToGraveyard && !bToGraveyard;
        // If `bIsWeak` it means that `b` points to graveyard while `a` doesn't. Force moving nodes then.
        const bIsWeak = bToGraveyard && !aToGraveyard;
        // Force move if `b` is weak or neither operation is weak but `a` is stronger through `context.aIsStrong`.
        const forceMove = bIsWeak || (!aIsWeak && context.aIsStrong);
        if (forceMove) {
            const sourcePosition = b.targetPosition._getTransformedByMergeOperation(b);
            const targetPosition = a.targetPosition._getTransformedByMergeOperation(b);
            return [new MoveOperation(sourcePosition, a.howMany, targetPosition, 0)];
        }
        else {
            return [new NoOperation(0)];
        }
    }
    // The default case.
    //
    if (a.sourcePosition.hasSameParentAs(b.targetPosition)) {
        a.howMany += b.howMany;
    }
    a.sourcePosition = a.sourcePosition._getTransformedByMergeOperation(b);
    a.targetPosition = a.targetPosition._getTransformedByMergeOperation(b);
    // Handle positions in graveyard.
    // If graveyard positions are same and `a` operation is strong - do not transform.
    if (!a.graveyardPosition.isEqual(b.graveyardPosition) || !context.aIsStrong) {
        a.graveyardPosition = a.graveyardPosition._getTransformedByMergeOperation(b);
    }
    return [a];
});
setTransformation(MergeOperation, MoveOperation, (a, b, context) => {
    // Case 1:
    //
    // The element to merge got removed.
    //
    // Merge operation does support merging elements which are not siblings. So it would not be a problem
    // from technical point of view. However, if the element was removed, the intention of the user deleting it
    // was to have it all deleted, together with its children. From user experience point of view, moving back the
    // removed nodes might be unexpected. This means that in this scenario we will block the merging.
    //
    // The exception of this rule would be if the remove operation was later undone.
    //
    const removedRange = Range._createFromPositionAndShift(b.sourcePosition, b.howMany);
    if (b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove) {
        if (a.deletionPosition.hasSameParentAs(b.sourcePosition) && removedRange.containsPosition(a.sourcePosition)) {
            return [new NoOperation(0)];
        }
    }
    // The default case.
    //
    if (a.sourcePosition.hasSameParentAs(b.targetPosition)) {
        a.howMany += b.howMany;
    }
    if (a.sourcePosition.hasSameParentAs(b.sourcePosition)) {
        a.howMany -= b.howMany;
    }
    a.sourcePosition = a.sourcePosition._getTransformedByMoveOperation(b);
    a.targetPosition = a.targetPosition._getTransformedByMoveOperation(b);
    // `MergeOperation` graveyard position is like `MoveOperation` target position. It is a position where element(s) will
    // be moved. Like in other similar cases, we need to consider the scenario when those positions are same.
    // Here, we will treat `MergeOperation` like it is always strong (see `InsertOperation` x `InsertOperation` for comparison).
    // This means that we won't transform graveyard position if it is equal to move operation target position.
    if (!a.graveyardPosition.isEqual(b.targetPosition)) {
        a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation(b);
    }
    return [a];
});
setTransformation(MergeOperation, SplitOperation, (a, b, context) => {
    if (b.graveyardPosition) {
        // If `b` operation defines graveyard position, a node from graveyard will be moved. This means that we need to
        // transform `a.graveyardPosition` accordingly.
        a.graveyardPosition = a.graveyardPosition._getTransformedByDeletion(b.graveyardPosition, 1);
        // This is a scenario foreseen in `MergeOperation` x `MergeOperation`, with two identical merge operations.
        //
        // So, there was `MergeOperation` x `MergeOperation` transformation earlier. Now, `a` is a merge operation which
        // source position is in graveyard. Interestingly, split operation wants to use the node to be merged by `a`. This
        // means that `b` is undoing that merge operation from earlier, which caused `a` to be in graveyard.
        //
        // If that's the case, at this point, we will only "fix" `a.howMany`. It was earlier set to `0` in
        // `MergeOperation` x `MergeOperation` transformation. Later transformations in this function will change other
        // properties.
        //
        if (a.deletionPosition.isEqual(b.graveyardPosition)) {
            a.howMany = b.howMany;
        }
    }
    // Case 1:
    //
    // Merge operation moves nodes to the place where split happens.
    // This is a classic situation when there are two paragraphs, and there is a split (enter) after the first
    // paragraph and there is a merge (delete) at the beginning of the second paragraph:
    //
    // <p>Foo{}</p><p>[]Bar</p>.
    //
    // Split is after `Foo`, while merge is from `Bar` to the end of `Foo`.
    //
    // State after split:
    // <p>Foo</p><p></p><p>Bar</p>
    //
    // Now, `Bar` should be merged to the new paragraph:
    // <p>Foo</p><p>Bar</p>
    //
    // Instead of merging it to the original paragraph:
    // <p>FooBar</p><p></p>
    //
    // This means that `targetPosition` needs to be transformed. This is the default case though.
    // For example, if the split would be after `F`, `targetPosition` should also be transformed.
    //
    // There are three exceptions, though, when we want to keep `targetPosition` as it was.
    //
    // First exception is when the merge target position is inside an element (not at the end, as usual). This
    // happens when the merge operation earlier was transformed by "the same" merge operation. If merge operation
    // targets inside the element we want to keep the original target position (and not transform it) because
    // we have additional context telling us that we want to merge to the original element. We can check if the
    // merge operation points inside element by checking what is `SplitOperation#howMany`. Since merge target position
    // is same as split position, if `howMany` is non-zero, it means that the merge target position is inside an element.
    //
    // Second exception is when the element to merge is in the graveyard and split operation uses it. In that case
    // if target position would be transformed, the merge operation would target at the source position:
    //
    // root: <p>Foo</p>				graveyard: <p></p>
    //
    // SplitOperation: root [ 0, 3 ] using graveyard [ 0 ] (howMany = 0)
    // MergeOperation: graveyard [ 0, 0 ] -> root [ 0, 3 ] (howMany = 0)
    //
    // Since split operation moves the graveyard node back to the root, the merge operation source position changes.
    // We would like to merge from the empty <p> to the "Foo" <p>:
    //
    // root: <p>Foo</p><p></p>			graveyard:
    //
    // MergeOperation#sourcePosition = root [ 1, 0 ]
    //
    // If `targetPosition` is transformed, it would become root [ 1, 0 ] as well. It has to be kept as it was.
    //
    // Third exception is connected with relations. If this happens during undo and we have explicit information
    // that target position has not been affected by the operation which is undone by this split then this split should
    // not move the target position either.
    //
    if (a.targetPosition.isEqual(b.splitPosition)) {
        const mergeInside = b.howMany != 0;
        const mergeSplittingElement = b.graveyardPosition && a.deletionPosition.isEqual(b.graveyardPosition);
        if (mergeInside || mergeSplittingElement || context.abRelation == 'mergeTargetNotMoved') {
            a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation(b);
            return [a];
        }
    }
    // Case 2:
    //
    // Merge source is at the same position as split position. This sometimes happen, mostly during undo.
    // The decision here is mostly to choose whether merge source position should stay where it is (so it will be at the end of the
    // split element) or should be move to the beginning of the new element.
    //
    if (a.sourcePosition.isEqual(b.splitPosition)) {
        // Use context to check if `SplitOperation` is not undoing a merge operation, that didn't change the `a` operation.
        // This scenario happens the undone merge operation moved nodes at the source position of `a` operation.
        // In that case `a` operation source position should stay where it is.
        if (context.abRelation == 'mergeSourceNotMoved') {
            a.howMany = 0;
            a.targetPosition = a.targetPosition._getTransformedBySplitOperation(b);
            return [a];
        }
        // This merge operation might have been earlier transformed by a merge operation which both merged the same element.
        // See that case in `MergeOperation` x `MergeOperation` transformation. In that scenario, if the merge operation has been undone,
        // the special case is not applied.
        //
        // Now, the merge operation is transformed by the split which has undone that previous merge operation.
        // So now we are fixing situation which was skipped in `MergeOperation` x `MergeOperation` case.
        //
        if (context.abRelation == 'mergeSameElement' || a.sourcePosition.offset > 0) {
            a.sourcePosition = b.moveTargetPosition.clone();
            a.targetPosition = a.targetPosition._getTransformedBySplitOperation(b);
            return [a];
        }
    }
    // The default case.
    //
    if (a.sourcePosition.hasSameParentAs(b.splitPosition)) {
        a.howMany = b.splitPosition.offset;
    }
    a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation(b);
    a.targetPosition = a.targetPosition._getTransformedBySplitOperation(b);
    return [a];
});
// -----------------------
setTransformation(MoveOperation, InsertOperation, (a, b) => {
    const moveRange = Range._createFromPositionAndShift(a.sourcePosition, a.howMany);
    const transformed = moveRange._getTransformedByInsertOperation(b, false)[0];
    a.sourcePosition = transformed.start;
    a.howMany = transformed.end.offset - transformed.start.offset;
    // See `InsertOperation` x `MoveOperation` transformation for details on this case.
    //
    // In summary, both operations point to the same place, so the order of nodes needs to be decided.
    // `MoveOperation` is considered weaker, so it is always transformed, unless there was a certain relation
    // between operations.
    //
    if (!a.targetPosition.isEqual(b.position)) {
        a.targetPosition = a.targetPosition._getTransformedByInsertOperation(b);
    }
    return [a];
});
setTransformation(MoveOperation, 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._createFromPositionAndShift(a.sourcePosition, a.howMany);
    const rangeB = Range._createFromPositionAndShift(b.sourcePosition, b.howMany);
    // Assign `context.aIsStrong` to a different variable, because the value may change during execution of
    // this algorithm and we do not want to override original `context.aIsStrong` that will be used in later transformations.
    let aIsStrong = context.aIsStrong;
    // This will be used to decide the order of nodes if both operations target at the same position.
    // By default, use strong/weak operation mechanism.
    let insertBefore = !context.aIsStrong;
    // If the relation is set, then use it to decide nodes order.
    if (context.abRelation == 'insertBefore' || context.baRelation == 'insertAfter') {
        insertBefore = true;
    }
    else if (context.abRelation == 'insertAfter' || context.baRelation == 'insertBefore') {
        insertBefore = false;
    }
    // `a.targetPosition` could be affected by the `b` operation. We will transform it.
    let newTargetPosition;
    if (a.targetPosition.isEqual(b.targetPosition) && insertBefore) {
        newTargetPosition = a.targetPosition._getTransformedByDeletion(b.sourcePosition, b.howMany);
    }
    else {
        newTargetPosition = a.targetPosition._getTransformedByMove(b.sourcePosition, b.targetPosition, b.howMany);
    }
    //
    // 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`.
    const bTargetsToA = rangeA.containsPosition(b.targetPosition);
    // 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);
        rangeA.end = rangeA.end._getTransformedByMove(b.sourcePosition, b.targetPosition, b.howMany);
        return _makeMoveOperationsFromRanges([rangeA], newTargetPosition);
    }
    //
    // Special case #2 mirror.
    //
    const aTargetsToB = rangeB.containsPosition(a.targetPosition);
    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);
    }
    //
    // 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);
        rangeA.end = rangeA.end._getTransformedByMove(b.sourcePosition, b.targetPosition, b.howMany);
        return _makeMoveOperationsFromRanges([rangeA], newTargetPosition);
    }
    //
    // 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 `aIsStrong` 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.
    if (a.type == 'remove' && b.type != 'remove' && !context.aWasUndone && !context.forceWeakRemove) {
        aIsStrong = true;
    }
    else if (a.type != 'remove' && b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove) {
        aIsStrong = 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);
        ranges.push(...newRanges);
    }
    // Then, we have to manage the "common part" of both move ranges.
    const common = rangeA.getIntersection(rangeB);
    if (common !== null && aIsStrong) {
        // 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 `aIsStrong == false` and `rangeA.isEqual( rangeB )`.
        return [new NoOperation(a.baseVersion)];
    }
    return _makeMoveOperationsFromRanges(ranges, newTargetPosition);
});
setTransformation(MoveOperation, SplitOperation, (a, b, context) => {
    let newTargetPosition = a.targetPosition.clone();
    // Do not transform if target position is same as split insertion position and this split comes from undo.
    // This should be done on relations but it is too much work for now as it would require relations working in collaboration.
    // We need to make a decision how we will resolve such conflict and this is less harmful way.
    if (!a.targetPosition.isEqual(b.insertionPosition) || !b.graveyardPosition || context.abRelation == 'moveTargetAfter') {
        newTargetPosition = a.targetPosition._getTransformedBySplitOperation(b);
    }
    // Case 1:
    //
    // Last element in the moved range got split.
    //
    // In this case the default range transformation will not work correctly as the element created by
    // split operation would be outside the range. The range to move needs to be fixed manually.
    //
    const moveRange = Range._createFromPositionAndShift(a.sourcePosition, a.howMany);
    if (moveRange.end.isEqual(b.insertionPosition)) {
        // Do it only if this is a "natural" split, not a one that comes from undo.
        // If this is undo split, only `targetPosition` needs to be changed (if the move is a remove).
        if (!b.graveyardPosition) {
            a.howMany++;
        }
        a.targetPosition = newTargetPosition;
        return [a];
    }
    // Case 2:
    //
    // Split happened between the moved nodes. In this case two ranges to move need to be generated.
    //
    // Characters `ozba` are moved to the end of paragraph `Xyz` but split happened.
    // <p>F[oz|ba]r</p><p>Xyz</p>
    //
    // After split:
    // <p>F[oz</p><p>ba]r</p><p>Xyz</p>
    //
    // Correct ranges:
    // <p>F[oz]</p><p>[ba]r</p><p>Xyz</p>
    //
    // After move:
    // <p>F</p><p>r</p><p>Xyzozba</p>
    //
    if (moveRange.start.hasSameParentAs(b.splitPosition) && moveRange.containsPosition(b.splitPosition)) {
        let rightRange = new Range(b.splitPosition, moveRange.end);
        rightRange = rightRange._getTransformedBySplitOperation(b);
        const ranges = [
            new Range(moveRange.start, b.splitPosition),
            rightRange
        ];
        return _makeMoveOperationsFromRanges(ranges, newTargetPosition);
    }
    // Case 3:
    //
    // Move operation targets at the split position. We need to decide if the nodes should be inserted
    // at the end of the split element or at the beginning of the new element.
    //
    if (a.targetPosition.isEqual(b.splitPosition) && context.abRelation == 'insertAtSource') {
        newTargetPosition = b.moveTargetPosition;
    }
    // Case 4:
    //
    // Move operation targets just after the split element. We need to decide if the nodes should be inserted
    // between two parts of split element, or after the new element.
    //
    // Split at `|`, while move operation moves `<p>Xyz</p>` and targets at `^`:
    // <p>Foo|bar</p>^<p>baz</p>
    // <p>Foo</p>^<p>bar</p><p>baz</p> or <p>Foo</p><p>bar</p>^<p>baz</p>?
    //
    // If there is no contextual information between operations (for example, they come from collaborative
    // editing), we don't want to put some unrelated content (move) between parts of related content (split parts).
    // However, if the split is from undo, in the past, the moved content might be targeting between the
    // split parts, meaning that was exactly user's intention:
    //
    // <p>Foo</p>^<p>bar</p>		<--- original situation, in "past".
    // <p>Foobar</p>^				<--- after merge target position is transformed.
    // <p>Foo|bar</p>^				<--- then the merge is undone, and split happens, which leads us to current situation.
    //
    // In this case it is pretty clear that the intention was to put new paragraph between those nodes,
    // so we need to transform accordingly. We can detect this scenario thanks to relations.
    //
    if (a.targetPosition.isEqual(b.insertionPosition) && context.abRelation == 'insertBetween') {
        newTargetPosition = a.targetPosition;
    }
    // The default case.
    //
    const transformed = moveRange._getTransformedBySplitOperation(b);
    const ranges = [transformed];
    // Case 5:
    //
    // Moved range contains graveyard element used by split operation. Add extra move operation to the result.
    //
    if (b.graveyardPosition) {
        const movesGraveyardElement = moveRange.start.isEqual(b.graveyardPosition) || moveRange.containsPosition(b.graveyardPosition);
        if (a.howMany > 1 && movesGraveyardElement && !context.aWasUndone) {
            ranges.push(Range._createFromPositionAndShift(b.insertionPosition, 1));
        }
    }
    return _makeMoveOperationsFromRanges(ranges, newTargetPosition);
});
setTransformation(MoveOperation, MergeOperation, (a, b, context) => {
    const movedRange = Range._createFromPositionAndShift(a.sourcePosition, a.howMany);
    if (b.deletionPosition.hasSameParentAs(a.sourcePosition) && movedRange.containsPosition(b.sourcePosition)) {
        if (a.type == 'remove' && !context.forceWeakRemove) {
            // Case 1:
            //
            // The element to remove got merged.
            //
            // Merge operation does support merging elements which are not siblings. So it would not be a problem
            // from technical point of view. However, if the element was removed, the intention of the user
            // deleting it was to have it all deleted. From user experience point of view, moving back the
            // removed nodes might be unexpected. This means that in this scenario we will reverse merging and remove the element.
            //
            if (!context.aWasUndone) {
                const results = [];
                let gyMoveSource = b.graveyardPosition.clone();
                let splitNodesMoveSource = b.targetPosition._getTransformedByMergeOperation(b);
                if (a.howMany > 1) {
                    results.push(new MoveOperation(a.sourcePosition, a.howMany - 1, a.targetPosition, 0));
                    gyMoveSource = gyMoveSource._getTransformedByMove(a.sourcePosition, a.targetPosition, a.howMany - 1);
                    splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove(a.sourcePosition, a.targetPosition, a.howMany - 1);
                }
                const gyMoveTarget = b.deletionPosition._getCombined(a.sourcePosition, a.targetPosition);
                const gyMove = new MoveOperation(gyMoveSource, 1, gyMoveTarget, 0);
                const splitNodesMoveTargetPath = gyMove.getMovedRangeStart().path.slice();
                splitNodesMoveTargetPath.push(0);
                const splitNodesMoveTarget = new Position(gyMove.targetPosition.root, splitNodesMoveTargetPath);
                splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove(gyMoveSource, gyMoveTarget, 1);
                const splitNodesMove = new MoveOperation(splitNodesMoveSource, b.howMany, splitNodesMoveTarget, 0);
                results.push(gyMove);
                results.push(splitNodesMove);
                return results;
            }
        }
        else {
            // Case 2:
            //
            // The element to move got merged and it was the only element to move.
            // In this case just don't do anything, leave the node in the graveyard. Without special case
            // it would be a move operation that moves 0 nodes, so maybe it is better just to return no-op.
            //
            if (a.howMany == 1) {
                if (!context.bWasUndone) {
                    return [new NoOperation(0)];
                }
                else {
                    a.sourcePosition = b.graveyardPosition.clone();
                    a.targetPosition = a.targetPosition._getTransformedByMergeOperation(b);
                    return [a];
                }
            }
        }
    }
    // The default case.
    //
    const moveRange = Range._createFromPositionAndShift(a.sourcePosition, a.howMany);
    const transformed = moveRange._getTransformedByMergeOperation(b);
    a.sourcePosition = transformed.start;
    a.howMany = transformed.end.offset - transformed.start.offset;
    a.targetPosition = a.targetPosition._getTransformedByMergeOperation(b);
    return [a];
});
// -----------------------
setTransformation(RenameOperation, InsertOperation, (a, b) => {
    a.position = a.position._getTransformedByInsertOperation(b);
    return [a];
});
setTransformation(RenameOperation, MergeOperation, (a, b) => {
    // Case 1:
    //
    // Element to rename got merged, so it was moved to `b.graveyardPosition`.
    //
    if (a.position.isEqual(b.deletionPosition)) {
        a.position = b.graveyardPosition.clone();
        a.position.stickiness = 'toNext';
        return [a];
    }
    a.position = a.position._getTransformedByMergeOperation(b);
    return [a];
});
setTransformation(RenameOperation, MoveOperation, (a, b) => {
    a.position = a.position._getTransformedByMoveOperation(b);
    return [a];
});
setTransformation(RenameOperation, RenameOperation, (a, b, context) => {
    if (a.position.isEqual(b.position)) {
        if (context.aIsStrong) {
            a.oldName = b.newName;
        }
        else {
            return [new NoOperation(0)];
        }
    }
    return [a];
});
setTransformation(RenameOperation, SplitOperation, (a, b) => {
    // Case 1:
    //
    // The element to rename has been split. In this case, the new element should be also renamed.
    //
    // User decides to change the paragraph to a list item:
    // <paragraph>Foobar</paragraph>
    //
    // However, in meantime, split happens:
    // <paragraph>Foo</paragraph><paragraph>bar</paragraph>
    //
    // As a result, rename both elements:
    // <listItem>Foo</listItem><listItem>bar</listItem>
    //
    const renamePath = a.position.path;
    const splitPath = b.splitPosition.getParentPath();
    if (compareArrays(renamePath, splitPath) == 'same' && !b.graveyardPosition) {
        const extraRename = new RenameOperation(a.position.getShiftedBy(1), a.oldName, a.newName, 0);
        return [a, extraRename];
    }
    // The default case.
    //
    a.position = a.position._getTransformedBySplitOperation(b);
    return [a];
});
// -----------------------
setTransformation(RootAttributeOperation, RootAttributeOperation, (a, b, context) => {
    if (a.root === b.root && a.key === b.key) {
        if (!context.aIsStrong || a.newValue === b.newValue) {
            return [new NoOperation(0)];
        }
        else {
            a.oldValue = b.newValue;
        }
    }
    return [a];
});
// -----------------------
setTransformation(SplitOperation, InsertOperation, (a, b) => {
    // The default case.
    //
    if (a.splitPosition.hasSameParentAs(b.position) && a.splitPosition.offset < b.position.offset) {
        a.howMany += b.howMany;
    }
    a.splitPosition = a.splitPosition._getTransformedByInsertOperation(b);
    a.insertionPosition = a.insertionPosition._getTransformedByInsertOperation(b);
    return [a];
});
setTransformation(SplitOperation, MergeOperation, (a, b, context) => {
    // Case 1:
    //
    // Split element got merged. If two different elements were merged, clients will have different content.
    //
    // Example. Merge at `{}`, split at `[]`:
    // <heading>Foo</heading>{}<paragraph>B[]ar</paragraph>
    //
    // On merge side it will look like this:
    // <heading>FooB[]ar</heading>
    // <heading>FooB</heading><heading>ar</heading>
    //
    // On split side it will look like this:
    // <heading>Foo</heading>{}<paragraph>B</paragraph><paragraph>ar</paragraph>
    // <heading>FooB</heading><paragraph>ar</paragraph>
    //
    // Clearly, the second element is different for both clients.
    //
    // We could use the removed merge element from graveyard as a split element but then clients would have a different
    // model state (in graveyard), because the split side client would still have an element in graveyard (removed by merge).
    //
    // To overcome this, in `SplitOperation` x `MergeOperation` transformation we will add additional `SplitOperation`
    // in the graveyard, which will actually clone the merged-and-deleted element. Then, that cloned element will be
    // used for splitting. Example below.
    //
    // Original state:
    // <heading>Foo</heading>{}<paragraph>B[]ar</paragraph>
    //
    // Merge side client:
    //
    // After merge:
    // <heading>FooB[]ar</heading>                                 graveyard: <paragraph></paragraph>
    //
    // Extra split:
    // <heading>FooB[]ar</heading>                                 graveyard: <paragraph></paragraph><paragraph></paragraph>
    //
    // Use the "cloned" element from graveyard:
    // <heading>FooB</heading><paragraph>ar</paragraph>            graveyard: <paragraph></paragraph>
    //
    // Split side client:
    //
    // After split:
    // <heading>Foo</heading>{}<paragraph>B</paragraph><paragraph>ar</paragraph>
    //
    // After merge:
    // <heading>FooB</heading><paragraph>ar</paragraph>            graveyard: <paragraph></paragraph>
    //
    // This special case scenario only applies if the original split operation clones the split element.
    // If the original split operation has `graveyardPosition` set, it all doesn't have sense because split operation
    // knows exactly which element it should use. So there would be no original problem with different contents.
    //
    // Additionally, the special case applies only if the merge wasn't already undone.
    //
    if (!a.graveyardPosition && !context.bWasUndone && a.splitPosition.hasSameParentAs(b.sourcePosition)) {
        const splitPath = b.graveyardPosition.path.slice();
        splitPath.push(0);
        const splitPosition = new Position(b.graveyardPosition.root, splitPath);
        const insertionPosition = SplitOperation.getInsertionPosition(new Position(b.graveyardPosition.root, splitPath));
        const additionalSplit = new SplitOperation(splitPosition, 0, insertionPosition, null, 0);
        a.splitPosition = a.splitPosition._getTransformedByMergeOperation(b);
        a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
        a.graveyardPosition = additionalSplit.insertionPosition.clone();
        a.graveyardPosition.stickiness = 'toNext';
        return [additionalSplit, a];
    }
    // The default case.
    //
    if (a.splitPosition.hasSameParentAs(b.deletionPosition) && !a.splitPosition.isAfter(b.deletionPosition)) {
        a.howMany--;
    }
    if (a.splitPosition.hasSameParentAs(b.targetPosition)) {
        a.howMany += b.howMany;
    }
    a.splitPosition = a.splitPosition._getTransformedByMergeOperation(b);
    a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
    if (a.graveyardPosition) {
        a.graveyardPosition = a.graveyardPosition._getTransformedByMergeOperation(b);
    }
    return [a];
});
setTransformation(SplitOperation, MoveOperation, (a, b, context) => {
    const rangeToMove = Range._createFromPositionAndShift(b.sourcePosition, b.howMany);
    if (a.graveyardPosition) {
        // Case 1:
        //
        // Split operation graveyard node was moved. In this case move operation is stronger. Since graveyard element
        // is already moved to the correct position, we need to only move the nodes after the split position.
        // This will be done by `MoveOperation` instead of `SplitOperation`.
        //
        const gyElementMoved = rangeToMove.start.isEqual(a.graveyardPosition) || rangeToMove.containsPosition(a.graveyardPosition);
        if (!context.bWasUndone && gyElementMoved) {
            const sourcePosition = a.splitPosition._getTransformedByMoveOperation(b);
            const newParentPosition = a.graveyardPosition._getTransformedByMoveOperation(b);
            const newTargetPath = newParentPosition.path.slice();
            newTargetPath.push(0);
            const newTargetPosition = new Position(newParentPosition.root, newTargetPath);
            const moveOp = new MoveOperation(sourcePosition, a.howMany, newTargetPosition, 0);
            return [moveOp];
        }
        a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation(b);
    }
    // Case 2:
    //
    // Split is at a position where nodes were moved.
    //
    // This is a scenario described in `MoveOperation` x `SplitOperation` transformation but from the
    // "split operation point of view".
    //
    const splitAtTarget = a.splitPosition.isEqual(b.targetPosition);
    if (splitAtTarget && (context.baRelation == 'insertAtSource' || context.abRelation == 'splitBefore')) {
        a.howMany += b.howMany;
        a.splitPosition = a.splitPosition._getTransformedByDeletion(b.sourcePosition, b.howMany);
        a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
        return [a];
    }
    if (splitAtTarget && context.abRelation && context.abRelation.howMany) {
        const { howMany, offset } = context.abRelation;
        a.howMany += howMany;
        a.splitPosition = a.splitPosition.getShiftedBy(offset);
        return [a];
    }
    // Case 3:
    //
    // If the split position is inside the moved range, we need to shift the split position to a proper place.
    // The position cannot be moved together with moved range because that would result in splitting of an incorrect element.
    //
    // Characters `bc` should be moved to the second paragraph while split position is between them:
    // <paragraph>A[b|c]d</paragraph><paragraph>Xyz</paragraph>
    //
    // After move, new split position is incorrect:
    // <paragraph>Ad</paragraph><paragraph>Xb|cyz</paragraph>
    //
    // Correct split position:
    // <paragraph>A|d</paragraph><paragraph>Xbcyz</paragraph>
    //
    // After split:
    // <paragraph>A</paragraph><paragraph>d</paragraph><paragraph>Xbcyz</paragraph>
    //
    if (a.splitPosition.hasSameParentAs(b.sourcePosition) && rangeToMove.containsPosition(a.splitPosition)) {
        const howManyRemoved = b.howMany - (a.splitPosition.offset - b.sourcePosition.offset);
        a.howMany -= howManyRemoved;
        if (a.splitPosition.hasSameParentAs(b.targetPosition) && a.splitPosition.offset < b.targetPosition.offset) {
            a.howMany += b.howMany;
        }
        a.splitPosition = b.sourcePosition.clone();
        a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
        return [a];
    }
    // The default case.
    // Don't change `howMany` if move operation does not really move anything.
    //
    if (!b.sourcePosition.isEqual(b.targetPosition)) {
        if (a.splitPosition.hasSameParentAs(b.sourcePosition) && a.splitPosition.offset <= b.sourcePosition.offset) {
            a.howMany -= b.howMany;
        }
        if (a.splitPosition.hasSameParentAs(b.targetPosition) && a.splitPosition.offset < b.targetPosition.offset) {
            a.howMany += b.howMany;
        }
    }
    // Change position stickiness to force a correct transformation.
    a.splitPosition.stickiness = 'toNone';
    a.splitPosition = a.splitPosition._getTransformedByMoveOperation(b);
    a.splitPosition.stickiness = 'toNext';
    if (a.graveyardPosition) {
        a.insertionPosition = a.insertionPosition._getTransformedByMoveOperation(b);
    }
    else {
        a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
    }
    return [a];
});
setTransformation(SplitOperation, SplitOperation, (a, b, context) => {
    // Case 1:
    //
    // Split at the same position.
    //
    // If there already was a split at the same position as in `a` operation, it means that the intention
    // conveyed by `a` operation has already been fulfilled and `a` should not do anything (to avoid double split).
    //
    // However, there is a difference if these are new splits or splits created by undo. These have different
    // intentions. Also splits moving back different elements from graveyard have different intentions. They
    // are just different operations.
    //
    // So we cancel split operation only if it was really identical.
    //
    // Also, there is additional case, where split operations aren't identical and should not be cancelled, however the
    // default transformation is incorrect too.
    //
    if (a.splitPosition.isEqual(b.splitPosition)) {
        if (!a.graveyardPosition && !b.graveyardPosition) {
            return [new NoOperation(0)];
        }
        if (a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual(b.graveyardPosition)) {
            return [new NoOperation(0)];
        }
        // Use context to know that the `a.splitPosition` should stay where it is.
        // This happens during undo when first a merge operation moved nodes to `a.splitPosition` and now `b` operation undoes that merge.
        if (context.abRelation == 'splitBefore') {
            // Since split is at the same position, there are no nodes left to split.
            a.howMany = 0;
            // Note: there was `if ( a.graveyardPosition )` here but it was uncovered in tests and I couldn't find any scenarios for now.
            // That would have to be a `SplitOperation` that didn't come from undo but is transformed by operations that were undone.
            // It could happen if `context` is enabled in collaboration.
            a.graveyardPosition = a.graveyardPosition._getTransformedBySplitOperation(b);
            return [a];
        }
    }
    // Case 2:
    //
    // Same node is using to split different elements. This happens in undo when previously same element was merged to
    // two different elements. This is described in `MergeOperation` x `MergeOperation` transformation.
    //
    // In this case we will follow the same logic. We will assume that `insertionPosition` is same for both
    // split operations. This might not always be true but in the real cases that were experienced it was. After all,
    // if these splits are reverses of merge operations that were merging the same element, then the `insertionPosition`
    // should be same for both of those splits.
    //
    // Again, we will decide which operation is stronger by checking if split happens in graveyard or in non-graveyard root.
    //
    if (a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual(b.graveyardPosition)) {
        const aInGraveyard = a.splitPosition.root.rootName == '$graveyard';
        const bInGraveyard = b.splitPosition.root.rootName == '$graveyard';
        // If `aIsWeak` it means that `a` points to graveyard while `b` doesn't. Don't move nodes then.
        const aIsWeak = aInGraveyard && !bInGraveyard;
        // If `bIsWeak` it means that `b` points to graveyard while `a` doesn't. Force moving nodes then.
        const bIsWeak = bInGraveyard && !aInGraveyard;
        // Force move if `b` is weak or neither operation is weak but `a` is stronger through `context.aIsStrong`.
        const forceMove = bIsWeak || (!aIsWeak && context.aIsStrong);
        if (forceMove) {
            const result = [];
            // First we need to move any nodes split by `b` back to where they were.
            // Do it only if `b` actually moved something.
            if (b.howMany) {
                result.push(new MoveOperation(b.moveTargetPosition, b.howMany, b.splitPosition, 0));
            }
            // Then we need to move nodes from `a` split position to their new element.
            // Do it only if `a` actually should move something.
            if (a.howMany) {
                result.push(new MoveOperation(a.splitPosition, a.howMany, a.moveTargetPosition, 0));
            }
            return result;
        }
        else {
            return [new NoOperation(0)];
        }
    }
    if (a.graveyardPosition) {
        a.graveyardPosition = a.graveyardPosition._getTransformedBySplitOperation(b);
    }
    // Case 3:
    //
    // Position where operation `b` inserted a new node after split is the same as the operation `a` split position.
    // As in similar cases, there is ambiguity if the split should be before the new node (created by `b`) or after.
    //
    if (a.splitPosition.isEqual(b.insertionPosition) && context.abRelation == 'splitBefore') {
        a.howMany++;
        return [a];
    }
    // Case 4:
    //
    // This is a mirror to the case 2. above.
    //
    if (b.splitPosition.isEqual(a.insertionPosition) && context.baRelation == 'splitBefore') {
        const newPositionPath = b.insertionPosition.path.slice();
        newPositionPath.push(0);
        const newPosition = new Position(b.insertionPosition.root, newPositionPath);
        const moveOp = new MoveOperation(a.insertionPosition, 1, newPosition, 0);
        return [a, moveOp];
    }
    // The default case.
    //
    if (a.splitPosition.hasSameParentAs(b.splitPosition) && a.splitPosition.offset < b.splitPosition.offset) {
        a.howMany -= b.howMany;
    }
    a.splitPosition = a.splitPosition._getTransformedBySplitOperation(b);
    a.insertionPosition = SplitOperation.getInsertionPosition(a.splitPosition);
    return [a];
});
// Checks whether `MoveOperation` `targetPosition` is inside a node from the moved range of the other `MoveOperation`.
//
// @private
// @param {module:engine/model/operation/moveoperation~MoveOperation} a
// @param {module:engine/model/operation/moveoperation~MoveOperation} b
// @returns {Boolean}
function _moveTargetIntoMovedRange(a, b) {
    return a.targetPosition._getTransformedByDeletion(b.sourcePosition, b.howMany) === null;
}
// Helper function for `MoveOperation` x `MoveOperation` transformation. Converts given ranges and target position to
// move operations and returns 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`.
//
// @private
// @param {Array.<module:engine/model/range~Range>} ranges
// @param {module:engine/model/position~Position} targetPosition
// @returns {Array.<module:engine/model/operation/moveoperation~MoveOperation>}
function _makeMoveOperationsFromRanges(ranges, targetPosition) {
    // 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 range = ranges[i];
        const op = new MoveOperation(range.start, range.end.offset - range.start.offset, targetPosition, 0);
        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);
    }
    return operations;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * {@link module:engine/view/document~Document#event:click Click} event observer.
 *
 * Note that this observer is not available by default. To make it available it needs to be added to
 * {@link module:engine/view/view~View view controller}
 * by a {@link module:engine/view/view~View#addObserver} method.
 *
 * @extends module:engine/view/observer/domeventobserver~DomEventObserver
 */
class ClickObserver extends DomEventObserver {
    constructor(view) {
        super(view);
        this.domEventType = 'click';
    }
    onDomEvent(domEvent) {
        this.fire(domEvent.type, domEvent);
    }
}
/**
 * Fired when one of the editables has been clicked.
 *
 * Introduced by {@link module:engine/view/observer/clickobserver~ClickObserver}.
 *
 * Note that this event is not available by default. To make it available
 * {@link module:engine/view/observer/clickobserver~ClickObserver} needs to be added
 * to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method.
 *
 * @see module:engine/view/observer/clickobserver~ClickObserver
 * @event module:engine/view/document~Document#event:click
 * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Mouse events observer.
 *
 * Note that this observer is not available by default. To make it available it needs to be added to
 * {@link module:engine/view/view~View} by {@link module:engine/view/view~View#addObserver} method.
 *
 * @extends module:engine/view/observer/domeventobserver~DomEventObserver
 */
class MouseObserver extends DomEventObserver {
    constructor(view) {
        super(view);
        this.domEventType = ['mousedown', 'mouseup', 'mouseover', 'mouseout'];
    }
    onDomEvent(domEvent) {
        this.fire(domEvent.type, domEvent);
    }
}
/**
 * Fired when the mouse button is pressed down on one of the editing roots of the editor.
 *
 * Introduced by {@link module:engine/view/observer/mouseobserver~MouseObserver}.
 *
 * Note that this event is not available by default. To make it available, {@link module:engine/view/observer/mouseobserver~MouseObserver}
 * needs to be added to {@link module:engine/view/view~View} by the {@link module:engine/view/view~View#addObserver} method.
 *
 * @see module:engine/view/observer/mouseobserver~MouseObserver
 * @event module:engine/view/document~Document#event:mousedown
 * @param {module:engine/view/observer/domeventdata~DomEventData} data The event data.
 */
/**
 * Fired when the mouse button is released over one of the editing roots of the editor.
 *
 * Introduced by {@link module:engine/view/observer/mouseobserver~MouseObserver}.
 *
 * Note that this event is not available by default. To make it available, {@link module:engine/view/observer/mouseobserver~MouseObserver}
 * needs to be added to {@link module:engine/view/view~View} by the {@link module:engine/view/view~View#addObserver} method.
 *
 * @see module:engine/view/observer/mouseobserver~MouseObserver
 * @event module:engine/view/document~Document#event:mouseup
 * @param {module:engine/view/observer/domeventdata~DomEventData} data The event data.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * View upcast writer. It provides a set of methods used to manipulate non-semantic view trees.
 *
 * It should be used only while working on a non-semantic view
 * (e.g. a view created from HTML string on paste).
 * To manipulate a view which was or is being downcasted from the the model use the
 * {@link module:engine/view/downcastwriter~DowncastWriter downcast writer}.
 *
 * Read more about changing the view in the {@glink framework/guides/architecture/editing-engine#changing-the-view Changing the view}
 * section of the {@glink framework/guides/architecture/editing-engine Editing engine architecture} guide.
 *
 * Unlike `DowncastWriter`, which is available in the {@link module:engine/view/view~View#change `View#change()`} block,
 * `UpcastWriter` can be created wherever you need it:
 *
 *		const writer = new UpcastWriter( viewDocument );
 *		const text = writer.createText( 'foo!' );
 *
 *		writer.appendChild( text, someViewElement );
 */
class UpcastWriter {
    /**
     * @param {module:engine/view/document~Document} document The view document instance in which this upcast writer operates.
     */
    constructor(document) {
        /**
         * The view document instance in which this upcast writer operates.
         *
         * @readonly
         * @type {module:engine/view/document~Document}
         */
        this.document = document;
    }
    /**
     * Creates a new {@link module:engine/view/documentfragment~DocumentFragment} instance.
     *
     * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
     * A list of nodes to be inserted into the created document fragment.
     * @returns {module:engine/view/documentfragment~DocumentFragment} The created document fragment.
     */
    createDocumentFragment(children) {
        return new DocumentFragment$1(this.document, children);
    }
    /**
     * Creates a new {@link module:engine/view/element~Element} instance.
     *
     * Attributes can be passed in various formats:
     *
     *		upcastWriter.createElement( 'div', { class: 'editor', contentEditable: 'true' } ); // object
     *		upcastWriter.createElement( 'div', [ [ 'class', 'editor' ], [ 'contentEditable', 'true' ] ] ); // map-like iterator
     *		upcastWriter.createElement( '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]
     * A list of nodes to be inserted into created element.
     * @returns {module:engine/view/element~Element} Created element.
     */
    createElement(name, attrs, children) {
        return new Element$1(this.document, name, attrs, children);
    }
    /**
     * Creates a new {@link module:engine/view/text~Text} instance.
     *
     * @param {String} data The text's data.
     * @returns {module:engine/view/text~Text} The created text node.
     */
    createText(data) {
        return new Text$2(this.document, data);
    }
    /**
     * Clones the provided element.
     *
     * @see module:engine/view/element~Element#_clone
     * @param {module:engine/view/element~Element} element Element to be cloned.
     * @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(element, deep = false) {
        return element._clone(deep);
    }
    /**
     * Appends 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.
     *
     * @see module:engine/view/element~Element#_appendChild
     * @param {module:engine/view/item~Item|Iterable.<module:engine/view/item~Item>} items Items to be inserted.
     * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} element Element
     * to which items will be appended.
     * @fires module:engine/view/node~Node#event:change
     * @returns {Number} Number of appended nodes.
     */
    appendChild(items, element) {
        return element._appendChild(items);
    }
    /**
     * Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to
     * this element.
     *
     * @see module:engine/view/element~Element#_insertChild
     * @param {Number} index Offset at which nodes should be inserted.
     * @param {module:engine/view/item~Item|Iterable.<module:engine/view/item~Item>} items Items to be inserted.
     * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} element Element
     * to which items will be inserted.
     * @fires module:engine/view/node~Node#event:change
     * @returns {Number} Number of inserted nodes.
     */
    insertChild(index, items, element) {
        return element._insertChild(index, items);
    }
    /**
     * Removes the given number of child nodes starting at the given index and set the parent of these nodes to `null`.
     *
     * @see module:engine/view/element~Element#_removeChildren
     * @param {Number} index Offset from which nodes will be removed.
     * @param {Number} howMany Number of nodes to remove.
     * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} element Element
     * which children will be removed.
     * @fires module:engine/view/node~Node#event:change
     * @returns {Array.<module:engine/view/node~Node>} The array containing removed nodes.
     */
    removeChildren(index, howMany, element) {
        return element._removeChildren(index, howMany);
    }
    /**
     * Removes given element from the view structure. Will not have effect on detached elements.
     *
     * @param {module:engine/view/element~Element} element Element which will be removed.
     * @returns {Array.<module:engine/view/node~Node>} The array containing removed nodes.
     */
    remove(element) {
        const parent = element.parent;
        if (parent) {
            return this.removeChildren(parent.getChildIndex(element), 1, parent);
        }
        return [];
    }
    /**
     * Replaces given element with the new one in the view structure. Will not have effect on detached elements.
     *
     * @param {module:engine/view/element~Element} oldElement Element which will be replaced.
     * @param {module:engine/view/element~Element} newElement Element which will be inserted in the place of the old element.
     * @returns {Boolean} Whether old element was successfully replaced.
     */
    replace(oldElement, newElement) {
        const parent = oldElement.parent;
        if (parent) {
            const index = parent.getChildIndex(oldElement);
            this.removeChildren(index, 1, parent);
            this.insertChild(index, newElement, parent);
            return true;
        }
        return false;
    }
    /**
     * Removes given element from view structure and places its children in its position.
     * It does nothing if element has no parent.
     *
     * @param {module:engine/view/element~Element} element Element to unwrap.
     */
    unwrapElement(element) {
        const parent = element.parent;
        if (parent) {
            const index = parent.getChildIndex(element);
            this.remove(element);
            this.insertChild(index, element.getChildren(), parent);
        }
    }
    /**
     * Renames element by creating a copy of a given element but with its name changed and then moving contents of the
     * old element to the new one.
     *
     * Since this function creates a new element and removes the given one, the new element is returned to keep reference.
     *
     * @param {String} newName New element name.
     * @param {module:engine/view/element~Element} element Element to be renamed.
     * @returns {module:engine/view/element~Element|null} New element or null if the old element
     * was not replaced (happens for detached elements).
     */
    rename(newName, element) {
        const newElement = new Element$1(this.document, newName, element.getAttributes(), element.getChildren());
        return this.replace(element, newElement) ? newElement : null;
    }
    /**
     * Adds or overwrites element's attribute with a specified key and value.
     *
     *		writer.setAttribute( 'href', 'http://ckeditor.com', linkElement );
     *
     * @see module:engine/view/element~Element#_setAttribute
     * @param {String} key Attribute key.
     * @param {String} value Attribute value.
     * @param {module:engine/view/element~Element} element Element for which attribute will be set.
     */
    setAttribute(key, value, element) {
        element._setAttribute(key, value);
    }
    /**
     * Removes attribute from the element.
     *
     *		writer.removeAttribute( 'href', linkElement );
     *
     * @see module:engine/view/element~Element#_removeAttribute
     * @param {String} key Attribute key.
     * @param {module:engine/view/element~Element} element Element from which attribute will be removed.
     */
    removeAttribute(key, element) {
        element._removeAttribute(key);
    }
    /**
     * Adds specified class to the element.
     *
     *		writer.addClass( 'foo', linkElement );
     *		writer.addClass( [ 'foo', 'bar' ], linkElement );
     *
     * @see module:engine/view/element~Element#_addClass
     * @param {Array.<String>|String} className Single class name or array of class names which will be added.
     * @param {module:engine/view/element~Element} element Element for which class will be added.
     */
    addClass(className, element) {
        element._addClass(className);
    }
    /**
     * Removes specified class from the element.
     *
     *		writer.removeClass( 'foo', linkElement );
     *		writer.removeClass( [ 'foo', 'bar' ], linkElement );
     *
     * @see module:engine/view/element~Element#_removeClass
     * @param {Array.<String>|String} className Single class name or array of class names which will be removed.
     * @param {module:engine/view/element~Element} element Element from which class will be removed.
     */
    removeClass(className, element) {
        element._removeClass(className);
    }
    setStyle(property, valueOrElement, element) {
        if (isPlainObject(property) && element === undefined) {
            valueOrElement._setStyle(property);
        }
        else {
            element._setStyle(property, valueOrElement);
        }
    }
    /**
     * Removes specified style from the element.
     *
     *		writer.removeStyle( 'color', element );  // Removes 'color' style.
     *		writer.removeStyle( [ 'color', 'border-top' ], element ); // Removes both 'color' and 'border-top' styles.
     *
     * **Note**: This method can work with normalized style names if
     * {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
     * See {@link module:engine/view/stylesmap~StylesMap#remove `StylesMap#remove()`} for details.
     *
     * @see module:engine/view/element~Element#_removeStyle
     * @param {Array.<String>|String} property Style property name or names to be removed.
     * @param {module:engine/view/element~Element} element Element from which style will be removed.
     */
    removeStyle(property, element) {
        element._removeStyle(property);
    }
    /**
     * Sets a custom property on element. Unlike attributes, custom properties are not rendered to the DOM,
     * so they can be used to add special data to elements.
     *
     * @see module:engine/view/element~Element#_setCustomProperty
     * @param {String|Symbol} key Custom property name/key.
     * @param {*} value Custom property value to be stored.
     * @param {module:engine/view/element~Element} element Element for which custom property will be set.
     */
    setCustomProperty(key, value, element) {
        element._setCustomProperty(key, value);
    }
    /**
     * Removes a custom property stored under the given key.
     *
     * @see module:engine/view/element~Element#_removeCustomProperty
     * @param {String|Symbol} key Name/key of the custom property to be removed.
     * @param {module:engine/view/element~Element} element Element from which the custom property will be removed.
     * @returns {Boolean} Returns true if property was removed.
     */
    removeCustomProperty(key, element) {
        return element._removeCustomProperty(key);
    }
    /**
     * 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 #createPositionBefore},
     * * {@link #createPositionAfter},
     *
     * @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
     * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
     * first parameter is a {@link module:engine/view/item~Item view item}.
     * @returns {module:engine/view/position~Position}
     */
    createPositionAt(itemOrPosition, offset) {
        return Position$1._createAt(itemOrPosition, 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}
     */
    createPositionAfter(item) {
        return Position$1._createAfter(item);
    }
    /**
     * 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}
     */
    createPositionBefore(item) {
        return Position$1._createBefore(item);
    }
    /**
     * Creates a range spanning from `start` position to `end` position.
     *
     * **Note:** This factory method 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.
     * @returns {module:engine/view/range~Range}
     */
    createRange(start, end) {
        return new Range$1(start, end);
    }
    /**
     * 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}
     */
    createRangeOn(item) {
        return Range$1._createOn(item);
    }
    /**
     * 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}
     */
    createRangeIn(element) {
        return Range$1._createIn(element);
    }
    /**
     * Creates a new {@link module:engine/view/selection~Selection} instance.
     *
     * 		// Creates empty selection without ranges.
     *		const selection = writer.createSelection();
     *
     *		// Creates selection at the given range.
     *		const range = writer.createRange( start, end );
     *		const selection = writer.createSelection( range );
     *
     *		// Creates selection at the given ranges
     * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
     *		const selection = writer.createSelection( ranges );
     *
     *		// Creates selection from the other selection.
     *		const otherSelection = writer.createSelection();
     *		const selection = writer.createSelection( otherSelection );
     *
     *		// Creates selection from the document selection.
     *		const selection = writer.createSelection( editor.editing.view.document.selection );
     *
     * 		// Creates selection at the given position.
     *		const position = writer.createPositionFromPath( root, path );
     *		const selection = writer.createSelection( position );
     *
     *		// Creates collapsed selection at the position of given item and offset.
     *		const paragraph = writer.createContainerElement( 'paragraph' );
     *		const selection = writer.createSelection( paragraph, offset );
     *
     *		// 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.
     *		const selection = writer.createSelection( paragraph, 'in' );
     *
     *		// Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
     *		// just after the item.
     *		const selection = writer.createSelection( paragraph, 'on' );
     *
     * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
     *
     *		// Creates backward selection.
     *		const selection = writer.createSelection( range, { backward: true } );
     *
     * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
     * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
     * represented in other way, for example by applying proper CSS class.
     *
     * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
     * (and be  properly handled by screen readers).
     *
     *		// Creates fake selection with label.
     *		const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
     *
     * @param {module:engine/view/selection~Selectable} [selectable=null]
     * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
     * @param {Object} [options]
     * @param {Boolean} [options.backward] Sets this selection instance to be backward.
     * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
     * @param {String} [options.label] Label for the fake selection.
     * @returns {module:engine/view/selection~Selection}
     */
    createSelection(...args) {
        return new Selection$1(...args);
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
const HEX_COLOR_REGEXP = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
const RGB_COLOR_REGEXP = /^rgb\([ ]?([0-9]{1,3}[ %]?,[ ]?){2,3}[0-9]{1,3}[ %]?\)$/i;
const RGBA_COLOR_REGEXP = /^rgba\([ ]?([0-9]{1,3}[ %]?,[ ]?){3}(1|[0-9]+%|[0]?\.?[0-9]+)\)$/i;
const HSL_COLOR_REGEXP = /^hsl\([ ]?([0-9]{1,3}[ %]?[,]?[ ]*){3}(1|[0-9]+%|[0]?\.?[0-9]+)?\)$/i;
const HSLA_COLOR_REGEXP = /^hsla\([ ]?([0-9]{1,3}[ %]?,[ ]?){2,3}(1|[0-9]+%|[0]?\.?[0-9]+)\)$/i;
const COLOR_NAMES = new Set([
    // CSS Level 1
    'black', 'silver', 'gray', 'white', 'maroon', 'red', 'purple', 'fuchsia',
    'green', 'lime', 'olive', 'yellow', 'navy', 'blue', 'teal', 'aqua',
    // CSS Level 2 (Revision 1)
    'orange',
    // CSS Color Module Level 3
    'aliceblue', 'antiquewhite', 'aquamarine', 'azure', 'beige', 'bisque', 'blanchedalmond', 'blueviolet', 'brown',
    'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan',
    'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey', 'darkkhaki', 'darkmagenta',
    'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue',
    'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey',
    'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod',
    'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush',
    'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray',
    'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray',
    'lightslategrey', 'lightsteelblue', 'lightyellow', 'limegreen', 'linen', 'magenta', 'mediumaquamarine',
    'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen',
    'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite',
    'oldlace', 'olivedrab', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred',
    'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon',
    'sandybrown', 'seagreen', 'seashell', 'sienna', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow',
    'springgreen', 'steelblue', 'tan', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'whitesmoke', 'yellowgreen',
    // CSS Color Module Level 3 (System Colors)
    'activeborder', 'activecaption', 'appworkspace', 'background', 'buttonface', 'buttonhighlight', 'buttonshadow',
    'buttontext', 'captiontext', 'graytext', 'highlight', 'highlighttext', 'inactiveborder', 'inactivecaption',
    'inactivecaptiontext', 'infobackground', 'infotext', 'menu', 'menutext', 'scrollbar', 'threeddarkshadow',
    'threedface', 'threedhighlight', 'threedlightshadow', 'threedshadow', 'window', 'windowframe', 'windowtext',
    // CSS Color Module Level 4
    'rebeccapurple',
    // Keywords
    'currentcolor', 'transparent'
]);
/**
 * Checks if string contains [color](https://developer.mozilla.org/en-US/docs/Web/CSS/color) CSS value.
 *
 *		isColor( '#f00' );						// true
 *		isColor( '#AA00BB33' );					// true
 *		isColor( 'rgb(0, 0, 250)' );			// true
 *		isColor( 'hsla(240, 100%, 50%, .7)' );	// true
 *		isColor( 'deepskyblue' );				// true
 *
 * **Note**: It does not support CSS Level 4 whitespace syntax, system colors and radius values for HSL colors.
 *
 * @param {String} string
 * @returns {Boolean}
 */
function isColor(string) {
    // As far as I was able to test checking some pre-conditions is faster than joining each test with ||.
    if (string.startsWith('#')) {
        return HEX_COLOR_REGEXP.test(string);
    }
    if (string.startsWith('rgb')) {
        return RGB_COLOR_REGEXP.test(string) || RGBA_COLOR_REGEXP.test(string);
    }
    if (string.startsWith('hsl')) {
        return HSL_COLOR_REGEXP.test(string) || HSLA_COLOR_REGEXP.test(string);
    }
    // Array check > RegExp test.
    return COLOR_NAMES.has(string.toLowerCase());
}
const lineStyleValues = ['none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'];
/**
 * Checks if string contains [line style](https://developer.mozilla.org/en-US/docs/Web/CSS/border-style) CSS value.
 *
 * @param {String} string
 * @returns {Boolean}
 */
function isLineStyle(string) {
    return lineStyleValues.includes(string);
}
const lengthRegExp = /^([+-]?[0-9]*([.][0-9]+)?(px|cm|mm|in|pc|pt|ch|em|ex|rem|vh|vw|vmin|vmax)|0)$/;
/**
 * Checks if string contains [length](https://developer.mozilla.org/en-US/docs/Web/CSS/length) CSS value.
 *
 * @param {String} string
 * @returns {Boolean}
 */
function isLength(string) {
    return lengthRegExp.test(string);
}
const PERCENTAGE_VALUE_REGEXP = /^[+-]?[0-9]*([.][0-9]+)?%$/;
/**
 * Checks if string contains [percentage](https://developer.mozilla.org/en-US/docs/Web/CSS/percentage) CSS value.
 *
 * @param {String} string
 * @returns {Boolean}
 */
function isPercentage(string) {
    return PERCENTAGE_VALUE_REGEXP.test(string);
}
const repeatValues = ['repeat-x', 'repeat-y', 'repeat', 'space', 'round', 'no-repeat'];
/**
 * Checks if string contains [background repeat](https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat) CSS value.
 *
 * @param {String} string
 * @returns {Boolean}
 */
function isRepeat(string) {
    return repeatValues.includes(string);
}
const positionValues = ['center', 'top', 'bottom', 'left', 'right'];
/**
 * Checks if string contains [background position](https://developer.mozilla.org/en-US/docs/Web/CSS/background-position) CSS value.
 *
 * @param {String} string
 * @returns {Boolean}
 */
function isPosition(string) {
    return positionValues.includes(string);
}
const attachmentValues = ['fixed', 'scroll', 'local'];
/**
 * Checks if string contains [background attachment](https://developer.mozilla.org/en-US/docs/Web/CSS/background-attachment) CSS value.
 *
 * @param {String} string
 * @returns {Boolean}
 */
function isAttachment(string) {
    return attachmentValues.includes(string);
}
const urlRegExp = /^url\(/;
/**
 * Checks if string contains [URL](https://developer.mozilla.org/en-US/docs/Web/CSS/url) CSS value.
 *
 * @param {String} string
 * @returns {Boolean}
 */
function isURL(string) {
    return urlRegExp.test(string);
}
/**
 * TODO: Docs
 */
function getBoxSidesValues(value = '') {
    if (value === '') {
        return { top: undefined, right: undefined, bottom: undefined, left: undefined };
    }
    const values = getShorthandValues(value);
    const top = values[0];
    const bottom = values[2] || top;
    const right = values[1] || top;
    const left = values[3] || right;
    return { top, bottom, right, left };
}
/**
 * Default reducer for CSS properties that concerns edges of a box
 * [shorthand](https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties) notations:
 *
 *		stylesProcessor.setReducer( 'padding', getBoxSidesValueReducer( 'padding' ) );
 *
 * @param {String} styleShorthand
 * @returns {Function}
 */
function getBoxSidesValueReducer(styleShorthand) {
    return (value) => {
        const { top, right, bottom, left } = value;
        const reduced = [];
        if (![top, right, left, bottom].every(value => !!value)) {
            if (top) {
                reduced.push([styleShorthand + '-top', top]);
            }
            if (right) {
                reduced.push([styleShorthand + '-right', right]);
            }
            if (bottom) {
                reduced.push([styleShorthand + '-bottom', bottom]);
            }
            if (left) {
                reduced.push([styleShorthand + '-left', left]);
            }
        }
        else {
            reduced.push([styleShorthand, getBoxSidesShorthandValue(value)]);
        }
        return reduced;
    };
}
/**
 * Returns a [shorthand](https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties) notation
 * of a CSS property value.
 *
 *		getBoxSidesShorthandValue( { top: '1px', right: '1px', bottom: '2px', left: '1px' } );
 *		// will return '1px 1px 2px'
 *
 * @param {module:engine/view/stylesmap~BoxSides} styleShorthand
 * @returns {String}
 */
function getBoxSidesShorthandValue({ top, right, bottom, left }) {
    const out = [];
    if (left !== right) {
        out.push(top, right, bottom, left);
    }
    else if (bottom !== top) {
        out.push(top, right, bottom);
    }
    else if (right !== top) {
        out.push(top, right);
    }
    else {
        out.push(top);
    }
    return out.join(' ');
}
/**
 * Creates a normalizer for a [shorthand](https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties) 1-to-4 value.
 *
 *		stylesProcessor.setNormalizer( 'margin', getPositionShorthandNormalizer( 'margin' ) );
 *
 * @param {String} shorthand
 * @returns {Function}
 */
function getPositionShorthandNormalizer(shorthand) {
    return (value) => {
        return {
            path: shorthand,
            value: getBoxSidesValues(value)
        };
    };
}
/**
 * Parses parts of a 1-to-4 value notation - handles some CSS values with spaces (like RGB()).
 *
 *		getShorthandValues( 'red blue RGB(0, 0, 0)');
 *		// will return [ 'red', 'blue', 'RGB(0, 0, 0)' ]
 *
 * @param {String} string
 * @returns {Array.<String>}
 */
function getShorthandValues(string) {
    return string
        .replace(/, /g, ',') // Exclude comma from spaces evaluation as values are separated by spaces.
        .split(' ')
        .map(string => string.replace(/,/g, ', ')); // Restore original notation.
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Adds a background CSS styles processing rules.
 *
 *		editor.data.addStyleProcessorRules( addBackgroundRules );
 *
 * The normalized value is stored as:
 *
 *		const styles = {
 *			background: {
 *				color,
 *				repeat,
 *				position,
 *				attachment,
 *				image
 *			}
 *		};
 *
 * **Note**: Currently only `'background-color'` longhand value is parsed besides `'background'` shorthand. The reducer also supports only
 * `'background-color'` value.
 *
 * @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor
 */
function addBackgroundRules(stylesProcessor) {
    stylesProcessor.setNormalizer('background', getBackgroundNormalizer());
    stylesProcessor.setNormalizer('background-color', getBackgroundColorNormalizer());
    stylesProcessor.setReducer('background', getBackgroundReducer());
    stylesProcessor.setStyleRelation('background', ['background-color']);
}
function getBackgroundNormalizer() {
    return value => {
        const background = {};
        const parts = getShorthandValues(value);
        for (const part of parts) {
            if (isRepeat(part)) {
                background.repeat = background.repeat || [];
                background.repeat.push(part);
            }
            else if (isPosition(part)) {
                background.position = background.position || [];
                background.position.push(part);
            }
            else if (isAttachment(part)) {
                background.attachment = part;
            }
            else if (isColor(part)) {
                background.color = part;
            }
            else if (isURL(part)) {
                background.image = part;
            }
        }
        return {
            path: 'background',
            value: background
        };
    };
}
function getBackgroundColorNormalizer() {
    return value => ({ path: 'background.color', value });
}
function getBackgroundReducer() {
    return value => {
        const ret = [];
        ret.push(['background-color', value.color]);
        return ret;
    };
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Adds a border CSS styles processing rules.
 *
 *		editor.data.addStyleProcessorRules( addBorderRules );
 *
 * This rules merges all [border](https://developer.mozilla.org/en-US/docs/Web/CSS/border) styles notation shorthands:
 *
 * - border
 * - border-top
 * - border-right
 * - border-bottom
 * - border-left
 * - border-color
 * - border-style
 * - border-width
 *
 * and all corresponding longhand forms (like `border-top-color`, `border-top-style`, etc).
 *
 * It does not handle other shorthands (like `border-radius` or `border-image`).
 *
 * The normalized model stores border values as:
 *
 *		const styles = {
 *			border: {
 *				color: { top, right, bottom, left },
 *				style: { top, right, bottom, left },
 *				width: { top, right, bottom, left },
 *			}
 *		};
 *
 * @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor
 */
function addBorderRules(stylesProcessor) {
    stylesProcessor.setNormalizer('border', getBorderNormalizer());
    // Border-position shorthands.
    stylesProcessor.setNormalizer('border-top', getBorderPositionNormalizer('top'));
    stylesProcessor.setNormalizer('border-right', getBorderPositionNormalizer('right'));
    stylesProcessor.setNormalizer('border-bottom', getBorderPositionNormalizer('bottom'));
    stylesProcessor.setNormalizer('border-left', getBorderPositionNormalizer('left'));
    // Border-property shorthands.
    stylesProcessor.setNormalizer('border-color', getBorderPropertyNormalizer('color'));
    stylesProcessor.setNormalizer('border-width', getBorderPropertyNormalizer('width'));
    stylesProcessor.setNormalizer('border-style', getBorderPropertyNormalizer('style'));
    // Border longhands.
    stylesProcessor.setNormalizer('border-top-color', getBorderPropertyPositionNormalizer('color', 'top'));
    stylesProcessor.setNormalizer('border-top-style', getBorderPropertyPositionNormalizer('style', 'top'));
    stylesProcessor.setNormalizer('border-top-width', getBorderPropertyPositionNormalizer('width', 'top'));
    stylesProcessor.setNormalizer('border-right-color', getBorderPropertyPositionNormalizer('color', 'right'));
    stylesProcessor.setNormalizer('border-right-style', getBorderPropertyPositionNormalizer('style', 'right'));
    stylesProcessor.setNormalizer('border-right-width', getBorderPropertyPositionNormalizer('width', 'right'));
    stylesProcessor.setNormalizer('border-bottom-color', getBorderPropertyPositionNormalizer('color', 'bottom'));
    stylesProcessor.setNormalizer('border-bottom-style', getBorderPropertyPositionNormalizer('style', 'bottom'));
    stylesProcessor.setNormalizer('border-bottom-width', getBorderPropertyPositionNormalizer('width', 'bottom'));
    stylesProcessor.setNormalizer('border-left-color', getBorderPropertyPositionNormalizer('color', 'left'));
    stylesProcessor.setNormalizer('border-left-style', getBorderPropertyPositionNormalizer('style', 'left'));
    stylesProcessor.setNormalizer('border-left-width', getBorderPropertyPositionNormalizer('width', 'left'));
    stylesProcessor.setExtractor('border-top', getBorderPositionExtractor('top'));
    stylesProcessor.setExtractor('border-right', getBorderPositionExtractor('right'));
    stylesProcessor.setExtractor('border-bottom', getBorderPositionExtractor('bottom'));
    stylesProcessor.setExtractor('border-left', getBorderPositionExtractor('left'));
    stylesProcessor.setExtractor('border-top-color', 'border.color.top');
    stylesProcessor.setExtractor('border-right-color', 'border.color.right');
    stylesProcessor.setExtractor('border-bottom-color', 'border.color.bottom');
    stylesProcessor.setExtractor('border-left-color', 'border.color.left');
    stylesProcessor.setExtractor('border-top-width', 'border.width.top');
    stylesProcessor.setExtractor('border-right-width', 'border.width.right');
    stylesProcessor.setExtractor('border-bottom-width', 'border.width.bottom');
    stylesProcessor.setExtractor('border-left-width', 'border.width.left');
    stylesProcessor.setExtractor('border-top-style', 'border.style.top');
    stylesProcessor.setExtractor('border-right-style', 'border.style.right');
    stylesProcessor.setExtractor('border-bottom-style', 'border.style.bottom');
    stylesProcessor.setExtractor('border-left-style', 'border.style.left');
    stylesProcessor.setReducer('border-color', getBoxSidesValueReducer('border-color'));
    stylesProcessor.setReducer('border-style', getBoxSidesValueReducer('border-style'));
    stylesProcessor.setReducer('border-width', getBoxSidesValueReducer('border-width'));
    stylesProcessor.setReducer('border-top', getBorderPositionReducer('top'));
    stylesProcessor.setReducer('border-right', getBorderPositionReducer('right'));
    stylesProcessor.setReducer('border-bottom', getBorderPositionReducer('bottom'));
    stylesProcessor.setReducer('border-left', getBorderPositionReducer('left'));
    stylesProcessor.setReducer('border', getBorderReducer());
    stylesProcessor.setStyleRelation('border', [
        'border-color', 'border-style', 'border-width',
        'border-top', 'border-right', 'border-bottom', 'border-left',
        'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
        'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
        'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'
    ]);
    stylesProcessor.setStyleRelation('border-color', [
        'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color'
    ]);
    stylesProcessor.setStyleRelation('border-style', [
        'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style'
    ]);
    stylesProcessor.setStyleRelation('border-width', [
        'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'
    ]);
    stylesProcessor.setStyleRelation('border-top', ['border-top-color', 'border-top-style', 'border-top-width']);
    stylesProcessor.setStyleRelation('border-right', ['border-right-color', 'border-right-style', 'border-right-width']);
    stylesProcessor.setStyleRelation('border-bottom', ['border-bottom-color', 'border-bottom-style', 'border-bottom-width']);
    stylesProcessor.setStyleRelation('border-left', ['border-left-color', 'border-left-style', 'border-left-width']);
}
function getBorderNormalizer() {
    return value => {
        const { color, style, width } = normalizeBorderShorthand(value);
        return {
            path: 'border',
            value: {
                color: getBoxSidesValues(color),
                style: getBoxSidesValues(style),
                width: getBoxSidesValues(width)
            }
        };
    };
}
function getBorderPositionNormalizer(side) {
    return value => {
        const { color, style, width } = normalizeBorderShorthand(value);
        const border = {};
        if (color !== undefined) {
            border.color = { [side]: color };
        }
        if (style !== undefined) {
            border.style = { [side]: style };
        }
        if (width !== undefined) {
            border.width = { [side]: width };
        }
        return {
            path: 'border',
            value: border
        };
    };
}
function getBorderPropertyNormalizer(propertyName) {
    return value => {
        return {
            path: 'border',
            value: toBorderPropertyShorthand(value, propertyName)
        };
    };
}
function toBorderPropertyShorthand(value, property) {
    return {
        [property]: getBoxSidesValues(value)
    };
}
function getBorderPropertyPositionNormalizer(property, side) {
    return value => {
        return {
            path: 'border',
            value: {
                [property]: {
                    [side]: value
                }
            }
        };
    };
}
function getBorderPositionExtractor(which) {
    return (name, styles) => {
        if (styles.border) {
            return extractBorderPosition(styles.border, which);
        }
    };
}
function extractBorderPosition(border, which) {
    const value = {};
    if (border.width && border.width[which]) {
        value.width = border.width[which];
    }
    if (border.style && border.style[which]) {
        value.style = border.style[which];
    }
    if (border.color && border.color[which]) {
        value.color = border.color[which];
    }
    return value;
}
function normalizeBorderShorthand(string) {
    const result = {};
    const parts = getShorthandValues(string);
    for (const part of parts) {
        if (isLength(part) || /thin|medium|thick/.test(part)) {
            result.width = part;
        }
        else if (isLineStyle(part)) {
            result.style = part;
        }
        else {
            result.color = part;
        }
    }
    return result;
}
// The border reducer factory.
//
// It tries to produce the most optimal output for the specified styles.
//
// For a border style:
//
//      style: {top: "solid", bottom: "solid", right: "solid", left: "solid"}
//
// It will produce: `border-style: solid`.
// For a border style and color:
//
//      color: {top: "#ff0", bottom: "#ff0", right: "#ff0", left: "#ff0"}
//      style: {top: "solid", bottom: "solid", right: "solid", left: "solid"}
//
// It will produce: `border-color: #ff0; border-style: solid`.
// If all border parameters are specified:
//
//      color: {top: "#ff0", bottom: "#ff0", right: "#ff0", left: "#ff0"}
//      style: {top: "solid", bottom: "solid", right: "solid", left: "solid"}
//      width: {top: "2px", bottom: "2px", right: "2px", left: "2px"}
//
// It will combine everything into a single property: `border: 2px solid #ff0`.
//
// The definitions are merged only if all border selectors have the same values.
//
// @returns {Function}
function getBorderReducer() {
    return value => {
        const topStyles = extractBorderPosition(value, 'top');
        const rightStyles = extractBorderPosition(value, 'right');
        const bottomStyles = extractBorderPosition(value, 'bottom');
        const leftStyles = extractBorderPosition(value, 'left');
        const borderStyles = [topStyles, rightStyles, bottomStyles, leftStyles];
        const borderStylesByType = {
            width: getReducedStyleValueForType(borderStyles, 'width'),
            style: getReducedStyleValueForType(borderStyles, 'style'),
            color: getReducedStyleValueForType(borderStyles, 'color')
        };
        // Try reducing to a single `border:` property.
        const reducedBorderStyle = reduceBorderPosition(borderStylesByType, 'all');
        if (reducedBorderStyle.length) {
            return reducedBorderStyle;
        }
        // Try reducing to `border-style:`, `border-width:`, `border-color:` properties.
        const reducedStyleTypes = Object.entries(borderStylesByType).reduce((reducedStyleTypes, [type, value]) => {
            if (value) {
                reducedStyleTypes.push([`border-${type}`, value]);
                // Remove it from the full set to not include it in the most specific properties later.
                borderStyles.forEach(style => delete style[type]);
            }
            return reducedStyleTypes;
        }, []);
        // The reduced properties (by type) and all that remains that could not be reduced.
        return [
            ...reducedStyleTypes,
            ...reduceBorderPosition(topStyles, 'top'),
            ...reduceBorderPosition(rightStyles, 'right'),
            ...reduceBorderPosition(bottomStyles, 'bottom'),
            ...reduceBorderPosition(leftStyles, 'left')
        ];
    };
    // @param {Array.<Object>} styles The array of objects with `style`, `color`, `width` properties.
    // @param {'width'|'style'|'color'} type
    function getReducedStyleValueForType(styles, type) {
        return styles
            .map(style => style[type])
            .reduce((result, style) => result == style ? result : null);
    }
}
function getBorderPositionReducer(which) {
    return value => reduceBorderPosition(value, which);
}
// Returns an array with reduced border styles depending on the specified values.
//
// If all border properties (width, style, color) are specified, the returned selector will be
// merged into a group: `border-*: [width] [style] [color]`.
//
// Otherwise, the specific definitions will be returned: `border-(width|style|color)-*: [value]`.
//
// @param {Object|null} value Styles if defined.
// @param {'top'|'right'|'bottom'|'left'|'all'} which The border position.
// @returns {Array}
function reduceBorderPosition(value, which) {
    const borderTypes = [];
    if (value && (value.width)) {
        borderTypes.push('width');
    }
    if (value && (value.style)) {
        borderTypes.push('style');
    }
    if (value && (value.color)) {
        borderTypes.push('color');
    }
    if (borderTypes.length == 3) {
        const borderValue = borderTypes.map(item => value[item]).join(' ');
        return [
            which == 'all' ? ['border', borderValue] : [`border-${which}`, borderValue]
        ];
    }
    // We are unable to reduce to a single `border:` property.
    if (which == 'all') {
        return [];
    }
    return borderTypes.map(type => {
        return [`border-${which}-${type}`, value[type]];
    });
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Adds a margin CSS styles processing rules.
 *
 *		editor.data.addStyleProcessorRules( addMarginRules );
 *
 * The normalized value is stored as:
 *
 *		const styles = {
 *			margin: {
 *				top,
 *				right,
 *				bottom,
 *				left
 *			}
 *		};
 *
 * @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor
 */
function addMarginRules(stylesProcessor) {
    stylesProcessor.setNormalizer('margin', getPositionShorthandNormalizer('margin'));
    stylesProcessor.setNormalizer('margin-top', value => ({ path: 'margin.top', value }));
    stylesProcessor.setNormalizer('margin-right', value => ({ path: 'margin.right', value }));
    stylesProcessor.setNormalizer('margin-bottom', value => ({ path: 'margin.bottom', value }));
    stylesProcessor.setNormalizer('margin-left', value => ({ path: 'margin.left', value }));
    stylesProcessor.setReducer('margin', getBoxSidesValueReducer('margin'));
    stylesProcessor.setStyleRelation('margin', ['margin-top', 'margin-right', 'margin-bottom', 'margin-left']);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * Adds a margin CSS styles processing rules.
 *
 *		editor.data.addStyleProcessorRules( addPaddingRules );
 *
 * The normalized value is stored as:
 *
 *		const styles = {
 *			padding: {
 *				top,
 *				right,
 *				bottom,
 *				left
 *			}
 *		};
 *
 * @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor
 */
function addPaddingRules(stylesProcessor) {
    stylesProcessor.setNormalizer('padding', getPositionShorthandNormalizer('padding'));
    stylesProcessor.setNormalizer('padding-top', value => ({ path: 'padding.top', value }));
    stylesProcessor.setNormalizer('padding-right', value => ({ path: 'padding.right', value }));
    stylesProcessor.setNormalizer('padding-bottom', value => ({ path: 'padding.bottom', value }));
    stylesProcessor.setNormalizer('padding-left', value => ({ path: 'padding.left', value }));
    stylesProcessor.setReducer('padding', getBoxSidesValueReducer('padding'));
    stylesProcessor.setStyleRelation('padding', ['padding-top', 'padding-right', 'padding-bottom', 'padding-left']);
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */var index$5=/*#__PURE__*/Object.freeze({__proto__:null,EditingController:EditingController,DataController:DataController,Conversion:Conversion,HtmlDataProcessor:HtmlDataProcessor,InsertOperation:InsertOperation,MarkerOperation:MarkerOperation,OperationFactory:OperationFactory,transformSets:transformSets,DocumentSelection:DocumentSelection,Range:Range,LiveRange:LiveRange,LivePosition:LivePosition,Model:Model$1,TreeWalker:TreeWalker,Element:Element,Position:Position,DocumentFragment:DocumentFragment,History:History,Text:Text$1,DomConverter:DomConverter,Renderer:Renderer,ViewDocument:Document$1,ViewText:Text$2,ViewElement:Element$1,ViewContainerElement:ContainerElement,ViewAttributeElement:AttributeElement,ViewEmptyElement:EmptyElement,ViewRawElement:RawElement,ViewUIElement:UIElement,ViewDocumentFragment:DocumentFragment$1,getFillerOffset:getFillerOffset$5,Observer:Observer,ClickObserver:ClickObserver,DomEventObserver:DomEventObserver,MouseObserver:MouseObserver,DowncastWriter:DowncastWriter,UpcastWriter:UpcastWriter,Matcher:Matcher,DomEventData:DomEventData,StylesProcessor:StylesProcessor,enablePlaceholder:enablePlaceholder,disablePlaceholder:disablePlaceholder,showPlaceholder:showPlaceholder,hidePlaceholder:hidePlaceholder,needsPlaceholder:needsPlaceholder,addBackgroundRules:addBackgroundRules,addBorderRules:addBorderRules,addMarginRules:addMarginRules,addPaddingRules:addPaddingRules,isColor:isColor,isLineStyle:isLineStyle,isLength:isLength,isPercentage:isPercentage,isRepeat:isRepeat,isPosition:isPosition,isAttachment:isAttachment,isURL:isURL,getBoxSidesValues:getBoxSidesValues,getBoxSidesValueReducer:getBoxSidesValueReducer,getBoxSidesShorthandValue:getBoxSidesShorthandValue,getPositionShorthandNormalizer:getPositionShorthandNormalizer,getShorthandValues:getShorthandValues});/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The classic editor UI class.
 *
 * @extends module:core/editor/editorui~EditorUI
 */
class ClassicEditorUI extends EditorUI {
	/**
	 * Creates an instance of the classic editor UI class.
	 *
	 * @param {module:core/editor/editor~Editor} editor The editor instance.
	 * @param {module:ui/editorui/editoruiview~EditorUIView} view The view of the UI.
	 */
	constructor( editor, view ) {
		super( editor );

		/**
		 * The main (top–most) view of the editor UI.
		 *
		 * @readonly
		 * @member {module:ui/editorui/editoruiview~EditorUIView} #view
		 */
		this.view = view;

		/**
		 * A normalized `config.toolbar` object.
		 *
		 * @private
		 * @member {Object}
		 */
		this._toolbarConfig = normalizeToolbarConfig( editor.config.get( 'toolbar' ) );

		/**
		 * The element replacer instance used to hide the editor's source element.
		 *
		 * @protected
		 * @member {module:utils/elementreplacer~ElementReplacer}
		 */
		this._elementReplacer = new ElementReplacer();
	}

	/**
	 * @inheritDoc
	 */
	get element() {
		return this.view.element;
	}

	/**
	 * Initializes the UI.
	 *
	 * @param {HTMLElement|null} replacementElement The DOM element that will be the source for the created editor.
	 */
	init( replacementElement ) {
		const editor = this.editor;
		const view = this.view;
		const editingView = editor.editing.view;
		const editable = view.editable;
		const editingRoot = editingView.document.getRoot();

		// The editable UI and editing root should share the same name. Then name is used
		// to recognize the particular editable, for instance in ARIA attributes.
		editable.name = editingRoot.rootName;

		view.render();

		// The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
		// But it can be available earlier if a DOM element has been passed to BalloonEditor.create().
		const editableElement = editable.element;

		// Register the editable UI view in the editor. A single editor instance can aggregate multiple
		// editable areas (roots) but the classic editor has only one.
		this.setEditableElement( editable.name, editableElement );

		// Let the editable UI element respond to the changes in the global editor focus
		// tracker. It has been added to the same tracker a few lines above but, in reality, there are
		// many focusable areas in the editor, like balloons, toolbars or dropdowns and as long
		// as they have focus, the editable should act like it is focused too (although technically
		// it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user.
		// Doing otherwise will result in editable focus styles disappearing, once e.g. the
		// toolbar gets focused.
		view.editable.bind( 'isFocused' ).to( this.focusTracker );

		// Bind the editable UI element to the editing view, making it an end– and entry–point
		// of the editor's engine. This is where the engine meets the UI.
		editingView.attachDomRoot( editableElement );

		// If an element containing the initial data of the editor was provided, replace it with
		// an editor instance's UI in DOM until the editor is destroyed. For instance, a <textarea>
		// can be such element.
		if ( replacementElement ) {
			this._elementReplacer.replace( replacementElement, this.element );
		}

		this._initPlaceholder();
		this._initToolbar();
		this.fire( 'ready' );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		const view = this.view;
		const editingView = this.editor.editing.view;

		this._elementReplacer.restore();
		editingView.detachDomRoot( view.editable.name );
		view.destroy();

		super.destroy();
	}

	/**
	 * Initializes the editor toolbar.
	 *
	 * @private
	 */
	_initToolbar() {
		const view = this.view;

		// Set–up the sticky panel with toolbar.
		view.stickyPanel.bind( 'isActive' ).to( this.focusTracker, 'isFocused' );
		view.stickyPanel.limiterElement = view.element;
		view.stickyPanel.bind( 'viewportTopOffset' ).to( this, 'viewportOffset', ( { top } ) => top );

		view.toolbar.fillFromConfig( this._toolbarConfig, this.componentFactory );

		// Register the toolbar so it becomes available for Alt+F10 and Esc navigation.
		this.addToolbar( view.toolbar );
	}

	/**
	 * Enable the placeholder text on the editing root, if any was configured.
	 *
	 * @private
	 */
	_initPlaceholder() {
		const editor = this.editor;
		const editingView = editor.editing.view;
		const editingRoot = editingView.document.getRoot();
		const sourceElement = editor.sourceElement;

		const placeholderText = editor.config.get( 'placeholder' ) ||
			sourceElement && sourceElement.tagName.toLowerCase() === 'textarea' && sourceElement.getAttribute( 'placeholder' );

		if ( placeholderText ) {
			enablePlaceholder( {
				view: editingView,
				element: editingRoot,
				text: placeholderText,
				isDirectHost: false,
				keepOnFocus: true
			} );
		}
	}
}var css_248z$y = ".ck.ck-editor{position:relative}.ck.ck-editor .ck-editor__top .ck-sticky-panel .ck-toolbar{z-index:var(--ck-z-modal)}.ck.ck-editor__top .ck-sticky-panel .ck-toolbar{border-radius:0}.ck-rounded-corners .ck.ck-editor__top .ck-sticky-panel .ck-toolbar,.ck.ck-editor__top .ck-sticky-panel .ck-toolbar.ck-rounded-corners{border-radius:var(--ck-border-radius);border-bottom-left-radius:0;border-bottom-right-radius:0}.ck.ck-editor__top .ck-sticky-panel .ck-toolbar{border-bottom-width:0}.ck.ck-editor__top .ck-sticky-panel .ck-sticky-panel__content_sticky .ck-toolbar{border-bottom-width:1px;border-radius:0}.ck-rounded-corners .ck.ck-editor__top .ck-sticky-panel .ck-sticky-panel__content_sticky .ck-toolbar,.ck.ck-editor__top .ck-sticky-panel .ck-sticky-panel__content_sticky .ck-toolbar.ck-rounded-corners{border-radius:var(--ck-border-radius);border-radius:0}.ck.ck-editor__main>.ck-editor__editable{background:var(--ck-color-base-background);border-radius:0}.ck-rounded-corners .ck.ck-editor__main>.ck-editor__editable,.ck.ck-editor__main>.ck-editor__editable.ck-rounded-corners{border-radius:var(--ck-border-radius);border-top-left-radius:0;border-top-right-radius:0}.ck.ck-editor__main>.ck-editor__editable:not(.ck-focused){border-color:var(--ck-color-base-border)}";
styleInject(css_248z$y);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Classic editor UI view. Uses an inline editable and a sticky toolbar, all
 * enclosed in a boxed UI view.
 *
 * @extends module:ui/editorui/boxed/boxededitoruiview~BoxedEditorUIView
 */
class ClassicEditorUIView extends BoxedEditorUIView {
	/**
	 * Creates an instance of the classic editor UI view.
	 *
	 * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
	 * @param {module:engine/view/view~View} editingView The editing view instance this view is related to.
	 * @param {Object} [options={}] Configuration options for the view instance.
	 * @param {Boolean} [options.shouldToolbarGroupWhenFull] When set `true` enables automatic items grouping
	 * in the main {@link module:editor-classic/classiceditoruiview~ClassicEditorUIView#toolbar toolbar}.
	 * See {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull} to learn more.
	 */
	constructor( locale, editingView, options = {} ) {
		super( locale );

		/**
		 * Sticky panel view instance. This is a parent view of a {@link #toolbar}
		 * that makes toolbar sticky.
		 *
		 * @readonly
		 * @member {module:ui/panel/sticky/stickypanelview~StickyPanelView}
		 */
		this.stickyPanel = new StickyPanelView( locale );

		/**
		 * Toolbar view instance.
		 *
		 * @readonly
		 * @member {module:ui/toolbar/toolbarview~ToolbarView}
		 */
		this.toolbar = new ToolbarView( locale, {
			shouldGroupWhenFull: options.shouldToolbarGroupWhenFull
		} );

		/**
		 * Editable UI view.
		 *
		 * @readonly
		 * @member {module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView}
		 */
		this.editable = new InlineEditableUIView( locale, editingView );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		// Set toolbar as a child of a stickyPanel and makes toolbar sticky.
		this.stickyPanel.content.add( this.toolbar );

		this.top.add( this.stickyPanel );
		this.main.add( this.editable );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The {@glink installation/getting-started/predefined-builds#classic-editor classic editor} implementation.
 * It uses an inline editable and a sticky toolbar, all enclosed in a boxed UI.
 * See the {@glink examples/builds/classic-editor demo}.
 *
 * In order to create a classic editor instance, use the static
 * {@link module:editor-classic/classiceditor~ClassicEditor.create `ClassicEditor.create()`} method.
 *
 * # Classic editor and classic build
 *
 * The classic editor can be used directly from source (if you installed the
 * [`@ckeditor/ckeditor5-editor-classic`](https://www.npmjs.com/package/@ckeditor/ckeditor5-editor-classic) package)
 * but it is also available in the {@glink installation/getting-started/predefined-builds#classic-editor classic build}.
 *
 * {@glink installation/getting-started/predefined-builds Builds}
 * are ready-to-use editors with plugins bundled in. When using the editor from
 * source you need to take care of loading all plugins by yourself
 * (through the {@link module:core/editor/editorconfig~EditorConfig#plugins `config.plugins`} option).
 * Using the editor from source gives much better flexibility and allows easier customization.
 *
 * Read more about initializing the editor from source or as a build in
 * {@link module:editor-classic/classiceditor~ClassicEditor.create `ClassicEditor.create()`}.
 *
 * @mixes module:core/editor/utils/dataapimixin~DataApiMixin
 * @mixes module:core/editor/utils/elementapimixin~ElementApiMixin
 * @implements module:core/editor/editorwithui~EditorWithUI
 * @extends module:core/editor/editor~Editor
 */
class ClassicEditor extends Editor {
	/**
	 * Creates an instance of the classic editor.
	 *
	 * **Note:** do not use the constructor to create editor instances. Use the static
	 * {@link module:editor-classic/classiceditor~ClassicEditor.create `ClassicEditor.create()`} method instead.
	 *
	 * @protected
	 * @param {HTMLElement|String} sourceElementOrData The DOM element that will be the source for the created editor
	 * or the editor's initial data. For more information see
	 * {@link module:editor-classic/classiceditor~ClassicEditor.create `ClassicEditor.create()`}.
	 * @param {module:core/editor/editorconfig~EditorConfig} [config] The editor configuration.
	 */
	constructor( sourceElementOrData, config = {} ) {
		// If both `config.initialData` is set and initial data is passed as the constructor parameter, then throw.
		if ( !isElement( sourceElementOrData ) && config.initialData !== undefined ) {
			// Documented in core/editor/editorconfig.jsdoc.
			// eslint-disable-next-line ckeditor5-rules/ckeditor-error-message
			throw new CKEditorError( 'editor-create-initial-data', null );
		}

		super( config );

		if ( this.config.get( 'initialData' ) === undefined ) {
			this.config.set( 'initialData', getInitialData( sourceElementOrData ) );
		}

		if ( isElement( sourceElementOrData ) ) {
			this.sourceElement = sourceElementOrData;
		}

		this.model.document.createRoot();

		const shouldToolbarGroupWhenFull = !this.config.get( 'toolbar.shouldNotGroupWhenFull' );
		const view = new ClassicEditorUIView( this.locale, this.editing.view, {
			shouldToolbarGroupWhenFull
		} );

		this.ui = new ClassicEditorUI( this, view );

		attachToForm( this );
	}

	/**
	 * Destroys the editor instance, releasing all resources used by it.
	 *
	 * Updates the original editor element with the data if the
	 * {@link module:core/editor/editorconfig~EditorConfig#updateSourceElementOnDestroy `updateSourceElementOnDestroy`}
	 * configuration option is set to `true`.
	 *
	 * @returns {Promise}
	 */
	destroy() {
		if ( this.sourceElement ) {
			this.updateSourceElement();
		}

		this.ui.destroy();

		return super.destroy();
	}

	/**
	 * Creates a new classic editor instance.
	 *
	 * There are three ways how the editor can be initialized.
	 *
	 * # Replacing a DOM element (and loading data from it)
	 *
	 * You can initialize the editor using an existing DOM element:
	 *
	 *		ClassicEditor
	 *			.create( document.querySelector( '#editor' ) )
	 *			.then( editor => {
	 *				console.log( 'Editor was initialized', editor );
	 *			} )
	 *			.catch( err => {
	 *				console.error( err.stack );
	 *			} );
	 *
	 * The element's content will be used as the editor data and the element will be replaced by the editor UI.
	 *
	 * # Creating a detached editor
	 *
	 * Alternatively, you can initialize the editor by passing the initial data directly as a string.
	 * In this case, the editor will render an element that must be inserted into the DOM:
	 *
	 *		ClassicEditor
	 *			.create( '<p>Hello world!</p>' )
	 *			.then( editor => {
	 *				console.log( 'Editor was initialized', editor );
	 *
	 *				// Initial data was provided so the editor UI element needs to be added manually to the DOM.
	 *				document.body.appendChild( editor.ui.element );
	 *			} )
	 *			.catch( err => {
	 *				console.error( err.stack );
	 *			} );
	 *
	 * This lets you dynamically append the editor to your web page whenever it is convenient for you. You may use this method if your
	 * web page content is generated on the client side and the DOM structure is not ready at the moment when you initialize the editor.
	 *
	 * # Replacing a DOM element (and data provided in `config.initialData`)
	 *
	 * You can also mix these two ways by providing a DOM element to be used and passing the initial data through the configuration:
	 *
	 *		ClassicEditor
	 *			.create( document.querySelector( '#editor' ), {
	 *				initialData: '<h2>Initial data</h2><p>Foo bar.</p>'
	 *			} )
	 *			.then( editor => {
	 *				console.log( 'Editor was initialized', editor );
	 *			} )
	 *			.catch( err => {
	 *				console.error( err.stack );
	 *			} );
	 *
	 * This method can be used to initialize the editor on an existing element with the specified content in case if your integration
	 * makes it difficult to set the content of the source element.
	 *
	 * Note that an error will be thrown if you pass the initial data both as the first parameter and also in the configuration.
	 *
	 * # Configuring the editor
	 *
	 * See the {@link module:core/editor/editorconfig~EditorConfig editor configuration documentation} to learn more about
	 * customizing plugins, toolbar and more.
	 *
	 * # Using the editor from source
	 *
	 * The code samples listed in the previous sections of this documentation assume that you are using an
	 * {@glink installation/getting-started/predefined-builds editor build} (for example – `@ckeditor/ckeditor5-build-classic`).
	 *
	 * If you want to use the classic editor from source (`@ckeditor/ckeditor5-editor-classic/src/classiceditor`),
	 * you need to define the list of
	 * {@link module:core/editor/editorconfig~EditorConfig#plugins plugins to be initialized} and
	 * {@link module:core/editor/editorconfig~EditorConfig#toolbar toolbar items}. Read more about using the editor from
	 * source in the {@glink installation/advanced/alternative-setups/integrating-from-source dedicated guide}.
	 *
	 * @param {HTMLElement|String} sourceElementOrData The DOM element that will be the source for the created editor
	 * or the editor's initial data.
	 *
	 * If a DOM element is passed, its content will be automatically loaded to the editor upon initialization
	 * and the {@link module:editor-classic/classiceditorui~ClassicEditorUI#element editor element} will replace the passed element
	 * in the DOM (the original one will be hidden and the editor will be injected next to it).
	 *
	 * If the {@link module:core/editor/editorconfig~EditorConfig#updateSourceElementOnDestroy updateSourceElementOnDestroy}
	 * option is set to `true`, the editor data will be set back to the original element once the editor is destroyed and when a form,
	 * in which this element is contained, is submitted (if the original element is a `<textarea>`). This ensures seamless integration
	 * with native web forms.
	 *
	 * If the initial data is passed, a detached editor will be created. In this case you need to insert it into the DOM manually.
	 * It is available under the {@link module:editor-classic/classiceditorui~ClassicEditorUI#element `editor.ui.element`} property.
	 *
	 * @param {module:core/editor/editorconfig~EditorConfig} [config] The editor configuration.
	 * @returns {Promise} A promise resolved once the editor is ready. The promise resolves with the created editor instance.
	 */
	static create( sourceElementOrData, config = {} ) {
		return new Promise( resolve => {
			const editor = new this( sourceElementOrData, config );

			resolve(
				editor.initPlugins()
					.then( () => editor.ui.init( isElement( sourceElementOrData ) ? sourceElementOrData : null ) )
					.then( () => editor.data.init( editor.config.get( 'initialData' ) ) )
					.then( () => editor.fire( 'ready' ) )
					.then( () => editor )
			);
		} );
	}
}

mix( ClassicEditor, DataApiMixin );
mix( ClassicEditor, ElementApiMixin );

function getInitialData( sourceElementOrData ) {
	return isElement( sourceElementOrData ) ? getDataFromElement( sourceElementOrData ) : sourceElementOrData;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module enter/utils
 */

/**
 * Returns attributes that should be preserved on the enter key.
 *
 * Filtering is realized based on `copyOnEnter` attribute property. Read more about attribute properties
 * {@link module:engine/model/schema~Schema#setAttributeProperties here}.
 *
 * @param {module:engine/model/schema~Schema} schema
 * @param {Iterable.<*>} allAttributes attributes to filter.
 * @returns {Iterable.<*>}
 */
function* getCopyOnEnterAttributes( schema, allAttributes ) {
	for ( const attribute of allAttributes ) {
		if ( attribute && schema.getAttributeProperties( attribute[ 0 ] ).copyOnEnter ) {
			yield attribute;
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Enter command. It is used by the {@link module:enter/enter~Enter Enter feature} to handle the <kbd>Enter</kbd> key.
 *
 * @extends module:core/command~Command
 */
class EnterCommand extends Command {
	/**
	 * @inheritDoc
	 */
	execute() {
		const model = this.editor.model;
		const doc = model.document;

		model.change( writer => {
			enterBlock( this.editor.model, writer, doc.selection, model.schema );
			this.fire( 'afterExecute', { writer } );
		} );
	}
}

// Creates a new block in the way that the <kbd>Enter</kbd> key is expected to work.
//
// @param {module:engine/model~Model} model
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
// Selection on which the action should be performed.
// @param {module:engine/model/schema~Schema} schema
function enterBlock( model, writer, selection, schema ) {
	const isSelectionEmpty = selection.isCollapsed;
	const range = selection.getFirstRange();
	const startElement = range.start.parent;
	const endElement = range.end.parent;

	// Don't touch the roots and other limit elements.
	if ( schema.isLimit( startElement ) || schema.isLimit( endElement ) ) {
		// Delete the selected content but only if inside a single limit element.
		// Abort, when crossing limit elements boundary (e.g. <limit1>x[x</limit1>donttouchme<limit2>y]y</limit2>).
		// This is an edge case and it's hard to tell what should actually happen because such a selection
		// is not entirely valid.
		if ( !isSelectionEmpty && startElement == endElement ) {
			model.deleteContent( selection );
		}

		return;
	}

	if ( isSelectionEmpty ) {
		const attributesToCopy = getCopyOnEnterAttributes( writer.model.schema, selection.getAttributes() );
		splitBlock( writer, range.start );
		writer.setSelectionAttribute( attributesToCopy );
	} else {
		const leaveUnmerged = !( range.start.isAtStart && range.end.isAtEnd );
		const isContainedWithinOneElement = ( startElement == endElement );

		model.deleteContent( selection, { leaveUnmerged } );

		if ( leaveUnmerged ) {
			// Partially selected elements.
			//
			// <h>x[xx]x</h>		-> <h>x^x</h>			-> <h>x</h><h>^x</h>
			if ( isContainedWithinOneElement ) {
				splitBlock( writer, selection.focus );
			}
			// Selection over multiple elements.
			//
			// <h>x[x</h><p>y]y<p>	-> <h>x^</h><p>y</p>	-> <h>x</h><p>^y</p>
			else {
				writer.setSelection( endElement, 0 );
			}
		}
	}
}

function splitBlock( writer, splitPos ) {
	writer.split( splitPos );
	writer.setSelection( splitPos.parent.nextSibling, 0 );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Enter observer introduces the {@link module:engine/view/document~Document#event:enter `Document#enter`} event.
 *
 * @extends module:engine/view/observer/observer~Observer
 */
class EnterObserver extends Observer {
	/**
	 * @inheritDoc
	 */
	constructor( view ) {
		super( view );

		const doc = this.document;

		doc.on( 'keydown', ( evt, data ) => {
			if ( this.isEnabled && data.keyCode == keyCodes.enter ) {
				const event = new BubblingEventInfo( doc, 'enter', doc.selection.getFirstRange() );

				doc.fire( event, new DomEventData( doc, data.domEvent, {
					isSoft: data.shiftKey
				} ) );

				// Stop `keydown` event if `enter` event was stopped.
				// https://github.com/ckeditor/ckeditor5/issues/753
				if ( event.stop.called ) {
					evt.stop();
				}
			}
		} );
	}

	/**
	 * @inheritDoc
	 */
	observe() {}
}

/**
 * Event fired when the user presses the <kbd>Enter</kbd> key.
 *
 * Note: This event is fired by the {@link module:enter/enterobserver~EnterObserver observer}
 * (usually registered by the {@link module:enter/enter~Enter Enter feature} and
 * {@link module:enter/shiftenter~ShiftEnter ShiftEnter feature}).
 *
 * @event module:engine/view/document~Document#event:enter
 * @param {module:engine/view/observer/domeventdata~DomEventData} data
 * @param {Boolean} data.isSoft Whether it's a soft enter (<kbd>Shift</kbd>+<kbd>Enter</kbd>) or hard enter (<kbd>Enter</kbd>).
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * This plugin handles the <kbd>Enter</kbd> key (hard line break) in the editor.
 *
 * See also the {@link module:enter/shiftenter~ShiftEnter} plugin.
 *
 * For more information about this feature see the {@glink api/enter package page}.
 *
 * @extends module:core/plugin~Plugin
 */
class Enter extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Enter';
	}

	init() {
		const editor = this.editor;
		const view = editor.editing.view;
		const viewDocument = view.document;

		view.addObserver( EnterObserver );

		editor.commands.add( 'enter', new EnterCommand( editor ) );

		this.listenTo( viewDocument, 'enter', ( evt, data ) => {
			data.preventDefault();

			// The soft enter key is handled by the ShiftEnter plugin.
			if ( data.isSoft ) {
				return;
			}

			editor.execute( 'enter' );

			view.scrollToTheSelection();
		}, { priority: 'low' } );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * ShiftEnter command. It is used by the {@link module:enter/shiftenter~ShiftEnter ShiftEnter feature} to handle
 * the <kbd>Shift</kbd>+<kbd>Enter</kbd> keystroke.
 *
 * @extends module:core/command~Command
 */
class ShiftEnterCommand extends Command {
	/**
	 * @inheritDoc
	 */
	execute() {
		const model = this.editor.model;
		const doc = model.document;

		model.change( writer => {
			softBreakAction( model, writer, doc.selection );
			this.fire( 'afterExecute', { writer } );
		} );
	}

	refresh() {
		const model = this.editor.model;
		const doc = model.document;

		this.isEnabled = isEnabled( model.schema, doc.selection );
	}
}

// Checks whether the ShiftEnter command should be enabled in the specified selection.
//
// @param {module:engine/model/schema~Schema} schema
// @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
function isEnabled( schema, selection ) {
	// At this moment it is okay to support single range selections only.
	// But in the future we may need to change that.
	if ( selection.rangeCount > 1 ) {
		return false;
	}

	const anchorPos = selection.anchor;

	// Check whether the break element can be inserted in the current selection anchor.
	if ( !anchorPos || !schema.checkChild( anchorPos, 'softBreak' ) ) {
		return false;
	}

	const range = selection.getFirstRange();
	const startElement = range.start.parent;
	const endElement = range.end.parent;

	// Do not modify the content if selection is cross-limit elements.
	if ( ( isInsideLimitElement( startElement, schema ) || isInsideLimitElement( endElement, schema ) ) && startElement !== endElement ) {
		return false;
	}

	return true;
}

// Creates a break in the way that the <kbd>Shift</kbd>+<kbd>Enter</kbd> keystroke is expected to work.
//
// @param {module:engine/model~Model} model
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
// Selection on which the action should be performed.
function softBreakAction( model, writer, selection ) {
	const isSelectionEmpty = selection.isCollapsed;
	const range = selection.getFirstRange();
	const startElement = range.start.parent;
	const endElement = range.end.parent;
	const isContainedWithinOneElement = ( startElement == endElement );

	if ( isSelectionEmpty ) {
		const attributesToCopy = getCopyOnEnterAttributes( model.schema, selection.getAttributes() );
		insertBreak( model, writer, range.end );

		writer.removeSelectionAttribute( selection.getAttributeKeys() );
		writer.setSelectionAttribute( attributesToCopy );
	} else {
		const leaveUnmerged = !( range.start.isAtStart && range.end.isAtEnd );
		model.deleteContent( selection, { leaveUnmerged } );

		// Selection within one element:
		//
		// <h>x[xx]x</h>		-> <h>x^x</h>			-> <h>x<br>^x</h>
		if ( isContainedWithinOneElement ) {
			insertBreak( model, writer, selection.focus );
		}
		// Selection over multiple elements.
		//
		// <h>x[x</h><p>y]y<p>	-> <h>x^</h><p>y</p>	-> <h>x</h><p>^y</p>
		//
		// We chose not to insert a line break in this case because:
		//
		// * it's not a very common scenario,
		// * it actually surprised me when I saw the "expected behavior" in real life.
		//
		// It's ok if the user will need to be more specific where they want the <br> to be inserted.
		else {
			// Move the selection to the 2nd element (last step of the example above).
			if ( leaveUnmerged ) {
				writer.setSelection( endElement, 0 );
			}
		}
	}
}

function insertBreak( model, writer, position ) {
	const breakLineElement = writer.createElement( 'softBreak' );

	model.insertContent( breakLineElement, position );
	writer.setSelection( breakLineElement, 'after' );
}

// Checks whether the specified `element` is a child of the limit element.
//
// Checking whether the `<p>` element is inside a limit element:
//   - <$root><p>Text.</p></$root> => false
//   - <$root><limitElement><p>Text</p></limitElement></$root> => true
//
// @param {module:engine/model/element~Element} element
// @param {module:engine/schema~Schema} schema
// @returns {Boolean}
function isInsideLimitElement( element, schema ) {
	// `$root` is a limit element but in this case is an invalid element.
	if ( element.is( 'rootElement' ) ) {
		return false;
	}

	return schema.isLimit( element ) || isInsideLimitElement( element.parent, schema );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * This plugin handles the <kbd>Shift</kbd>+<kbd>Enter</kbd> keystroke (soft line break) in the editor.
 *
 * See also the {@link module:enter/enter~Enter} plugin.
 *
 * For more information about this feature see the {@glink api/enter package page}.
 *
 * @extends module:core/plugin~Plugin
 */
class ShiftEnter extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'ShiftEnter';
	}

	init() {
		const editor = this.editor;
		const schema = editor.model.schema;
		const conversion = editor.conversion;
		const view = editor.editing.view;
		const viewDocument = view.document;

		// Configure the schema.
		schema.register( 'softBreak', {
			allowWhere: '$text',
			isInline: true
		} );

		// Configure converters.
		conversion.for( 'upcast' )
			.elementToElement( {
				model: 'softBreak',
				view: 'br'
			} );

		conversion.for( 'downcast' )
			.elementToElement( {
				model: 'softBreak',
				view: ( modelElement, { writer } ) => writer.createEmptyElement( 'br' )
			} );

		view.addObserver( EnterObserver );

		editor.commands.add( 'shiftEnter', new ShiftEnterCommand( editor ) );

		this.listenTo( viewDocument, 'enter', ( evt, data ) => {
			data.preventDefault();

			// The hard enter key is handled by the Enter plugin.
			if ( !data.isSoft ) {
				return;
			}

			editor.execute( 'shiftEnter' );
			view.scrollToTheSelection();
		}, { priority: 'low' } );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module typing/utils/changebuffer
 */

/**
 * Change buffer allows to group atomic changes (like characters that have been typed) into
 * {@link module:engine/model/batch~Batch batches}.
 *
 * Batches represent single undo steps, hence changes added to one single batch are undone together.
 *
 * The buffer has a configurable limit of atomic changes that it can accommodate. After the limit was
 * exceeded (see {@link ~ChangeBuffer#input}), a new batch is created in {@link ~ChangeBuffer#batch}.
 *
 * To use the change buffer you need to let it know about the number of changes that were added to the batch:
 *
 *		const buffer = new ChangeBuffer( model, LIMIT );
 *
 *		// Later on in your feature:
 *		buffer.batch.insert( pos, insertedCharacters );
 *		buffer.input( insertedCharacters.length );
 *
 */
class ChangeBuffer {
	/**
	 * Creates a new instance of the change buffer.
	 *
	 * @param {module:engine/model/model~Model} model
	 * @param {Number} [limit=20] The maximum number of atomic changes which can be contained in one batch.
	 */
	constructor( model, limit = 20 ) {
		/**
		 * The model instance.
		 *
		 * @readonly
		 * @member {module:engine/model/model~Model} #model
		 */
		this.model = model;

		/**
		 * The number of atomic changes in the buffer. Once it exceeds the {@link #limit},
		 * the {@link #batch batch} is set to a new one.
		 *
		 * @readonly
		 * @member {Number} #size
		 */
		this.size = 0;

		/**
		 * The maximum number of atomic changes which can be contained in one batch.
		 *
		 * @readonly
		 * @member {Number} #limit
		 */
		this.limit = limit;

		/**
		 * Whether the buffer is locked. A locked buffer cannot be reset unless it gets unlocked.
		 *
		 * @readonly
		 * @member {Boolean} #isLocked
		 */
		this.isLocked = false;

		// The function to be called in order to notify the buffer about batches which appeared in the document.
		// The callback will check whether it is a new batch and in that case the buffer will be flushed.
		//
		// The reason why the buffer needs to be flushed whenever a new batch appears is that the changes added afterwards
		// should be added to a new batch. For instance, when the user types, then inserts an image, and then types again,
		// the characters typed after inserting the image should be added to a different batch than the characters typed before.
		this._changeCallback = ( evt, batch ) => {
			if ( batch.isLocal && batch.isUndoable && batch !== this._batch ) {
				this._reset( true );
			}
		};

		this._selectionChangeCallback = () => {
			this._reset();
		};

		this.model.document.on( 'change', this._changeCallback );

		this.model.document.selection.on( 'change:range', this._selectionChangeCallback );
		this.model.document.selection.on( 'change:attribute', this._selectionChangeCallback );

		/**
		 * The current batch instance.
		 *
		 * @private
		 * @member #_batch
		 */

		/**
		 * The callback to document the change event which later needs to be removed.
		 *
		 * @private
		 * @member #_changeCallback
		 */

		/**
		 * The callback to document selection `change:attribute` and `change:range` events which resets the buffer.
		 *
		 * @private
		 * @member #_selectionChangeCallback
		 */
	}

	/**
	 * The current batch to which a feature should add its operations. Once the {@link #size}
	 * is reached or exceeds the {@link #limit}, the batch is set to a new instance and the size is reset.
	 *
	 * @type {module:engine/model/batch~Batch}
	 */
	get batch() {
		if ( !this._batch ) {
			this._batch = this.model.createBatch( { isTyping: true } );
		}

		return this._batch;
	}

	/**
	 * The input number of changes into the buffer. Once the {@link #size} is
	 * reached or exceeds the {@link #limit}, the batch is set to a new instance and the size is reset.
	 *
	 * @param {Number} changeCount The number of atomic changes to input.
	 */
	input( changeCount ) {
		this.size += changeCount;

		if ( this.size >= this.limit ) {
			this._reset( true );
		}
	}

	/**
	 * Locks the buffer.
	 */
	lock() {
		this.isLocked = true;
	}

	/**
	 * Unlocks the buffer.
	 */
	unlock() {
		this.isLocked = false;
	}

	/**
	 * Destroys the buffer.
	 */
	destroy() {
		this.model.document.off( 'change', this._changeCallback );
		this.model.document.selection.off( 'change:range', this._selectionChangeCallback );
		this.model.document.selection.off( 'change:attribute', this._selectionChangeCallback );
	}

	/**
	 * Resets the change buffer.
	 *
	 * @private
	 * @param {Boolean} [ignoreLock] Whether internal lock {@link #isLocked} should be ignored.
	 */
	_reset( ignoreLock ) {
		if ( !this.isLocked || ignoreLock ) {
			this._batch = null;
			this.size = 0;
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The input command. Used by the {@link module:typing/input~Input input feature} to handle typing.
 *
 * @extends module:core/command~Command
 */
class InputCommand extends Command {
	/**
	 * Creates an instance of the command.
	 *
	 * @param {module:core/editor/editor~Editor} editor
	 * @param {Number} undoStepSize The maximum number of atomic changes
	 * which can be contained in one batch in the command buffer.
	 */
	constructor( editor, undoStepSize ) {
		super( editor );

		/**
		 * Typing's change buffer used to group subsequent changes into batches.
		 *
		 * @readonly
		 * @private
		 * @member {module:typing/utils/changebuffer~ChangeBuffer} #_buffer
		 */
		this._buffer = new ChangeBuffer( editor.model, undoStepSize );
	}

	/**
	 * The current change buffer.
	 *
	 * @type {module:typing/utils/changebuffer~ChangeBuffer}
	 */
	get buffer() {
		return this._buffer;
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this._buffer.destroy();
	}

	/**
	 * Executes the input command. It replaces the content within the given range with the given text.
	 * Replacing is a two step process, first the content within the range is removed and then the new text is inserted
	 * at the beginning of the range (which after the removal is a collapsed range).
	 *
	 * @fires execute
	 * @param {Object} [options] The command options.
	 * @param {String} [options.text=''] The text to be inserted.
	 * @param {module:engine/model/range~Range} [options.range] The range in which the text is inserted. Defaults
	 * to the first range in the current selection.
	 * @param {module:engine/model/range~Range} [options.resultRange] The range where the selection
	 * should be placed after the insertion. If not specified, the selection will be placed right after
	 * the inserted text.
	 */
	execute( options = {} ) {
		const model = this.editor.model;
		const doc = model.document;
		const text = options.text || '';
		const textInsertions = text.length;
		const selection = options.range ? model.createSelection( options.range ) : doc.selection;
		const resultRange = options.resultRange;

		model.enqueueChange( this._buffer.batch, writer => {
			this._buffer.lock();

			model.deleteContent( selection );

			if ( text ) {
				model.insertContent( writer.createText( text, doc.selection.getAttributes() ), selection );
			}

			if ( resultRange ) {
				writer.setSelection( resultRange );
			} else if ( !selection.is( 'documentSelection' ) ) {
				writer.setSelection( selection );
			}

			this._buffer.unlock();

			this._buffer.input( textInsertions );
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
/**
 * @module utils/difftochanges
 */
/**
 * Creates a set of changes which need to be applied to the input in order to transform
 * it into the output. This function can be used with strings or arrays.
 *
 *		const input = Array.from( 'abc' );
 *		const output = Array.from( 'xaby' );
 *		const changes = diffToChanges( diff( input, output ), output );
 *
 *		changes.forEach( change => {
 *			if ( change.type == 'insert' ) {
 *				input.splice( change.index, 0, ...change.values );
 *			} else if ( change.type == 'delete' ) {
 *				input.splice( change.index, change.howMany );
 *			}
 *		} );
 *
 *		input.join( '' ) == output.join( '' ); // -> true
 *
 * @param {Array.<module:utils/diff~DiffResult>} diff Result of {@link module:utils/diff~diff}.
 * @param {String|Array} output The string or array which was passed as diff's output.
 * @returns {Array.<module:utils/difftochanges~Change>} Set of changes (insert or delete) which need to be applied to the input
 * in order to transform it into the output.
 */
function diffToChanges(diff, output) {
    const changes = [];
    let index = 0;
    let lastOperation = null;
    diff.forEach(change => {
        if (change == 'equal') {
            pushLast();
            index++;
        }
        else if (change == 'insert') {
            if (lastOperation && lastOperation.type == 'insert') {
                lastOperation.values.push(output[index]);
            }
            else {
                pushLast();
                lastOperation = {
                    type: 'insert',
                    index,
                    values: [output[index]]
                };
            }
            index++;
        }
        else /* if ( change == 'delete' ) */ {
            if (lastOperation && lastOperation.type == 'delete') {
                lastOperation.howMany++;
            }
            else {
                pushLast();
                lastOperation = {
                    type: 'delete',
                    index,
                    howMany: 1
                };
            }
        }
    });
    pushLast();
    return changes;
    function pushLast() {
        if (lastOperation) {
            changes.push(lastOperation);
            lastOperation = null;
        }
    }
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Returns true if container children have mutated or more than a single text node was changed.
 *
 * @private
 * @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
 * module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
 * @returns {Boolean}
 */
function containerChildrenMutated( mutations ) {
	if ( mutations.length == 0 ) {
		return false;
	}

	// Check if there is any mutation of `children` type or any mutation that changes more than one text node.
	for ( const mutation of mutations ) {
		if ( mutation.type === 'children' && !getSingleTextNodeChange( mutation ) ) {
			return true;
		}
	}

	return false;
}

/**
 * Returns change made to a single text node.
 *
 * @private
 * @param {module:engine/view/observer/mutationobserver~MutatedText|
 * module:engine/view/observer/mutationobserver~MutatedChildren} mutation
 * @returns {Object|undefined} Change object (see {@link module:utils/difftochanges~diffToChanges} output)
 * or undefined if more than a single text node was changed.
 */
function getSingleTextNodeChange( mutation ) {
	// One new node.
	if ( mutation.newChildren.length - mutation.oldChildren.length != 1 ) {
		return;
	}

	// Which is text.
	const diffResult = diff( mutation.oldChildren, mutation.newChildren, compareChildNodes );
	const changes = diffToChanges( diffResult, mutation.newChildren );

	// In case of [ delete, insert, insert ] the previous check will not exit.
	if ( changes.length > 1 ) {
		return;
	}

	const change = changes[ 0 ];

	// Which is text.
	if ( !( !!change.values[ 0 ] && change.values[ 0 ].is( '$text' ) ) ) {
		return;
	}

	return change;
}

/**
 * Checks whether two view nodes are identical, which means they are the same object
 * or contain exactly same data (in case of text nodes).
 *
 * @private
 * @param {module:engine/view/node~Node} oldChild
 * @param {module:engine/view/node~Node} newChild
 * @returns {Boolean}
 */
function compareChildNodes( oldChild, newChild ) {
	if ( !!oldChild && oldChild.is( '$text' ) && !!newChild && newChild.is( '$text' ) ) {
		return oldChild.data === newChild.data;
	} else {
		return oldChild === newChild;
	}
}

/**
 * Checks if <kbd>Shift</kbd> + <kbd>Delete</kbd> keystroke was pressed on a non-collapsed selection.
 *
 * This key combination has a special meaning on Windows machines and it should work in the same way as the `cut` event on a non-collapsed
 * selection.
 *
 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData Event data.
 * @param {module:engine/view/document~Document} document The document instance on which the event has been fired.
 * @returns {Boolean}
 */
function isShiftDeleteOnNonCollapsedSelection( domEventData, document ) {
	const selection = document.selection;
	const isShiftDelete = domEventData.shiftKey && domEventData.keyCode === keyCodes.delete;
	const isNonCollapsedSelection = !selection.isCollapsed;

	return isShiftDelete && isNonCollapsedSelection;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Handles keystrokes which are unsafe for typing. This handler's logic is explained
 * in https://github.com/ckeditor/ckeditor5-typing/issues/83#issuecomment-398690251.
 *
 * @param {module:core/editor/editor~Editor} editor The editor instance.
 */
function injectUnsafeKeystrokesHandling( editor ) {
	let latestCompositionSelection = null;

	const model = editor.model;
	const view = editor.editing.view;
	const inputCommand = editor.commands.get( 'input' );

	// For Android, we want to handle keystrokes on `beforeinput` to be sure that code in `DeleteObserver` already had a chance to be fired.
	if ( env.isAndroid ) {
		view.document.on( 'beforeinput', ( evt, evtData ) => handleUnsafeKeystroke( evtData ), { priority: 'lowest' } );
	} else {
		view.document.on( 'keydown', ( evt, evtData ) => handleUnsafeKeystroke( evtData ), { priority: 'lowest' } );
	}

	view.document.on( 'compositionstart', handleCompositionStart, { priority: 'lowest' } );

	view.document.on( 'compositionend', () => {
		latestCompositionSelection = model.createSelection( model.document.selection );
	}, { priority: 'lowest' } );

	// Handles the keydown event. We need to guess whether such keystroke is going to result
	// in typing. If so, then before character insertion happens, any selected content needs
	// to be deleted. Otherwise the default browser deletion mechanism would be
	// triggered, resulting in:
	//
	// * Hundreds of mutations which could not be handled.
	// * But most importantly, loss of control over how the content is being deleted.
	//
	// The method is used in a low-priority listener, hence allowing other listeners (e.g. delete or enter features)
	// to handle the event.
	//
	// @param {module:engine/view/observer/keyobserver~KeyEventData} evtData
	function handleUnsafeKeystroke( evtData ) {
		// Do not delete the content, if Shift + Delete key combination was pressed on a non-collapsed selection on Windows.
		//
		// The Shift + Delete key combination should work in the same way as the `cut` event on a non-collapsed selection on Windows.
		// In fact, the native `cut` event is actually emitted in this case, but with lower priority. Therefore, in order to handle the
		// Shift + Delete key combination correctly, it is enough to prevent the content deletion here.
		if ( env.isWindows && isShiftDeleteOnNonCollapsedSelection( evtData, view.document ) ) {
			return;
		}

		const doc = model.document;
		const isComposing = view.document.isComposing;
		const isSelectionUnchanged = latestCompositionSelection && latestCompositionSelection.isEqual( doc.selection );

		// Reset stored composition selection.
		latestCompositionSelection = null;

		// By relying on the state of the input command we allow disabling the entire input easily
		// by just disabling the input command. We could’ve used here the delete command but that
		// would mean requiring the delete feature which would block loading one without the other.
		// We could also check the editor.isReadOnly property, but that wouldn't allow to block
		// the input without blocking other features.
		if ( !inputCommand.isEnabled ) {
			return;
		}

		if ( isNonTypingKeystroke( evtData ) || doc.selection.isCollapsed ) {
			return;
		}

		// If during composition, deletion should be prevented as it may remove composed sequence (#83).
		if ( isComposing && evtData.keyCode === 229 ) {
			return;
		}

		// If there is a `keydown` event fired with '229' keycode it might be related
		// to recent composition. Check if selection is the same as upon ending recent composition,
		// if so do not remove selected content as it will remove composed sequence (#83).
		if ( !isComposing && evtData.keyCode === 229 && isSelectionUnchanged ) {
			return;
		}

		deleteSelectionContent();
	}

	// Handles the `compositionstart` event. It is used only in special cases to remove the contents
	// of a non-collapsed selection so composition itself does not result in complex mutations.
	//
	// The special case mentioned above is a situation in which the `keydown` event is fired after
	// `compositionstart` event. In such cases {@link #handleKeydown} cannot clear current selection
	// contents (because it is too late and will break the composition) so the composition handler takes care of it.
	function handleCompositionStart() {
		const doc = model.document;
		const isFlatSelection = doc.selection.rangeCount === 1 ? doc.selection.getFirstRange().isFlat : true;

		// If on `compositionstart` there is a non-collapsed selection which start and end have different parents
		// it means the `handleKeydown()` method did not remove its contents. It happens usually because
		// of different order of events (`compositionstart` before `keydown` - in Safari). In such cases
		// we need to remove selection contents on composition start (#83).
		if ( doc.selection.isCollapsed || isFlatSelection ) {
			return;
		}

		deleteSelectionContent();
	}

	function deleteSelectionContent() {
		const buffer = inputCommand.buffer;

		buffer.lock();

		const batch = buffer.batch;

		model.enqueueChange( batch, () => {
			model.deleteContent( model.document.selection );
		} );

		buffer.unlock();
	}
}

const safeKeycodes = [
	getCode( 'arrowUp' ),
	getCode( 'arrowRight' ),
	getCode( 'arrowDown' ),
	getCode( 'arrowLeft' ),
	9, // Tab
	16, // Shift
	17, // Ctrl
	18, // Alt
	19, // Pause
	20, // CapsLock
	27, // Escape
	33, // PageUp
	34, // PageDown
	35, // Home
	36, // End,
	45, // Insert,
	91, // Windows,
	93, // Menu key,
	144, // NumLock
	145, // ScrollLock,
	173, // Mute/Unmute
	174, // Volume up
	175, // Volume down,
	176, // Next song,
	177, // Previous song,
	178, // Stop,
	179, // Play/Pause,
	255 // Display brightness (increase and decrease)
];

// Function keys.
for ( let code = 112; code <= 135; code++ ) {
	safeKeycodes.push( code );
}

/**
 * Returns `true` if a keystroke will **not** result in "typing".
 *
 * For instance, keystrokes that result in typing are letters "a-zA-Z", numbers "0-9", delete, backspace, etc.
 *
 * Keystrokes that do not cause typing are, for instance, Fn keys (F5, F8, etc.), arrow keys (←, →, ↑, ↓),
 * Tab (↹), "Windows logo key" (⊞ Win), etc.
 *
 * Note: This implementation is very simple and will need to be refined with time.
 *
 * @param {module:engine/view/observer/keyobserver~KeyEventData} keyData
 * @returns {Boolean}
 */
function isNonTypingKeystroke( keyData ) {
	// Keystrokes which contain Ctrl or Cmd don't represent typing.
	if ( keyData.ctrlKey || keyData.metaKey ) {
		return true;
	}

	return safeKeycodes.includes( keyData.keyCode );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Handles mutations caused by normal typing.
 *
 * @param {module:core/editor/editor~Editor} editor The editor instance.
 */
function injectTypingMutationsHandling( editor ) {
	editor.editing.view.document.on( 'mutations', ( evt, mutations, viewSelection ) => {
		new MutationHandler( editor ).handle( mutations, viewSelection );
	} );
}

/**
 * Helper class for translating DOM mutations into model changes.
 *
 * @private
 */
class MutationHandler {
	/**
	 * Creates an instance of the mutation handler.
	 *
	 * @param {module:core/editor/editor~Editor} editor
	 */
	constructor( editor ) {
		/**
		 * Editor instance for which mutations are handled.
		 *
		 * @readonly
		 * @member {module:core/editor/editor~Editor} #editor
		 */
		this.editor = editor;

		/**
		 * The editing controller.
		 *
		 * @readonly
		 * @member {module:engine/controller/editingcontroller~EditingController} #editing
		 */
		this.editing = this.editor.editing;
	}

	/**
	 * Handles given mutations.
	 *
	 * @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
	 * module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
	 * @param {module:engine/view/selection~Selection|null} viewSelection
	 */
	handle( mutations, viewSelection ) {
		if ( containerChildrenMutated( mutations ) ) {
			this._handleContainerChildrenMutations( mutations, viewSelection );
		} else {
			for ( const mutation of mutations ) {
				// Fortunately it will never be both.
				this._handleTextMutation( mutation, viewSelection );
				this._handleTextNodeInsertion( mutation );
			}
		}
	}

	/**
	 * Handles situations when container's children mutated during input. This can happen when
	 * the browser is trying to "fix" DOM in certain situations. For example, when the user starts to type
	 * in `<p><a href=""><i>Link{}</i></a></p>`, the browser might change the order of elements
	 * to `<p><i><a href="">Link</a>x{}</i></p>`. A similar situation happens when the spell checker
	 * replaces a word wrapped with `<strong>` with a word wrapped with a `<b>` element.
	 *
	 * To handle such situations, the common DOM ancestor of all mutations is converted to the model representation
	 * and then compared with the current model to calculate the proper text change.
	 *
	 * Note: Single text node insertion is handled in {@link #_handleTextNodeInsertion} and text node mutation is handled
	 * in {@link #_handleTextMutation}).
	 *
	 * @private
	 * @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
	 * module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
	 * @param {module:engine/view/selection~Selection|null} viewSelection
	 */
	_handleContainerChildrenMutations( mutations, viewSelection ) {
		// Get common ancestor of all mutations.
		const mutationsCommonAncestor = getMutationsContainer( mutations );

		// Quit if there is no common ancestor.
		if ( !mutationsCommonAncestor ) {
			return;
		}

		const domConverter = this.editor.editing.view.domConverter;

		// Get common ancestor in DOM.
		const domMutationCommonAncestor = domConverter.mapViewToDom( mutationsCommonAncestor );

		// Create fresh DomConverter so it will not use existing mapping and convert current DOM to model.
		// This wouldn't be needed if DomConverter would allow to create fresh view without checking any mappings.
		const freshDomConverter = new DomConverter( this.editor.editing.view.document );
		const modelFromCurrentDom = this.editor.data.toModel(
			freshDomConverter.domToView( domMutationCommonAncestor )
		).getChild( 0 );

		// Current model.
		const currentModel = this.editor.editing.mapper.toModelElement( mutationsCommonAncestor );

		// If common ancestor is not mapped, do not do anything. It probably is a parent of another view element.
		// That means that we would need to diff model elements (see `if` below). Better return early instead of
		// trying to get a reasonable model ancestor. It will fell into the `if` below anyway.
		// This situation happens for example for lists. If `<ul>` is a common ancestor, `currentModel` is `undefined`
		// because `<ul>` is not mapped (`<li>`s are).
		// See https://github.com/ckeditor/ckeditor5/issues/718.
		if ( !currentModel ) {
			return;
		}

		// Get children from both ancestors.
		const modelFromDomChildren = Array.from( modelFromCurrentDom.getChildren() );
		const currentModelChildren = Array.from( currentModel.getChildren() );

		// Remove the last `<softBreak>` from the end of `modelFromDomChildren` if there is no `<softBreak>` in current model.
		// If the described scenario happened, it means that this is a bogus `<br />` added by a browser.
		const lastDomChild = modelFromDomChildren[ modelFromDomChildren.length - 1 ];
		const lastCurrentChild = currentModelChildren[ currentModelChildren.length - 1 ];

		const isLastDomChildSoftBreak = lastDomChild && lastDomChild.is( 'element', 'softBreak' );
		const isLastCurrentChildSoftBreak = lastCurrentChild && !lastCurrentChild.is( 'element', 'softBreak' );

		if ( isLastDomChildSoftBreak && isLastCurrentChildSoftBreak ) {
			modelFromDomChildren.pop();
		}

		const schema = this.editor.model.schema;

		// Skip situations when common ancestor has any container elements.
		if ( !isSafeForTextMutation( modelFromDomChildren, schema ) || !isSafeForTextMutation( currentModelChildren, schema ) ) {
			return;
		}

		// Replace &nbsp; inserted by the browser with normal space. See comment in `_handleTextMutation`.
		// Replace non-texts with any character. This is potentially dangerous but passes in manual tests. The thing is
		// that we need to take care of proper indexes so we cannot simply remove non-text elements from the content.
		// By inserting a character we keep all the real texts on their indexes.
		const newText = modelFromDomChildren.map( item => item.is( '$text' ) ? item.data : '@' ).join( '' ).replace( /\u00A0/g, ' ' );
		const oldText = currentModelChildren.map( item => item.is( '$text' ) ? item.data : '@' ).join( '' ).replace( /\u00A0/g, ' ' );

		// Do nothing if mutations created same text.
		if ( oldText === newText ) {
			return;
		}

		const diffResult = diff( oldText, newText );

		const { firstChangeAt, insertions, deletions } = calculateChanges( diffResult );

		// Try setting new model selection according to passed view selection.
		let modelSelectionRange = null;

		if ( viewSelection ) {
			modelSelectionRange = this.editing.mapper.toModelRange( viewSelection.getFirstRange() );
		}

		const insertText = newText.substr( firstChangeAt, insertions );
		const removeRange = this.editor.model.createRange(
			this.editor.model.createPositionAt( currentModel, firstChangeAt ),
			this.editor.model.createPositionAt( currentModel, firstChangeAt + deletions )
		);

		this.editor.execute( 'input', {
			text: insertText,
			range: removeRange,
			resultRange: modelSelectionRange
		} );
	}

	/**
	 * @private
	 */
	_handleTextMutation( mutation, viewSelection ) {
		if ( mutation.type != 'text' ) {
			return;
		}

		// Replace &nbsp; inserted by the browser with normal space.
		// We want only normal spaces in the model and in the view. Renderer and DOM Converter will be then responsible
		// for rendering consecutive spaces using &nbsp;, but the model and the view has to be clear.
		// Other feature may introduce inserting non-breakable space on specific key stroke (for example shift + space).
		// However then it will be handled outside of mutations, like enter key is.
		// The replacing is here because it has to be done before `diff` and `diffToChanges` functions, as they
		// take `newText` and compare it to (cleaned up) view.
		// It could also be done in mutation observer too, however if any outside plugin would like to
		// introduce additional events for mutations, they would get already cleaned up version (this may be good or not).
		const newText = mutation.newText.replace( /\u00A0/g, ' ' );
		// To have correct `diffResult`, we also compare view node text data with &nbsp; replaced by space.
		const oldText = mutation.oldText.replace( /\u00A0/g, ' ' );

		// Do nothing if mutations created same text.
		if ( oldText === newText ) {
			return;
		}

		const diffResult = diff( oldText, newText );

		const { firstChangeAt, insertions, deletions } = calculateChanges( diffResult );

		// Try setting new model selection according to passed view selection.
		let modelSelectionRange = null;

		if ( viewSelection ) {
			modelSelectionRange = this.editing.mapper.toModelRange( viewSelection.getFirstRange() );
		}

		// Get the position in view and model where the changes will happen.
		const viewPos = this.editing.view.createPositionAt( mutation.node, firstChangeAt );
		const modelPos = this.editing.mapper.toModelPosition( viewPos );
		const removeRange = this.editor.model.createRange( modelPos, modelPos.getShiftedBy( deletions ) );
		const insertText = newText.substr( firstChangeAt, insertions );

		this.editor.execute( 'input', {
			text: insertText,
			range: removeRange,
			resultRange: modelSelectionRange
		} );
	}

	/**
	 * @private
	 */
	_handleTextNodeInsertion( mutation ) {
		if ( mutation.type != 'children' ) {
			return;
		}

		const change = getSingleTextNodeChange( mutation );
		const viewPos = this.editing.view.createPositionAt( mutation.node, change.index );
		const modelPos = this.editing.mapper.toModelPosition( viewPos );
		const insertedText = change.values[ 0 ].data;

		this.editor.execute( 'input', {
			// Replace &nbsp; inserted by the browser with normal space.
			// See comment in `_handleTextMutation`.
			// In this case we don't need to do this before `diff` because we diff whole nodes.
			// Just change &nbsp; in case there are some.
			text: insertedText.replace( /\u00A0/g, ' ' ),
			range: this.editor.model.createRange( modelPos )
		} );
	}
}

// Returns first common ancestor of all mutations that is either {@link module:engine/view/containerelement~ContainerElement}
// or {@link module:engine/view/rootelement~RootElement}.
//
// @private
// @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
// module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
// @returns {module:engine/view/containerelement~ContainerElement|engine/view/rootelement~RootElement|undefined}
function getMutationsContainer( mutations ) {
	const lca = mutations
		.map( mutation => mutation.node )
		.reduce( ( commonAncestor, node ) => {
			return commonAncestor.getCommonAncestor( node, { includeSelf: true } );
		} );

	if ( !lca ) {
		return;
	}

	// We need to look for container and root elements only, so check all LCA's
	// ancestors (starting from itself).
	return lca.getAncestors( { includeSelf: true, parentFirst: true } )
		.find( element => element.is( 'containerElement' ) || element.is( 'rootElement' ) );
}

// Returns true if provided array contains content that won't be problematic during diffing and text mutation handling.
//
// @param {Array.<module:engine/model/node~Node>} children
// @param {module:engine/model/schema~Schema} schema
// @returns {Boolean}
function isSafeForTextMutation( children, schema ) {
	return children.every( child => schema.isInline( child ) );
}

// Calculates first change index and number of characters that should be inserted and deleted starting from that index.
//
// @private
// @param diffResult
// @returns {{insertions: number, deletions: number, firstChangeAt: *}}
function calculateChanges( diffResult ) {
	// Index where the first change happens. Used to set the position from which nodes will be removed and where will be inserted.
	let firstChangeAt = null;
	// Index where the last change happens. Used to properly count how many characters have to be removed and inserted.
	let lastChangeAt = null;

	// Get `firstChangeAt` and `lastChangeAt`.
	for ( let i = 0; i < diffResult.length; i++ ) {
		const change = diffResult[ i ];

		if ( change != 'equal' ) {
			firstChangeAt = firstChangeAt === null ? i : firstChangeAt;
			lastChangeAt = i;
		}
	}

	// How many characters, starting from `firstChangeAt`, should be removed.
	let deletions = 0;
	// How many characters, starting from `firstChangeAt`, should be inserted.
	let insertions = 0;

	for ( let i = firstChangeAt; i <= lastChangeAt; i++ ) {
		// If there is no change (equal) or delete, the character is existing in `oldText`. We count it for removing.
		if ( diffResult[ i ] != 'insert' ) {
			deletions++;
		}

		// If there is no change (equal) or insert, the character is existing in `newText`. We count it for inserting.
		if ( diffResult[ i ] != 'delete' ) {
			insertions++;
		}
	}

	return { insertions, deletions, firstChangeAt };
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Handles text input coming from the keyboard or other input methods.
 *
 * @extends module:core/plugin~Plugin
 */
class Input extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Input';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		// TODO The above default configuration value should be defined using editor.config.define() once it's fixed.
		const inputCommand = new InputCommand( editor, editor.config.get( 'typing.undoStep' ) || 20 );

		editor.commands.add( 'input', inputCommand );

		injectUnsafeKeystrokesHandling( editor );
		injectTypingMutationsHandling( editor );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The delete command. Used by the {@link module:typing/delete~Delete delete feature} to handle the <kbd>Delete</kbd> and
 * <kbd>Backspace</kbd> keys.
 *
 * @extends module:core/command~Command
 */
class DeleteCommand extends Command {
	/**
	 * Creates an instance of the command.
	 *
	 * @param {module:core/editor/editor~Editor} editor
	 * @param {'forward'|'backward'} direction The directionality of the delete describing in what direction it
	 * should consume the content when the selection is collapsed.
	 */
	constructor( editor, direction ) {
		super( editor );

		/**
		 * The directionality of the delete describing in what direction it should
		 * consume the content when the selection is collapsed.
		 *
		 * @readonly
		 * @member {'forward'|'backward'} #direction
		 */
		this.direction = direction;

		/**
		 * Delete's change buffer used to group subsequent changes into batches.
		 *
		 * @readonly
		 * @private
		 * @type {module:typing/utils/changebuffer~ChangeBuffer}
		 */
		this._buffer = new ChangeBuffer( editor.model, editor.config.get( 'typing.undoStep' ) );
	}

	/**
	 * The current change buffer.
	 *
	 * @type {module:typing/utils/changebuffer~ChangeBuffer}
	 */
	get buffer() {
		return this._buffer;
	}

	/**
	 * Executes the delete command. Depending on whether the selection is collapsed or not, deletes its content
	 * or a piece of content in the {@link #direction defined direction}.
	 *
	 * @fires execute
	 * @param {Object} [options] The command options.
	 * @param {'character'|'codePoint'|'word'} [options.unit='character']
	 * See {@link module:engine/model/utils/modifyselection~modifySelection}'s options.
	 * @param {Number} [options.sequence=1] A number describing which subsequent delete event it is without the key being released.
	 * See the {@link module:engine/view/document~Document#event:delete} event data.
	 * @param {module:engine/model/selection~Selection} [options.selection] Selection to remove. If not set, current model selection
	 * will be used.
	 */
	execute( options = {} ) {
		const model = this.editor.model;
		const doc = model.document;

		model.enqueueChange( this._buffer.batch, writer => {
			this._buffer.lock();

			const selection = writer.createSelection( options.selection || doc.selection );
			const sequence = options.sequence || 1;

			// Do not replace the whole selected content if selection was collapsed.
			// This prevents such situation:
			//
			// <h1></h1><p>[]</p>	-->  <h1>[</h1><p>]</p> 		-->  <p></p>
			// starting content		-->   after `modifySelection`	-->  after `deleteContent`.
			const doNotResetEntireContent = selection.isCollapsed;

			// Try to extend the selection in the specified direction.
			if ( selection.isCollapsed ) {
				model.modifySelection( selection, {
					direction: this.direction,
					unit: options.unit,
					treatEmojiAsSingleUnit: true
				} );
			}

			// Check if deleting in an empty editor. See #61.
			if ( this._shouldEntireContentBeReplacedWithParagraph( sequence ) ) {
				this._replaceEntireContentWithParagraph( writer );

				return;
			}

			// Check if deleting in the first empty block.
			// See https://github.com/ckeditor/ckeditor5/issues/8137.
			if ( this._shouldReplaceFirstBlockWithParagraph( selection, sequence ) ) {
				this.editor.execute( 'paragraph', { selection } );

				return;
			}

			// If selection is still collapsed, then there's nothing to delete.
			if ( selection.isCollapsed ) {
				return;
			}

			let changeCount = 0;

			selection.getFirstRange().getMinimalFlatRanges().forEach( range => {
				changeCount += count(
					range.getWalker( { singleCharacters: true, ignoreElementEnd: true, shallow: true } )
				);
			} );

			model.deleteContent( selection, {
				doNotResetEntireContent,
				direction: this.direction
			} );

			this._buffer.input( changeCount );

			writer.setSelection( selection );

			this._buffer.unlock();
		} );
	}

	/**
	 * If the user keeps <kbd>Backspace</kbd> or <kbd>Delete</kbd> key pressed, the content of the current
	 * editable will be cleared. However, this will not yet lead to resetting the remaining block to a paragraph
	 * (which happens e.g. when the user does <kbd>Ctrl</kbd> + <kbd>A</kbd>, <kbd>Backspace</kbd>).
	 *
	 * But, if the user pressed the key in an empty editable for the first time,
	 * we want to replace the entire content with a paragraph if:
	 *
	 * * the current limit element is empty,
	 * * the paragraph is allowed in the limit element,
	 * * the limit doesn't already have a paragraph inside.
	 *
	 * See https://github.com/ckeditor/ckeditor5-typing/issues/61.
	 *
	 * @private
	 * @param {Number} sequence A number describing which subsequent delete event it is without the key being released.
	 * @returns {Boolean}
	 */
	_shouldEntireContentBeReplacedWithParagraph( sequence ) {
		// Does nothing if user pressed and held the "Backspace" or "Delete" key.
		if ( sequence > 1 ) {
			return false;
		}

		const model = this.editor.model;
		const doc = model.document;
		const selection = doc.selection;
		const limitElement = model.schema.getLimitElement( selection );

		// If a collapsed selection contains the whole content it means that the content is empty
		// (from the user perspective).
		const limitElementIsEmpty = selection.isCollapsed && selection.containsEntireContent( limitElement );

		if ( !limitElementIsEmpty ) {
			return false;
		}

		if ( !model.schema.checkChild( limitElement, 'paragraph' ) ) {
			return false;
		}

		const limitElementFirstChild = limitElement.getChild( 0 );

		// Does nothing if the limit element already contains only a paragraph.
		// We ignore the case when paragraph might have some inline elements (<p><inlineWidget>[]</inlineWidget></p>)
		// because we don't support such cases yet and it's unclear whether inlineWidget shouldn't be a limit itself.
		if ( limitElementFirstChild && limitElementFirstChild.name === 'paragraph' ) {
			return false;
		}

		return true;
	}

	/**
	 * The entire content is replaced with the paragraph. Selection is moved inside the paragraph.
	 *
	 * @private
	 * @param {module:engine/model/writer~Writer} writer The model writer.
	 */
	_replaceEntireContentWithParagraph( writer ) {
		const model = this.editor.model;
		const doc = model.document;
		const selection = doc.selection;
		const limitElement = model.schema.getLimitElement( selection );
		const paragraph = writer.createElement( 'paragraph' );

		writer.remove( writer.createRangeIn( limitElement ) );
		writer.insert( paragraph, limitElement );

		writer.setSelection( paragraph, 0 );
	}

	/**
	 * Checks if the selection is inside an empty element that is the first child of the limit element
	 * and should be replaced with a paragraph.
	 *
	 * @private
	 * @param {module:engine/model/selection~Selection} selection The selection.
	 * @param {Number} sequence A number describing which subsequent delete event it is without the key being released.
	 * @returns {Boolean}
	 */
	_shouldReplaceFirstBlockWithParagraph( selection, sequence ) {
		const model = this.editor.model;

		// Does nothing if user pressed and held the "Backspace" key or it was a "Delete" button.
		if ( sequence > 1 || this.direction != 'backward' ) {
			return false;
		}

		if ( !selection.isCollapsed ) {
			return false;
		}

		const position = selection.getFirstPosition();
		const limitElement = model.schema.getLimitElement( position );
		const limitElementFirstChild = limitElement.getChild( 0 );

		// Only elements that are direct children of the limit element can be replaced.
		// Unwrapping from a block quote should be handled in a dedicated feature.
		if ( position.parent != limitElementFirstChild ) {
			return false;
		}

		// A block should be replaced only if it was empty.
		if ( !selection.containsEntireContent( limitElementFirstChild ) ) {
			return false;
		}

		// Replace with a paragraph only if it's allowed there.
		if ( !model.schema.checkChild( limitElement, 'paragraph' ) ) {
			return false;
		}

		// Does nothing if the limit element already contains only a paragraph.
		if ( limitElementFirstChild.name == 'paragraph' ) {
			return false;
		}

		return true;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Delete observer introduces the {@link module:engine/view/document~Document#event:delete} event.
 *
 * @extends module:engine/view/observer/observer~Observer
 */
class DeleteObserver extends Observer {
	/**
	 * @inheritDoc
	 */
	constructor( view ) {
		super( view );

		const document = view.document;
		let sequence = 0;

		document.on( 'keyup', ( evt, data ) => {
			if ( data.keyCode == keyCodes.delete || data.keyCode == keyCodes.backspace ) {
				sequence = 0;
			}
		} );

		document.on( 'keydown', ( evt, data ) => {
			// Do not fire the `delete` event, if Shift + Delete key combination was pressed on a non-collapsed selection on Windows.
			//
			// The Shift + Delete key combination should work in the same way as the `cut` event on a non-collapsed selection on Windows.
			// In fact, the native `cut` event is actually emitted in this case, but with lower priority. Therefore, in order to handle the
			// Shift + Delete key combination correctly, it is enough not to emit the `delete` event.
			if ( env.isWindows && isShiftDeleteOnNonCollapsedSelection( data, document ) ) {
				return;
			}

			const deleteData = {};

			if ( data.keyCode == keyCodes.delete ) {
				deleteData.direction = 'forward';
				deleteData.unit = 'character';
			} else if ( data.keyCode == keyCodes.backspace ) {
				deleteData.direction = 'backward';
				deleteData.unit = 'codePoint';
			} else {
				return;
			}

			const hasWordModifier = env.isMac ? data.altKey : data.ctrlKey;
			deleteData.unit = hasWordModifier ? 'word' : deleteData.unit;
			deleteData.sequence = ++sequence;

			fireViewDeleteEvent( evt, data.domEvent, deleteData );
		} );

		// `beforeinput` is handled only for Android devices. Desktop Chrome and iOS are skipped because they are working fine now.
		if ( env.isAndroid ) {
			document.on( 'beforeinput', ( evt, data ) => {
				// If event type is other than `deleteContentBackward` then this is not deleting.
				if ( data.domEvent.inputType != 'deleteContentBackward' ) {
					return;
				}

				const deleteData = {
					unit: 'codepoint',
					direction: 'backward',
					sequence: 1
				};

				// Android IMEs may change the DOM selection on `beforeinput` event so that the selection contains all the text
				// that the IME wants to remove. We will pass this information to `delete` event so proper part of the content is removed.
				//
				// Sometimes it is only expanding by a one character (in case of collapsed selection). In this case we don't need to
				// set a different selection to remove, it will work just fine.
				const domSelection = data.domTarget.ownerDocument.defaultView.getSelection();

				if ( domSelection.anchorNode == domSelection.focusNode && domSelection.anchorOffset + 1 != domSelection.focusOffset ) {
					deleteData.selectionToRemove = view.domConverter.domSelectionToView( domSelection );
				}

				fireViewDeleteEvent( evt, data.domEvent, deleteData );
			} );
		}

		function fireViewDeleteEvent( originalEvent, domEvent, deleteData ) {
			const event = new BubblingEventInfo( document, 'delete', document.selection.getFirstRange() );

			document.fire( event, new DomEventData( document, domEvent, deleteData ) );

			// Stop the original event if `delete` event was stopped.
			// https://github.com/ckeditor/ckeditor5/issues/753
			if ( event.stop.called ) {
				originalEvent.stop();
			}
		}
	}

	/**
	 * @inheritDoc
	 */
	observe() {}
}

/**
 * Event fired when the user tries to delete content (e.g. presses <kbd>Delete</kbd> or <kbd>Backspace</kbd>).
 *
 * Note: This event is fired by the {@link module:typing/deleteobserver~DeleteObserver observer}
 * (usually registered by the {@link module:typing/delete~Delete delete feature}).
 *
 * @event module:engine/view/document~Document#event:delete
 * @param {module:engine/view/observer/domeventdata~DomEventData} data
 * @param {'forward'|'delete'} data.direction The direction in which the deletion should happen.
 * @param {'character'|'codePoint'|'word'} data.unit The "amount" of content that should be deleted.
 * @param {Number} data.sequence A number describing which subsequent delete event it is without the key being released.
 * If it's 2 or more it means that the key was pressed and hold.
 * @param {module:engine/view/selection~Selection} [data.selectionToRemove] View selection which content should be removed. If not set,
 * current selection should be used.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The delete and backspace feature. Handles the <kbd>Delete</kbd> and <kbd>Backspace</kbd> keys in the editor.
 *
 * @extends module:core/plugin~Plugin
 */
class Delete extends Plugin {
	/**
	 * Whether pressing backspace should trigger undo action
	 *
	 * @private
	 * @member {Boolean} #_undoOnBackspace
	 */

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Delete';
	}

	init() {
		const editor = this.editor;
		const view = editor.editing.view;
		const viewDocument = view.document;
		const modelDocument = editor.model.document;

		view.addObserver( DeleteObserver );

		this._undoOnBackspace = false;

		const deleteForwardCommand = new DeleteCommand( editor, 'forward' );

		// Register `deleteForward` command and add `forwardDelete` command as an alias for backward compatibility.
		editor.commands.add( 'deleteForward', deleteForwardCommand );
		editor.commands.add( 'forwardDelete', deleteForwardCommand );

		editor.commands.add( 'delete', new DeleteCommand( editor, 'backward' ) );

		this.listenTo( viewDocument, 'delete', ( evt, data ) => {
			const deleteCommandParams = { unit: data.unit, sequence: data.sequence };

			// If a specific (view) selection to remove was set, convert it to a model selection and set as a parameter for `DeleteCommand`.
			if ( data.selectionToRemove ) {
				const modelSelection = editor.model.createSelection();
				const ranges = [];

				for ( const viewRange of data.selectionToRemove.getRanges() ) {
					ranges.push( editor.editing.mapper.toModelRange( viewRange ) );
				}

				modelSelection.setTo( ranges );

				deleteCommandParams.selection = modelSelection;
			}

			editor.execute( data.direction == 'forward' ? 'deleteForward' : 'delete', deleteCommandParams );

			data.preventDefault();

			view.scrollToTheSelection();
		}, { priority: 'low' } );

		// Android IMEs have a quirk - they change DOM selection after the input changes were performed by the browser.
		// This happens on `keyup` event. Android doesn't know anything about our deletion and selection handling. Even if the selection
		// was changed during input events, IME remembers the position where the selection "should" be placed and moves it there.
		//
		// To prevent incorrect selection, we save the selection after deleting here and then re-set it on `keyup`. This has to be done
		// on DOM selection level, because on `keyup` the model selection is still the same as it was just after deletion, so it
		// wouldn't be changed and the fix would do nothing.
		//
		if ( env.isAndroid ) {
			let domSelectionAfterDeletion = null;

			this.listenTo( viewDocument, 'delete', ( evt, data ) => {
				const domSelection = data.domTarget.ownerDocument.defaultView.getSelection();

				domSelectionAfterDeletion = {
					anchorNode: domSelection.anchorNode,
					anchorOffset: domSelection.anchorOffset,
					focusNode: domSelection.focusNode,
					focusOffset: domSelection.focusOffset
				};
			}, { priority: 'lowest' } );

			this.listenTo( viewDocument, 'keyup', ( evt, data ) => {
				if ( domSelectionAfterDeletion ) {
					const domSelection = data.domTarget.ownerDocument.defaultView.getSelection();

					domSelection.collapse( domSelectionAfterDeletion.anchorNode, domSelectionAfterDeletion.anchorOffset );
					domSelection.extend( domSelectionAfterDeletion.focusNode, domSelectionAfterDeletion.focusOffset );

					domSelectionAfterDeletion = null;
				}
			} );
		}

		if ( this.editor.plugins.has( 'UndoEditing' ) ) {
			this.listenTo( viewDocument, 'delete', ( evt, data ) => {
				if ( this._undoOnBackspace && data.direction == 'backward' && data.sequence == 1 && data.unit == 'codePoint' ) {
					this._undoOnBackspace = false;

					editor.execute( 'undo' );

					data.preventDefault();
					evt.stop();
				}
			}, { context: '$capture' } );

			this.listenTo( modelDocument, 'change', () => {
				this._undoOnBackspace = false;
			} );
		}
	}

	/**
	 * If the next user action after calling this method is pressing backspace, it would undo the last change.
	 *
	 * Requires {@link module:undo/undoediting~UndoEditing} plugin. If not loaded, does nothing.
	 */
	requestUndoOnBackspace() {
		if ( this.editor.plugins.has( 'UndoEditing' ) ) {
			this._undoOnBackspace = true;
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The typing feature. It handles typing.
 *
 * This is a "glue" plugin which loads the {@link module:typing/input~Input} and {@link module:typing/delete~Delete}
 * plugins.
 *
 * @extends module:core/plugin~Plugin
 */
class Typing extends Plugin {
	static get requires() {
		return [ Input, Delete ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Typing';
	}
}

/**
 * The configuration of the typing features. Used by the features from the `@ckeditor/ckeditor5-typing` package.
 *
 * Read more in {@link module:typing/typing~TypingConfig}.
 *
 * @member {module:typing/typing~TypingConfig} module:core/editor/editorconfig~EditorConfig#typing
 */

/**
 * The configuration of the typing features. Used by the typing features in `@ckeditor/ckeditor5-typing` package.
 *
 *		ClassicEditor
 *			.create( editorElement, {
 * 				typing: ... // Typing feature options.
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
 *
 * @interface TypingConfig
 */

/**
 * The granularity of undo/redo for typing and deleting. The value `20` means (more or less) that a new undo step
 * is created every 20 characters are inserted or deleted.
 *
 * @member {Number} [module:typing/typing~TypingConfig#undoStep=20]
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module typing/utils/getlasttextline
 */

/**
 * Returns the last text line from the given range.
 *
 * "The last text line" is understood as text (from one or more text nodes) which is limited either by a parent block
 * or by inline elements (e.g. `<softBreak>`).
 *
 *		const rangeToCheck = model.createRange(
 *			model.createPositionAt( paragraph, 0 ),
 *			model.createPositionAt( paragraph, 'end' )
 *		);
 *
 *		const { text, range } = getLastTextLine( rangeToCheck, model );
 *
 * For model below, the returned `text` will be "Foo bar baz" and `range` will be set on whole `<paragraph>` content:
 *
 *		<paragraph>Foo bar baz<paragraph>
 *
 * However, in below case, `text` will be set to "baz" and `range` will be set only on "baz".
 *
 *		<paragraph>Foo<softBreak></softBreak>bar<softBreak></softBreak>baz<paragraph>
 *
 * @protected
 * @param {module:engine/model/range~Range} range
 * @param {module:engine/model/model~Model} model
 * @returns {module:typing/utils/getlasttextline~LastTextLineData}
 */
function getLastTextLine( range, model ) {
	let start = range.start;

	const text = Array.from( range.getItems() ).reduce( ( rangeText, node ) => {
		// Trim text to a last occurrence of an inline element and update range start.
		if ( !( node.is( '$text' ) || node.is( '$textProxy' ) ) ) {
			start = model.createPositionAfter( node );

			return '';
		}

		return rangeText + node.data;
	}, '' );

	return { text, range: model.createRange( start, range.end ) };
}

/**
 * The value returned by {@link module:typing/utils/getlasttextline~getLastTextLine}.
 *
 * @typedef {Object} module:typing/utils/getlasttextline~LastTextLineData
 *
 * @property {String} text The text from the text nodes in the last text line.
 * @property {module:engine/model/range~Range} range The range set on the text nodes in the last text line.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The text watcher feature.
 *
 * Fires the {@link module:typing/textwatcher~TextWatcher#event:matched:data `matched:data`},
 * {@link module:typing/textwatcher~TextWatcher#event:matched:selection `matched:selection`} and
 * {@link module:typing/textwatcher~TextWatcher#event:unmatched `unmatched`} events on typing or selection changes.
 *
 * @private
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class TextWatcher {
	/**
	 * Creates a text watcher instance.
	 *
	 * @param {module:engine/model/model~Model} model
	 * @param {Function} testCallback See {@link module:typing/textwatcher~TextWatcher#testCallback}.
	 */
	constructor( model, testCallback ) {
		/**
		 * The editor's model.
		 *
		 * @readonly
		 * @member {module:engine/model/model~Model}
		 */
		this.model = model;

		/**
		 * The function used to match the text.
		 *
		 * The test callback can return 3 values:
		 *
		 * * `false` if there is no match,
		 * * `true` if there is a match,
		 * * an object if there is a match and we want to pass some additional information to the {@link #event:matched:data} event.
		 *
		 * @member {Function} #testCallback
		 * @returns {Object} testResult
		 */
		this.testCallback = testCallback;

		/**
		 * Whether there is a match currently.
		 *
		 * @readonly
		 * @member {Boolean}
		 */
		this.hasMatch = false;

		/**
		 * Flag indicating whether the `TextWatcher` instance is enabled or disabled.
		 * A disabled TextWatcher will not evaluate text.
		 *
		 * To disable TextWatcher:
		 *
		 *		const watcher = new TextWatcher( editor.model, testCallback );
		 *
		 *		// After this a testCallback will not be called.
		 *		watcher.isEnabled = false;
		 *
		 * @observable
		 * @member {Boolean} #isEnabled
		 */
		this.set( 'isEnabled', true );

		// Toggle text watching on isEnabled state change.
		this.on( 'change:isEnabled', () => {
			if ( this.isEnabled ) {
				this._startListening();
			} else {
				this.stopListening( model.document.selection );
				this.stopListening( model.document );
			}
		} );

		this._startListening();
	}

	/**
	 * Starts listening to the editor for typing and selection events.
	 *
	 * @private
	 */
	_startListening() {
		const model = this.model;
		const document = model.document;

		this.listenTo( document.selection, 'change:range', ( evt, { directChange } ) => {
			// Indirect changes (i.e. when the user types or external changes are applied) are handled in the document's change event.
			if ( !directChange ) {
				return;
			}

			// Act only on collapsed selection.
			if ( !document.selection.isCollapsed ) {
				if ( this.hasMatch ) {
					this.fire( 'unmatched' );
					this.hasMatch = false;
				}

				return;
			}

			this._evaluateTextBeforeSelection( 'selection' );
		} );

		this.listenTo( document, 'change:data', ( evt, batch ) => {
			if ( batch.isUndo || !batch.isLocal ) {
				return;
			}

			this._evaluateTextBeforeSelection( 'data', { batch } );
		} );
	}

	/**
	 * Checks the editor content for matched text.
	 *
	 * @fires matched:data
	 * @fires matched:selection
	 * @fires unmatched
	 *
	 * @private
	 * @param {'data'|'selection'} suffix A suffix used for generating the event name.
	 * @param {Object} data Data object for event.
	 */
	_evaluateTextBeforeSelection( suffix, data = {} ) {
		const model = this.model;
		const document = model.document;
		const selection = document.selection;

		const rangeBeforeSelection = model.createRange( model.createPositionAt( selection.focus.parent, 0 ), selection.focus );

		const { text, range } = getLastTextLine( rangeBeforeSelection, model );

		const testResult = this.testCallback( text );

		if ( !testResult && this.hasMatch ) {
			this.fire( 'unmatched' );
		}

		this.hasMatch = !!testResult;

		if ( testResult ) {
			const eventData = Object.assign( data, { text, range } );

			// If the test callback returns an object with additional data, assign the data as well.
			if ( typeof testResult == 'object' ) {
				Object.assign( eventData, testResult );
			}

			this.fire( `matched:${ suffix }`, eventData );
		}
	}
}

mix( TextWatcher, ObservableMixin );

/**
 * Fired whenever the text watcher found a match for data changes.
 *
 * @event matched:data
 * @param {Object} data Event data.
 * @param {String} data.text The full text before selection to which the regexp was applied.
 * @param {module:engine/model/range~Range} data.range The range representing the position of the `data.text`.
 * @param {Object} [data.testResult] The additional data returned from the {@link module:typing/textwatcher~TextWatcher#testCallback}.
 */

/**
 * Fired whenever the text watcher found a match for selection changes.
 *
 * @event matched:selection
 * @param {Object} data Event data.
 * @param {String} data.text The full text before selection.
 * @param {module:engine/model/range~Range} data.range The range representing the position of the `data.text`.
 * @param {Object} [data.testResult] The additional data returned from the {@link module:typing/textwatcher~TextWatcher#testCallback}.
 */

/**
 * Fired whenever the text does not match anymore. Fired only when the text watcher found a match.
 *
 * @event unmatched
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * This plugin enables the two-step caret (phantom) movement behavior for
 * {@link module:typing/twostepcaretmovement~TwoStepCaretMovement#registerAttribute registered attributes}
 * on arrow right (<kbd>→</kbd>) and left (<kbd>←</kbd>) key press.
 *
 * Thanks to this (phantom) caret movement the user is able to type before/after as well as at the
 * beginning/end of an attribute.
 *
 * **Note:** This plugin support right–to–left (Arabic, Hebrew, etc.) content by mirroring its behavior
 * but for the sake of simplicity examples showcase only left–to–right use–cases.
 *
 * # Forward movement
 *
 * ## "Entering" an attribute:
 *
 * When this plugin is enabled and registered for the `a` attribute and the selection is right before it
 * (at the attribute boundary), pressing the right arrow key will not move the selection but update its
 * attributes accordingly:
 *
 * * When enabled:
 *
 *   		foo{}<$text a="true">bar</$text>
 *
 *    <kbd>→</kbd>
 *
 *   		foo<$text a="true">{}bar</$text>
 *
 * * When disabled:
 *
 *   		foo{}<$text a="true">bar</$text>
 *
 *   <kbd>→</kbd>
 *
 *   		foo<$text a="true">b{}ar</$text>
 *
 *
 * ## "Leaving" an attribute:
 *
 * * When enabled:
 *
 *   		<$text a="true">bar{}</$text>baz
 *
 *    <kbd>→</kbd>
 *
 *   		<$text a="true">bar</$text>{}baz
 *
 * * When disabled:
 *
 *   		<$text a="true">bar{}</$text>baz
 *
 *   <kbd>→</kbd>
 *
 *   		<$text a="true">bar</$text>b{}az
 *
 * # Backward movement
 *
 * * When enabled:
 *
 *   		<$text a="true">bar</$text>{}baz
 *
 *    <kbd>←</kbd>
 *
 *   		<$text a="true">bar{}</$text>baz
 *
 * * When disabled:
 *
 *   		<$text a="true">bar</$text>{}baz
 *
 *   <kbd>←</kbd>
 *
 *   		<$text a="true">ba{}r</$text>b{}az
 *
 * # Multiple attributes
 *
 * * When enabled and many attributes starts or ends at the same position:
 *
 *   		<$text a="true" b="true">bar</$text>{}baz
 *
 *    <kbd>←</kbd>
 *
 *   		<$text a="true" b="true">bar{}</$text>baz
 *
 * * When enabled and one procedes another:
 *
 *   		<$text a="true">bar</$text><$text b="true">{}bar</$text>
 *
 *    <kbd>←</kbd>
 *
 *   		<$text a="true">bar{}</$text><$text b="true">bar</$text>
 *
 */
class TwoStepCaretMovement extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TwoStepCaretMovement';
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		/**
		 * A set of attributes to handle.
		 *
		 * @protected
		 * @property {module:typing/twostepcaretmovement~TwoStepCaretMovement}
		 */
		this.attributes = new Set();

		/**
		 * The current UID of the overridden gravity, as returned by
		 * {@link module:engine/model/writer~Writer#overrideSelectionGravity}.
		 *
		 * @private
		 * @member {String}
		 */
		this._overrideUid = null;
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const model = editor.model;
		const view = editor.editing.view;
		const locale = editor.locale;

		const modelSelection = model.document.selection;

		// Listen to keyboard events and handle the caret movement according to the 2-step caret logic.
		this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
			// This implementation works only for collapsed selection.
			if ( !modelSelection.isCollapsed ) {
				return;
			}

			// When user tries to expand the selection or jump over the whole word or to the beginning/end then
			// two-steps movement is not necessary.
			if ( data.shiftKey || data.altKey || data.ctrlKey ) {
				return;
			}

			const arrowRightPressed = data.keyCode == keyCodes.arrowright;
			const arrowLeftPressed = data.keyCode == keyCodes.arrowleft;

			// When neither left or right arrow has been pressed then do noting.
			if ( !arrowRightPressed && !arrowLeftPressed ) {
				return;
			}

			const contentDirection = locale.contentLanguageDirection;
			let isMovementHandled = false;

			if ( ( contentDirection === 'ltr' && arrowRightPressed ) || ( contentDirection === 'rtl' && arrowLeftPressed ) ) {
				isMovementHandled = this._handleForwardMovement( data );
			} else {
				isMovementHandled = this._handleBackwardMovement( data );
			}

			// Stop the keydown event if the two-step caret movement handled it. Avoid collisions
			// with other features which may also take over the caret movement (e.g. Widget).
			if ( isMovementHandled === true ) {
				evt.stop();
			}
		}, { context: '$text', priority: 'highest' } );

		/**
		 * A flag indicating that the automatic gravity restoration should not happen upon the next
		 * gravity restoration.
		 * {@link module:engine/model/selection~Selection#event:change:range} event.
		 *
		 * @private
		 * @member {String}
		 */
		this._isNextGravityRestorationSkipped = false;

		// The automatic gravity restoration logic.
		this.listenTo( modelSelection, 'change:range', ( evt, data ) => {
			// Skipping the automatic restoration is needed if the selection should change
			// but the gravity must remain overridden afterwards. See the #handleBackwardMovement
			// to learn more.
			if ( this._isNextGravityRestorationSkipped ) {
				this._isNextGravityRestorationSkipped = false;

				return;
			}

			// Skip automatic restore when the gravity is not overridden — simply, there's nothing to restore
			// at this moment.
			if ( !this._isGravityOverridden ) {
				return;
			}

			// Skip automatic restore when the change is indirect AND the selection is at the attribute boundary.
			// It means that e.g. if the change was external (collaboration) and the user had their
			// selection around the link, its gravity should remain intact in this change:range event.
			if ( !data.directChange && isBetweenDifferentAttributes( modelSelection.getFirstPosition(), this.attributes ) ) {
				return;
			}

			this._restoreGravity();
		} );
	}

	/**
	 * Registers a given attribute for the two-step caret movement.
	 *
	 * @param {String} attribute Name of the attribute to handle.
	 */
	registerAttribute( attribute ) {
		this.attributes.add( attribute );
	}

	/**
	 * Updates the document selection and the view according to the two–step caret movement state
	 * when moving **forwards**. Executed upon `keypress` in the {@link module:engine/view/view~View}.
	 *
	 * @private
	 * @param {module:engine/view/observer/domeventdata~DomEventData} data Data of the key press.
	 * @returns {Boolean} `true` when the handler prevented caret movement
	 */
	_handleForwardMovement( data ) {
		const attributes = this.attributes;
		const model = this.editor.model;
		const selection = model.document.selection;
		const position = selection.getFirstPosition();
		// DON'T ENGAGE 2-SCM if gravity is already overridden. It means that we just entered
		//
		// 		<paragraph>foo<$text attribute>{}bar</$text>baz</paragraph>
		//
		// or left the attribute
		//
		// 		<paragraph>foo<$text attribute>bar</$text>{}baz</paragraph>
		//
		// and the gravity will be restored automatically.
		if ( this._isGravityOverridden ) {
			return false;
		}

		// DON'T ENGAGE 2-SCM when the selection is at the beginning of the block AND already has the
		// attribute:
		// * when the selection was initially set there using the mouse,
		// * when the editor has just started
		//
		//		<paragraph><$text attribute>{}bar</$text>baz</paragraph>
		//
		if ( position.isAtStart && hasAnyAttribute$1( selection, attributes ) ) {
			return false;
		}

		// ENGAGE 2-SCM When at least one of the observed attributes changes its value (incl. starts, ends).
		//
		//		<paragraph>foo<$text attribute>bar{}</$text>baz</paragraph>
		//		<paragraph>foo<$text attribute>bar{}</$text><$text otherAttribute>baz</$text></paragraph>
		//		<paragraph>foo<$text attribute=1>bar{}</$text><$text attribute=2>baz</$text></paragraph>
		//		<paragraph>foo{}<$text attribute>bar</$text>baz</paragraph>
		//
		if ( isBetweenDifferentAttributes( position, attributes ) ) {
			preventCaretMovement( data );
			this._overrideGravity();
			return true;
		}
	}

	/**
	 * Updates the document selection and the view according to the two–step caret movement state
	 * when moving **backwards**. Executed upon `keypress` in the {@link module:engine/view/view~View}.
	 *
	 * @private
	 * @param {module:engine/view/observer/domeventdata~DomEventData} data Data of the key press.
	 * @returns {Boolean} `true` when the handler prevented caret movement
	 */
	_handleBackwardMovement( data ) {
		const attributes = this.attributes;
		const model = this.editor.model;
		const selection = model.document.selection;
		const position = selection.getFirstPosition();

		// When the gravity is already overridden (by this plugin), it means we are on the two-step position.
		// Prevent the movement, restore the gravity and update selection attributes.
		//
		//		<paragraph>foo<$text attribute=1>bar</$text><$text attribute=2>{}baz</$text></paragraph>
		//		<paragraph>foo<$text attribute>bar</$text><$text otherAttribute>{}baz</$text></paragraph>
		//		<paragraph>foo<$text attribute>{}bar</$text>baz</paragraph>
		//		<paragraph>foo<$text attribute>bar</$text>{}baz</paragraph>
		//
		if ( this._isGravityOverridden ) {
			preventCaretMovement( data );
			this._restoreGravity();
			setSelectionAttributesFromTheNodeBefore( model, attributes, position );

			return true;
		} else {
			// REMOVE SELECTION ATTRIBUTE when restoring gravity towards a non-existent content at the
			// beginning of the block.
			//
			// 		<paragraph>{}<$text attribute>bar</$text></paragraph>
			//
			if ( position.isAtStart ) {
				if ( hasAnyAttribute$1( selection, attributes ) ) {
					preventCaretMovement( data );
					setSelectionAttributesFromTheNodeBefore( model, attributes, position );

					return true;
				}

				return false;
			}

			// When we are moving from natural gravity, to the position of the 2SCM, we need to override the gravity,
			// and make sure it won't be restored. Unless it's at the end of the block and an observed attribute.
			// We need to check if the caret is a one position before the attribute boundary:
			//
			//		<paragraph>foo<$text attribute=1>bar</$text><$text attribute=2>b{}az</$text></paragraph>
			//		<paragraph>foo<$text attribute>bar</$text><$text otherAttribute>b{}az</$text></paragraph>
			//		<paragraph>foo<$text attribute>b{}ar</$text>baz</paragraph>
			//		<paragraph>foo<$text attribute>bar</$text>b{}az</paragraph>
			//
			if ( isStepAfterAnyAttributeBoundary( position, attributes ) ) {
				// ENGAGE 2-SCM if the selection has no attribute. This may happen when the user
				// left the attribute using a FORWARD 2-SCM.
				//
				// 		<paragraph><$text attribute>bar</$text>{}</paragraph>
				//
				if (
					position.isAtEnd &&
					!hasAnyAttribute$1( selection, attributes ) &&
					isBetweenDifferentAttributes( position, attributes )
				) {
					preventCaretMovement( data );
					setSelectionAttributesFromTheNodeBefore( model, attributes, position );

					return true;
				}
				// Skip the automatic gravity restore upon the next selection#change:range event.
				// If not skipped, it would automatically restore the gravity, which should remain
				// overridden.
				this._isNextGravityRestorationSkipped = true;
				this._overrideGravity();

				// Don't return "true" here because we didn't call _preventCaretMovement.
				// Returning here will destabilize the filler logic, which also listens to
				// keydown (and the event would be stopped).
				return false;
			}
		}
	}

	/**
	 * `true` when the gravity is overridden for the plugin.
	 *
	 * @readonly
	 * @private
	 * @type {Boolean}
	 */
	get _isGravityOverridden() {
		return !!this._overrideUid;
	}

	/**
	 * Overrides the gravity using the {@link module:engine/model/writer~Writer model writer}
	 * and stores the information about this fact in the {@link #_overrideUid}.
	 *
	 * A shorthand for {@link module:engine/model/writer~Writer#overrideSelectionGravity}.
	 *
	 * @private
	 */
	_overrideGravity() {
		this._overrideUid = this.editor.model.change( writer => {
			return writer.overrideSelectionGravity();
		} );
	}

	/**
	 * Restores the gravity using the {@link module:engine/model/writer~Writer model writer}.
	 *
	 * A shorthand for {@link module:engine/model/writer~Writer#restoreSelectionGravity}.
	 *
	 * @private
	 */
	_restoreGravity() {
		this.editor.model.change( writer => {
			writer.restoreSelectionGravity( this._overrideUid );
			this._overrideUid = null;
		} );
	}
}

// Checks whether the selection has any of given attributes.
//
// @param {module:engine/model/documentselection~DocumentSelection} selection
// @param {Iterable.<String>} attributes
function hasAnyAttribute$1( selection, attributes ) {
	for ( const observedAttribute of attributes ) {
		if ( selection.hasAttribute( observedAttribute ) ) {
			return true;
		}
	}

	return false;
}

// Applies the given attributes to the current selection using using the
// values from the node before the current position. Uses
// the {@link module:engine/model/writer~Writer model writer}.
//
// @param {module:engine/model/model~Model}
// @param {Iterable.<String>} attributess
// @param {module:engine/model/position~Position} position
function setSelectionAttributesFromTheNodeBefore( model, attributes, position ) {
	const nodeBefore = position.nodeBefore;
	model.change( writer => {
		if ( nodeBefore ) {
			writer.setSelectionAttribute( nodeBefore.getAttributes() );
		} else {
			writer.removeSelectionAttribute( attributes );
		}
	} );
}

// Prevents the caret movement in the view by calling `preventDefault` on the event data.
//
// @alias data.preventDefault
function preventCaretMovement( data ) {
	data.preventDefault();
}

// Checks whether the step before `isBetweenDifferentAttributes()`.
//
// @param {module:engine/model/position~Position} position
// @param {String} attribute
function isStepAfterAnyAttributeBoundary( position, attributes ) {
	const positionBefore = position.getShiftedBy( -1 );
	return isBetweenDifferentAttributes( positionBefore, attributes );
}

// Checks whether the given position is between different values of given attributes.
//
// @param {module:engine/model/position~Position} position
// @param {Iterable.<String>} attributes
function isBetweenDifferentAttributes( position, attributes ) {
	const { nodeBefore, nodeAfter } = position;
	for ( const observedAttribute of attributes ) {
		const attrBefore = nodeBefore ? nodeBefore.getAttribute( observedAttribute ) : undefined;
		const attrAfter = nodeAfter ? nodeAfter.getAttribute( observedAttribute ) : undefined;

		if ( attrAfter !== attrBefore ) {
			return true;
		}
	}
	return false;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

// All named transformations.
const TRANSFORMATIONS = {
	// Common symbols:
	copyright: { from: '(c)', to: '©' },
	registeredTrademark: { from: '(r)', to: '®' },
	trademark: { from: '(tm)', to: '™' },

	// Mathematical:
	oneHalf: { from: /(^|[^/a-z0-9])(1\/2)([^/a-z0-9])$/i, to: [ null, '½', null ] },
	oneThird: { from: /(^|[^/a-z0-9])(1\/3)([^/a-z0-9])$/i, to: [ null, '⅓', null ] },
	twoThirds: { from: /(^|[^/a-z0-9])(2\/3)([^/a-z0-9])$/i, to: [ null, '⅔', null ] },
	oneForth: { from: /(^|[^/a-z0-9])(1\/4)([^/a-z0-9])$/i, to: [ null, '¼', null ] },
	threeQuarters: { from: /(^|[^/a-z0-9])(3\/4)([^/a-z0-9])$/i, to: [ null, '¾', null ] },
	lessThanOrEqual: { from: '<=', to: '≤' },
	greaterThanOrEqual: { from: '>=', to: '≥' },
	notEqual: { from: '!=', to: '≠' },
	arrowLeft: { from: '<-', to: '←' },
	arrowRight: { from: '->', to: '→' },

	// Typography:
	horizontalEllipsis: { from: '...', to: '…' },
	enDash: { from: /(^| )(--)( )$/, to: [ null, '–', null ] },
	emDash: { from: /(^| )(---)( )$/, to: [ null, '—', null ] },
	// Quotations:
	// English, US
	quotesPrimary: { from: buildQuotesRegExp( '"' ), to: [ null, '“', null, '”' ] },
	quotesSecondary: { from: buildQuotesRegExp( '\'' ), to: [ null, '‘', null, '’' ] },

	// English, UK
	quotesPrimaryEnGb: { from: buildQuotesRegExp( '\'' ), to: [ null, '‘', null, '’' ] },
	quotesSecondaryEnGb: { from: buildQuotesRegExp( '"' ), to: [ null, '“', null, '”' ] },

	// Polish
	quotesPrimaryPl: { from: buildQuotesRegExp( '"' ), to: [ null, '„', null, '”' ] },
	quotesSecondaryPl: { from: buildQuotesRegExp( '\'' ), to: [ null, '‚', null, '’' ] }
};

// Transformation groups.
const TRANSFORMATION_GROUPS = {
	symbols: [ 'copyright', 'registeredTrademark', 'trademark' ],
	mathematical: [
		'oneHalf', 'oneThird', 'twoThirds', 'oneForth', 'threeQuarters',
		'lessThanOrEqual', 'greaterThanOrEqual', 'notEqual',
		'arrowLeft', 'arrowRight'
	],
	typography: [ 'horizontalEllipsis', 'enDash', 'emDash' ],
	quotes: [ 'quotesPrimary', 'quotesSecondary' ]
};

// A set of default transformations provided by the feature.
const DEFAULT_TRANSFORMATIONS = [
	'symbols',
	'mathematical',
	'typography',
	'quotes'
];

/**
 * The text transformation plugin.
 *
 * @extends module:core/plugin~Plugin
 */
class TextTransformation extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ 'Delete', 'Input' ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TextTransformation';
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		editor.config.define( 'typing', {
			transformations: {
				include: DEFAULT_TRANSFORMATIONS
			}
		} );
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const model = this.editor.model;
		const modelSelection = model.document.selection;

		modelSelection.on( 'change:range', () => {
			// Disable plugin when selection is inside a code block.
			this.isEnabled = !modelSelection.anchor.parent.is( 'element', 'codeBlock' );
		} );

		this._enableTransformationWatchers();
	}

	/**
	 * Create new TextWatcher listening to the editor for typing and selection events.
	 *
	 * @private
	 */
	_enableTransformationWatchers() {
		const editor = this.editor;
		const model = editor.model;
		const deletePlugin = editor.plugins.get( 'Delete' );
		const normalizedTransformations = normalizeTransformations( editor.config.get( 'typing.transformations' ) );

		const testCallback = text => {
			for ( const normalizedTransformation of normalizedTransformations ) {
				const from = normalizedTransformation.from;
				const match = from.test( text );

				if ( match ) {
					return { normalizedTransformation };
				}
			}
		};

		const watcherCallback = ( evt, data ) => {
			if ( !data.batch.isTyping ) {
				return;
			}

			const { from, to } = data.normalizedTransformation;

			const matches = from.exec( data.text );
			const replaces = to( matches.slice( 1 ) );

			const matchedRange = data.range;

			let changeIndex = matches.index;

			model.enqueueChange( writer => {
				for ( let i = 1; i < matches.length; i++ ) {
					const match = matches[ i ];
					const replaceWith = replaces[ i - 1 ];

					if ( replaceWith == null ) {
						changeIndex += match.length;

						continue;
					}

					const replacePosition = matchedRange.start.getShiftedBy( changeIndex );
					const replaceRange = model.createRange( replacePosition, replacePosition.getShiftedBy( match.length ) );
					const attributes = getTextAttributesAfterPosition( replacePosition );

					model.insertContent( writer.createText( replaceWith, attributes ), replaceRange );

					changeIndex += replaceWith.length;
				}

				model.enqueueChange( () => {
					deletePlugin.requestUndoOnBackspace();
				} );
			} );
		};

		const watcher = new TextWatcher( editor.model, testCallback );

		watcher.on( 'matched:data', watcherCallback );
		watcher.bind( 'isEnabled' ).to( this );
	}
}

// Normalizes the configuration `from` parameter value.
// The normalized value for the `from` parameter is a RegExp instance. If the passed `from` is already a RegExp instance,
// it is returned unchanged.
//
// @param {String|RegExp} from
// @returns {RegExp}
function normalizeFrom( from ) {
	if ( typeof from == 'string' ) {
		return new RegExp( `(${ escapeRegExp( from ) })$` );
	}

	// `from` is already a regular expression.
	return from;
}

// Normalizes the configuration `to` parameter value.
// The normalized value for the `to` parameter is a function that takes an array and returns an array. See more in the
// configuration description. If the passed `to` is already a function, it is returned unchanged.
//
// @param {String|Array.<null|String>|Function} to
// @returns {Function}
function normalizeTo( to ) {
	if ( typeof to == 'string' ) {
		return () => [ to ];
	} else if ( to instanceof Array ) {
		return () => to;
	}

	// `to` is already a function.
	return to;
}

// For given `position` returns attributes for the text that is after that position.
// The text can be in the same text node as the position (`foo[]bar`) or in the next text node (`foo[]<$text bold="true">bar</$text>`).
//
// @param {module:engine/model/position~Position} position
// @returns {Iterable.<*>}
function getTextAttributesAfterPosition( position ) {
	const textNode = position.textNode ? position.textNode : position.nodeAfter;

	return textNode.getAttributes();
}

// Returns a RegExp pattern string that detects a sentence inside a quote.
//
// @param {String} quoteCharacter The character to create a pattern for.
// @returns {String}
function buildQuotesRegExp( quoteCharacter ) {
	return new RegExp( `(^|\\s)(${ quoteCharacter })([^${ quoteCharacter }]*)(${ quoteCharacter })$` );
}

// Reads text transformation config and returns normalized array of transformations objects.
//
// @param {module:typing/texttransformation~TextTransformationDescription} config
// @returns {Array.<{from:String,to:Function}>}
function normalizeTransformations( config ) {
	const extra = config.extra || [];
	const remove = config.remove || [];
	const isNotRemoved = transformation => !remove.includes( transformation );

	const configured = config.include.concat( extra ).filter( isNotRemoved );

	return expandGroupsAndRemoveDuplicates( configured )
		.filter( isNotRemoved ) // Filter out 'remove' transformations as they might be set in group.
		.map( transformation => TRANSFORMATIONS[ transformation ] || transformation )
		.filter( transformation => typeof transformation === 'object' ) // Filter out transformations set as string that has not been found.
		.map( transformation => ( {
			from: normalizeFrom( transformation.from ),
			to: normalizeTo( transformation.to )
		} ) );
}

// Reads definitions and expands named groups if needed to transformation names.
// This method also removes duplicated named transformations if any.
//
// @param {Array.<String|Object>} definitions
// @returns {Array.<String|Object>}
function expandGroupsAndRemoveDuplicates( definitions ) {
	// Set is using to make sure that transformation names are not duplicated.
	const definedTransformations = new Set();

	for ( const transformationOrGroup of definitions ) {
		if ( TRANSFORMATION_GROUPS[ transformationOrGroup ] ) {
			for ( const transformation of TRANSFORMATION_GROUPS[ transformationOrGroup ] ) {
				definedTransformations.add( transformation );
			}
		} else {
			definedTransformations.add( transformationOrGroup );
		}
	}

	return Array.from( definedTransformations );
}

/**
 * The text transformation definition object. It describes what should be replaced with what.
 *
 * The input value (`from`) can be passed either as a string or as a regular expression.
 *
 * * If a string is passed, it will be simply checked if the end of the input matches it.
 * * If a regular expression is passed, its entire length must be covered with capturing groups (e.g. `/(foo)(bar)$/`).
 * Also, since it is compared against the end of the input, it has to end with  `$` to be correctly matched.
 * See examples below.
 *
 * The output value (`to`) can be passed as a string, as an array or as a function.
 *
 * * If a string is passed, it will be used as a replacement value as-is. Note that a string output value can be used only if
 * the input value is a string, too.
 * * If an array is passed, it has to have the same number of elements as there are capturing groups in the input value regular expression.
 * Each capture group will be replaced with a corresponding string from the passed array. If a given capturing group should not be replaced,
 * use `null` instead of passing a string.
 * * If a function is used, it should return an array as described above. The function is passed one parameter &mdash; an array with matches
 * by the regular expression. See the examples below.
 *
 * A simple string-to-string replacement:
 *
 *		{ from: '(c)', to: '©' }
 *
 * Change quote styles using a regular expression. Note how all the parts are in separate capturing groups and the space at the beginning
 * and the text inside quotes are not replaced (`null` passed as the first and the third value in the `to` parameter):
 *
 *		{
 *			from: /(^|\s)(")([^"]*)(")$/,
 *			to: [ null, '“', null, '”' ]
 *		}
 *
 * Automatic uppercase after a dot using a callback:
 *
 *		{
 *			from: /(\. )([a-z])$/,
 *			to: matches => [ null, matches[ 1 ].toUpperCase() ]
 *		}
 *
 * @typedef {Object} module:typing/texttransformation~TextTransformationDescription
 * @property {String|RegExp} from The string or regular expression to transform.
 * @property {String} to The text to transform compatible with `String.replace()`.
 */

/**
 * The configuration of the {@link module:typing/texttransformation~TextTransformation} feature.
 *
 * Read more in {@link module:typing/texttransformation~TextTransformationConfig}.
 *
 * @member {module:typing/texttransformation~TextTransformationConfig} module:typing/typing~TypingConfig#transformations
 */

/**
 * The configuration of the text transformation feature.
 *
 *		ClassicEditor
 *			.create( editorElement, {
 *				typing: {
 *					transformations: ... // Text transformation feature options.
 *				}
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * By default, the feature comes pre-configured
 * (via {@link module:typing/texttransformation~TextTransformationConfig#include `config.typing.transformations.include`}) with the
 * following groups of transformations:
 *
 * * Typography (group name: `typography`)
 *   - `ellipsis`: transforms `...` to `…`
 *   - `enDash`: transforms ` -- ` to ` – `
 *   - `emDash`: transforms ` --- ` to ` — `
 * * Quotations (group name: `quotes`)
 *   - `quotesPrimary`: transforms `"Foo bar"` to `“Foo bar”`
 *   - `quotesSecondary`: transforms `'Foo bar'` to `‘Foo bar’`
 * * Symbols (group name: `symbols`)
 *   - `trademark`: transforms `(tm)` to `™`
 *   - `registeredTrademark`: transforms `(r)` to `®`
 *   - `copyright`: transforms `(c)` to `©`
 * * Mathematical (group name: `mathematical`)
 *   - `oneHalf`: transforms `1/2` to: `½`
 *   - `oneThird`: transforms `1/3` to: `⅓`
 *   - `twoThirds`: transforms `2/3` to: `⅔`
 *   - `oneForth`: transforms `1/4` to: `¼`
 *   - `threeQuarters`: transforms `3/4` to: `¾`
 *   - `lessThanOrEqual`: transforms `<=` to: `≤`
 *   - `greaterThanOrEqual`: transforms `>=` to: `≥`
 *   - `notEqual`: transforms `!=` to: `≠`
 *   - `arrowLeft`: transforms `<-` to: `←`
 *   - `arrowRight`: transforms `->` to: `→`
 * * Misc:
 *   - `quotesPrimaryEnGb`: transforms `'Foo bar'` to `‘Foo bar’`
 *   - `quotesSecondaryEnGb`: transforms `"Foo bar"` to `“Foo bar”`
 *   - `quotesPrimaryPl`: transforms `"Foo bar"` to `„Foo bar”`
 *   - `quotesSecondaryPl`:  transforms `'Foo bar'` to `‚Foo bar’`
 *
 * In order to load additional transformations, use the
 * {@link module:typing/texttransformation~TextTransformationConfig#extra `transformations.extra` option}.
 *
 * In order to narrow down the list of transformations, use the
 * {@link module:typing/texttransformation~TextTransformationConfig#remove `transformations.remove` option}.
 *
 * In order to completely override the supported transformations, use the
 * {@link module:typing/texttransformation~TextTransformationConfig#include `transformations.include` option}.
 *
 * Examples:
 *
 *		const transformationsConfig = {
 *			include: [
 *				// Use only the 'quotes' and 'typography' groups.
 *				'quotes',
 *				'typography',
 *
 *				// Plus, some custom transformation.
 *				{ from: 'CKE', to: 'CKEditor' }
 *			]
 *		};
 *
 *		const transformationsConfig = {
 *			// Remove the 'ellipsis' transformation loaded by the 'typography' group.
 *			remove: [ 'ellipsis' ]
 *		}
 *
 * @interface TextTransformationConfig
 */

/* eslint-disable max-len */
/**
 * The standard list of text transformations supported by the editor. By default it comes pre-configured with a couple dozen of them
 * (see {@link module:typing/texttransformation~TextTransformationConfig} for the full list). You can override this list completely
 * by setting this option or use the other two options
 * ({@link module:typing/texttransformation~TextTransformationConfig#extra `transformations.extra`},
 * {@link module:typing/texttransformation~TextTransformationConfig#remove `transformations.remove`}) to fine-tune the default list.
 *
 * @member {Array.<module:typing/texttransformation~TextTransformationDescription>} module:typing/texttransformation~TextTransformationConfig#include
 */

/**
 * Additional text transformations that are added to the transformations defined in
 * {@link module:typing/texttransformation~TextTransformationConfig#include `transformations.include`}.
 *
 *		const transformationsConfig = {
 *			extra: [
 *				{ from: 'CKE', to: 'CKEditor' }
 *			]
 *		};
 *
 * @member {Array.<module:typing/texttransformation~TextTransformationDescription>} module:typing/texttransformation~TextTransformationConfig#extra
 */

/**
 * The text transformation names that are removed from transformations defined in
 * {@link module:typing/texttransformation~TextTransformationConfig#include `transformations.include`} or
 * {@link module:typing/texttransformation~TextTransformationConfig#extra `transformations.extra`}.
 *
 *		const transformationsConfig = {
 *			remove: [
 *				'ellipsis',    // Remove only 'ellipsis' from the 'typography' group.
 *				'mathematical' // Remove all transformations from the 'mathematical' group.
 *			]
 *		}
 *
 * @member {Array.<module:typing/texttransformation~TextTransformationDescription>} module:typing/texttransformation~TextTransformationConfig#remove
 */
/* eslint-enable max-len *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module typing/utils/findattributerange
 */

/**
 * Returns a model range that covers all consecutive nodes with the same `attributeName` and its `value`
 * that intersect the given `position`.
 *
 * It can be used e.g. to get the entire range on which the `linkHref` attribute needs to be changed when having a
 * selection inside a link.
 *
 * @param {module:engine/model/position~Position} position The start position.
 * @param {String} attributeName The attribute name.
 * @param {String} value The attribute value.
 * @param {module:engine/model/model~Model} model The model instance.
 * @returns {module:engine/model/range~Range} The link range.
 */
function findAttributeRange( position, attributeName, value, model ) {
	return model.createRange(
		_findBound( position, attributeName, value, true, model ),
		_findBound( position, attributeName, value, false, model )
	);
}

// Walks forward or backward (depends on the `lookBack` flag), node by node, as long as they have the same attribute value
// and returns a position just before or after (depends on the `lookBack` flag) the last matched node.
//
// @param {module:engine/model/position~Position} position The start position.
// @param {String} attributeName The attribute name.
// @param {String} value The attribute value.
// @param {Boolean} lookBack Whether the walk direction is forward (`false`) or backward (`true`).
// @returns {module:engine/model/position~Position} The position just before the last matched node.
function _findBound( position, attributeName, value, lookBack, model ) {
	// Get node before or after position (depends on `lookBack` flag).
	// When position is inside text node then start searching from text node.
	let node = position.textNode || ( lookBack ? position.nodeBefore : position.nodeAfter );

	let lastNode = null;

	while ( node && node.getAttribute( attributeName ) == value ) {
		lastNode = node;
		node = lookBack ? node.previousSibling : node.nextSibling;
	}

	return lastNode ? model.createPositionAt( lastNode, lookBack ? 'before' : 'after' ) : position;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module typing/utils/inlinehighlight
 */

/**
 * Adds a visual highlight style to an attribute element in which the selection is anchored.
 * Together with two-step caret movement, they indicate that the user is typing inside the element.
 *
 * Highlight is turned on by adding the given class to the attribute element in the view:
 *
 * * The class is removed before the conversion has started, as callbacks added with the `'highest'` priority
 * to {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} events.
 * * The class is added in the view post fixer, after other changes in the model tree were converted to the view.
 *
 * This way, adding and removing the highlight does not interfere with conversion.
 *
 * Usage:
 *
 *		import inlineHighlight from '@ckeditor/ckeditor5-typing/src/utils/inlinehighlight';
 *
 *		// Make `ck-link_selected` class be applied on an `a` element
 *		// whenever the corresponding `linkHref` attribute element is selected.
 *		inlineHighlight( editor, 'linkHref', 'a', 'ck-link_selected' );
 *
 * @param {module:core/editor/editor~Editor} editor The editor instance.
 * @param {String} attributeName The attribute name to check.
 * @param {String} tagName The tagName of a view item.
 * @param {String} className The class name to apply in the view.
 */
function inlineHighlight( editor, attributeName, tagName, className ) {
	const view = editor.editing.view;
	const highlightedElements = new Set();

	// Adding the class.
	view.document.registerPostFixer( writer => {
		const selection = editor.model.document.selection;
		let changed = false;

		if ( selection.hasAttribute( attributeName ) ) {
			const modelRange = findAttributeRange(
				selection.getFirstPosition(),
				attributeName,
				selection.getAttribute( attributeName ),
				editor.model
			);
			const viewRange = editor.editing.mapper.toViewRange( modelRange );

			// There might be multiple view elements in the `viewRange`, for example, when the `a` element is
			// broken by a UIElement.
			for ( const item of viewRange.getItems() ) {
				if ( item.is( 'element', tagName ) && !item.hasClass( className ) ) {
					writer.addClass( className, item );
					highlightedElements.add( item );
					changed = true;
				}
			}
		}

		return changed;
	} );

	// Removing the class.
	editor.conversion.for( 'editingDowncast' ).add( dispatcher => {
		// Make sure the highlight is removed on every possible event, before conversion is started.
		dispatcher.on( 'insert', removeHighlight, { priority: 'highest' } );
		dispatcher.on( 'remove', removeHighlight, { priority: 'highest' } );
		dispatcher.on( 'attribute', removeHighlight, { priority: 'highest' } );
		dispatcher.on( 'selection', removeHighlight, { priority: 'highest' } );

		function removeHighlight() {
			view.change( writer => {
				for ( const item of highlightedElements.values() ) {
					writer.removeClass( className, item );
					highlightedElements.delete( item );
				}
			} );
		}
	} );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */var index$4=/*#__PURE__*/Object.freeze({__proto__:null,Typing:Typing,Input:Input,Delete:Delete,TextWatcher:TextWatcher,TwoStepCaretMovement:TwoStepCaretMovement,TextTransformation:TextTransformation,inlineHighlight:inlineHighlight,findAttributeRange:findAttributeRange,getLastTextLine:getLastTextLine,isNonTypingKeystroke:isNonTypingKeystroke});/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The block quote command plugin.
 *
 * @extends module:core/command~Command
 */
class BlockQuoteCommand extends Command {
	/**
	 * Whether the selection starts in a block quote.
	 *
	 * @observable
	 * @readonly
	 * @member {Boolean} #value
	 */

	/**
	 * @inheritDoc
	 */
	refresh() {
		this.value = this._getValue();
		this.isEnabled = this._checkEnabled();
	}

	/**
	 * Executes the command. When the command {@link #value is on}, all top-most block quotes within
	 * the selection will be removed. If it is off, all selected blocks will be wrapped with
	 * a block quote.
	 *
	 * @fires execute
	 * @param {Object} [options] Command options.
	 * @param {Boolean} [options.forceValue] If set, it will force the command behavior. If `true`, the command will apply a block quote,
	 * otherwise the command will remove the block quote. If not set, the command will act basing on its current value.
	 */
	execute( options = {} ) {
		const model = this.editor.model;
		const schema = model.schema;
		const selection = model.document.selection;

		const blocks = Array.from( selection.getSelectedBlocks() );

		const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue;

		model.change( writer => {
			if ( !value ) {
				this._removeQuote( writer, blocks.filter( findQuote ) );
			} else {
				const blocksToQuote = blocks.filter( block => {
					// Already quoted blocks needs to be considered while quoting too
					// in order to reuse their <bQ> elements.
					return findQuote( block ) || checkCanBeQuoted( schema, block );
				} );

				this._applyQuote( writer, blocksToQuote );
			}
		} );
	}

	/**
	 * Checks the command's {@link #value}.
	 *
	 * @private
	 * @returns {Boolean} The current value.
	 */
	_getValue() {
		const selection = this.editor.model.document.selection;

		const firstBlock = first( selection.getSelectedBlocks() );

		// In the current implementation, the block quote must be an immediate parent of a block element.
		return !!( firstBlock && findQuote( firstBlock ) );
	}

	/**
	 * Checks whether the command can be enabled in the current context.
	 *
	 * @private
	 * @returns {Boolean} Whether the command should be enabled.
	 */
	_checkEnabled() {
		if ( this.value ) {
			return true;
		}

		const selection = this.editor.model.document.selection;
		const schema = this.editor.model.schema;

		const firstBlock = first( selection.getSelectedBlocks() );

		if ( !firstBlock ) {
			return false;
		}

		return checkCanBeQuoted( schema, firstBlock );
	}

	/**
	 * Removes the quote from given blocks.
	 *
	 * If blocks which are supposed to be "unquoted" are in the middle of a quote,
	 * start it or end it, then the quote will be split (if needed) and the blocks
	 * will be moved out of it, so other quoted blocks remained quoted.
	 *
	 * @private
	 * @param {module:engine/model/writer~Writer} writer
	 * @param {Array.<module:engine/model/element~Element>} blocks
	 */
	_removeQuote( writer, blocks ) {
		// Unquote all groups of block. Iterate in the reverse order to not break following ranges.
		getRangesOfBlockGroups( writer, blocks ).reverse().forEach( groupRange => {
			if ( groupRange.start.isAtStart && groupRange.end.isAtEnd ) {
				writer.unwrap( groupRange.start.parent );

				return;
			}

			// The group of blocks are at the beginning of an <bQ> so let's move them left (out of the <bQ>).
			if ( groupRange.start.isAtStart ) {
				const positionBefore = writer.createPositionBefore( groupRange.start.parent );

				writer.move( groupRange, positionBefore );

				return;
			}

			// The blocks are in the middle of an <bQ> so we need to split the <bQ> after the last block
			// so we move the items there.
			if ( !groupRange.end.isAtEnd ) {
				writer.split( groupRange.end );
			}

			// Now we are sure that groupRange.end.isAtEnd is true, so let's move the blocks right.

			const positionAfter = writer.createPositionAfter( groupRange.end.parent );

			writer.move( groupRange, positionAfter );
		} );
	}

	/**
	 * Applies the quote to given blocks.
	 *
	 * @private
	 * @param {module:engine/model/writer~Writer} writer
	 * @param {Array.<module:engine/model/element~Element>} blocks
	 */
	_applyQuote( writer, blocks ) {
		const quotesToMerge = [];

		// Quote all groups of block. Iterate in the reverse order to not break following ranges.
		getRangesOfBlockGroups( writer, blocks ).reverse().forEach( groupRange => {
			let quote = findQuote( groupRange.start );

			if ( !quote ) {
				quote = writer.createElement( 'blockQuote' );

				writer.wrap( groupRange, quote );
			}

			quotesToMerge.push( quote );
		} );

		// Merge subsequent <bQ> elements. Reverse the order again because this time we want to go through
		// the <bQ> elements in the source order (due to how merge works – it moves the right element's content
		// to the first element and removes the right one. Since we may need to merge a couple of subsequent `<bQ>` elements
		// we want to keep the reference to the first (furthest left) one.
		quotesToMerge.reverse().reduce( ( currentQuote, nextQuote ) => {
			if ( currentQuote.nextSibling == nextQuote ) {
				writer.merge( writer.createPositionAfter( currentQuote ) );

				return currentQuote;
			}

			return nextQuote;
		} );
	}
}

function findQuote( elementOrPosition ) {
	return elementOrPosition.parent.name == 'blockQuote' ? elementOrPosition.parent : null;
}

// Returns a minimal array of ranges containing groups of subsequent blocks.
//
// content:         abcdefgh
// blocks:          [ a, b, d, f, g, h ]
// output ranges:   [ab]c[d]e[fgh]
//
// @param {Array.<module:engine/model/element~Element>} blocks
// @returns {Array.<module:engine/model/range~Range>}
function getRangesOfBlockGroups( writer, blocks ) {
	let startPosition;
	let i = 0;
	const ranges = [];

	while ( i < blocks.length ) {
		const block = blocks[ i ];
		const nextBlock = blocks[ i + 1 ];

		if ( !startPosition ) {
			startPosition = writer.createPositionBefore( block );
		}

		if ( !nextBlock || block.nextSibling != nextBlock ) {
			ranges.push( writer.createRange( startPosition, writer.createPositionAfter( block ) ) );
			startPosition = null;
		}

		i++;
	}

	return ranges;
}

// Checks whether <bQ> can wrap the block.
function checkCanBeQuoted( schema, block ) {
	// TMP will be replaced with schema.checkWrap().
	const isBQAllowed = schema.checkChild( block.parent, 'blockQuote' );
	const isBlockAllowedInBQ = schema.checkChild( [ '$root', 'blockQuote' ], block );

	return isBQAllowed && isBlockAllowedInBQ;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The block quote editing.
 *
 * Introduces the `'blockQuote'` command and the `'blockQuote'` model element.
 *
 * @extends module:core/plugin~Plugin
 */
class BlockQuoteEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'BlockQuoteEditing';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ Enter, Delete ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const schema = editor.model.schema;

		editor.commands.add( 'blockQuote', new BlockQuoteCommand( editor ) );

		schema.register( 'blockQuote', {
			inheritAllFrom: '$container'
		} );

		editor.conversion.elementToElement( { model: 'blockQuote', view: 'blockquote' } );

		// Postfixer which cleans incorrect model states connected with block quotes.
		editor.model.document.registerPostFixer( writer => {
			const changes = editor.model.document.differ.getChanges();

			for ( const entry of changes ) {
				if ( entry.type == 'insert' ) {
					const element = entry.position.nodeAfter;

					if ( !element ) {
						// We are inside a text node.
						continue;
					}

					if ( element.is( 'element', 'blockQuote' ) && element.isEmpty ) {
						// Added an empty blockQuote - remove it.
						writer.remove( element );

						return true;
					} else if ( element.is( 'element', 'blockQuote' ) && !schema.checkChild( entry.position, element ) ) {
						// Added a blockQuote in incorrect place. Unwrap it so the content inside is not lost.
						writer.unwrap( element );

						return true;
					} else if ( element.is( 'element' ) ) {
						// Just added an element. Check that all children meet the scheme rules.
						const range = writer.createRangeIn( element );

						for ( const child of range.getItems() ) {
							if (
								child.is( 'element', 'blockQuote' ) &&
								!schema.checkChild( writer.createPositionBefore( child ), child )
							) {
								writer.unwrap( child );

								return true;
							}
						}
					}
				} else if ( entry.type == 'remove' ) {
					const parent = entry.position.parent;

					if ( parent.is( 'element', 'blockQuote' ) && parent.isEmpty ) {
						// Something got removed and now blockQuote is empty. Remove the blockQuote as well.
						writer.remove( parent );

						return true;
					}
				}
			}

			return false;
		} );

		const viewDocument = this.editor.editing.view.document;
		const selection = editor.model.document.selection;
		const blockQuoteCommand = editor.commands.get( 'blockQuote' );

		// Overwrite default Enter key behavior.
		// If Enter key is pressed with selection collapsed in empty block inside a quote, break the quote.
		this.listenTo( viewDocument, 'enter', ( evt, data ) => {
			if ( !selection.isCollapsed || !blockQuoteCommand.value ) {
				return;
			}

			const positionParent = selection.getLastPosition().parent;

			if ( positionParent.isEmpty ) {
				editor.execute( 'blockQuote' );
				editor.editing.view.scrollToTheSelection();

				data.preventDefault();
				evt.stop();
			}
		}, { context: 'blockquote' } );

		// Overwrite default Backspace key behavior.
		// If Backspace key is pressed with selection collapsed in first empty block inside a quote, break the quote.
		this.listenTo( viewDocument, 'delete', ( evt, data ) => {
			if ( data.direction != 'backward' || !selection.isCollapsed || !blockQuoteCommand.value ) {
				return;
			}

			const positionParent = selection.getLastPosition().parent;

			if ( positionParent.isEmpty && !positionParent.previousSibling ) {
				editor.execute( 'blockQuote' );
				editor.editing.view.scrollToTheSelection();

				data.preventDefault();
				evt.stop();
			}
		}, { context: 'blockquote' } );
	}
}var css_248z$x = ".ck-content blockquote{border-left:5px solid #ccc;font-style:italic;margin-left:0;margin-right:0;overflow:hidden;padding-left:1.5em;padding-right:1.5em}.ck-content[dir=rtl] blockquote{border-left:0;border-right:5px solid #ccc}";
styleInject(css_248z$x);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The block quote UI plugin.
 *
 * It introduces the `'blockQuote'` button.
 *
 * @extends module:core/plugin~Plugin
 */
class BlockQuoteUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'BlockQuoteUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		editor.ui.componentFactory.add( 'blockQuote', locale => {
			const command = editor.commands.get( 'blockQuote' );
			const buttonView = new ButtonView( locale );

			buttonView.set( {
				label: t( 'Block quote' ),
				icon: icons.quote,
				tooltip: true,
				isToggleable: true
			} );

			// Bind button model to command.
			buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

			// Execute command.
			this.listenTo( buttonView, 'execute', () => {
				editor.execute( 'blockQuote' );
				editor.editing.view.focus();
			} );

			return buttonView;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The block quote plugin.
 *
 * For more information about this feature check the {@glink api/block-quote package page}.
 *
 * This is a "glue" plugin which loads the {@link module:block-quote/blockquoteediting~BlockQuoteEditing block quote editing feature}
 * and {@link module:block-quote/blockquoteui~BlockQuoteUI block quote UI feature}.
 *
 * @extends module:core/plugin~Plugin
 */
class BlockQuote extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ BlockQuoteEditing, BlockQuoteUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'BlockQuote';
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module clipboard/datatransfer
 */

/**
 * A facade over the native [`DataTransfer`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer) object.
 */
class DataTransfer {
	constructor( nativeDataTransfer ) {
		/**
		 * The array of files created from the native `DataTransfer#files` or `DataTransfer#items`.
		 *
		 * @readonly
		 * @member {Array.<File>} #files
		 */
		this.files = getFiles( nativeDataTransfer );

		/**
		 * The native DataTransfer object.
		 *
		 * @private
		 * @member {DataTransfer} #_native
		 */
		this._native = nativeDataTransfer;
	}

	/**
	 * Returns an array of available native content types.
	 *
	 * @returns {Array.<String>}
	 */
	get types() {
		return this._native.types;
	}

	/**
	 * Gets the data from the data transfer by its MIME type.
	 *
	 *		dataTransfer.getData( 'text/plain' );
	 *
	 * @param {String} type The MIME type. E.g. `text/html` or `text/plain`.
	 * @returns {String}
	 */
	getData( type ) {
		return this._native.getData( type );
	}

	/**
	 * Sets the data in the data transfer.
	 *
	 * @param {String} type The MIME type. E.g. `text/html` or `text/plain`.
	 * @param {String} data
	 */
	setData( type, data ) {
		this._native.setData( type, data );
	}

	/**
	 * The effect that is allowed for a drag operation.
	 *
	 * @param {String} value
	 */
	set effectAllowed( value ) {
		this._native.effectAllowed = value;
	}

	get effectAllowed() {
		return this._native.effectAllowed;
	}

	/**
	 * The actual drop effect.
	 *
	 * @param {String} value
	 */
	set dropEffect( value ) {
		this._native.dropEffect = value;
	}

	get dropEffect() {
		return this._native.dropEffect;
	}

	/**
	 * Whether the dragging operation was canceled.
	 *
	 * @returns {Boolean}
	 */
	get isCanceled() {
		return this._native.dropEffect == 'none' || !!this._native.mozUserCancelled;
	}
}

function getFiles( nativeDataTransfer ) {
	// DataTransfer.files and items are array-like and might not have an iterable interface.
	const files = Array.from( nativeDataTransfer.files || [] );
	const items = Array.from( nativeDataTransfer.items || [] );

	if ( files.length ) {
		return files;
	}

	// Chrome has empty DataTransfer.files, but allows getting files through the items interface.
	return items
		.filter( item => item.kind === 'file' )
		.map( item => item.getAsFile() );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Clipboard events observer.
 *
 * Fires the following events:
 *
 * * {@link module:engine/view/document~Document#event:clipboardInput},
 * * {@link module:engine/view/document~Document#event:paste},
 * * {@link module:engine/view/document~Document#event:copy},
 * * {@link module:engine/view/document~Document#event:cut},
 * * {@link module:engine/view/document~Document#event:drop},
 * * {@link module:engine/view/document~Document#event:dragover},
 * * {@link module:engine/view/document~Document#event:dragging},
 * * {@link module:engine/view/document~Document#event:dragstart},
 * * {@link module:engine/view/document~Document#event:dragend},
 * * {@link module:engine/view/document~Document#event:dragenter},
 * * {@link module:engine/view/document~Document#event:dragleave}.
 *
 * **Note**: This observer is not available by default (ckeditor5-engine does not add it on its own).
 * To make it available, it needs to be added to {@link module:engine/view/document~Document} by using
 * the {@link module:engine/view/view~View#addObserver `View#addObserver()`} method. Alternatively, you can load the
 * {@link module:clipboard/clipboard~Clipboard} plugin which adds this observer automatically (because it uses it).
 *
 * @extends module:engine/view/observer/domeventobserver~DomEventObserver
 */
class ClipboardObserver extends DomEventObserver {
	constructor( view ) {
		super( view );

		const viewDocument = this.document;

		this.domEventType = [ 'paste', 'copy', 'cut', 'drop', 'dragover', 'dragstart', 'dragend', 'dragenter', 'dragleave' ];

		this.listenTo( viewDocument, 'paste', handleInput( 'clipboardInput' ), { priority: 'low' } );
		this.listenTo( viewDocument, 'drop', handleInput( 'clipboardInput' ), { priority: 'low' } );
		this.listenTo( viewDocument, 'dragover', handleInput( 'dragging' ), { priority: 'low' } );

		function handleInput( type ) {
			return ( evt, data ) => {
				data.preventDefault();

				const targetRanges = data.dropRange ? [ data.dropRange ] : null;
				const eventInfo = new EventInfo( viewDocument, type );

				viewDocument.fire( eventInfo, {
					dataTransfer: data.dataTransfer,
					method: evt.name,
					targetRanges,
					target: data.target
				} );

				// If CKEditor handled the input, do not bubble the original event any further.
				// This helps external integrations recognize that fact and act accordingly.
				// https://github.com/ckeditor/ckeditor5-upload/issues/92
				if ( eventInfo.stop.called ) {
					data.stopPropagation();
				}
			};
		}
	}

	onDomEvent( domEvent ) {
		const evtData = {
			dataTransfer: new DataTransfer( domEvent.clipboardData ? domEvent.clipboardData : domEvent.dataTransfer )
		};

		if ( domEvent.type == 'drop' || domEvent.type == 'dragover' ) {
			evtData.dropRange = getDropViewRange( this.view, domEvent );
		}

		this.fire( domEvent.type, domEvent, evtData );
	}
}

function getDropViewRange( view, domEvent ) {
	const domDoc = domEvent.target.ownerDocument;
	const x = domEvent.clientX;
	const y = domEvent.clientY;
	let domRange;

	// Webkit & Blink.
	if ( domDoc.caretRangeFromPoint && domDoc.caretRangeFromPoint( x, y ) ) {
		domRange = domDoc.caretRangeFromPoint( x, y );
	}
	// FF.
	else if ( domEvent.rangeParent ) {
		domRange = domDoc.createRange();
		domRange.setStart( domEvent.rangeParent, domEvent.rangeOffset );
		domRange.collapse( true );
	}

	if ( domRange ) {
		return view.domConverter.domRangeToView( domRange );
	}

	return null;
}

/**
 * Fired as a continuation of the {@link #event:paste} and {@link #event:drop} events.
 *
 * It is a part of the {@glink framework/guides/deep-dive/clipboard#input-pipeline clipboard input pipeline}.
 *
 * This event carries a `dataTransfer` object which comes from the clipboard and whose content should be processed
 * and inserted into the editor.
 *
 * **Note**: This event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
 * needs to be added to the {@link module:engine/view/document~Document} by using the {@link module:engine/view/view~View#addObserver}
 * method. This is usually done by the {@link module:clipboard/clipboard~Clipboard} plugin, but if for some reason it is not loaded,
 * the observer must be added manually.
 *
 * @see module:clipboard/clipboardobserver~ClipboardObserver
 * @see module:clipboard/clipboard~Clipboard
 * @event module:engine/view/document~Document#event:clipboardInput
 * @param {Object} data The event data.
 * @param {module:clipboard/datatransfer~DataTransfer} data.dataTransfer The data transfer instance.
 * @param {'paste'|'drop'} method Whether the event was triggered by a paste or drop operation.
 * @param {module:engine/view/element~Element} target The tree view element representing the target.
 * @param {Array.<module:engine/view/range~Range>} data.targetRanges Ranges which are the target of the operation
 * (usually – into which the content should be inserted).
 * If the clipboard input was triggered by a paste operation, this property is not set. If by a drop operation,
 * then it is the drop position (which can be different than the selection at the moment of drop).
 */

/**
 * Fired when the user drags the content over one of the editing roots of the editor.
 *
 * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
 *
 * **Note**: This event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
 * needs to be added to the {@link module:engine/view/document~Document} by using the {@link module:engine/view/view~View#addObserver}
 * method. This is usually done by the {@link module:clipboard/clipboard~Clipboard} plugin, but if for some reason it is not loaded,
 * the observer must be added manually.
 *
 * @see module:engine/view/document~Document#event:clipboardInput
 * @event module:engine/view/document~Document#event:dragover
 * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
 */

/**
 * Fired when the user dropped the content into one of the editing roots of the editor.
 *
 * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
 *
 * **Note**: This event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
 * needs to be added to the {@link module:engine/view/document~Document} by using the {@link module:engine/view/view~View#addObserver}
 * method. This is usually done by the {@link module:clipboard/clipboard~Clipboard} plugin, but if for some reason it is not loaded,
 * the observer must be added manually.
 *
 * @see module:engine/view/document~Document#event:clipboardInput
 * @event module:engine/view/document~Document#event:drop
 * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
 * @param {module:engine/view/range~Range} dropRange The position into which the content is dropped.
 */

/**
 * Fired when the user pasted the content into one of the editing roots of the editor.
 *
 * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
 *
 * **Note**: This event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
 * needs to be added to the {@link module:engine/view/document~Document} by using the {@link module:engine/view/view~View#addObserver}
 * method. This is usually done by the {@link module:clipboard/clipboard~Clipboard} plugin, but if for some reason it is not loaded,
 * the observer must be added manually.
 *
 * @see module:engine/view/document~Document#event:clipboardInput
 * @event module:engine/view/document~Document#event:paste
 * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
 */

/**
 * Fired when the user copied the content from one of the editing roots of the editor.
 *
 * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
 *
 * **Note**: This event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
 * needs to be added to the {@link module:engine/view/document~Document} by using the {@link module:engine/view/view~View#addObserver}
 * method. This is usually done by the {@link module:clipboard/clipboard~Clipboard} plugin, but if for some reason it is not loaded,
 * the observer must be added manually.
 *
 * @see module:clipboard/clipboardobserver~ClipboardObserver
 * @event module:engine/view/document~Document#event:copy
 * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
 */

/**
 * Fired when the user cut the content from one of the editing roots of the editor.
 *
 * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
 *
 * **Note**: This event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
 * needs to be added to the {@link module:engine/view/document~Document} by using the {@link module:engine/view/view~View#addObserver}
 * method. This is usually done by the {@link module:clipboard/clipboard~Clipboard} plugin, but if for some reason it is not loaded,
 * the observer must be added manually.
 *
 * @see module:clipboard/clipboardobserver~ClipboardObserver
 * @event module:engine/view/document~Document#event:cut
 * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
 */

/**
 * The value of the {@link module:engine/view/document~Document#event:paste},
 * {@link module:engine/view/document~Document#event:copy} and {@link module:engine/view/document~Document#event:cut} events.
 *
 * In order to access the clipboard data, use the `dataTransfer` property.
 *
 * @class module:clipboard/clipboardobserver~ClipboardEventData
 * @extends module:engine/view/observer/domeventdata~DomEventData
 */

/**
 * The data transfer instance.
 *
 * @readonly
 * @member {module:clipboard/datatransfer~DataTransfer} module:clipboard/clipboardobserver~ClipboardEventData#dataTransfer
 */

/**
 * Fired as a continuation of the {@link #event:dragover} event.
 *
 * It is a part of the {@glink framework/guides/deep-dive/clipboard#input-pipeline clipboard input pipeline}.
 *
 * This event carries a `dataTransfer` object which comes from the clipboard and whose content should be processed
 * and inserted into the editor.
 *
 * **Note**: This event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
 * needs to be added to the {@link module:engine/view/document~Document} by using the {@link module:engine/view/view~View#addObserver}
 * method. This is usually done by the {@link module:clipboard/clipboard~Clipboard} plugin, but if for some reason it is not loaded,
 * the observer must be added manually.
 *
 * @see module:clipboard/clipboardobserver~ClipboardObserver
 * @see module:clipboard/clipboard~Clipboard
 * @event module:engine/view/document~Document#event:dragging
 * @param {Object} data The event data.
 * @param {module:clipboard/datatransfer~DataTransfer} data.dataTransfer The data transfer instance.
 * @param {module:engine/view/element~Element} target The tree view element representing the target.
 * @param {Array.<module:engine/view/range~Range>} data.targetRanges Ranges which are the target of the operation
 * (usually – into which the content should be inserted).
 * It is the drop position (which can be different than the selection at the moment of drop).
 */

/**
 * Fired when the user starts dragging the content in one of the editing roots of the editor.
 *
 * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
 *
 * **Note**: This event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
 * needs to be added to the {@link module:engine/view/document~Document} by using the {@link module:engine/view/view~View#addObserver}
 * method. This is usually done by the {@link module:clipboard/clipboard~Clipboard} plugin, but if for some reason it is not loaded,
 * the observer must be added manually.
 *
 * @see module:engine/view/document~Document#event:clipboardInput
 * @event module:engine/view/document~Document#event:dragstart
 * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
 */

/**
 * Fired when the user ended dragging the content.
 *
 * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
 *
 * **Note**: This event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
 * needs to be added to the {@link module:engine/view/document~Document} by using the {@link module:engine/view/view~View#addObserver}
 * method. This is usually done by the {@link module:clipboard/clipboard~Clipboard} plugin, but if for some reason it is not loaded,
 * the observer must be added manually.
 *
 * @see module:engine/view/document~Document#event:clipboardInput
 * @event module:engine/view/document~Document#event:dragend
 * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
 */

/**
 * Fired when the user drags the content into one of the editing roots of the editor.
 *
 * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
 *
 * **Note**: This event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
 * needs to be added to the {@link module:engine/view/document~Document} by using the {@link module:engine/view/view~View#addObserver}
 * method. This is usually done by the {@link module:clipboard/clipboard~Clipboard} plugin, but if for some reason it is not loaded,
 * the observer must be added manually.
 *
 * @see module:engine/view/document~Document#event:clipboardInput
 * @event module:engine/view/document~Document#event:dragenter
 * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
 */

/**
 * Fired when the user drags the content out of one of the editing roots of the editor.
 *
 * Introduced by {@link module:clipboard/clipboardobserver~ClipboardObserver}.
 *
 * **Note**: This event is not available by default. To make it available, {@link module:clipboard/clipboardobserver~ClipboardObserver}
 * needs to be added to the {@link module:engine/view/document~Document} by using the {@link module:engine/view/view~View#addObserver}
 * method. This is usually done by the {@link module:clipboard/clipboard~Clipboard} plugin, but if for some reason it is not loaded,
 * the observer must be added manually.
 *
 * @see module:engine/view/document~Document#event:clipboardInput
 * @event module:engine/view/document~Document#event:dragleave
 * @param {module:clipboard/clipboardobserver~ClipboardEventData} data The event data.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module clipboard/utils/plaintexttohtml
 */

/**
 * Converts plain text to its HTML-ized version.
 *
 * @param {String} text The plain text to convert.
 * @returns {String} HTML generated from the plain text.
 */
function plainTextToHtml( text ) {
	text = text
		// Encode <>.
		.replace( /</g, '&lt;' )
		.replace( />/g, '&gt;' )
		// Creates a paragraph for each double line break.
		.replace( /\r?\n\r?\n/g, '</p><p>' )
		// Creates a line break for each single line break.
		.replace( /\r?\n/g, '<br>' )
		// Preserve trailing spaces (only the first and last one – the rest is handled below).
		.replace( /^\s/, '&nbsp;' )
		.replace( /\s$/, '&nbsp;' )
		// Preserve other subsequent spaces now.
		.replace( /\s\s/g, ' &nbsp;' );

	if ( text.includes( '</p><p>' ) || text.includes( '<br>' ) ) {
		// If we created paragraphs above, add the trailing ones.
		text = `<p>${ text }</p>`;
	}

	// TODO:
	// * What about '\nfoo' vs ' foo'?

	return text;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module clipboard/utils/normalizeclipboarddata
 */

/**
 * Removes some popular browser quirks out of the clipboard data (HTML).
 * Removes all HTML comments. These are considered an internal thing and it makes little sense if they leak into the editor data.
 *
 * @param {String} data The HTML data to normalize.
 * @returns {String} Normalized HTML.
 */
function normalizeClipboardData( data ) {
	return data
		.replace( /<span(?: class="Apple-converted-space"|)>(\s+)<\/span>/g, ( fullMatch, spaces ) => {
			// Handle the most popular and problematic case when even a single space becomes an nbsp;.
			// Decode those to normal spaces. Read more in https://github.com/ckeditor/ckeditor5-clipboard/issues/2.
			if ( spaces.length == 1 ) {
				return ' ';
			}

			return spaces;
		} )
		// Remove all HTML comments.
		.replace( /<!--[\s\S]*?-->/g, '' );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module clipboard/utils/viewtoplaintext
 */

// Elements which should not have empty-line padding.
// Most `view.ContainerElement` want to be separate by new-line, but some are creating one structure
// together (like `<li>`) so it is better to separate them by only one "\n".
const smallPaddingElements = [ 'figcaption', 'li' ];

/**
 * Converts {@link module:engine/view/item~Item view item} and all of its children to plain text.
 *
 * @param {module:engine/view/item~Item} viewItem View item to convert.
 * @returns {String} Plain text representation of `viewItem`.
 */
function viewToPlainText( viewItem ) {
	let text = '';

	if ( viewItem.is( '$text' ) || viewItem.is( '$textProxy' ) ) {
		// If item is `Text` or `TextProxy` simple take its text data.
		text = viewItem.data;
	} else if ( viewItem.is( 'element', 'img' ) && viewItem.hasAttribute( 'alt' ) ) {
		// Special case for images - use alt attribute if it is provided.
		text = viewItem.getAttribute( 'alt' );
	} else if ( viewItem.is( 'element', 'br' ) ) {
		// A soft break should be converted into a single line break (#8045).
		text = '\n';
	} else {
		// Other elements are document fragments, attribute elements or container elements.
		// They don't have their own text value, so convert their children.
		let prev = null;

		for ( const child of viewItem.getChildren() ) {
			const childText = viewToPlainText( child );

			// Separate container element children with one or more new-line characters.
			if ( prev && ( prev.is( 'containerElement' ) || child.is( 'containerElement' ) ) ) {
				if ( smallPaddingElements.includes( prev.name ) || smallPaddingElements.includes( child.name ) ) {
					text += '\n';
				} else {
					text += '\n\n';
				}
			}

			text += childText;
			prev = child;
		}
	}

	return text;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

// Input pipeline events overview:
//
//              ┌──────────────────────┐          ┌──────────────────────┐
//              │     view.Document    │          │     view.Document    │
//              │         paste        │          │         drop         │
//              └───────────┬──────────┘          └───────────┬──────────┘
//                          │                                 │
//                          └────────────────┌────────────────┘
//                                           │
//                                 ┌─────────V────────┐
//                                 │   view.Document  │   Retrieves text/html or text/plain from data.dataTransfer
//                                 │  clipboardInput  │   and processes it to view.DocumentFragment.
//                                 └─────────┬────────┘
//                                           │
//                               ┌───────────V───────────┐
//                               │   ClipboardPipeline   │   Converts view.DocumentFragment to model.DocumentFragment.
//                               │  inputTransformation  │
//                               └───────────┬───────────┘
//                                           │
//                                ┌──────────V──────────┐
//                                │  ClipboardPipeline  │   Calls model.insertContent().
//                                │   contentInsertion  │
//                                └─────────────────────┘
//
//
// Output pipeline events overview:
//
//              ┌──────────────────────┐          ┌──────────────────────┐
//              │     view.Document    │          │     view.Document    │   Retrieves the selected model.DocumentFragment
//              │         copy         │          │          cut         │   and converts it to view.DocumentFragment.
//              └───────────┬──────────┘          └───────────┬──────────┘
//                          │                                 │
//                          └────────────────┌────────────────┘
//                                           │
//                                 ┌─────────V────────┐
//                                 │   view.Document  │   Processes view.DocumentFragment to text/html and text/plain
//                                 │  clipboardOutput │   and stores the results in data.dataTransfer.
//                                 └──────────────────┘
//

/**
 * The clipboard pipeline feature. It is responsible for intercepting the `paste` and `drop` events and
 * passing the pasted content through a series of events in order to insert it into the editor's content.
 * It also handles the `cut` and `copy` events to fill the native clipboard with the serialized editor's data.
 *
 * # Input pipeline
 *
 * The behavior of the default handlers (all at a `low` priority):
 *
 * ## Event: `paste` or `drop`
 *
 * 1. Translates the event data.
 * 2. Fires the {@link module:engine/view/document~Document#event:clipboardInput `view.Document#clipboardInput`} event.
 *
 * ## Event: `view.Document#clipboardInput`
 *
 * 1. If the `data.content` event field is already set (by some listener on a higher priority), it takes this content and fires the event
 *    from the last point.
 * 2. Otherwise, it retrieves `text/html` or `text/plain` from `data.dataTransfer`.
 * 3. Normalizes the raw data by applying simple filters on string data.
 * 4. Processes the raw data to {@link module:engine/view/documentfragment~DocumentFragment `view.DocumentFragment`} with the
 *    {@link module:engine/controller/datacontroller~DataController#htmlProcessor `DataController#htmlProcessor`}.
 * 5. Fires the {@link module:clipboard/clipboardpipeline~ClipboardPipeline#event:inputTransformation
 *   `ClipboardPipeline#inputTransformation`} event with the view document fragment in the `data.content` event field.
 *
 * ## Event: `ClipboardPipeline#inputTransformation`
 *
 * 1. Converts {@link module:engine/view/documentfragment~DocumentFragment `view.DocumentFragment`} from the `data.content` field to
 *    {@link module:engine/model/documentfragment~DocumentFragment `model.DocumentFragment`}.
 * 2. Fires the {@link module:clipboard/clipboardpipeline~ClipboardPipeline#event:contentInsertion `ClipboardPipeline#contentInsertion`}
 *    event with the model document fragment in the `data.content` event field.
 *    **Note**: The `ClipboardPipeline#contentInsertion` event is fired within a model change block to allow other handlers
 *    to run in the same block without post-fixers called in between (i.e., the selection post-fixer).
 *
 * ## Event: `ClipboardPipeline#contentInsertion`
 *
 * 1. Calls {@link module:engine/model/model~Model#insertContent `model.insertContent()`} to insert `data.content`
 *    at the current selection position.
 *
 * # Output pipeline
 *
 * The behavior of the default handlers (all at a `low` priority):
 *
 * ## Event: `copy`, `cut` or `dragstart`
 *
 * 1. Retrieves the selected {@link module:engine/model/documentfragment~DocumentFragment `model.DocumentFragment`} by calling
 *    {@link module:engine/model/model~Model#getSelectedContent `model#getSelectedContent()`}.
 * 2. Converts the model document fragment to {@link module:engine/view/documentfragment~DocumentFragment `view.DocumentFragment`}.
 * 3. Fires the {@link module:engine/view/document~Document#event:clipboardOutput `view.Document#clipboardOutput`} event
 *    with the view document fragment in the `data.content` event field.
 *
 * ## Event: `view.Document#clipboardOutput`
 *
 * 1. Processes `data.content` to HTML and plain text with the
 *    {@link module:engine/controller/datacontroller~DataController#htmlProcessor `DataController#htmlProcessor`}.
 * 2. Updates the `data.dataTransfer` data for `text/html` and `text/plain` with the processed data.
 * 3. For the `cut` method, calls {@link module:engine/model/model~Model#deleteContent `model.deleteContent()`}
 *    on the current selection.
 *
 * Read more about the clipboard integration in the {@glink framework/guides/deep-dive/clipboard clipboard deep dive guide}.
 *
 * @extends module:core/plugin~Plugin
 */
class ClipboardPipeline extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'ClipboardPipeline';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const view = editor.editing.view;

		view.addObserver( ClipboardObserver );

		this._setupPasteDrop();
		this._setupCopyCut();
	}

	/**
	 * The clipboard paste pipeline.
	 *
	 * @private
	 */
	_setupPasteDrop() {
		const editor = this.editor;
		const model = editor.model;
		const view = editor.editing.view;
		const viewDocument = view.document;

		// Pasting and dropping is disabled when editor is in the read-only mode.
		// See: https://github.com/ckeditor/ckeditor5-clipboard/issues/26.
		this.listenTo( viewDocument, 'clipboardInput', evt => {
			if ( editor.isReadOnly ) {
				evt.stop();
			}
		}, { priority: 'highest' } );

		this.listenTo( viewDocument, 'clipboardInput', ( evt, data ) => {
			const dataTransfer = data.dataTransfer;
			let content = data.content || '';

			// Some feature could already inject content in the higher priority event handler (i.e., codeBlock).
			if ( !content ) {
				if ( dataTransfer.getData( 'text/html' ) ) {
					content = normalizeClipboardData( dataTransfer.getData( 'text/html' ) );
				} else if ( dataTransfer.getData( 'text/plain' ) ) {
					content = plainTextToHtml( dataTransfer.getData( 'text/plain' ) );
				}

				content = this.editor.data.htmlProcessor.toView( content );
			}

			const eventInfo = new EventInfo( this, 'inputTransformation' );

			this.fire( eventInfo, {
				content,
				dataTransfer,
				targetRanges: data.targetRanges,
				method: data.method
			} );

			// If CKEditor handled the input, do not bubble the original event any further.
			// This helps external integrations recognize this fact and act accordingly.
			// https://github.com/ckeditor/ckeditor5-upload/issues/92
			if ( eventInfo.stop.called ) {
				evt.stop();
			}

			view.scrollToTheSelection();
		}, { priority: 'low' } );

		this.listenTo( this, 'inputTransformation', ( evt, data ) => {
			if ( data.content.isEmpty ) {
				return;
			}

			const dataController = this.editor.data;

			// Convert the pasted content into a model document fragment.
			// The conversion is contextual, but in this case an "all allowed" context is needed
			// and for that we use the $clipboardHolder item.
			const modelFragment = dataController.toModel( data.content, '$clipboardHolder' );

			if ( modelFragment.childCount == 0 ) {
				return;
			}

			evt.stop();

			// Fire content insertion event in a single change block to allow other handlers to run in the same block
			// without post-fixers called in between (i.e., the selection post-fixer).
			model.change( () => {
				this.fire( 'contentInsertion', {
					content: modelFragment,
					method: data.method,
					dataTransfer: data.dataTransfer,
					targetRanges: data.targetRanges
				} );
			} );
		}, { priority: 'low' } );

		this.listenTo( this, 'contentInsertion', ( evt, data ) => {
			data.resultRange = model.insertContent( data.content );
		}, { priority: 'low' } );
	}

	/**
	 * The clipboard copy/cut pipeline.
	 *
	 * @private
	 */
	_setupCopyCut() {
		const editor = this.editor;
		const modelDocument = editor.model.document;
		const view = editor.editing.view;
		const viewDocument = view.document;

		function onCopyCut( evt, data ) {
			const dataTransfer = data.dataTransfer;

			data.preventDefault();

			const content = editor.data.toView( editor.model.getSelectedContent( modelDocument.selection ) );

			viewDocument.fire( 'clipboardOutput', { dataTransfer, content, method: evt.name } );
		}

		this.listenTo( viewDocument, 'copy', onCopyCut, { priority: 'low' } );
		this.listenTo( viewDocument, 'cut', ( evt, data ) => {
			// Cutting is disabled when editor is in the read-only mode.
			// See: https://github.com/ckeditor/ckeditor5-clipboard/issues/26.
			if ( editor.isReadOnly ) {
				data.preventDefault();
			} else {
				onCopyCut( evt, data );
			}
		}, { priority: 'low' } );

		this.listenTo( viewDocument, 'clipboardOutput', ( evt, data ) => {
			if ( !data.content.isEmpty ) {
				data.dataTransfer.setData( 'text/html', this.editor.data.htmlProcessor.toData( data.content ) );
				data.dataTransfer.setData( 'text/plain', viewToPlainText( data.content ) );
			}

			if ( data.method == 'cut' ) {
				editor.model.deleteContent( modelDocument.selection );
			}
		}, { priority: 'low' } );
	}
}

/**
 * Fired with the `content`, `dataTransfer`, `method`, and `targetRanges` properties:
 *
 * * The `content` which comes from the clipboard (it was pasted or dropped) should be processed in order to be inserted into the editor.
 * * The `dataTransfer` object is available in case the transformation functions need access to the raw clipboard data.
 * * The `method` indicates the original DOM event (for example `'drop'` or `'paste'`).
 * * The `targetRanges` property is an array of view ranges (it is available only for `'drop'`).
 *
 * It is a part of the {@glink framework/guides/deep-dive/clipboard#input-pipeline clipboard input pipeline}.
 *
 * **Note**: You should not stop this event if you want to change the input data. You should modify the `content` property instead.
 *
 * @see module:clipboard/clipboardobserver~ClipboardObserver
 * @see module:clipboard/clipboardpipeline~ClipboardPipeline
 * @event module:clipboard/clipboardpipeline~ClipboardPipeline#event:inputTransformation
 * @param {Object} data The event data.
 * @param {module:engine/view/documentfragment~DocumentFragment} data.content The event data. The content to be inserted into the editor.
 * It can be modified by event listeners. Read more about the clipboard pipelines in
 * the {@glink framework/guides/deep-dive/clipboard clipboard deep dive guide}.
 * @param {module:clipboard/datatransfer~DataTransfer} data.dataTransfer The data transfer instance.
 * @param {'paste'|'drop'} data.method Whether the event was triggered by a paste or drop operation.
 * @param {Array.<module:engine/view/range~Range>} data.targetRanges The target drop ranges.
 */

/**
 * Fired with the `content`, `dataTransfer`, `method`, and `targetRanges` properties:
 *
 * * The `content` which comes from the clipboard (was pasted or dropped) should be processed in order to be inserted into the editor.
 * * The `dataTransfer` object is available in case the transformation functions need access to the raw clipboard data.
 * * The `method` indicates the original DOM event (for example `'drop'` or `'paste'`).
 * * The `targetRanges` property is an array of view ranges (it is available only for `'drop'`).
 *
 * Event handlers can modify the content according to the final insertion position.
 *
 * It is a part of the {@glink framework/guides/deep-dive/clipboard#input-pipeline clipboard input pipeline}.
 *
 * **Note**: You should not stop this event if you want to change the input data. You should modify the `content` property instead.
 *
 * @see module:clipboard/clipboardobserver~ClipboardObserver
 * @see module:clipboard/clipboardpipeline~ClipboardPipeline
 * @see module:clipboard/clipboardpipeline~ClipboardPipeline#event:inputTransformation
 * @event module:clipboard/clipboardpipeline~ClipboardPipeline#event:contentInsertion
 * @param {Object} data The event data.
 * @param {module:engine/model/documentfragment~DocumentFragment} data.content The event data. The content to be inserted into the editor.
 * Read more about the clipboard pipelines in the {@glink framework/guides/deep-dive/clipboard clipboard deep dive guide}.
 * @param {module:clipboard/datatransfer~DataTransfer} data.dataTransfer The data transfer instance.
 * @param {'paste'|'drop'} data.method Whether the event was triggered by a paste or drop operation.
 * @param {Array.<module:engine/view/range~Range>} data.targetRanges The target drop ranges.
 * @param {module:engine/model/range~Range} data.resultRange The result of the `model.insertContent()` call
 *  (inserted by the event handler at a low priority).
 */

/**
 * Fired on {@link module:engine/view/document~Document#event:copy} and {@link module:engine/view/document~Document#event:cut}
 * with a copy of the selected content. The content can be processed before it ends up in the clipboard.
 *
 * It is a part of the {@glink framework/guides/deep-dive/clipboard#output-pipeline clipboard output pipeline}.
 *
 * @see module:clipboard/clipboardobserver~ClipboardObserver
 * @see module:clipboard/clipboardpipeline~ClipboardPipeline
 * @event module:engine/view/document~Document#event:clipboardOutput
 * @param {module:clipboard/clipboardpipeline~ClipboardOutputEventData} data The event data.
 */

/**
 * The value of the {@link module:engine/view/document~Document#event:clipboardOutput} event.
 *
 * @class module:clipboard/clipboardpipeline~ClipboardOutputEventData
 */

/**
 * The data transfer instance.
 *
 * @readonly
 * @member {module:clipboard/datatransfer~DataTransfer} module:clipboard/clipboardpipeline~ClipboardOutputEventData#dataTransfer
 */

/**
 * Content to be put into the clipboard. It can be modified by the event listeners.
 * Read more about the clipboard pipelines in the {@glink framework/guides/deep-dive/clipboard clipboard deep dive guide}.
 *
 * @member {module:engine/view/documentfragment~DocumentFragment} module:clipboard/clipboardpipeline~ClipboardOutputEventData#content
 */

/**
 * Whether the event was triggered by a copy or cut operation.
 *
 * @member {'copy'|'cut'} module:clipboard/clipboardpipeline~ClipboardOutputEventData#method
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Class used to handle correct order of highlights on elements.
 *
 * When different highlights are applied to same element correct order should be preserved:
 *
 * * highlight with highest priority should be applied,
 * * if two highlights have same priority - sort by CSS class provided in
 * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor}.
 *
 * This way, highlight will be applied with the same rules it is applied on texts.
 */
class HighlightStack {
	/**
	 * Creates class instance.
	 */
	constructor() {
		this._stack = [];
	}

	/**
	 * Adds highlight descriptor to the stack.
	 *
	 * @fires change:top
	 * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor
	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer
	 */
	add( descriptor, writer ) {
		const stack = this._stack;

		// Save top descriptor and insert new one. If top is changed - fire event.
		const oldTop = stack[ 0 ];
		this._insertDescriptor( descriptor );
		const newTop = stack[ 0 ];

		// When new object is at the top and stores different information.
		if ( oldTop !== newTop && !compareDescriptors( oldTop, newTop ) ) {
			this.fire( 'change:top', {
				oldDescriptor: oldTop,
				newDescriptor: newTop,
				writer
			} );
		}
	}

	/**
	 * Removes highlight descriptor from the stack.
	 *
	 * @fires change:top
	 * @param {String} id Id of the descriptor to remove.
	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer
	 */
	remove( id, writer ) {
		const stack = this._stack;

		const oldTop = stack[ 0 ];
		this._removeDescriptor( id );
		const newTop = stack[ 0 ];

		// When new object is at the top and stores different information.
		if ( oldTop !== newTop && !compareDescriptors( oldTop, newTop ) ) {
			this.fire( 'change:top', {
				oldDescriptor: oldTop,
				newDescriptor: newTop,
				writer
			} );
		}
	}

	/**
	 * Inserts given descriptor in correct place in the stack. It also takes care about updating information when
	 * descriptor with same id is already present.
	 *
	 * @private
	 * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor
	 */
	_insertDescriptor( descriptor ) {
		const stack = this._stack;
		const index = stack.findIndex( item => item.id === descriptor.id );

		// Inserting exact same descriptor - do nothing.
		if ( compareDescriptors( descriptor, stack[ index ] ) ) {
			return;
		}

		// If descriptor with same id but with different information is on the stack - remove it.
		if ( index > -1 ) {
			stack.splice( index, 1 );
		}

		// Find correct place to insert descriptor in the stack.
		// It have different information (for example priority) so it must be re-inserted in correct place.
		let i = 0;

		while ( stack[ i ] && shouldABeBeforeB( stack[ i ], descriptor ) ) {
			i++;
		}

		stack.splice( i, 0, descriptor );
	}

	/**
	 * Removes descriptor with given id from the stack.
	 *
	 * @private
	 * @param {String} id Descriptor's id.
	 */
	_removeDescriptor( id ) {
		const stack = this._stack;
		const index = stack.findIndex( item => item.id === id );

		// If descriptor with same id is on the list - remove it.
		if ( index > -1 ) {
			stack.splice( index, 1 );
		}
	}
}

mix( HighlightStack, EmitterMixin );

// Compares two descriptors by checking their priority and class list.
//
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} a
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} b
// @returns {Boolean} Returns true if both descriptors are defined and have same priority and classes.
function compareDescriptors( a, b ) {
	return a && b && a.priority == b.priority && classesToString( a.classes ) == classesToString( b.classes );
}

// Checks whenever first descriptor should be placed in the stack before second one.
//
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} a
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} b
// @returns {Boolean}
function shouldABeBeforeB( 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 classes to compare.
	return classesToString( a.classes ) > classesToString( b.classes );
}

// Converts CSS classes passed with {@link module:engine/conversion/downcasthelpers~HighlightDescriptor} to
// sorted string.
//
// @param {String|Array<String>} descriptor
// @returns {String}
function classesToString( classes ) {
	return Array.isArray( classes ) ? classes.sort().join( ',' ) : classes;
}

/**
 * Fired when top element on {@link module:widget/highlightstack~HighlightStack} has been changed
 *
 * @event change:top
 * @param {Object} data Additional information about the change.
 * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} [data.newDescriptor] New highlight
 * descriptor. It will be `undefined` when last descriptor is removed from the stack.
 * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} [data.oldDescriptor] Old highlight
 * descriptor. It will be `undefined` when first descriptor is added to the stack.
 * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that can be used to modify element.
 */var dragHandleIcon = "<svg viewBox=\"0 0 16 16\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M4 0v1H1v3H0V.5A.5.5 0 0 1 .5 0H4zm8 0h3.5a.5.5 0 0 1 .5.5V4h-1V1h-3V0zM4 16H.5a.5.5 0 0 1-.5-.5V12h1v3h3v1zm8 0v-1h3v-3h1v3.5a.5.5 0 0 1-.5.5H12z\"/><path fill-opacity=\".256\" d=\"M1 1h14v14H1z\"/><g class=\"ck-icon__selected-indicator\"><path d=\"M7 0h2v1H7V0zM0 7h1v2H0V7zm15 0h1v2h-1V7zm-8 8h2v1H7v-1z\"/><path fill-opacity=\".254\" d=\"M1 1h14v14H1z\"/></g></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * CSS class added to each widget element.
 *
 * @const {String}
 */
const WIDGET_CLASS_NAME = 'ck-widget';

/**
 * CSS class added to currently selected widget element.
 *
 * @const {String}
 */
const WIDGET_SELECTED_CLASS_NAME = 'ck-widget_selected';

/**
 * Returns `true` if given {@link module:engine/view/node~Node} is an {@link module:engine/view/element~Element} and a widget.
 *
 * @param {module:engine/view/node~Node} node
 * @returns {Boolean}
 */
function isWidget( node ) {
	if ( !node.is( 'element' ) ) {
		return false;
	}

	return !!node.getCustomProperty( 'widget' );
}

/**
 * Converts the given {@link module:engine/view/element~Element} to a widget in the following way:
 *
 * * sets the `contenteditable` attribute to `"false"`,
 * * adds the `ck-widget` CSS class,
 * * adds a custom {@link module:engine/view/element~Element#getFillerOffset `getFillerOffset()`} method returning `null`,
 * * adds a custom property allowing to recognize widget elements by using {@link ~isWidget `isWidget()`},
 * * implements the {@link ~setHighlightHandling view highlight on widgets}.
 *
 * This function needs to be used in conjunction with
 * {@link module:engine/conversion/downcasthelpers~DowncastHelpers downcast conversion helpers}
 * like {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`}.
 * Moreover, typically you will want to use `toWidget()` only for `editingDowncast`, while keeping the `dataDowncast` clean.
 *
 * For example, in order to convert a `<widget>` model element to `<div class="widget">` in the view, you can define
 * such converters:
 *
 *		editor.conversion.for( 'editingDowncast' )
 *			.elementToElement( {
 *				model: 'widget',
 *				view: ( modelItem, { writer } ) => {
 *					const div = writer.createContainerElement( 'div', { class: 'widget' } );
 *
 *					return toWidget( div, writer, { label: 'some widget' } );
 *				}
 *			} );
 *
 *		editor.conversion.for( 'dataDowncast' )
 *			.elementToElement( {
 *				model: 'widget',
 *				view: ( modelItem, { writer } ) => {
 *					return writer.createContainerElement( 'div', { class: 'widget' } );
 *				}
 *			} );
 *
 * See the full source code of the widget (with a nested editable) schema definition and converters in
 * [this sample](https://github.com/ckeditor/ckeditor5-widget/blob/master/tests/manual/widget-with-nestededitable.js).
 *
 * @param {module:engine/view/element~Element} element
 * @param {module:engine/view/downcastwriter~DowncastWriter} writer
 * @param {Object} [options={}]
 * @param {String|Function} [options.label] Element's label provided to the {@link ~setLabel} function. It can be passed as
 * a plain string or a function returning a string. It represents the widget for assistive technologies (like screen readers).
 * @param {Boolean} [options.hasSelectionHandle=false] If `true`, the widget will have a selection handle added.
 * @returns {module:engine/view/element~Element} Returns the same element.
 */
function toWidget( element, writer, options = {} ) {
	if ( !element.is( 'containerElement' ) ) {
		/**
		 * The element passed to `toWidget()` must be a {@link module:engine/view/containerelement~ContainerElement}
		 * instance.
		 *
		 * @error widget-to-widget-wrong-element-type
		 * @param {String} element The view element passed to `toWidget()`.
		 */
		throw new CKEditorError(
			'widget-to-widget-wrong-element-type',
			null,
			{ element }
		);
	}

	writer.setAttribute( 'contenteditable', 'false', element );

	writer.addClass( WIDGET_CLASS_NAME, element );
	writer.setCustomProperty( 'widget', true, element );
	element.getFillerOffset = getFillerOffset;

	if ( options.label ) {
		setLabel( element, options.label, writer );
	}

	if ( options.hasSelectionHandle ) {
		addSelectionHandle( element, writer );
	}

	setHighlightHandling( element, writer );

	return element;
}

// Default handler for adding a highlight on a widget.
// It adds CSS class and attributes basing on the given highlight descriptor.
//
// @param {module:engine/view/element~Element} element
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor
// @param {module:engine/view/downcastwriter~DowncastWriter} writer
function addHighlight( element, descriptor, writer ) {
	if ( descriptor.classes ) {
		writer.addClass( toArray( descriptor.classes ), element );
	}

	if ( descriptor.attributes ) {
		for ( const key in descriptor.attributes ) {
			writer.setAttribute( key, descriptor.attributes[ key ], element );
		}
	}
}

// Default handler for removing a highlight from a widget.
// It removes CSS class and attributes basing on the given highlight descriptor.
//
// @param {module:engine/view/element~Element} element
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor
// @param {module:engine/view/downcastwriter~DowncastWriter} writer
function removeHighlight( element, descriptor, writer ) {
	if ( descriptor.classes ) {
		writer.removeClass( toArray( descriptor.classes ), element );
	}

	if ( descriptor.attributes ) {
		for ( const key in descriptor.attributes ) {
			writer.removeAttribute( key, element );
		}
	}
}

/**
 * Sets highlight handling methods. Uses {@link module:widget/highlightstack~HighlightStack} to
 * properly determine which highlight descriptor should be used at given time.
 *
 * @param {module:engine/view/element~Element} element
 * @param {module:engine/view/downcastwriter~DowncastWriter} writer
 * @param {Function} [add]
 * @param {Function} [remove]
 */
function setHighlightHandling( element, writer, add = addHighlight, remove = removeHighlight ) {
	const stack = new HighlightStack();

	stack.on( 'change:top', ( evt, data ) => {
		if ( data.oldDescriptor ) {
			remove( element, data.oldDescriptor, data.writer );
		}

		if ( data.newDescriptor ) {
			add( element, data.newDescriptor, data.writer );
		}
	} );

	writer.setCustomProperty( 'addHighlight', ( element, descriptor, writer ) => stack.add( descriptor, writer ), element );
	writer.setCustomProperty( 'removeHighlight', ( element, id, writer ) => stack.remove( id, writer ), element );
}

/**
 * Sets label for given element.
 * It can be passed as a plain string or a function returning a string. Function will be called each time label is retrieved by
 * {@link ~getLabel `getLabel()`}.
 *
 * @param {module:engine/view/element~Element} element
 * @param {String|Function} labelOrCreator
 * @param {module:engine/view/downcastwriter~DowncastWriter} writer
 */
function setLabel( element, labelOrCreator, writer ) {
	writer.setCustomProperty( 'widgetLabel', labelOrCreator, element );
}

/**
 * Returns the label of the provided element.
 *
 * @param {module:engine/view/element~Element} element
 * @returns {String}
 */
function getLabel( element ) {
	const labelCreator = element.getCustomProperty( 'widgetLabel' );

	if ( !labelCreator ) {
		return '';
	}

	return typeof labelCreator == 'function' ? labelCreator() : labelCreator;
}

/**
 * Adds functionality to the provided {@link module:engine/view/editableelement~EditableElement} to act as a widget's editable:
 *
 * * sets the `contenteditable` attribute to `true` when {@link module:engine/view/editableelement~EditableElement#isReadOnly} is `false`,
 * otherwise sets it to `false`,
 * * adds the `ck-editor__editable` and `ck-editor__nested-editable` CSS classes,
 * * adds the `ck-editor__nested-editable_focused` CSS class when the editable is focused and removes it when it is blurred.
 * * implements the {@link ~setHighlightHandling view highlight on widget's editable}.
 *
 * Similarly to {@link ~toWidget `toWidget()`} this function should be used in `editingDowncast` only and it is usually
 * used together with {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`}.
 *
 * For example, in order to convert a `<nested>` model element to `<div class="nested">` in the view, you can define
 * such converters:
 *
 *		editor.conversion.for( 'editingDowncast' )
 *			.elementToElement( {
 *				model: 'nested',
 *				view: ( modelItem, { writer } ) => {
 *					const div = writer.createEditableElement( 'div', { class: 'nested' } );
 *
 *					return toWidgetEditable( nested, writer, { label: 'label for editable' } );
 *				}
 *			} );
 *
 *		editor.conversion.for( 'dataDowncast' )
 *			.elementToElement( {
 *				model: 'nested',
 *				view: ( modelItem, { writer } ) => {
 *					return writer.createContainerElement( 'div', { class: 'nested' } );
 *				}
 *			} );
 *
 * See the full source code of the widget (with nested editable) schema definition and converters in
 * [this sample](https://github.com/ckeditor/ckeditor5-widget/blob/master/tests/manual/widget-with-nestededitable.js).
 *
 * @param {module:engine/view/editableelement~EditableElement} editable
 * @param {module:engine/view/downcastwriter~DowncastWriter} writer
 * @param {Object} [options] Additional options.
 * @param {String} [options.label] Editable's label used by assistive technologies (e.g. screen readers).
 * @returns {module:engine/view/editableelement~EditableElement} Returns the same element that was provided in the `editable` parameter
 */
function toWidgetEditable( editable, writer, options = {} ) {
	writer.addClass( [ 'ck-editor__editable', 'ck-editor__nested-editable' ], editable );

	writer.setAttribute( 'role', 'textbox', editable );

	if ( options.label ) {
		writer.setAttribute( 'aria-label', options.label, editable );
	}

	// Set initial contenteditable value.
	writer.setAttribute( 'contenteditable', editable.isReadOnly ? 'false' : 'true', editable );

	// Bind the contenteditable property to element#isReadOnly.
	editable.on( 'change:isReadOnly', ( evt, property, is ) => {
		writer.setAttribute( 'contenteditable', is ? 'false' : 'true', editable );
	} );

	editable.on( 'change:isFocused', ( evt, property, is ) => {
		if ( is ) {
			writer.addClass( 'ck-editor__nested-editable_focused', editable );
		} else {
			writer.removeClass( 'ck-editor__nested-editable_focused', editable );
		}
	} );

	setHighlightHandling( editable, writer );

	return editable;
}

/**
 * Returns a model range which is optimal (in terms of UX) for inserting a widget block.
 *
 * For instance, if a selection is in the middle of a paragraph, the collapsed range before this paragraph
 * will be returned so that it is not split. If the selection is at the end of a paragraph,
 * the collapsed range after this paragraph will be returned.
 *
 * Note: If the selection is placed in an empty block, the range in that block will be returned. If that range
 * is then passed to {@link module:engine/model/model~Model#insertContent}, the block will be fully replaced
 * by the inserted widget block.
 *
 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
 * The selection based on which the insertion position should be calculated.
 * @param {module:engine/model/model~Model} model Model instance.
 * @returns {module:engine/model/range~Range} The optimal range.
 */
function findOptimalInsertionRange( selection, model ) {
	const selectedElement = selection.getSelectedElement();

	if ( selectedElement ) {
		const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition( selection );

		// If the WidgetTypeAround "fake caret" is displayed, use its position for the insertion
		// to provide the most predictable UX (https://github.com/ckeditor/ckeditor5/issues/7438).
		if ( typeAroundFakeCaretPosition ) {
			return model.createRange( model.createPositionAt( selectedElement, typeAroundFakeCaretPosition ) );
		}
	}

	return findOptimalInsertionRange$1( selection, model );
}

/**
 * A util to be used in order to map view positions to correct model positions when implementing a widget
 * which renders non-empty view element for an empty model element.
 *
 * For example:
 *
 *		// Model:
 *		<placeholder type="name"></placeholder>
 *
 *		// View:
 *		<span class="placeholder">name</span>
 *
 * In such case, view positions inside `<span>` cannot be correct mapped to the model (because the model element is empty).
 * To handle mapping positions inside `<span class="placeholder">` to the model use this util as follows:
 *
 *		editor.editing.mapper.on(
 *			'viewToModelPosition',
 *			viewToModelPositionOutsideModelElement( model, viewElement => viewElement.hasClass( 'placeholder' ) )
 *		);
 *
 * The callback will try to map the view offset of selection to an expected model position.
 *
 * 1. When the position is at the end (or in the middle) of the inline widget:
 *
 *		// View:
 *		<p>foo <span class="placeholder">name|</span> bar</p>
 *
 *		// Model:
 *		<paragraph>foo <placeholder type="name"></placeholder>| bar</paragraph>
 *
 * 2. When the position is at the beginning of the inline widget:
 *
 *		// View:
 *		<p>foo <span class="placeholder">|name</span> bar</p>
 *
 *		// Model:
 *		<paragraph>foo |<placeholder type="name"></placeholder> bar</paragraph>
 *
 * @param {module:engine/model/model~Model} model Model instance on which the callback operates.
 * @param {Function} viewElementMatcher Function that is passed a view element and should return `true` if the custom mapping
 * should be applied to the given view element.
 * @return {Function}
 */
function viewToModelPositionOutsideModelElement( model, viewElementMatcher ) {
	return ( evt, data ) => {
		const { mapper, viewPosition } = data;

		const viewParent = mapper.findMappedViewAncestor( viewPosition );

		if ( !viewElementMatcher( viewParent ) ) {
			return;
		}

		const modelParent = mapper.toModelElement( viewParent );

		data.modelPosition = model.createPositionAt( modelParent, viewPosition.isAtStart ? 'before' : 'after' );
	};
}

// Default filler offset function applied to all widget elements.
//
// @returns {null}
function getFillerOffset() {
	return null;
}

// Adds a drag handle to the widget.
//
// @param {module:engine/view/containerelement~ContainerElement}
// @param {module:engine/view/downcastwriter~DowncastWriter} writer
function addSelectionHandle( widgetElement, writer ) {
	const selectionHandle = writer.createUIElement( 'div', { class: 'ck ck-widget__selection-handle' }, function( domDocument ) {
		const domElement = this.toDomElement( domDocument );

		// Use the IconView from the ui library.
		const icon = new IconView();
		icon.set( 'content', dragHandleIcon );

		// Render the icon view right away to append its #element to the selectionHandle DOM element.
		icon.render();

		domElement.appendChild( icon.element );

		return domElement;
	} );

	// Append the selection handle into the widget wrapper.
	writer.insert( writer.createPositionAt( widgetElement, 0 ), selectionHandle );
	writer.addClass( [ 'ck-widget_with-selection-handle' ], widgetElement );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The name of the type around model selection attribute responsible for
 * displaying a fake caret next to a selected widget.
 */
const TYPE_AROUND_SELECTION_ATTRIBUTE = 'widget-type-around';

/**
 * Checks if an element is a widget that qualifies to get the widget type around UI.
 *
 * @param {module:engine/view/element~Element} viewElement
 * @param {module:engine/model/element~Element} modelElement
 * @param {module:engine/model/schema~Schema} schema
 * @returns {Boolean}
 */
function isTypeAroundWidget( viewElement, modelElement, schema ) {
	return viewElement && isWidget( viewElement ) && !schema.isInline( modelElement );
}

/**
 * For the passed HTML element, this helper finds the closest widget type around button ancestor.
 *
 * @param {HTMLElement} domElement
 * @returns {HTMLElement|null}
 */
function getClosestTypeAroundDomButton( domElement ) {
	return domElement.closest( '.ck-widget__type-around__button' );
}

/**
 * For the passed widget type around button element, this helper determines at which position
 * the paragraph would be inserted into the content if, for instance, the button was
 * clicked by the user.
 *
 * @param {HTMLElement} domElement
 * @returns {'before'|'after'} The position of the button.
 */
function getTypeAroundButtonPosition( domElement ) {
	return domElement.classList.contains( 'ck-widget__type-around__button_before' ) ? 'before' : 'after';
}

/**
 * For the passed HTML element, this helper returns the closest view widget ancestor.
 *
 * @param {HTMLElement} domElement
 * @param {module:engine/view/domconverter~DomConverter} domConverter
 * @returns {module:engine/view/element~Element}
 */
function getClosestWidgetViewElement( domElement, domConverter ) {
	const widgetDomElement = domElement.closest( '.ck-widget' );

	return domConverter.mapDomToView( widgetDomElement );
}

/**
 * For the passed selection instance, it returns the position of the fake caret displayed next to a widget.
 *
 * **Note**: If the fake caret is not currently displayed, `null` is returned.
 *
 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
 * @returns {'before'|'after'|null} The position of the fake caret or `null` when none is present.
 */
function getTypeAroundFakeCaretPosition( selection ) {
	return selection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
}var returnIcon = "<svg viewBox=\"0 0 10 8\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M9.055.263v3.972h-6.77M1 4.216l2-2.038m-2 2 2 2.038\"/></svg>";
var css_248z$w = ".ck .ck-widget .ck-widget__type-around__button{display:block;overflow:hidden;position:absolute;z-index:var(--ck-z-default)}.ck .ck-widget .ck-widget__type-around__button svg{left:50%;position:absolute;top:50%;z-index:calc(var(--ck-z-default) + 2)}.ck .ck-widget .ck-widget__type-around__button.ck-widget__type-around__button_before{left:min(10%,30px);top:calc(var(--ck-widget-outline-thickness)*-.5);transform:translateY(-50%)}.ck .ck-widget .ck-widget__type-around__button.ck-widget__type-around__button_after{bottom:calc(var(--ck-widget-outline-thickness)*-.5);right:min(10%,30px);transform:translateY(50%)}.ck .ck-widget.ck-widget_selected>.ck-widget__type-around>.ck-widget__type-around__button:after,.ck .ck-widget>.ck-widget__type-around>.ck-widget__type-around__button:hover:after{content:\"\";display:block;left:1px;position:absolute;top:1px;z-index:calc(var(--ck-z-default) + 1)}.ck .ck-widget>.ck-widget__type-around>.ck-widget__type-around__fake-caret{display:none;left:0;position:absolute;right:0}.ck .ck-widget:hover>.ck-widget__type-around>.ck-widget__type-around__fake-caret{left:calc(var(--ck-widget-outline-thickness)*-1);right:calc(var(--ck-widget-outline-thickness)*-1)}.ck .ck-widget.ck-widget_type-around_show-fake-caret_before>.ck-widget__type-around>.ck-widget__type-around__fake-caret{display:block;top:calc(var(--ck-widget-outline-thickness)*-1 - 1px)}.ck .ck-widget.ck-widget_type-around_show-fake-caret_after>.ck-widget__type-around>.ck-widget__type-around__fake-caret{bottom:calc(var(--ck-widget-outline-thickness)*-1 - 1px);display:block}.ck.ck-editor__editable.ck-read-only .ck-widget__type-around,.ck.ck-editor__editable.ck-restricted-editing_mode_restricted .ck-widget__type-around,.ck.ck-editor__editable.ck-widget__type-around_disabled .ck-widget__type-around{display:none}:root{--ck-widget-type-around-button-size:20px;--ck-color-widget-type-around-button-active:var(--ck-color-focus-border);--ck-color-widget-type-around-button-hover:var(--ck-color-widget-hover-border);--ck-color-widget-type-around-button-blurred-editable:var(--ck-color-widget-blurred-border);--ck-color-widget-type-around-button-radar-start-alpha:0;--ck-color-widget-type-around-button-radar-end-alpha:.3;--ck-color-widget-type-around-button-icon:var(--ck-color-base-background)}.ck .ck-widget .ck-widget__type-around__button{background:var(--ck-color-widget-type-around-button);border-radius:100px;height:var(--ck-widget-type-around-button-size);opacity:0;pointer-events:none;transition:opacity var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve),background var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve);width:var(--ck-widget-type-around-button-size)}.ck .ck-widget .ck-widget__type-around__button svg{height:8px;margin-top:1px;transform:translate(-50%,-50%);transition:transform .5s ease;width:10px}.ck .ck-widget .ck-widget__type-around__button svg *{stroke-dasharray:10;stroke-dashoffset:0;fill:none;stroke:var(--ck-color-widget-type-around-button-icon);stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:round}.ck .ck-widget .ck-widget__type-around__button svg line{stroke-dasharray:7}.ck .ck-widget .ck-widget__type-around__button:hover{animation:ck-widget-type-around-button-sonar 1s ease infinite}.ck .ck-widget .ck-widget__type-around__button:hover svg polyline{animation:ck-widget-type-around-arrow-dash 2s linear}.ck .ck-widget .ck-widget__type-around__button:hover svg line{animation:ck-widget-type-around-arrow-tip-dash 2s linear}.ck .ck-widget.ck-widget_selected>.ck-widget__type-around>.ck-widget__type-around__button,.ck .ck-widget:hover>.ck-widget__type-around>.ck-widget__type-around__button{opacity:1;pointer-events:auto}.ck .ck-widget:not(.ck-widget_selected)>.ck-widget__type-around>.ck-widget__type-around__button{background:var(--ck-color-widget-type-around-button-hover)}.ck .ck-widget.ck-widget_selected>.ck-widget__type-around>.ck-widget__type-around__button,.ck .ck-widget>.ck-widget__type-around>.ck-widget__type-around__button:hover{background:var(--ck-color-widget-type-around-button-active)}.ck .ck-widget.ck-widget_selected>.ck-widget__type-around>.ck-widget__type-around__button:after,.ck .ck-widget>.ck-widget__type-around>.ck-widget__type-around__button:hover:after{background:linear-gradient(135deg,hsla(0,0%,100%,0),hsla(0,0%,100%,.3));border-radius:100px;height:calc(var(--ck-widget-type-around-button-size) - 2px);width:calc(var(--ck-widget-type-around-button-size) - 2px)}.ck .ck-widget.ck-widget_with-selection-handle>.ck-widget__type-around>.ck-widget__type-around__button_before{margin-left:20px}.ck .ck-widget .ck-widget__type-around__fake-caret{animation:ck-widget-type-around-fake-caret-pulse 1s linear infinite normal forwards;background:var(--ck-color-base-text);height:1px;outline:1px solid hsla(0,0%,100%,.5);pointer-events:none}.ck .ck-widget.ck-widget_selected.ck-widget_type-around_show-fake-caret_after,.ck .ck-widget.ck-widget_selected.ck-widget_type-around_show-fake-caret_before{outline-color:transparent}.ck .ck-widget.ck-widget_type-around_show-fake-caret_after.ck-widget_selected:hover,.ck .ck-widget.ck-widget_type-around_show-fake-caret_before.ck-widget_selected:hover{outline-color:var(--ck-color-widget-hover-border)}.ck .ck-widget.ck-widget_type-around_show-fake-caret_after>.ck-widget__type-around>.ck-widget__type-around__button,.ck .ck-widget.ck-widget_type-around_show-fake-caret_before>.ck-widget__type-around>.ck-widget__type-around__button{opacity:0;pointer-events:none}.ck .ck-widget.ck-widget_type-around_show-fake-caret_after.ck-widget_selected.ck-widget_with-resizer>.ck-widget__resizer,.ck .ck-widget.ck-widget_type-around_show-fake-caret_after.ck-widget_with-selection-handle.ck-widget_selected:hover>.ck-widget__selection-handle,.ck .ck-widget.ck-widget_type-around_show-fake-caret_after.ck-widget_with-selection-handle.ck-widget_selected>.ck-widget__selection-handle,.ck .ck-widget.ck-widget_type-around_show-fake-caret_before.ck-widget_selected.ck-widget_with-resizer>.ck-widget__resizer,.ck .ck-widget.ck-widget_type-around_show-fake-caret_before.ck-widget_with-selection-handle.ck-widget_selected:hover>.ck-widget__selection-handle,.ck .ck-widget.ck-widget_type-around_show-fake-caret_before.ck-widget_with-selection-handle.ck-widget_selected>.ck-widget__selection-handle{opacity:0}.ck[dir=rtl] .ck-widget.ck-widget_with-selection-handle .ck-widget__type-around>.ck-widget__type-around__button_before{margin-left:0;margin-right:20px}.ck-editor__nested-editable.ck-editor__editable_selected .ck-widget.ck-widget_selected>.ck-widget__type-around>.ck-widget__type-around__button,.ck-editor__nested-editable.ck-editor__editable_selected .ck-widget:hover>.ck-widget__type-around>.ck-widget__type-around__button{opacity:0;pointer-events:none}.ck-editor__editable.ck-blurred .ck-widget.ck-widget_selected>.ck-widget__type-around>.ck-widget__type-around__button:not(:hover){background:var(--ck-color-widget-type-around-button-blurred-editable)}.ck-editor__editable.ck-blurred .ck-widget.ck-widget_selected>.ck-widget__type-around>.ck-widget__type-around__button:not(:hover) svg *{stroke:#999}@keyframes ck-widget-type-around-arrow-dash{0%{stroke-dashoffset:10}20%,to{stroke-dashoffset:0}}@keyframes ck-widget-type-around-arrow-tip-dash{0%,20%{stroke-dashoffset:7}40%,to{stroke-dashoffset:0}}@keyframes ck-widget-type-around-button-sonar{0%{box-shadow:0 0 0 0 hsla(var(--ck-color-focus-border-coordinates),var(--ck-color-widget-type-around-button-radar-start-alpha))}50%{box-shadow:0 0 0 5px hsla(var(--ck-color-focus-border-coordinates),var(--ck-color-widget-type-around-button-radar-end-alpha))}to{box-shadow:0 0 0 5px hsla(var(--ck-color-focus-border-coordinates),var(--ck-color-widget-type-around-button-radar-start-alpha))}}@keyframes ck-widget-type-around-fake-caret-pulse{0%{opacity:1}49%{opacity:1}50%{opacity:0}99%{opacity:0}to{opacity:1}}";
styleInject(css_248z$w);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const POSSIBLE_INSERTION_POSITIONS = [ 'before', 'after' ];

// Do the SVG parsing once and then clone the result <svg> DOM element for each new button.
const RETURN_ARROW_ICON_ELEMENT = new DOMParser().parseFromString( returnIcon, 'image/svg+xml' ).firstChild;

const PLUGIN_DISABLED_EDITING_ROOT_CLASS = 'ck-widget__type-around_disabled';

/**
 * A plugin that allows users to type around widgets where normally it is impossible to place the caret due
 * to limitations of web browsers. These "tight spots" occur, for instance, before (or after) a widget being
 * the first (or last) child of its parent or between two block widgets.
 *
 * This plugin extends the {@link module:widget/widget~Widget `Widget`} plugin and injects the user interface
 * with two buttons into each widget instance in the editor. Each of the buttons can be clicked by the
 * user if the widget is next to the "tight spot". Once clicked, a paragraph is created with the selection anchored
 * in it so that users can type (or insert content, paste, etc.) straight away.
 *
 * @extends module:core/plugin~Plugin
 */
class WidgetTypeAround extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'WidgetTypeAround';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ Enter, Delete ];
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		/**
		 * A reference to the model widget element that has the fake caret active
		 * on either side of it. It is later used to remove CSS classes associated with the fake caret
		 * when the widget no longer needs it.
		 *
		 * @private
		 * @member {module:engine/model/element~Element|null}
		 */
		this._currentFakeCaretModelElement = null;
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const editingView = editor.editing.view;

		// Set a CSS class on the view editing root when the plugin is disabled so all the buttons
		// and lines visually disappear. All the interactions are disabled in individual plugin methods.
		this.on( 'change:isEnabled', ( evt, data, isEnabled ) => {
			editingView.change( writer => {
				for ( const root of editingView.document.roots ) {
					if ( isEnabled ) {
						writer.removeClass( PLUGIN_DISABLED_EDITING_ROOT_CLASS, root );
					} else {
						writer.addClass( PLUGIN_DISABLED_EDITING_ROOT_CLASS, root );
					}
				}
			} );

			if ( !isEnabled ) {
				editor.model.change( writer => {
					writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
				} );
			}
		} );

		this._enableTypeAroundUIInjection();
		this._enableInsertingParagraphsOnButtonClick();
		this._enableInsertingParagraphsOnEnterKeypress();
		this._enableInsertingParagraphsOnTypingKeystroke();
		this._enableTypeAroundFakeCaretActivationUsingKeyboardArrows();
		this._enableDeleteIntegration();
		this._enableInsertContentIntegration();
		this._enableInsertObjectIntegration();
		this._enableDeleteContentIntegration();
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		this._currentFakeCaretModelElement = null;
	}

	/**
	 * Inserts a new paragraph next to a widget element with the selection anchored in it.
	 *
	 * **Note**: This method is heavily user-oriented and will both focus the editing view and scroll
	 * the viewport to the selection in the inserted paragraph.
	 *
	 * @protected
	 * @param {module:engine/model/element~Element} widgetModelElement The model widget element next to which a paragraph is inserted.
	 * @param {'before'|'after'} position The position where the paragraph is inserted. Either `'before'` or `'after'` the widget.
	 */
	_insertParagraph( widgetModelElement, position ) {
		const editor = this.editor;
		const editingView = editor.editing.view;

		const attributesToCopy = editor.model.schema.getAttributesWithProperty( widgetModelElement, 'copyOnReplace', true );

		editor.execute( 'insertParagraph', {
			position: editor.model.createPositionAt( widgetModelElement, position ),
			attributes: attributesToCopy
		} );

		editingView.focus();
		editingView.scrollToTheSelection();
	}

	/**
	 * A wrapper for the {@link module:utils/emittermixin~EmitterMixin#listenTo} method that executes the callbacks only
	 * when the plugin {@link #isEnabled is enabled}.
	 *
	 * @private
	 * @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.
	 */
	_listenToIfEnabled( emitter, event, callback, options ) {
		this.listenTo( emitter, event, ( ...args ) => {
			// Do not respond if the plugin is disabled.
			if ( this.isEnabled ) {
				callback( ...args );
			}
		}, options );
	}

	/**
	 * Similar to {@link #_insertParagraph}, this method inserts a paragraph except that it
	 * does not expect a position. Instead, it performs the insertion next to a selected widget
	 * according to the `widget-type-around` model selection attribute value (fake caret position).
	 *
	 * Because this method requires the `widget-type-around` attribute to be set,
	 * the insertion can only happen when the widget's fake caret is active (e.g. activated
	 * using the keyboard).
	 *
	 * @private
	 * @returns {Boolean} Returns `true` when the paragraph was inserted (the attribute was present) and `false` otherwise.
	 */
	_insertParagraphAccordingToFakeCaretPosition() {
		const editor = this.editor;
		const model = editor.model;
		const modelSelection = model.document.selection;
		const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition( modelSelection );

		if ( !typeAroundFakeCaretPosition ) {
			return false;
		}

		const selectedModelElement = modelSelection.getSelectedElement();

		this._insertParagraph( selectedModelElement, typeAroundFakeCaretPosition );

		return true;
	}

	/**
	 * Creates a listener in the editing conversion pipeline that injects the widget type around
	 * UI into every single widget instance created in the editor.
	 *
	 * The UI is delivered as a {@link module:engine/view/uielement~UIElement}
	 * wrapper which renders DOM buttons that users can use to insert paragraphs.
	 *
	 * @private
	 */
	_enableTypeAroundUIInjection() {
		const editor = this.editor;
		const schema = editor.model.schema;
		const t = editor.locale.t;
		const buttonTitles = {
			before: t( 'Insert paragraph before block' ),
			after: t( 'Insert paragraph after block' )
		};

		editor.editing.downcastDispatcher.on( 'insert', ( evt, data, conversionApi ) => {
			const viewElement = conversionApi.mapper.toViewElement( data.item );

			// Filter out non-widgets and inline widgets.
			if ( isTypeAroundWidget( viewElement, data.item, schema ) ) {
				injectUIIntoWidget( conversionApi.writer, buttonTitles, viewElement );
			}
		}, { priority: 'low' } );
	}

	/**
	 * Brings support for the fake caret that appears when either:
	 *
	 * * the selection moves to a widget from a position next to it using arrow keys,
	 * * the arrow key is pressed when the widget is already selected.
	 *
	 * The fake caret lets the user know that they can start typing or just press
	 * <kbd>Enter</kbd> to insert a paragraph at the position next to a widget as suggested by the fake caret.
	 *
	 * The fake caret disappears when the user changes the selection or the editor
	 * gets blurred.
	 *
	 * The whole idea is as follows:
	 *
	 * 1. A user does one of the 2 scenarios described at the beginning.
	 * 2. The "keydown" listener is executed and the decision is made whether to show or hide the fake caret.
	 * 3. If it should show up, the `widget-type-around` model selection attribute is set indicating
	 *    on which side of the widget it should appear.
	 * 4. The selection dispatcher reacts to the selection attribute and sets CSS classes responsible for the
	 *    fake caret on the view widget.
	 * 5. If the fake caret should disappear, the selection attribute is removed and the dispatcher
	 *    does the CSS class clean-up in the view.
	 * 6. Additionally, `change:range` and `FocusTracker#isFocused` listeners also remove the selection
	 *    attribute (the former also removes widget CSS classes).
	 *
	 * @private
	 */
	_enableTypeAroundFakeCaretActivationUsingKeyboardArrows() {
		const editor = this.editor;
		const model = editor.model;
		const modelSelection = model.document.selection;
		const schema = model.schema;
		const editingView = editor.editing.view;

		// This is the main listener responsible for the fake caret.
		// Note: The priority must precede the default Widget class keydown handler ("high").
		this._listenToIfEnabled( editingView.document, 'arrowKey', ( evt, domEventData ) => {
			this._handleArrowKeyPress( evt, domEventData );
		}, { context: [ isWidget, '$text' ], priority: 'high' } );

		// This listener makes sure the widget type around selection attribute will be gone from the model
		// selection as soon as the model range changes. This attribute only makes sense when a widget is selected
		// (and the "fake horizontal caret" is visible) so whenever the range changes (e.g. selection moved somewhere else),
		// let's get rid of the attribute so that the selection downcast dispatcher isn't even bothered.
		this._listenToIfEnabled( modelSelection, 'change:range', ( evt, data ) => {
			// Do not reset the selection attribute when the change was indirect.
			if ( !data.directChange ) {
				return;
			}

			// Get rid of the widget type around attribute of the selection on every change:range.
			// If the range changes, it means for sure, the user is no longer in the active ("fake horizontal caret") mode.
			editor.model.change( writer => {
				writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
			} );
		} );

		// Get rid of the widget type around attribute of the selection on every document change
		// that makes widget not selected any more (i.e. widget was removed).
		this._listenToIfEnabled( model.document, 'change:data', () => {
			const selectedModelElement = modelSelection.getSelectedElement();

			if ( selectedModelElement ) {
				const selectedViewElement = editor.editing.mapper.toViewElement( selectedModelElement );

				if ( isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) {
					return;
				}
			}

			editor.model.change( writer => {
				writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
			} );
		} );

		// React to changes of the model selection attribute made by the arrow keys listener.
		// If the block widget is selected and the attribute changes, downcast the attribute to special
		// CSS classes associated with the active ("fake horizontal caret") mode of the widget.
		this._listenToIfEnabled( editor.editing.downcastDispatcher, 'selection', ( evt, data, conversionApi ) => {
			const writer = conversionApi.writer;

			if ( this._currentFakeCaretModelElement ) {
				const selectedViewElement = conversionApi.mapper.toViewElement( this._currentFakeCaretModelElement );

				if ( selectedViewElement ) {
					// Get rid of CSS classes associated with the active ("fake horizontal caret") mode from the view widget.
					writer.removeClass( POSSIBLE_INSERTION_POSITIONS.map( positionToWidgetCssClass ), selectedViewElement );

					this._currentFakeCaretModelElement = null;
				}
			}

			const selectedModelElement = data.selection.getSelectedElement();

			if ( !selectedModelElement ) {
				return;
			}

			const selectedViewElement = conversionApi.mapper.toViewElement( selectedModelElement );

			if ( !isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) {
				return;
			}

			const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition( data.selection );

			if ( !typeAroundFakeCaretPosition ) {
				return;
			}

			writer.addClass( positionToWidgetCssClass( typeAroundFakeCaretPosition ), selectedViewElement );

			// Remember the view widget that got the "fake-caret" CSS class. This class should be removed ASAP when the
			// selection changes
			this._currentFakeCaretModelElement = selectedModelElement;
		} );

		this._listenToIfEnabled( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
			if ( !isFocused ) {
				editor.model.change( writer => {
					writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );
				} );
			}
		} );

		function positionToWidgetCssClass( position ) {
			return `ck-widget_type-around_show-fake-caret_${ position }`;
		}
	}

	/**
	 * A listener executed on each "keydown" in the view document, a part of
	 * {@link #_enableTypeAroundFakeCaretActivationUsingKeyboardArrows}.
	 *
	 * It decides whether the arrow keypress should activate the fake caret or not (also whether it should
	 * be deactivated).
	 *
	 * The fake caret activation is done by setting the `widget-type-around` model selection attribute
	 * in this listener, and stopping and preventing the event that would normally be handled by the widget
	 * plugin that is responsible for the regular keyboard navigation near/across all widgets (that
	 * includes inline widgets, which are ignored by the widget type around plugin).
	 *
	 * @private
	 */
	_handleArrowKeyPress( evt, domEventData ) {
		const editor = this.editor;
		const model = editor.model;
		const modelSelection = model.document.selection;
		const schema = model.schema;
		const editingView = editor.editing.view;

		const keyCode = domEventData.keyCode;
		const isForward = isForwardArrowKeyCode( keyCode, editor.locale.contentLanguageDirection );
		const selectedViewElement = editingView.document.selection.getSelectedElement();
		const selectedModelElement = editor.editing.mapper.toModelElement( selectedViewElement );
		let shouldStopAndPreventDefault;

		// Handle keyboard navigation when a type-around-compatible widget is currently selected.
		if ( isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) {
			shouldStopAndPreventDefault = this._handleArrowKeyPressOnSelectedWidget( isForward );
		}
		// Handle keyboard arrow navigation when the selection is next to a type-around-compatible widget
		// and the widget is about to be selected.
		else if ( modelSelection.isCollapsed ) {
			shouldStopAndPreventDefault = this._handleArrowKeyPressWhenSelectionNextToAWidget( isForward );
		}
		// Handle collapsing a non-collapsed selection that is wider than on a single widget.
		else if ( !domEventData.shiftKey ) {
			shouldStopAndPreventDefault = this._handleArrowKeyPressWhenNonCollapsedSelection( isForward );
		}

		if ( shouldStopAndPreventDefault ) {
			domEventData.preventDefault();
			evt.stop();
		}
	}

	/**
	 * Handles the keyboard navigation on "keydown" when a widget is currently selected and activates or deactivates
	 * the fake caret for that widget, depending on the current value of the `widget-type-around` model
	 * selection attribute and the direction of the pressed arrow key.
	 *
	 * @private
	 * @param {Boolean} isForward `true` when the pressed arrow key was responsible for the forward model selection movement
	 * as in {@link module:utils/keyboard~isForwardArrowKeyCode}.
	 * @returns {Boolean} Returns `true` when the keypress was handled and no other keydown listener of the editor should
	 * process the event any further. Returns `false` otherwise.
	 */
	_handleArrowKeyPressOnSelectedWidget( isForward ) {
		const editor = this.editor;
		const model = editor.model;
		const modelSelection = model.document.selection;
		const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition( modelSelection );

		return model.change( writer => {
			// If the fake caret is displayed...
			if ( typeAroundFakeCaretPosition ) {
				const isLeavingWidget = typeAroundFakeCaretPosition === ( isForward ? 'after' : 'before' );

				// If the keyboard arrow works against the value of the selection attribute...
				// then remove the selection attribute but prevent default DOM actions
				// and do not let the Widget plugin listener move the selection. This brings
				// the widget back to the state, for instance, like if was selected using the mouse.
				//
				// **Note**: If leaving the widget when the fake caret is active, then the default
				// Widget handler will change the selection and, in turn, this will automatically discard
				// the selection attribute.
				if ( !isLeavingWidget ) {
					writer.removeSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE );

					return true;
				}
			}
			// If the fake caret wasn't displayed, let's set it now according to the direction of the arrow
			// key press. This also means we cannot let the Widget plugin listener move the selection.
			else {
				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'after' : 'before' );

				return true;
			}

			return false;
		} );
	}

	/**
	 * Handles the keyboard navigation on "keydown" when **no** widget is selected but the selection is **directly** next
	 * to one and upon the fake caret should become active for this widget upon arrow keypress
	 * (AKA entering/selecting the widget).
	 *
	 * **Note**: This code mirrors the implementation from the widget plugin but also adds the selection attribute.
	 * Unfortunately, there is no safe way to let the widget plugin do the selection part first and then just set the
	 * selection attribute here in the widget type around plugin. This is why this code must duplicate some from the widget plugin.
	 *
	 * @private
	 * @param {Boolean} isForward `true` when the pressed arrow key was responsible for the forward model selection movement
	 * as in {@link module:utils/keyboard~isForwardArrowKeyCode}.
	 * @returns {Boolean} Returns `true` when the keypress was handled and no other keydown listener of the editor should
	 * process the event any further. Returns `false` otherwise.
	 */
	_handleArrowKeyPressWhenSelectionNextToAWidget( isForward ) {
		const editor = this.editor;
		const model = editor.model;
		const schema = model.schema;
		const widgetPlugin = editor.plugins.get( 'Widget' );

		// This is the widget the selection is about to be set on.
		const modelElementNextToSelection = widgetPlugin._getObjectElementNextToSelection( isForward );
		const viewElementNextToSelection = editor.editing.mapper.toViewElement( modelElementNextToSelection );

		if ( isTypeAroundWidget( viewElementNextToSelection, modelElementNextToSelection, schema ) ) {
			model.change( writer => {
				widgetPlugin._setSelectionOverElement( modelElementNextToSelection );
				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'before' : 'after' );
			} );

			// The change() block above does the same job as the Widget plugin. The event can
			// be safely canceled.
			return true;
		}

		return false;
	}

	/**
	 * Handles the keyboard navigation on "keydown" when a widget is currently selected (together with some other content)
	 * and the widget is the first or last element in the selection. It activates or deactivates the fake caret for that widget.
	 *
	 * @private
	 * @param {Boolean} isForward `true` when the pressed arrow key was responsible for the forward model selection movement
	 * as in {@link module:utils/keyboard~isForwardArrowKeyCode}.
	 * @returns {Boolean} Returns `true` when the keypress was handled and no other keydown listener of the editor should
	 * process the event any further. Returns `false` otherwise.
	 */
	_handleArrowKeyPressWhenNonCollapsedSelection( isForward ) {
		const editor = this.editor;
		const model = editor.model;
		const schema = model.schema;
		const mapper = editor.editing.mapper;
		const modelSelection = model.document.selection;

		const selectedModelNode = isForward ?
			modelSelection.getLastPosition().nodeBefore :
			modelSelection.getFirstPosition().nodeAfter;

		const selectedViewNode = mapper.toViewElement( selectedModelNode );

		// There is a widget at the collapse position so collapse the selection to the fake caret on it.
		if ( isTypeAroundWidget( selectedViewNode, selectedModelNode, schema ) ) {
			model.change( writer => {
				writer.setSelection( selectedModelNode, 'on' );
				writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, isForward ? 'after' : 'before' );
			} );

			return true;
		}

		return false;
	}

	/**
	 * Registers a `mousedown` listener for the view document which intercepts events
	 * coming from the widget type around UI, which happens when a user clicks one of the buttons
	 * that insert a paragraph next to a widget.
	 *
	 * @private
	 */
	_enableInsertingParagraphsOnButtonClick() {
		const editor = this.editor;
		const editingView = editor.editing.view;

		this._listenToIfEnabled( editingView.document, 'mousedown', ( evt, domEventData ) => {
			const button = getClosestTypeAroundDomButton( domEventData.domTarget );

			if ( !button ) {
				return;
			}

			const buttonPosition = getTypeAroundButtonPosition( button );
			const widgetViewElement = getClosestWidgetViewElement( button, editingView.domConverter );
			const widgetModelElement = editor.editing.mapper.toModelElement( widgetViewElement );

			this._insertParagraph( widgetModelElement, buttonPosition );

			domEventData.preventDefault();
			evt.stop();
		} );
	}

	/**
	 * Creates the <kbd>Enter</kbd> key listener on the view document that allows the user to insert a paragraph
	 * near the widget when either:
	 *
	 * * The fake caret was first activated using the arrow keys,
	 * * The entire widget is selected in the model.
	 *
	 * In the first case, the new paragraph is inserted according to the `widget-type-around` selection
	 * attribute (see {@link #_handleArrowKeyPress}).
	 *
	 * In the second case, the new paragraph is inserted based on whether a soft (<kbd>Shift</kbd>+<kbd>Enter</kbd>) keystroke
	 * was pressed or not.
	 *
	 * @private
	 */
	_enableInsertingParagraphsOnEnterKeypress() {
		const editor = this.editor;
		const selection = editor.model.document.selection;
		const editingView = editor.editing.view;

		this._listenToIfEnabled( editingView.document, 'enter', ( evt, domEventData ) => {
			// This event could be triggered from inside the widget but we are interested
			// only when the widget is selected itself.
			if ( evt.eventPhase != 'atTarget' ) {
				return;
			}

			const selectedModelElement = selection.getSelectedElement();
			const selectedViewElement = editor.editing.mapper.toViewElement( selectedModelElement );

			const schema = editor.model.schema;
			let wasHandled;

			// First check if the widget is selected and there's a type around selection attribute associated
			// with the fake caret that would tell where to insert a new paragraph.
			if ( this._insertParagraphAccordingToFakeCaretPosition() ) {
				wasHandled = true;
			}
			// Then, if there is no selection attribute associated with the fake caret, check if the widget
			// simply is selected and create a new paragraph according to the keystroke (Shift+)Enter.
			else if ( isTypeAroundWidget( selectedViewElement, selectedModelElement, schema ) ) {
				this._insertParagraph( selectedModelElement, domEventData.isSoft ? 'before' : 'after' );

				wasHandled = true;
			}

			if ( wasHandled ) {
				domEventData.preventDefault();
				evt.stop();
			}
		}, { context: isWidget } );
	}

	/**
	 * Similar to the {@link #_enableInsertingParagraphsOnEnterKeypress}, it allows the user
	 * to insert a paragraph next to a widget when the fake caret was activated using arrow
	 * keys but it responds to typing keystrokes instead of <kbd>Enter</kbd>.
	 *
	 * "Typing keystrokes" are keystrokes that insert new content into the document,
	 * for instance, letters ("a") or numbers ("4"). The "keydown" listener enabled by this method
	 * will insert a new paragraph according to the `widget-type-around` model selection attribute
	 * as the user simply starts typing, which creates the impression that the fake caret
	 * behaves like a real one rendered by the browser (AKA your text appears where the caret was).
	 *
	 * **Note**: At the moment this listener creates 2 undo steps: one for the `insertParagraph` command
	 * and another one for actual typing. It is not a disaster but this may need to be fixed
	 * sooner or later.
	 *
	 * Learn more in {@link module:typing/utils/injectunsafekeystrokeshandling}.
	 *
	 * @private
	 */
	_enableInsertingParagraphsOnTypingKeystroke() {
		const editor = this.editor;
		const editingView = editor.editing.view;
		const keyCodesHandledSomewhereElse = [
			keyCodes.enter,
			keyCodes.delete,
			keyCodes.backspace
		];

		// Note: The priority must precede the default observers.
		this._listenToIfEnabled( editingView.document, 'keydown', ( evt, domEventData ) => {
			// Don't handle enter/backspace/delete here. They are handled in dedicated listeners.
			if ( !keyCodesHandledSomewhereElse.includes( domEventData.keyCode ) && !isNonTypingKeystroke( domEventData ) ) {
				this._insertParagraphAccordingToFakeCaretPosition();
			}
		}, { priority: 'high' } );
	}

	/**
	 * It creates a "delete" event listener on the view document to handle cases when the <kbd>Delete</kbd> or <kbd>Backspace</kbd>
	 * is pressed and the fake caret is currently active.
	 *
	 * The fake caret should create an illusion of a real browser caret so that when it appears before or after
	 * a widget, pressing <kbd>Delete</kbd> or <kbd>Backspace</kbd> should remove a widget or delete the content
	 * before or after a widget (depending on the content surrounding the widget).
	 *
	 * @private
	 */
	_enableDeleteIntegration() {
		const editor = this.editor;
		const editingView = editor.editing.view;
		const model = editor.model;
		const schema = model.schema;

		this._listenToIfEnabled( editingView.document, 'delete', ( evt, domEventData ) => {
			// This event could be triggered from inside the widget but we are interested
			// only when the widget is selected itself.
			if ( evt.eventPhase != 'atTarget' ) {
				return;
			}

			const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition( model.document.selection );

			// This listener handles only these cases when the fake caret is active.
			if ( !typeAroundFakeCaretPosition ) {
				return;
			}

			const direction = domEventData.direction;
			const selectedModelWidget = model.document.selection.getSelectedElement();

			const isFakeCaretBefore = typeAroundFakeCaretPosition === 'before';
			const isDeleteForward = direction == 'forward';
			const shouldDeleteEntireWidget = isFakeCaretBefore === isDeleteForward;

			if ( shouldDeleteEntireWidget ) {
				editor.execute( 'delete', {
					selection: model.createSelection( selectedModelWidget, 'on' )
				} );
			} else {
				const range = schema.getNearestSelectionRange(
					model.createPositionAt( selectedModelWidget, typeAroundFakeCaretPosition ),
					direction
				);

				// If there is somewhere to move selection to, then there will be something to delete.
				if ( range ) {
					// If the range is NOT collapsed, then we know that the range contains an object (see getNearestSelectionRange() docs).
					if ( !range.isCollapsed ) {
						model.change( writer => {
							writer.setSelection( range );
							editor.execute( isDeleteForward ? 'deleteForward' : 'delete' );
						} );
					} else {
						const probe = model.createSelection( range.start );
						model.modifySelection( probe, { direction } );

						// If the range is collapsed, let's see if a non-collapsed range exists that can could be deleted.
						// If such range exists, use the editor command because it it safe for collaboration (it merges where it can).
						if ( !probe.focus.isEqual( range.start ) ) {
							model.change( writer => {
								writer.setSelection( range );
								editor.execute( isDeleteForward ? 'deleteForward' : 'delete' );
							} );
						}
						// If there is no non-collapsed range to be deleted then we are sure that there is an empty element
						// next to a widget that should be removed. "delete" and "deleteForward" commands cannot get rid of it
						// so calling Model#deleteContent here manually.
						else {
							const deepestEmptyRangeAncestor = getDeepestEmptyElementAncestor( schema, range.start.parent );

							model.deleteContent( model.createSelection( deepestEmptyRangeAncestor, 'on' ), {
								doNotAutoparagraph: true
							} );
						}
					}
				}
			}

			// If some content was deleted, don't let the handler from the Widget plugin kick in.
			// If nothing was deleted, then the default handler will have nothing to do anyway.
			domEventData.preventDefault();
			evt.stop();
		}, { context: isWidget } );
	}

	/**
	 * Attaches the {@link module:engine/model/model~Model#event:insertContent} event listener that, for instance, allows the user to paste
	 * content near a widget when the fake caret is first activated using the arrow keys.
	 *
	 * The content is inserted according to the `widget-type-around` selection attribute (see {@link #_handleArrowKeyPress}).
	 *
	 * @private
	 */
	_enableInsertContentIntegration() {
		const editor = this.editor;
		const model = this.editor.model;
		const documentSelection = model.document.selection;

		this._listenToIfEnabled( editor.model, 'insertContent', ( evt, [ content, selectable ] ) => {
			if ( selectable && !selectable.is( 'documentSelection' ) ) {
				return;
			}

			const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition( documentSelection );

			if ( !typeAroundFakeCaretPosition ) {
				return;
			}

			evt.stop();

			return model.change( writer => {
				const selectedElement = documentSelection.getSelectedElement();
				const position = model.createPositionAt( selectedElement, typeAroundFakeCaretPosition );
				const selection = writer.createSelection( position );

				const result = model.insertContent( content, selection );

				writer.setSelection( selection );

				return result;
			} );
		}, { priority: 'high' } );
	}

	/**
	 * Attaches the {@link module:engine/model/model~Model#event:insertObject} event listener that modifies the
	 * `options.findOptimalPosition`parameter to position of fake caret in relation to selected element
	 * to reflect user's intent of desired insertion position.
	 *
	 * The object is inserted according to the `widget-type-around` selection attribute (see {@link #_handleArrowKeyPress}).
	 *
	 * @private
	 */
	_enableInsertObjectIntegration() {
		const editor = this.editor;
		const model = this.editor.model;
		const documentSelection = model.document.selection;

		this._listenToIfEnabled( editor.model, 'insertObject', ( evt, args ) => {
			const [ , selectable, , options = {} ] = args;

			if ( selectable && !selectable.is( 'documentSelection' ) ) {
				return;
			}

			const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition( documentSelection );

			if ( !typeAroundFakeCaretPosition ) {
				return;
			}

			options.findOptimalPosition = typeAroundFakeCaretPosition;
			args[ 3 ] = options;
		}, { priority: 'high' } );
	}

	/**
	 * Attaches the {@link module:engine/model/model~Model#event:deleteContent} event listener to block the event when the fake
	 * caret is active.
	 *
	 * This is required for cases that trigger {@link module:engine/model/model~Model#deleteContent `model.deleteContent()`}
	 * before calling {@link module:engine/model/model~Model#insertContent `model.insertContent()`} like, for instance,
	 * plain text pasting.
	 *
	 * @private
	 */
	_enableDeleteContentIntegration() {
		const editor = this.editor;
		const model = this.editor.model;
		const documentSelection = model.document.selection;

		this._listenToIfEnabled( editor.model, 'deleteContent', ( evt, [ selection ] ) => {
			if ( selection && !selection.is( 'documentSelection' ) ) {
				return;
			}

			const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition( documentSelection );

			// Disable removing the selection content while pasting plain text.
			if ( typeAroundFakeCaretPosition ) {
				evt.stop();
			}
		}, { priority: 'high' } );
	}
}

// Injects the type around UI into a view widget instance.
//
// @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
// @param {Object.<String,String>} buttonTitles
// @param {module:engine/view/element~Element} widgetViewElement
function injectUIIntoWidget( viewWriter, buttonTitles, widgetViewElement ) {
	const typeAroundWrapper = viewWriter.createUIElement( 'div', {
		class: 'ck ck-reset_all ck-widget__type-around'
	}, function( domDocument ) {
		const wrapperDomElement = this.toDomElement( domDocument );

		injectButtons( wrapperDomElement, buttonTitles );
		injectFakeCaret( wrapperDomElement );

		return wrapperDomElement;
	} );

	// Inject the type around wrapper into the widget's wrapper.
	viewWriter.insert( viewWriter.createPositionAt( widgetViewElement, 'end' ), typeAroundWrapper );
}

// FYI: Not using the IconView class because each instance would need to be destroyed to avoid memory leaks
// and it's pretty hard to figure out when a view (widget) is gone for good so it's cheaper to use raw
// <svg> here.
//
// @param {HTMLElement} wrapperDomElement
// @param {Object.<String,String>} buttonTitles
function injectButtons( wrapperDomElement, buttonTitles ) {
	for ( const position of POSSIBLE_INSERTION_POSITIONS ) {
		const buttonTemplate = new Template( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-widget__type-around__button',
					`ck-widget__type-around__button_${ position }`
				],
				title: buttonTitles[ position ]
			},
			children: [
				wrapperDomElement.ownerDocument.importNode( RETURN_ARROW_ICON_ELEMENT, true )
			]
		} );

		wrapperDomElement.appendChild( buttonTemplate.render() );
	}
}

// @param {HTMLElement} wrapperDomElement
function injectFakeCaret( wrapperDomElement ) {
	const caretTemplate = new Template( {
		tag: 'div',
		attributes: {
			class: [
				'ck',
				'ck-widget__type-around__fake-caret'
			]
		}
	} );

	wrapperDomElement.appendChild( caretTemplate.render() );
}

// Returns the ancestor of an element closest to the root which is empty. For instance,
// for `<baz>`:
//
//		<foo>abc<bar><baz></baz></bar></foo>
//
// it returns `<bar>`.
//
// @param {module:engine/model/schema~Schema} schema
// @param {module:engine/model/element~Element} element
// @returns {module:engine/model/element~Element|null}
function getDeepestEmptyElementAncestor( schema, element ) {
	let deepestEmptyAncestor = element;

	for ( const ancestor of element.getAncestors( { parentFirst: true } ) ) {
		if ( ancestor.childCount > 1 || schema.isLimit( ancestor ) ) {
			break;
		}

		deepestEmptyAncestor = ancestor;
	}

	return deepestEmptyAncestor;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module widget/verticalnavigationhandler
 */

/**
 * Returns 'keydown' handler for up/down arrow keys that modifies the caret movement if it's in a text line next to an object.
 *
 * @param {module:engine/controller/editingcontroller~EditingController} editing The editing controller.
 * @returns {Function}
 */
function verticalNavigationHandler( editing ) {
	const model = editing.model;

	return ( evt, data ) => {
		const arrowUpPressed = data.keyCode == keyCodes.arrowup;
		const arrowDownPressed = data.keyCode == keyCodes.arrowdown;
		const expandSelection = data.shiftKey;
		const selection = model.document.selection;

		if ( !arrowUpPressed && !arrowDownPressed ) {
			return;
		}

		const isForward = arrowDownPressed;

		// Navigation is in the opposite direction than the selection direction so this is shrinking of the selection.
		// Selection for sure will not approach any object.
		if ( expandSelection && selectionWillShrink( selection, isForward ) ) {
			return;
		}

		// Find a range between selection and closest limit element.
		const range = findTextRangeFromSelection( editing, selection, isForward );

		// There is no selection position inside the limit element.
		if ( !range ) {
			return;
		}

		// If already at the edge of a limit element.
		if ( range.isCollapsed ) {
			// A collapsed selection at limit edge - nothing more to do.
			if ( selection.isCollapsed ) {
				return;
			}

			// A non collapsed selection is at the limit edge while expanding the selection - let others do their stuff.
			else if ( expandSelection ) {
				return;
			}
		}

		// If the range is a single line (there is no word wrapping) then move the selection to the position closest to the limit element.
		//
		// We can't move the selection directly to the isObject element (eg. table cell) because of dual position at the end/beginning
		// of wrapped line (it's at the same time at the end of one line and at the start of the next line).
		if ( range.isCollapsed || isSingleLineRange( editing, range, isForward ) ) {
			model.change( writer => {
				const newPosition = isForward ? range.end : range.start;

				if ( expandSelection ) {
					const newSelection = model.createSelection( selection.anchor );
					newSelection.setFocus( newPosition );

					writer.setSelection( newSelection );
				} else {
					writer.setSelection( newPosition );
				}
			} );

			evt.stop();
			data.preventDefault();
			data.stopPropagation();
		}
	};
}

// Finds the range between selection and closest limit element (in the direction of navigation).
// The position next to limit element is adjusted to the closest allowed `$text` position.
//
// Returns `null` if, according to the schema, the resulting range cannot contain a `$text` element.
//
// @param {module:engine/controller/editingcontroller~EditingController} editing The editing controller.
// @param {module:engine/model/selection~Selection} selection The current selection.
// @param {Boolean} isForward The expected navigation direction.
// @returns {module:engine/model/range~Range|null}
//
function findTextRangeFromSelection( editing, selection, isForward ) {
	const model = editing.model;

	if ( isForward ) {
		const startPosition = selection.isCollapsed ? selection.focus : selection.getLastPosition();
		const endPosition = getNearestNonInlineLimit( model, startPosition, 'forward' );

		// There is no limit element, browser should handle this.
		if ( !endPosition ) {
			return null;
		}

		const range = model.createRange( startPosition, endPosition );
		const lastRangePosition = getNearestTextPosition( model.schema, range, 'backward' );

		if ( lastRangePosition ) {
			return model.createRange( startPosition, lastRangePosition );
		}

		return null;
	} else {
		const endPosition = selection.isCollapsed ? selection.focus : selection.getFirstPosition();
		const startPosition = getNearestNonInlineLimit( model, endPosition, 'backward' );

		// There is no limit element, browser should handle this.
		if ( !startPosition ) {
			return null;
		}

		const range = model.createRange( startPosition, endPosition );
		const firstRangePosition = getNearestTextPosition( model.schema, range, 'forward' );

		if ( firstRangePosition ) {
			return model.createRange( firstRangePosition, endPosition );
		}

		return null;
	}
}

// Finds the limit element position that is closest to startPosition.
//
// @param {module:engine/model/model~Model} model
// @param {<module:engine/model/position~Position>} startPosition
// @param {'forward'|'backward'} direction Search direction.
// @returns {<module:engine/model/position~Position>|null}
//
function getNearestNonInlineLimit( model, startPosition, direction ) {
	const schema = model.schema;
	const range = model.createRangeIn( startPosition.root );

	const walkerValueType = direction == 'forward' ? 'elementStart' : 'elementEnd';

	for ( const { previousPosition, item, type } of range.getWalker( { startPosition, direction } ) ) {
		if ( schema.isLimit( item ) && !schema.isInline( item ) ) {
			return previousPosition;
		}

		// Stop looking for isLimit element if the next element is a block element (it is for sure not single line).
		if ( type == walkerValueType && schema.isBlock( item ) ) {
			return null;
		}
	}

	return null;
}

// Basing on the provided range, finds the first or last (depending on `direction`) position inside the range
// that can contain `$text` (according to schema).
//
// @param {module:engine/model/schema~Schema} schema The schema.
// @param {module:engine/model/range~Range} range The range to find the position in.
// @param {'forward'|'backward'} direction Search direction.
// @returns {module:engine/model/position~Position|null} The nearest selection position.
//
function getNearestTextPosition( schema, range, direction ) {
	const position = direction == 'backward' ? range.end : range.start;

	if ( schema.checkChild( position, '$text' ) ) {
		return position;
	}

	for ( const { nextPosition } of range.getWalker( { direction } ) ) {
		if ( schema.checkChild( nextPosition, '$text' ) ) {
			return nextPosition;
		}
	}

	return null;
}

// Checks if the DOM range corresponding to the provided model range renders as a single line by analyzing DOMRects
// (verifying if they visually wrap content to the next line).
//
// @param {module:engine/controller/editingcontroller~EditingController} editing The editing controller.
// @param {module:engine/model/range~Range} modelRange The current table cell content range.
// @param {Boolean} isForward The expected navigation direction.
// @returns {Boolean}
//
function isSingleLineRange( editing, modelRange, isForward ) {
	const model = editing.model;
	const domConverter = editing.view.domConverter;

	// Wrapped lines contain exactly the same position at the end of current line
	// and at the beginning of next line. That position's client rect is at the end
	// of current line. In case of caret at first position of the last line that 'dual'
	// position would be detected as it's not the last line.
	if ( isForward ) {
		const probe = model.createSelection( modelRange.start );

		model.modifySelection( probe );

		// If the new position is at the end of the container then we can't use this position
		// because it would provide incorrect result for eg caption of image and selection
		// just before end of it. Also in this case there is no "dual" position.
		if ( !probe.focus.isAtEnd && !modelRange.start.isEqual( probe.focus ) ) {
			modelRange = model.createRange( probe.focus, modelRange.end );
		}
	}

	const viewRange = editing.mapper.toViewRange( modelRange );
	const domRange = domConverter.viewRangeToDom( viewRange );
	const rects = Rect.getDomRangeRects( domRange );

	let boundaryVerticalPosition;

	for ( const rect of rects ) {
		if ( boundaryVerticalPosition === undefined ) {
			boundaryVerticalPosition = Math.round( rect.bottom );
			continue;
		}

		// Let's check if this rect is in new line.
		if ( Math.round( rect.top ) >= boundaryVerticalPosition ) {
			return false;
		}

		boundaryVerticalPosition = Math.max( boundaryVerticalPosition, Math.round( rect.bottom ) );
	}

	return true;
}

function selectionWillShrink( selection, isForward ) {
	return !selection.isCollapsed && selection.isBackward == isForward;
}var css_248z$v = ":root{--ck-color-resizer:var(--ck-color-focus-border);--ck-color-resizer-tooltip-background:#262626;--ck-color-resizer-tooltip-text:#f2f2f2;--ck-resizer-border-radius:var(--ck-border-radius);--ck-resizer-tooltip-offset:10px;--ck-resizer-tooltip-height:calc(var(--ck-spacing-small)*2 + 10px)}.ck .ck-widget,.ck .ck-widget.ck-widget_with-selection-handle{position:relative}.ck .ck-widget.ck-widget_with-selection-handle .ck-widget__selection-handle{position:absolute}.ck .ck-widget.ck-widget_with-selection-handle .ck-widget__selection-handle .ck-icon{display:block}.ck .ck-widget.ck-widget_with-selection-handle.ck-widget_selected>.ck-widget__selection-handle,.ck .ck-widget.ck-widget_with-selection-handle:hover>.ck-widget__selection-handle{visibility:visible}.ck .ck-size-view{background:var(--ck-color-resizer-tooltip-background);border:1px solid var(--ck-color-resizer-tooltip-text);border-radius:var(--ck-resizer-border-radius);color:var(--ck-color-resizer-tooltip-text);display:block;font-size:var(--ck-font-size-tiny);height:var(--ck-resizer-tooltip-height);line-height:var(--ck-resizer-tooltip-height);padding:0 var(--ck-spacing-small)}.ck .ck-size-view.ck-orientation-above-center,.ck .ck-size-view.ck-orientation-bottom-left,.ck .ck-size-view.ck-orientation-bottom-right,.ck .ck-size-view.ck-orientation-top-left,.ck .ck-size-view.ck-orientation-top-right{position:absolute}.ck .ck-size-view.ck-orientation-top-left{left:var(--ck-resizer-tooltip-offset);top:var(--ck-resizer-tooltip-offset)}.ck .ck-size-view.ck-orientation-top-right{right:var(--ck-resizer-tooltip-offset);top:var(--ck-resizer-tooltip-offset)}.ck .ck-size-view.ck-orientation-bottom-right{bottom:var(--ck-resizer-tooltip-offset);right:var(--ck-resizer-tooltip-offset)}.ck .ck-size-view.ck-orientation-bottom-left{bottom:var(--ck-resizer-tooltip-offset);left:var(--ck-resizer-tooltip-offset)}.ck .ck-size-view.ck-orientation-above-center{left:50%;top:calc(var(--ck-resizer-tooltip-height)*-1);transform:translate(-50%)}:root{--ck-widget-outline-thickness:3px;--ck-widget-handler-icon-size:16px;--ck-widget-handler-animation-duration:200ms;--ck-widget-handler-animation-curve:ease;--ck-color-widget-blurred-border:#dedede;--ck-color-widget-hover-border:#ffc83d;--ck-color-widget-editable-focus-background:var(--ck-color-base-background);--ck-color-widget-drag-handler-icon-color:var(--ck-color-base-background)}.ck .ck-widget{outline-color:transparent;outline-style:solid;outline-width:var(--ck-widget-outline-thickness);transition:outline-color var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve)}.ck .ck-widget.ck-widget_selected,.ck .ck-widget.ck-widget_selected:hover{outline:var(--ck-widget-outline-thickness) solid var(--ck-color-focus-border)}.ck .ck-widget:hover{outline-color:var(--ck-color-widget-hover-border)}.ck .ck-editor__nested-editable{border:1px solid transparent}.ck .ck-editor__nested-editable.ck-editor__nested-editable_focused,.ck .ck-editor__nested-editable:focus{background-color:var(--ck-color-widget-editable-focus-background);border:var(--ck-focus-ring);box-shadow:var(--ck-inner-shadow),0 0;outline:none}.ck .ck-widget.ck-widget_with-selection-handle .ck-widget__selection-handle{background-color:transparent;border-radius:var(--ck-border-radius) var(--ck-border-radius) 0 0;box-sizing:border-box;left:calc(0px - var(--ck-widget-outline-thickness));opacity:0;padding:4px;top:0;transform:translateY(-100%);transition:background-color var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve),visibility var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve),opacity var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve)}.ck .ck-widget.ck-widget_with-selection-handle .ck-widget__selection-handle .ck-icon{color:var(--ck-color-widget-drag-handler-icon-color);height:var(--ck-widget-handler-icon-size);width:var(--ck-widget-handler-icon-size)}.ck .ck-widget.ck-widget_with-selection-handle .ck-widget__selection-handle .ck-icon .ck-icon__selected-indicator{opacity:0;transition:opacity .3s var(--ck-widget-handler-animation-curve)}.ck .ck-widget.ck-widget_with-selection-handle .ck-widget__selection-handle:hover .ck-icon .ck-icon__selected-indicator{opacity:1}.ck .ck-widget.ck-widget_with-selection-handle:hover>.ck-widget__selection-handle{background-color:var(--ck-color-widget-hover-border);opacity:1}.ck .ck-widget.ck-widget_with-selection-handle.ck-widget_selected:hover>.ck-widget__selection-handle,.ck .ck-widget.ck-widget_with-selection-handle.ck-widget_selected>.ck-widget__selection-handle{background-color:var(--ck-color-focus-border);opacity:1}.ck .ck-widget.ck-widget_with-selection-handle.ck-widget_selected:hover>.ck-widget__selection-handle .ck-icon .ck-icon__selected-indicator,.ck .ck-widget.ck-widget_with-selection-handle.ck-widget_selected>.ck-widget__selection-handle .ck-icon .ck-icon__selected-indicator{opacity:1}.ck[dir=rtl] .ck-widget.ck-widget_with-selection-handle .ck-widget__selection-handle{left:auto;right:calc(0px - var(--ck-widget-outline-thickness))}.ck.ck-editor__editable.ck-read-only .ck-widget{transition:none}.ck.ck-editor__editable.ck-read-only .ck-widget:not(.ck-widget_selected){--ck-widget-outline-thickness:0px}.ck.ck-editor__editable.ck-read-only .ck-widget.ck-widget_with-selection-handle .ck-widget__selection-handle,.ck.ck-editor__editable.ck-read-only .ck-widget.ck-widget_with-selection-handle .ck-widget__selection-handle:hover{background:var(--ck-color-widget-blurred-border)}.ck.ck-editor__editable.ck-blurred .ck-widget.ck-widget_selected,.ck.ck-editor__editable.ck-blurred .ck-widget.ck-widget_selected:hover{outline-color:var(--ck-color-widget-blurred-border)}.ck.ck-editor__editable.ck-blurred .ck-widget.ck-widget_selected.ck-widget_with-selection-handle:hover>.ck-widget__selection-handle,.ck.ck-editor__editable.ck-blurred .ck-widget.ck-widget_selected.ck-widget_with-selection-handle:hover>.ck-widget__selection-handle:hover,.ck.ck-editor__editable.ck-blurred .ck-widget.ck-widget_selected.ck-widget_with-selection-handle>.ck-widget__selection-handle,.ck.ck-editor__editable.ck-blurred .ck-widget.ck-widget_selected.ck-widget_with-selection-handle>.ck-widget__selection-handle:hover{background:var(--ck-color-widget-blurred-border)}.ck.ck-editor__editable blockquote>.ck-widget.ck-widget_with-selection-handle:first-child,.ck.ck-editor__editable>.ck-widget.ck-widget_with-selection-handle:first-child{margin-top:calc(1em + var(--ck-widget-handler-icon-size))}";
styleInject(css_248z$v);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The widget plugin. It enables base support for widgets.
 *
 * See {@glink api/widget package page} for more details and documentation.
 *
 * This plugin enables multiple behaviors required by widgets:
 *
 * * The model to view selection converter for the editing pipeline (it handles widget custom selection rendering).
 * If a converted selection wraps around a widget element, that selection is marked as
 * {@link module:engine/view/selection~Selection#isFake fake}. Additionally, the `ck-widget_selected` CSS class
 * is added to indicate that widget has been selected.
 * * The mouse and keyboard events handling on and around widget elements.
 *
 * @extends module:core/plugin~Plugin
 */
class Widget extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Widget';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ WidgetTypeAround, Delete ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const view = editor.editing.view;
		const viewDocument = view.document;

		/**
		 * Holds previously selected widgets.
		 *
		 * @private
		 * @type {Set.<module:engine/view/element~Element>}
		 */
		this._previouslySelected = new Set();

		// Model to view selection converter.
		// Converts selection placed over widget element to fake selection.
		//
		// By default, the selection is downcasted by the engine to surround the attribute element, even though its only
		// child is an inline widget. A similar thing also happens when a collapsed marker is rendered as a UI element
		// next to an inline widget: the view selection contains both the widget and the marker.
		//
		// This prevents creating a correct fake selection when this inline widget is selected. Normalize the selection
		// in these cases based on the model:
		//
		//		[<attributeElement><inlineWidget /></attributeElement>] -> <attributeElement>[<inlineWidget />]</attributeElement>
		//		[<uiElement></uiElement><inlineWidget />] -> <uiElement></uiElement>[<inlineWidget />]
		//
		// Thanks to this:
		//
		// * fake selection can be set correctly,
		// * any logic depending on (View)Selection#getSelectedElement() also works OK.
		//
		// See https://github.com/ckeditor/ckeditor5/issues/9524.
		this.editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => {
			const viewWriter = conversionApi.writer;
			const modelSelection = data.selection;

			// The collapsed selection can't contain any widget.
			if ( modelSelection.isCollapsed ) {
				return;
			}

			const selectedModelElement = modelSelection.getSelectedElement();

			if ( !selectedModelElement ) {
				return;
			}

			const selectedViewElement = editor.editing.mapper.toViewElement( selectedModelElement );

			if ( !isWidget( selectedViewElement ) ) {
				return;
			}

			if ( !conversionApi.consumable.consume( modelSelection, 'selection' ) ) {
				return;
			}

			viewWriter.setSelection( viewWriter.createRangeOn( selectedViewElement ), {
				fake: true,
				label: getLabel( selectedViewElement )
			} );
		} );

		// Mark all widgets inside the selection with the css class.
		// This handler is registered at the 'low' priority so it's triggered after the real selection conversion.
		this.editor.editing.downcastDispatcher.on( 'selection', ( evt, data, conversionApi ) => {
			// Remove selected class from previously selected widgets.
			this._clearPreviouslySelectedWidgets( conversionApi.writer );

			const viewWriter = conversionApi.writer;
			const viewSelection = viewWriter.document.selection;

			let lastMarked = null;

			for ( const range of viewSelection.getRanges() ) {
				// Note: There could be multiple selected widgets in a range but no fake selection.
				// All of them must be marked as selected, for instance [<widget></widget><widget></widget>]
				for ( const value of range ) {
					const node = value.item;
					// Do not mark nested widgets in selected one. See: #4594
					if ( isWidget( node ) && !isChild( node, lastMarked ) ) {
						viewWriter.addClass( WIDGET_SELECTED_CLASS_NAME, node );
						this._previouslySelected.add( node );
						lastMarked = node;
					}
				}
			}
		}, { priority: 'low' } );

		// If mouse down is pressed on widget - create selection over whole widget.
		view.addObserver( MouseObserver );
		this.listenTo( viewDocument, 'mousedown', ( ...args ) => this._onMousedown( ...args ) );

		// There are two keydown listeners working on different priorities. This allows other
		// features such as WidgetTypeAround or TableKeyboard to attach their listeners in between
		// and customize the behavior even further in different content/selection scenarios.
		//
		// * The first listener handles changing the selection on arrow key press
		// if the widget is selected or if the selection is next to a widget and the widget
		// should become selected upon the arrow key press.
		//
		// * The second (late) listener makes sure the default browser action on arrow key press is
		// prevented when a widget is selected. This prevents the selection from being moved
		// from a fake selection container.
		this.listenTo( viewDocument, 'arrowKey', ( ...args ) => {
			this._handleSelectionChangeOnArrowKeyPress( ...args );
		}, { context: [ isWidget, '$text' ] } );

		this.listenTo( viewDocument, 'arrowKey', ( ...args ) => {
			this._preventDefaultOnArrowKeyPress( ...args );
		}, { context: '$root' } );

		this.listenTo( viewDocument, 'arrowKey', verticalNavigationHandler( this.editor.editing ), { context: '$text' } );

		// Handle custom delete behaviour.
		this.listenTo( viewDocument, 'delete', ( evt, data ) => {
			if ( this._handleDelete( data.direction == 'forward' ) ) {
				data.preventDefault();
				evt.stop();
			}
		}, { context: '$root' } );
	}

	/**
	 * Handles {@link module:engine/view/document~Document#event:mousedown mousedown} events on widget elements.
	 *
	 * @private
	 * @param {module:utils/eventinfo~EventInfo} eventInfo
	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
	 */
	_onMousedown( eventInfo, domEventData ) {
		const editor = this.editor;
		const view = editor.editing.view;
		const viewDocument = view.document;
		let element = domEventData.target;

		// Do nothing for single or double click inside nested editable.
		if ( isInsideNestedEditable( element ) ) {
			// But at least triple click inside nested editable causes broken selection in Safari.
			// For such event, we select the entire nested editable element.
			// See: https://github.com/ckeditor/ckeditor5/issues/1463.
			if ( ( env.isSafari || env.isGecko ) && domEventData.domEvent.detail >= 3 ) {
				const mapper = editor.editing.mapper;
				const viewElement = element.is( 'attributeElement' ) ?
					element.findAncestor( element => !element.is( 'attributeElement' ) ) : element;
				const modelElement = mapper.toModelElement( viewElement );

				domEventData.preventDefault();

				this.editor.model.change( writer => {
					writer.setSelection( modelElement, 'in' );
				} );
			}

			return;
		}

		// If target is not a widget element - check if one of the ancestors is.
		if ( !isWidget( element ) ) {
			element = element.findAncestor( isWidget );

			if ( !element ) {
				return;
			}
		}

		// On Android selection would jump to the first table cell, on other devices
		// we can't block it (and don't need to) because of drag and drop support.
		if ( env.isAndroid ) {
			domEventData.preventDefault();
		}

		// Focus editor if is not focused already.
		if ( !viewDocument.isFocused ) {
			view.focus();
		}

		// Create model selection over widget.
		const modelElement = editor.editing.mapper.toModelElement( element );

		this._setSelectionOverElement( modelElement );
	}

	/**
	 * Handles {@link module:engine/view/document~Document#event:keydown keydown} events and changes
	 * the model selection when:
	 *
	 * * arrow key is pressed when the widget is selected,
	 * * the selection is next to a widget and the widget should become selected upon the arrow key press.
	 *
	 * See {@link #_preventDefaultOnArrowKeyPress}.
	 *
	 * @private
	 * @param {module:utils/eventinfo~EventInfo} eventInfo
	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
	 */
	_handleSelectionChangeOnArrowKeyPress( eventInfo, domEventData ) {
		const keyCode = domEventData.keyCode;

		const model = this.editor.model;
		const schema = model.schema;
		const modelSelection = model.document.selection;
		const objectElement = modelSelection.getSelectedElement();
		const direction = getLocalizedArrowKeyCodeDirection( keyCode, this.editor.locale.contentLanguageDirection );
		const isForward = direction == 'down' || direction == 'right';
		const isVerticalNavigation = direction == 'up' || direction == 'down';

		// If object element is selected.
		if ( objectElement && schema.isObject( objectElement ) ) {
			const position = isForward ? modelSelection.getLastPosition() : modelSelection.getFirstPosition();
			const newRange = schema.getNearestSelectionRange( position, isForward ? 'forward' : 'backward' );

			if ( newRange ) {
				model.change( writer => {
					writer.setSelection( newRange );
				} );

				domEventData.preventDefault();
				eventInfo.stop();
			}

			return;
		}

		// Handle collapsing of the selection when there is any widget on the edge of selection.
		// This is needed because browsers have problems with collapsing such selection.
		if ( !modelSelection.isCollapsed && !domEventData.shiftKey ) {
			const firstPosition = modelSelection.getFirstPosition();
			const lastPosition = modelSelection.getLastPosition();

			const firstSelectedNode = firstPosition.nodeAfter;
			const lastSelectedNode = lastPosition.nodeBefore;

			if ( firstSelectedNode && schema.isObject( firstSelectedNode ) || lastSelectedNode && schema.isObject( lastSelectedNode ) ) {
				model.change( writer => {
					writer.setSelection( isForward ? lastPosition : firstPosition );
				} );

				domEventData.preventDefault();
				eventInfo.stop();
			}

			return;
		}

		// Return if not collapsed.
		if ( !modelSelection.isCollapsed ) {
			return;
		}

		// If selection is next to object element.

		const objectElementNextToSelection = this._getObjectElementNextToSelection( isForward );

		if ( objectElementNextToSelection && schema.isObject( objectElementNextToSelection ) ) {
			// Do not select an inline widget while handling up/down arrow.
			if ( schema.isInline( objectElementNextToSelection ) && isVerticalNavigation ) {
				return;
			}

			this._setSelectionOverElement( objectElementNextToSelection );

			domEventData.preventDefault();
			eventInfo.stop();
		}
	}

	/**
	 * Handles {@link module:engine/view/document~Document#event:keydown keydown} events and prevents
	 * the default browser behavior to make sure the fake selection is not being moved from a fake selection
	 * container.
	 *
	 * See {@link #_handleSelectionChangeOnArrowKeyPress}.
	 *
	 * @private
	 * @param {module:utils/eventinfo~EventInfo} eventInfo
	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
	 */
	_preventDefaultOnArrowKeyPress( eventInfo, domEventData ) {
		const model = this.editor.model;
		const schema = model.schema;
		const objectElement = model.document.selection.getSelectedElement();

		// If object element is selected.
		if ( objectElement && schema.isObject( objectElement ) ) {
			domEventData.preventDefault();
			eventInfo.stop();
		}
	}

	/**
	 * Handles delete keys: backspace and delete.
	 *
	 * @private
	 * @param {Boolean} isForward Set to true if delete was performed in forward direction.
	 * @returns {Boolean|undefined} Returns `true` if keys were handled correctly.
	 */
	_handleDelete( isForward ) {
		// Do nothing when the read only mode is enabled.
		if ( this.editor.isReadOnly ) {
			return;
		}

		const modelDocument = this.editor.model.document;
		const modelSelection = modelDocument.selection;

		// Do nothing on non-collapsed selection.
		if ( !modelSelection.isCollapsed ) {
			return;
		}

		const objectElement = this._getObjectElementNextToSelection( isForward );

		if ( objectElement ) {
			this.editor.model.change( writer => {
				let previousNode = modelSelection.anchor.parent;

				// Remove previous element if empty.
				while ( previousNode.isEmpty ) {
					const nodeToRemove = previousNode;
					previousNode = nodeToRemove.parent;

					writer.remove( nodeToRemove );
				}

				this._setSelectionOverElement( objectElement );
			} );

			return true;
		}
	}

	/**
	 * Sets {@link module:engine/model/selection~Selection document's selection} over given element.
	 *
	 * @protected
	 * @param {module:engine/model/element~Element} element
	 */
	_setSelectionOverElement( element ) {
		this.editor.model.change( writer => {
			writer.setSelection( writer.createRangeOn( element ) );
		} );
	}

	/**
	 * Checks if {@link module:engine/model/element~Element element} placed next to the current
	 * {@link module:engine/model/selection~Selection model selection} exists and is marked in
	 * {@link module:engine/model/schema~Schema schema} as `object`.
	 *
	 * @protected
	 * @param {Boolean} forward Direction of checking.
	 * @returns {module:engine/model/element~Element|null}
	 */
	_getObjectElementNextToSelection( forward ) {
		const model = this.editor.model;
		const schema = model.schema;
		const modelSelection = model.document.selection;

		// Clone current selection to use it as a probe. We must leave default selection as it is so it can return
		// to its current state after undo.
		const probe = model.createSelection( modelSelection );
		model.modifySelection( probe, { direction: forward ? 'forward' : 'backward' } );

		// The selection didn't change so there is nothing there.
		if ( probe.isEqual( modelSelection ) ) {
			return null;
		}

		const objectElement = forward ? probe.focus.nodeBefore : probe.focus.nodeAfter;

		if ( !!objectElement && schema.isObject( objectElement ) ) {
			return objectElement;
		}

		return null;
	}

	/**
	 * Removes CSS class from previously selected widgets.
	 *
	 * @private
	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer
	 */
	_clearPreviouslySelectedWidgets( writer ) {
		for ( const widget of this._previouslySelected ) {
			writer.removeClass( WIDGET_SELECTED_CLASS_NAME, widget );
		}

		this._previouslySelected.clear();
	}
}

// Returns `true` when element is a nested editable or is placed inside one.
//
// @param {module:engine/view/element~Element}
// @returns {Boolean}
function isInsideNestedEditable( element ) {
	while ( element ) {
		if ( element.is( 'editableElement' ) && !element.is( 'rootElement' ) ) {
			return true;
		}

		// Click on nested widget should select it.
		if ( isWidget( element ) ) {
			return false;
		}

		element = element.parent;
	}

	return false;
}

// Checks whether the specified `element` is a child of the `parent` element.
//
// @param {module:engine/view/element~Element} element An element to check.
// @param {module:engine/view/element~Element|null} parent A parent for the element.
// @returns {Boolean}
function isChild( element, parent ) {
	if ( !parent ) {
		return false;
	}

	return Array.from( element.getAncestors() ).includes( parent );
}var css_248z$u = ".ck.ck-editor__editable .ck.ck-clipboard-drop-target-position{display:inline;pointer-events:none;position:relative}.ck.ck-editor__editable .ck.ck-clipboard-drop-target-position span{position:absolute;width:0}.ck.ck-editor__editable .ck-widget:-webkit-drag>.ck-widget__selection-handle,.ck.ck-editor__editable .ck-widget:-webkit-drag>.ck-widget__type-around{display:none}:root{--ck-clipboard-drop-target-dot-width:12px;--ck-clipboard-drop-target-dot-height:8px;--ck-clipboard-drop-target-color:var(--ck-color-focus-border)}.ck.ck-editor__editable .ck.ck-clipboard-drop-target-position span{background:var(--ck-clipboard-drop-target-color);border:1px solid var(--ck-clipboard-drop-target-color);bottom:calc(var(--ck-clipboard-drop-target-dot-height)*-.5);margin-left:-1px;top:calc(var(--ck-clipboard-drop-target-dot-height)*-.5)}.ck.ck-editor__editable .ck.ck-clipboard-drop-target-position span:after{border-color:var(--ck-clipboard-drop-target-color) transparent transparent transparent;border-style:solid;border-width:calc(var(--ck-clipboard-drop-target-dot-height)) calc(var(--ck-clipboard-drop-target-dot-width)*.5) 0 calc(var(--ck-clipboard-drop-target-dot-width)*.5);content:\"\";display:block;height:0;left:50%;position:absolute;top:calc(var(--ck-clipboard-drop-target-dot-height)*-.5);transform:translateX(-50%);width:0}.ck.ck-editor__editable .ck-widget.ck-clipboard-drop-target-range{outline:var(--ck-widget-outline-thickness) solid var(--ck-clipboard-drop-target-color)!important}.ck.ck-editor__editable .ck-widget:-webkit-drag{zoom:.6;outline:none!important}";
styleInject(css_248z$u);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

// Drag and drop events overview:
//
//                ┌──────────────────┐
//                │     mousedown    │   Sets the draggable attribute.
//                └─────────┬────────┘
//                          │
//                          └─────────────────────┐
//                          │                     │
//                          │           ┌─────────V────────┐
//                          │           │      mouseup     │   Dragging did not start, removes the draggable attribute.
//                          │           └──────────────────┘
//                          │
//                ┌─────────V────────┐   Retrieves the selected model.DocumentFragment
//                │     dragstart    │   and converts it to view.DocumentFragment.
//                └─────────┬────────┘
//                          │
//                ┌─────────V────────┐   Processes view.DocumentFragment to text/html and text/plain
//                │  clipboardOutput │   and stores the results in data.dataTransfer.
//                └─────────┬────────┘
//                          │
//                          │   DOM dragover
//                          ┌────────────┐
//                          │            │
//                ┌─────────V────────┐   │
//                │     dragging     │   │   Updates the drop target marker.
//                └─────────┬────────┘   │
//                          │            │
//            ┌─────────────└────────────┘
//            │             │            │
//            │   ┌─────────V────────┐   │
//            │   │     dragleave    │   │   Removes the drop target marker.
//            │   └─────────┬────────┘   │
//            │             │            │
//        ┌───│─────────────┘            │
//        │   │             │            │
//        │   │   ┌─────────V────────┐   │
//        │   │   │     dragenter    │   │   Focuses the editor view.
//        │   │   └─────────┬────────┘   │
//        │   │             │            │
//        │   │             └────────────┘
//        │   │
//        │   └─────────────┐
//        │   │             │
//        │   │   ┌─────────V────────┐
//        └───┐   │       drop       │   (The default handler of the clipboard pipeline).
//            │   └─────────┬────────┘
//            │             │
//            │   ┌─────────V────────┐   Resolves the final data.targetRanges.
//            │   │  clipboardInput  │   Aborts if dropping on dragged content.
//            │   └─────────┬────────┘
//            │             │
//            │   ┌─────────V────────┐
//            │   │  clipboardInput  │   (The default handler of the clipboard pipeline).
//            │   └─────────┬────────┘
//            │             │
//            │ ┌───────────V───────────┐
//            │ │  inputTransformation  │   (The default handler of the clipboard pipeline).
//            │ └───────────┬───────────┘
//            │             │
//            │  ┌──────────V──────────┐
//            │  │   contentInsertion  │   Updates the document selection to drop range.
//            │  └──────────┬──────────┘
//            │             │
//            │  ┌──────────V──────────┐
//            │  │   contentInsertion  │   (The default handler of the clipboard pipeline).
//            │  └──────────┬──────────┘
//            │             │
//            │  ┌──────────V──────────┐
//            │  │   contentInsertion  │   Removes the content from the original range if the insertion was successful.
//            │  └──────────┬──────────┘
//            │             │
//            └─────────────┐
//                          │
//                ┌─────────V────────┐
//                │      dragend     │   Removes the drop marker and cleans the state.
//                └──────────────────┘
//

/**
 * The drag and drop feature. It works on top of the {@link module:clipboard/clipboardpipeline~ClipboardPipeline}.
 *
 * Read more about the clipboard integration in the {@glink framework/guides/deep-dive/clipboard clipboard deep dive guide}.
 *
 * @extends module:core/plugin~Plugin
 */
class DragDrop extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'DragDrop';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ ClipboardPipeline, Widget ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const view = editor.editing.view;

		/**
		 * The live range over the original content that is being dragged.
		 *
		 * @private
		 * @type {module:engine/model/liverange~LiveRange}
		 */
		this._draggedRange = null;

		/**
		 * The UID of current dragging that is used to verify if the drop started in the same editor as the drag start.
		 *
		 * **Note**: This is a workaround for broken 'dragend' events (they are not fired if the source text node got removed).
		 *
		 * @private
		 * @type {String}
		 */
		this._draggingUid = '';

		/**
		 * The reference to the model element that currently has a `draggable` attribute set (it is set while dragging).
		 *
		 * @private
		 * @type {module:engine/model/element~Element}
		 */
		this._draggableElement = null;

		/**
		 * A throttled callback updating the drop marker.
		 *
		 * @private
		 * @type {Function}
		 */
		this._updateDropMarkerThrottled = throttle( targetRange => this._updateDropMarker( targetRange ), 40 );

		/**
		 * A delayed callback removing the drop marker.
		 *
		 * @private
		 * @type {Function}
		 */
		this._removeDropMarkerDelayed = delay( () => this._removeDropMarker(), 40 );

		/**
		 * A delayed callback removing draggable attributes.
		 *
		 * @private
		 * @type {Function}
		 */
		this._clearDraggableAttributesDelayed = delay( () => this._clearDraggableAttributes(), 40 );

		view.addObserver( ClipboardObserver );
		view.addObserver( MouseObserver );

		this._setupDragging();
		this._setupContentInsertionIntegration();
		this._setupClipboardInputIntegration();
		this._setupDropMarker();
		this._setupDraggableAttributeHandling();

		this.listenTo( editor, 'change:isReadOnly', ( evt, name, isReadOnly ) => {
			if ( isReadOnly ) {
				this.forceDisabled( 'readOnlyMode' );
			} else {
				this.clearForceDisabled( 'readOnlyMode' );
			}
		} );

		this.on( 'change:isEnabled', ( evt, name, isEnabled ) => {
			if ( !isEnabled ) {
				this._finalizeDragging( false );
			}
		} );

		if ( env.isAndroid ) {
			this.forceDisabled( 'noAndroidSupport' );
		}
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		if ( this._draggedRange ) {
			this._draggedRange.detach();
			this._draggedRange = null;
		}

		this._updateDropMarkerThrottled.cancel();
		this._removeDropMarkerDelayed.cancel();
		this._clearDraggableAttributesDelayed.cancel();

		return super.destroy();
	}

	/**
	 * Drag and drop events handling.
	 *
	 * @private
	 */
	_setupDragging() {
		const editor = this.editor;
		const model = editor.model;
		const modelDocument = model.document;
		const view = editor.editing.view;
		const viewDocument = view.document;

		// The handler for the drag start; it is responsible for setting data transfer object.
		this.listenTo( viewDocument, 'dragstart', ( evt, data ) => {
			const selection = modelDocument.selection;

			// Don't drag the editable element itself.
			if ( data.target && data.target.is( 'editableElement' ) ) {
				data.preventDefault();

				return;
			}

			// TODO we could clone this node somewhere and style it to match editing view but without handles,
			//  selection outline, WTA buttons, etc.
			// data.dataTransfer._native.setDragImage( data.domTarget, 0, 0 );

			// Check if this is dragstart over the widget (but not a nested editable).
			const draggableWidget = data.target ? findDraggableWidget( data.target ) : null;

			if ( draggableWidget ) {
				const modelElement = editor.editing.mapper.toModelElement( draggableWidget );

				this._draggedRange = LiveRange.fromRange( model.createRangeOn( modelElement ) );

				// Disable toolbars so they won't obscure the drop area.
				if ( editor.plugins.has( 'WidgetToolbarRepository' ) ) {
					editor.plugins.get( 'WidgetToolbarRepository' ).forceDisabled( 'dragDrop' );
				}
			}

			// If this was not a widget we should check if we need to drag some text content.
			else if ( !viewDocument.selection.isCollapsed ) {
				const selectedElement = viewDocument.selection.getSelectedElement();

				if ( !selectedElement || !isWidget( selectedElement ) ) {
					this._draggedRange = LiveRange.fromRange( selection.getFirstRange() );
				}
			}

			if ( !this._draggedRange ) {
				data.preventDefault();

				return;
			}

			this._draggingUid = uid();

			data.dataTransfer.effectAllowed = this.isEnabled ? 'copyMove' : 'copy';
			data.dataTransfer.setData( 'application/ckeditor5-dragging-uid', this._draggingUid );

			const draggedSelection = model.createSelection( this._draggedRange.toRange() );
			const content = editor.data.toView( model.getSelectedContent( draggedSelection ) );

			viewDocument.fire( 'clipboardOutput', { dataTransfer: data.dataTransfer, content, method: evt.name } );

			if ( !this.isEnabled ) {
				this._draggedRange.detach();
				this._draggedRange = null;
				this._draggingUid = '';
			}
		}, { priority: 'low' } );

		// The handler for finalizing drag and drop. It should always be triggered after dragging completes
		// even if it was completed in a different application.
		// Note: This is not fired if source text node got removed while downcasting a marker.
		this.listenTo( viewDocument, 'dragend', ( evt, data ) => {
			this._finalizeDragging( !data.dataTransfer.isCanceled && data.dataTransfer.dropEffect == 'move' );
		}, { priority: 'low' } );

		// Dragging over the editable.
		this.listenTo( viewDocument, 'dragenter', () => {
			if ( !this.isEnabled ) {
				return;
			}

			view.focus();
		} );

		// Dragging out of the editable.
		this.listenTo( viewDocument, 'dragleave', () => {
			// We do not know if the mouse left the editor or just some element in it, so let us wait a few milliseconds
			// to check if 'dragover' is not fired.
			this._removeDropMarkerDelayed();
		} );

		// Handler for moving dragged content over the target area.
		this.listenTo( viewDocument, 'dragging', ( evt, data ) => {
			if ( !this.isEnabled ) {
				data.dataTransfer.dropEffect = 'none';

				return;
			}

			this._removeDropMarkerDelayed.cancel();

			const targetRange = findDropTargetRange( editor, data.targetRanges, data.target );

			// If this is content being dragged from another editor, moving out of current editor instance
			// is not possible until 'dragend' event case will be fixed.
			if ( !this._draggedRange ) {
				data.dataTransfer.dropEffect = 'copy';
			}

			// In Firefox it is already set and effect allowed remains the same as originally set.
			if ( !env.isGecko ) {
				if ( data.dataTransfer.effectAllowed == 'copy' ) {
					data.dataTransfer.dropEffect = 'copy';
				} else if ( [ 'all', 'copyMove' ].includes( data.dataTransfer.effectAllowed ) ) {
					data.dataTransfer.dropEffect = 'move';
				}
			}

			/* istanbul ignore else */
			if ( targetRange ) {
				this._updateDropMarkerThrottled( targetRange );
			}
		}, { priority: 'low' } );
	}

	/**
	 * Integration with the `clipboardInput` event.
	 *
	 * @private
	 */
	_setupClipboardInputIntegration() {
		const editor = this.editor;
		const view = editor.editing.view;
		const viewDocument = view.document;

		// Update the event target ranges and abort dropping if dropping over itself.
		this.listenTo( viewDocument, 'clipboardInput', ( evt, data ) => {
			if ( data.method != 'drop' ) {
				return;
			}

			const targetRange = findDropTargetRange( editor, data.targetRanges, data.target );

			// The dragging markers must be removed after searching for the target range because sometimes
			// the target lands on the marker itself.
			this._removeDropMarker();

			/* istanbul ignore if */
			if ( !targetRange ) {
				this._finalizeDragging( false );
				evt.stop();

				return;
			}

			// Since we cannot rely on the drag end event, we must check if the local drag range is from the current drag and drop
			// or it is from some previous not cleared one.
			if ( this._draggedRange && this._draggingUid != data.dataTransfer.getData( 'application/ckeditor5-dragging-uid' ) ) {
				this._draggedRange.detach();
				this._draggedRange = null;
				this._draggingUid = '';
			}

			// Do not do anything if some content was dragged within the same document to the same position.
			const isMove = getFinalDropEffect( data.dataTransfer ) == 'move';

			if ( isMove && this._draggedRange && this._draggedRange.containsRange( targetRange, true ) ) {
				this._finalizeDragging( false );
				evt.stop();

				return;
			}

			// Override the target ranges with the one adjusted to the best one for a drop.
			data.targetRanges = [ editor.editing.mapper.toViewRange( targetRange ) ];
		}, { priority: 'high' } );
	}

	/**
	 * Integration with the `contentInsertion` event of the clipboard pipeline.
	 *
	 * @private
	 */
	_setupContentInsertionIntegration() {
		const clipboardPipeline = this.editor.plugins.get( ClipboardPipeline );

		clipboardPipeline.on( 'contentInsertion', ( evt, data ) => {
			if ( !this.isEnabled || data.method !== 'drop' ) {
				return;
			}

			// Update the selection to the target range in the same change block to avoid selection post-fixing
			// and to be able to clone text attributes for plain text dropping.
			const ranges = data.targetRanges.map( viewRange => this.editor.editing.mapper.toModelRange( viewRange ) );

			this.editor.model.change( writer => writer.setSelection( ranges ) );
		}, { priority: 'high' } );

		clipboardPipeline.on( 'contentInsertion', ( evt, data ) => {
			if ( !this.isEnabled || data.method !== 'drop' ) {
				return;
			}

			// Remove dragged range content, remove markers, clean after dragging.
			const isMove = getFinalDropEffect( data.dataTransfer ) == 'move';

			// Whether any content was inserted (insertion might fail if the schema is disallowing some elements
			// (for example an image caption allows only the content of a block but not blocks themselves.
			// Some integrations might not return valid range (i.e., table pasting).
			const isSuccess = !data.resultRange || !data.resultRange.isCollapsed;

			this._finalizeDragging( isSuccess && isMove );
		}, { priority: 'lowest' } );
	}

	/**
	 * Adds listeners that add the `draggable` attribute to the elements while the mouse button is down so the dragging could start.
	 *
	 * @private
	 */
	_setupDraggableAttributeHandling() {
		const editor = this.editor;
		const view = editor.editing.view;
		const viewDocument = view.document;

		// Add the 'draggable' attribute to the widget while pressing the selection handle.
		// This is required for widgets to be draggable. In Chrome it will enable dragging text nodes.
		this.listenTo( viewDocument, 'mousedown', ( evt, data ) => {
			// The lack of data can be caused by editor tests firing fake mouse events. This should not occur
			// in real-life scenarios but this greatly simplifies editor tests that would otherwise fail a lot.
			if ( env.isAndroid || !data ) {
				return;
			}

			this._clearDraggableAttributesDelayed.cancel();

			// Check if this is a mousedown over the widget (but not a nested editable).
			let draggableElement = findDraggableWidget( data.target );

			// Note: There is a limitation that if more than a widget is selected (a widget and some text)
			// and dragging starts on the widget, then only the widget is dragged.

			// If this was not a widget then we should check if we need to drag some text content.
			// In Chrome set a 'draggable' attribute on closest editable to allow immediate dragging of the selected text range.
			// In Firefox this is not needed. In Safari it makes the whole editable draggable (not just textual content).
			// Disabled in read-only mode because draggable="true" + contenteditable="false" results
			// in not firing selectionchange event ever, which makes the selection stuck in read-only mode.
			if ( env.isBlink && !editor.isReadOnly && !draggableElement && !viewDocument.selection.isCollapsed ) {
				const selectedElement = viewDocument.selection.getSelectedElement();

				if ( !selectedElement || !isWidget( selectedElement ) ) {
					draggableElement = viewDocument.selection.editableElement;
				}
			}

			if ( draggableElement ) {
				view.change( writer => {
					writer.setAttribute( 'draggable', 'true', draggableElement );
				} );

				// Keep the reference to the model element in case the view element gets removed while dragging.
				this._draggableElement = editor.editing.mapper.toModelElement( draggableElement );
			}
		} );

		// Remove the draggable attribute in case no dragging started (only mousedown + mouseup).
		this.listenTo( viewDocument, 'mouseup', () => {
			if ( !env.isAndroid ) {
				this._clearDraggableAttributesDelayed();
			}
		} );
	}

	/**
	 * Removes the `draggable` attribute from the element that was used for dragging.
	 *
	 * @private
	 */
	_clearDraggableAttributes() {
		const editing = this.editor.editing;

		editing.view.change( writer => {
			// Remove 'draggable' attribute.
			if ( this._draggableElement && this._draggableElement.root.rootName != '$graveyard' ) {
				writer.removeAttribute( 'draggable', editing.mapper.toViewElement( this._draggableElement ) );
			}

			this._draggableElement = null;
		} );
	}

	/**
	 * Creates downcast conversion for the drop target marker.
	 *
	 * @private
	 */
	_setupDropMarker() {
		const editor = this.editor;

		// Drop marker conversion for hovering over widgets.
		editor.conversion.for( 'editingDowncast' ).markerToHighlight( {
			model: 'drop-target',
			view: {
				classes: [ 'ck-clipboard-drop-target-range' ]
			}
		} );

		// Drop marker conversion for in text drop target.
		editor.conversion.for( 'editingDowncast' ).markerToElement( {
			model: 'drop-target',
			view: ( data, { writer } ) => {
				const inText = editor.model.schema.checkChild( data.markerRange.start, '$text' );

				if ( !inText ) {
					return;
				}

				return writer.createUIElement( 'span', { class: 'ck ck-clipboard-drop-target-position' }, function( domDocument ) {
					const domElement = this.toDomElement( domDocument );

					// Using word joiner to make this marker as high as text and also making text not break on marker.
					domElement.append( '\u2060', domDocument.createElement( 'span' ), '\u2060' );

					return domElement;
				} );
			}
		} );
	}

	/**
	 * Updates the drop target marker to the provided range.
	 *
	 * @private
	 * @param {module:engine/model/range~Range} targetRange The range to set the marker to.
	 */
	_updateDropMarker( targetRange ) {
		const editor = this.editor;
		const markers = editor.model.markers;

		editor.model.change( writer => {
			if ( markers.has( 'drop-target' ) ) {
				if ( !markers.get( 'drop-target' ).getRange().isEqual( targetRange ) ) {
					writer.updateMarker( 'drop-target', { range: targetRange } );
				}
			} else {
				writer.addMarker( 'drop-target', {
					range: targetRange,
					usingOperation: false,
					affectsData: false
				} );
			}
		} );
	}

	/**
	 * Removes the drop target marker.
	 *
	 * @private
	 */
	_removeDropMarker() {
		const model = this.editor.model;

		this._removeDropMarkerDelayed.cancel();
		this._updateDropMarkerThrottled.cancel();

		if ( model.markers.has( 'drop-target' ) ) {
			model.change( writer => {
				writer.removeMarker( 'drop-target' );
			} );
		}
	}

	/**
	 * Deletes the dragged content from its original range and clears the dragging state.
	 *
	 * @private
	 * @param {Boolean} moved Whether the move succeeded.
	 */
	_finalizeDragging( moved ) {
		const editor = this.editor;
		const model = editor.model;

		this._removeDropMarker();
		this._clearDraggableAttributes();

		if ( editor.plugins.has( 'WidgetToolbarRepository' ) ) {
			editor.plugins.get( 'WidgetToolbarRepository' ).clearForceDisabled( 'dragDrop' );
		}

		this._draggingUid = '';

		if ( !this._draggedRange ) {
			return;
		}

		// Delete moved content.
		if ( moved && this.isEnabled ) {
			model.deleteContent( model.createSelection( this._draggedRange ), { doNotAutoparagraph: true } );
		}

		this._draggedRange.detach();
		this._draggedRange = null;
	}
}

// Returns fixed selection range for given position and target element.
//
// @param {module:core/editor/editor~Editor} editor
// @param {Array.<module:engine/view/range~Range>} targetViewRanges
// @param {module:engine/view/element~Element} targetViewElement
// @returns {module:engine/model/range~Range|null}
function findDropTargetRange( editor, targetViewRanges, targetViewElement ) {
	const model = editor.model;
	const mapper = editor.editing.mapper;

	let range = null;

	const targetViewPosition = targetViewRanges ? targetViewRanges[ 0 ].start : null;

	// A UIElement is not a valid drop element, use parent (this could be a drop marker or any other UIElement).
	if ( targetViewElement.is( 'uiElement' ) ) {
		targetViewElement = targetViewElement.parent;
	}

	// Quick win if the target is a widget (but not a nested editable).
	range = findDropTargetRangeOnWidget( editor, targetViewElement );

	if ( range ) {
		return range;
	}

	// The easiest part is over, now we need to move to the model space.

	// Find target model element and position.
	const targetModelElement = getClosestMappedModelElement( editor, targetViewElement );
	const targetModelPosition = targetViewPosition ? mapper.toModelPosition( targetViewPosition ) : null;

	// There is no target position while hovering over an empty table cell.
	// In Safari, target position can be empty while hovering over a widget (e.g., a page-break).
	// Find the drop position inside the element.
	if ( !targetModelPosition ) {
		return findDropTargetRangeInElement( editor, targetModelElement );
	}

	// Check if target position is between blocks and adjust drop position to the next object.
	// This is because while hovering over a root element next to a widget the target position can jump in crazy places.
	range = findDropTargetRangeBetweenBlocks( editor, targetModelPosition, targetModelElement );

	if ( range ) {
		return range;
	}

	// Try fixing selection position.
	// In Firefox, the target position lands before widgets but in other browsers it tends to land after a widget.
	range = model.schema.getNearestSelectionRange( targetModelPosition, env.isGecko ? 'forward' : 'backward' );

	if ( range ) {
		return range;
	}

	// There is no valid selection position inside the current limit element so find a closest object ancestor.
	// This happens if the model position lands directly in the <table> element itself (view target element was a `<td>`
	// so a nested editable, but view target position was directly in the `<figure>` element).
	return findDropTargetRangeOnAncestorObject( editor, targetModelPosition.parent );
}

// Returns fixed selection range for a given position and a target element if it is over the widget but not over its nested editable.
//
// @param {module:core/editor/editor~Editor} editor
// @param {module:engine/view/element~Element} targetViewElement
// @returns {module:engine/model/range~Range|null}
function findDropTargetRangeOnWidget( editor, targetViewElement ) {
	const model = editor.model;
	const mapper = editor.editing.mapper;

	// Quick win if the target is a widget.
	if ( isWidget( targetViewElement ) ) {
		return model.createRangeOn( mapper.toModelElement( targetViewElement ) );
	}

	// Check if we are deeper over a widget (but not over a nested editable).
	if ( !targetViewElement.is( 'editableElement' ) ) {
		// Find a closest ancestor that is either a widget or an editable element...
		const ancestor = targetViewElement.findAncestor( node => isWidget( node ) || node.is( 'editableElement' ) );

		// ...and if the widget was closer then it is a drop target.
		if ( isWidget( ancestor ) ) {
			return model.createRangeOn( mapper.toModelElement( ancestor ) );
		}
	}

	return null;
}

// Returns fixed selection range inside a model element.
//
// @param {module:core/editor/editor~Editor} editor
// @param {module:engine/model/element~Element} targetModelElement
// @returns {module:engine/model/range~Range}
function findDropTargetRangeInElement( editor, targetModelElement ) {
	const model = editor.model;
	const schema = model.schema;

	const positionAtElementStart = model.createPositionAt( targetModelElement, 0 );

	return schema.getNearestSelectionRange( positionAtElementStart, 'forward' );
}

// Returns fixed selection range for a given position and a target element if the drop is between blocks.
//
// @param {module:core/editor/editor~Editor} editor
// @param {module:engine/model/position~Position} targetModelPosition
// @param {module:engine/model/element~Element} targetModelElement
// @returns {module:engine/model/range~Range|null}
function findDropTargetRangeBetweenBlocks( editor, targetModelPosition, targetModelElement ) {
	const model = editor.model;

	// Check if target is between blocks.
	if ( !model.schema.checkChild( targetModelElement, '$block' ) ) {
		return null;
	}

	// Find position between blocks.
	const positionAtElementStart = model.createPositionAt( targetModelElement, 0 );

	// Get the common part of the path (inside the target element and the target position).
	const commonPath = targetModelPosition.path.slice( 0, positionAtElementStart.path.length );

	// Position between the blocks.
	const betweenBlocksPosition = model.createPositionFromPath( targetModelPosition.root, commonPath );
	const nodeAfter = betweenBlocksPosition.nodeAfter;

	// Adjust drop position to the next object.
	// This is because while hovering over a root element next to a widget the target position can jump in crazy places.
	if ( nodeAfter && model.schema.isObject( nodeAfter ) ) {
		return model.createRangeOn( nodeAfter );
	}

	return null;
}

// Returns a selection range on the ancestor object.
//
// @param {module:core/editor/editor~Editor} editor
// @param {module:engine/model/element~Element} element
// @returns {module:engine/model/range~Range}
function findDropTargetRangeOnAncestorObject( editor, element ) {
	const model = editor.model;

	while ( element ) {
		if ( model.schema.isObject( element ) ) {
			return model.createRangeOn( element );
		}

		element = element.parent;
	}
}

// Returns the closest model element for the specified view element.
//
// @param {module:core/editor/editor~Editor} editor
// @param {module:engine/view/element~Element} element
// @returns {module:engine/model/element~Element}
function getClosestMappedModelElement( editor, element ) {
	const mapper = editor.editing.mapper;
	const view = editor.editing.view;

	const targetModelElement = mapper.toModelElement( element );

	if ( targetModelElement ) {
		return targetModelElement;
	}

	// Find mapped ancestor if the target is inside not mapped element (for example inline code element).
	const viewPosition = view.createPositionBefore( element );
	const viewElement = mapper.findMappedViewAncestor( viewPosition );

	return mapper.toModelElement( viewElement );
}

// Returns the drop effect that should be a result of dragging the content.
// This function is handling a quirk when checking the effect in the 'drop' DOM event.
function getFinalDropEffect( dataTransfer ) {
	if ( env.isGecko ) {
		return dataTransfer.dropEffect;
	}

	return [ 'all', 'copyMove' ].includes( dataTransfer.effectAllowed ) ? 'move' : 'copy';
}

// Returns a function wrapper that will trigger a function after a specified wait time.
// The timeout can be canceled by calling the cancel function on the returned wrapped function.
//
// @param {Function} func The function to wrap.
// @param {Number} wait The timeout in ms.
// @returns {Function}
function delay( func, wait ) {
	let timer;

	function delayed( ...args ) {
		delayed.cancel();
		timer = setTimeout( () => func( ...args ), wait );
	}

	delayed.cancel = () => {
		clearTimeout( timer );
	};

	return delayed;
}

// Returns a widget element that should be dragged.
//
// @param {module:engine/view/element~Element} target
// @returns {module:engine/view/element~Element}
function findDraggableWidget( target ) {
	// This is directly an editable so not a widget for sure.
	if ( target.is( 'editableElement' ) ) {
		return null;
	}

	// TODO: Let's have a isWidgetSelectionHandleDomElement() helper in ckeditor5-widget utils.
	if ( target.hasClass( 'ck-widget__selection-handle' ) ) {
		return target.findAncestor( isWidget );
	}

	// Direct hit on a widget.
	if ( isWidget( target ) ) {
		return target;
	}

	// Find closest ancestor that is either a widget or an editable element...
	const ancestor = target.findAncestor( node => isWidget( node ) || node.is( 'editableElement' ) );

	// ...and if closer was the widget then enable dragging it.
	if ( isWidget( ancestor ) ) {
		return ancestor;
	}

	return null;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The plugin detects the user's intention to paste plain text.
 *
 * For example, it detects the <kbd>Ctrl/Cmd</kbd> + <kbd>Shift</kbd> + <kbd>V</kbd> keystroke.
 *
 * @extends module:core/plugin~Plugin
 */
class PastePlainText extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'PastePlainText';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ ClipboardPipeline ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const model = editor.model;
		const view = editor.editing.view;
		const viewDocument = view.document;
		const selection = model.document.selection;

		let shiftPressed = false;

		view.addObserver( ClipboardObserver );

		this.listenTo( viewDocument, 'keydown', ( evt, data ) => {
			shiftPressed = data.shiftKey;
		} );

		editor.plugins.get( ClipboardPipeline ).on( 'contentInsertion', ( evt, data ) => {
			// Plain text can be determined based on the event flag (#7799) or auto-detection (#1006). If detected,
			// preserve selection attributes on pasted items.
			if ( !shiftPressed && !isPlainTextFragment( data.content, model.schema ) ) {
				return;
			}

			model.change( writer => {
				// Formatting attributes should be preserved.
				const textAttributes = Array.from( selection.getAttributes() )
					.filter( ( [ key ] ) => model.schema.getAttributeProperties( key ).isFormatting );

				if ( !selection.isCollapsed ) {
					model.deleteContent( selection, { doNotAutoparagraph: true } );
				}

				// Also preserve other attributes if they survived the content deletion (because they were not fully selected).
				// For example linkHref is not a formatting attribute but it should be preserved if pasted text was in the middle
				// of a link.
				textAttributes.push( ...selection.getAttributes() );

				const range = writer.createRangeIn( data.content );

				for ( const item of range.getItems() ) {
					if ( item.is( '$textProxy' ) ) {
						writer.setAttributes( textAttributes, item );
					}
				}
			} );
		} );
	}
}

// Returns true if specified `documentFragment` represents a plain text.
//
// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment
// @param {module:engine/model/schema~Schema} schema
// @returns {Boolean}
function isPlainTextFragment( documentFragment, schema ) {
	if ( documentFragment.childCount > 1 ) {
		return false;
	}

	const child = documentFragment.getChild( 0 );

	if ( schema.isObject( child ) ) {
		return false;
	}

	return [ ...child.getAttributeKeys() ].length == 0;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The clipboard feature.
 *
 * Read more about the clipboard integration in the {@glink framework/guides/deep-dive/clipboard clipboard deep dive guide}.
 *
 * This is a "glue" plugin which loads the following plugins:
 * * {@link module:clipboard/clipboardpipeline~ClipboardPipeline}
 * * {@link module:clipboard/dragdrop~DragDrop}
 * * {@link module:clipboard/pasteplaintext~PastePlainText}
 *
 * @extends module:core/plugin~Plugin
 */
class Clipboard extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Clipboard';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ ClipboardPipeline, DragDrop, PastePlainText ];
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */var index$3=/*#__PURE__*/Object.freeze({__proto__:null,Clipboard:Clipboard,ClipboardPipeline:ClipboardPipeline,DragDrop:DragDrop,PastePlainText:PastePlainText});/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The select all command.
 *
 * It is used by the {@link module:select-all/selectallediting~SelectAllEditing select all editing feature} to handle
 * the <kbd>Ctrl/⌘</kbd>+<kbd>A</kbd> keystroke.
 *
 * Executing this command changes the {@glink framework/guides/architecture/editing-engine#model model}
 * selection so it contains the entire content of the editable root of the editor the selection is
 * {@link module:engine/model/selection~Selection#anchor anchored} in.
 *
 * If the selection was anchored in a {@glink framework/guides/tutorials/implementing-a-block-widget nested editable}
 * (e.g. a caption of an image), the new selection will contain its entire content. Successive executions of this command
 * will expand the selection to encompass more and more content up to the entire editable root of the editor.
 *
 * @extends module:core/command~Command
 */
class SelectAllCommand extends Command {
	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		// It does not affect data so should be enabled in read-only mode.
		this.affectsData = false;
	}

	/**
	 * @inheritDoc
	 */
	execute() {
		const model = this.editor.model;
		const selection = model.document.selection;
		let scopeElement = model.schema.getLimitElement( selection );

		// If an entire scope is selected, or the selection's ancestor is not a scope yet,
		// browse through ancestors to find the enclosing parent scope.
		if ( selection.containsEntireContent( scopeElement ) || !isSelectAllScope( model.schema, scopeElement ) ) {
			do {
				scopeElement = scopeElement.parent;

				// Do nothing, if the entire `root` is already selected.
				if ( !scopeElement ) {
					return;
				}
			} while ( !isSelectAllScope( model.schema, scopeElement ) );
		}

		model.change( writer => {
			writer.setSelection( scopeElement, 'in' );
		} );
	}
}

// Checks whether the element is a valid select-all scope.
// Returns true, if the element is a {@link module:engine/model/schema~Schema#isLimit limit},
// and can contain any text or paragraph.
//
// @param {module:engine/model/schema~Schema} schema The schema to check against.
// @param {module:engine/model/element~Element} element
// @return {Boolean}
function isSelectAllScope( schema, element ) {
	return schema.isLimit( element ) && ( schema.checkChild( element, '$text' ) || schema.checkChild( element, 'paragraph' ) );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const SELECT_ALL_KEYSTROKE = parseKeystroke( 'Ctrl+A' );

/**
 * The select all editing feature.
 *
 * It registers the `'selectAll'` {@link module:select-all/selectallcommand~SelectAllCommand command}
 * and the <kbd>Ctrl/⌘</kbd>+<kbd>A</kbd> keystroke listener which executes it.
 *
 * @extends module:core/plugin~Plugin
 */
class SelectAllEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SelectAllEditing';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const view = editor.editing.view;
		const viewDocument = view.document;

		editor.commands.add( 'selectAll', new SelectAllCommand( editor ) );

		this.listenTo( viewDocument, 'keydown', ( eventInfo, domEventData ) => {
			if ( getCode( domEventData ) === SELECT_ALL_KEYSTROKE ) {
				editor.execute( 'selectAll' );
				domEventData.preventDefault();
			}
		} );
	}
}var selectAllIcon = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\"><path d=\"M.75 15.5a.75.75 0 0 1 .75.75V18l.008.09A.5.5 0 0 0 2 18.5h1.75a.75.75 0 1 1 0 1.5H1.5l-.144-.007a1.5 1.5 0 0 1-1.35-1.349L0 18.5v-2.25a.75.75 0 0 1 .75-.75zm18.5 0a.75.75 0 0 1 .75.75v2.25l-.007.144a1.5 1.5 0 0 1-1.349 1.35L18.5 20h-2.25a.75.75 0 1 1 0-1.5H18a.5.5 0 0 0 .492-.41L18.5 18v-1.75a.75.75 0 0 1 .75-.75zm-10.45 3c.11 0 .2.09.2.2v1.1a.2.2 0 0 1-.2.2H7.2a.2.2 0 0 1-.2-.2v-1.1c0-.11.09-.2.2-.2h1.6zm4 0c.11 0 .2.09.2.2v1.1a.2.2 0 0 1-.2.2h-1.6a.2.2 0 0 1-.2-.2v-1.1c0-.11.09-.2.2-.2h1.6zm.45-5.5a.75.75 0 1 1 0 1.5h-8.5a.75.75 0 1 1 0-1.5h8.5zM1.3 11c.11 0 .2.09.2.2v1.6a.2.2 0 0 1-.2.2H.2a.2.2 0 0 1-.2-.2v-1.6c0-.11.09-.2.2-.2h1.1zm18.5 0c.11 0 .2.09.2.2v1.6a.2.2 0 0 1-.2.2h-1.1a.2.2 0 0 1-.2-.2v-1.6c0-.11.09-.2.2-.2h1.1zm-4.55-2a.75.75 0 1 1 0 1.5H4.75a.75.75 0 1 1 0-1.5h10.5zM1.3 7c.11 0 .2.09.2.2v1.6a.2.2 0 0 1-.2.2H.2a.2.2 0 0 1-.2-.2V7.2c0-.11.09-.2.2-.2h1.1zm18.5 0c.11 0 .2.09.2.2v1.6a.2.2 0 0 1-.2.2h-1.1a.2.2 0 0 1-.2-.2V7.2c0-.11.09-.2.2-.2h1.1zm-4.55-2a.75.75 0 1 1 0 1.5h-2.5a.75.75 0 1 1 0-1.5h2.5zm-5 0a.75.75 0 1 1 0 1.5h-5.5a.75.75 0 0 1 0-1.5h5.5zm-6.5-5a.75.75 0 0 1 0 1.5H2a.5.5 0 0 0-.492.41L1.5 2v1.75a.75.75 0 0 1-1.5 0V1.5l.007-.144A1.5 1.5 0 0 1 1.356.006L1.5 0h2.25zM18.5 0l.144.007a1.5 1.5 0 0 1 1.35 1.349L20 1.5v2.25a.75.75 0 1 1-1.5 0V2l-.008-.09A.5.5 0 0 0 18 1.5h-1.75a.75.75 0 1 1 0-1.5h2.25zM8.8 0c.11 0 .2.09.2.2v1.1a.2.2 0 0 1-.2.2H7.2a.2.2 0 0 1-.2-.2V.2c0-.11.09-.2.2-.2h1.6zm4 0c.11 0 .2.09.2.2v1.1a.2.2 0 0 1-.2.2h-1.6a.2.2 0 0 1-.2-.2V.2c0-.11.09-.2.2-.2h1.6z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The select all UI feature.
 *
 * It registers the `'selectAll'` UI button in the editor's
 * {@link module:ui/componentfactory~ComponentFactory component factory}. When clicked, the button
 * executes the {@link module:select-all/selectallcommand~SelectAllCommand select all command}.
 *
 * @extends module:core/plugin~Plugin
 */
class SelectAllUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SelectAllUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		editor.ui.componentFactory.add( 'selectAll', locale => {
			const command = editor.commands.get( 'selectAll' );
			const view = new ButtonView( locale );
			const t = locale.t;

			view.set( {
				label: t( 'Select all' ),
				icon: selectAllIcon,
				keystroke: 'Ctrl+A',
				tooltip: true
			} );

			view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

			// Execute the command.
			this.listenTo( view, 'execute', () => {
				editor.execute( 'selectAll' );
				editor.editing.view.focus();
			} );

			return view;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The select all feature.
 *
 * This is a "glue" plugin which loads the {@link module:select-all/selectallediting~SelectAllEditing select all editing feature}
 * and the {@link module:select-all/selectallui~SelectAllUI select all UI feature}.
 *
 * Please refer to the documentation of individual features to learn more.
 *
 * @extends module:core/plugin~Plugin
 */
class SelectAll extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ SelectAllEditing, SelectAllUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SelectAll';
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Base class for undo feature commands: {@link module:undo/undocommand~UndoCommand} and {@link module:undo/redocommand~RedoCommand}.
 *
 * @protected
 * @extends module:core/command~Command
 */
class BaseCommand extends Command {
	constructor( editor ) {
		super( editor );

		/**
		 * Stack of items stored by the command. These are pairs of:
		 *
		 * * {@link module:engine/model/batch~Batch batch} saved by the command,
		 * * {@link module:engine/model/selection~Selection selection} state at the moment of saving the batch.
		 *
		 * @protected
		 * @member {Array} #_stack
		 */
		this._stack = [];

		/**
		 * Stores all batches that were created by this command.
		 *
		 * @protected
		 * @member {WeakSet.<module:engine/model/batch~Batch>} #_createdBatches
		 */
		this._createdBatches = new WeakSet();

		// Refresh state, so the command is inactive right after initialization.
		this.refresh();

		// Set the transparent batch for the `editor.data.set()` call if the
		// batch type is not set already.
		this.listenTo( editor.data, 'set', ( evt, data ) => {
			// Create a shallow copy of the options to not change the original args.
			// And make sure that an object is assigned to data[ 1 ].
			data[ 1 ] = { ...data[ 1 ] };

			const options = data[ 1 ];

			// If batch type is not set, default to non-undoable batch.
			if ( !options.batchType ) {
				options.batchType = { isUndoable: false };
			}
		}, { priority: 'high' } );

		// Clear the stack for the `transparent` batches.
		this.listenTo( editor.data, 'set', ( evt, data ) => {
			// We can assume that the object exists and it has a `batchType` property.
			// It was ensured with a higher priority listener before.
			const options = data[ 1 ];

			if ( !options.batchType.isUndoable ) {
				this.clearStack();
			}
		} );
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		this.isEnabled = this._stack.length > 0;
	}

	/**
	 * Stores a batch in the command, together with the selection state of the {@link module:engine/model/document~Document document}
	 * created by the editor which this command is registered to.
	 *
	 * @param {module:engine/model/batch~Batch} batch The batch to add.
	 */
	addBatch( batch ) {
		const docSelection = this.editor.model.document.selection;

		const selection = {
			ranges: docSelection.hasOwnRange ? Array.from( docSelection.getRanges() ) : [],
			isBackward: docSelection.isBackward
		};

		this._stack.push( { batch, selection } );
		this.refresh();
	}

	/**
	 * Removes all items from the stack.
	 */
	clearStack() {
		this._stack = [];
		this.refresh();
	}

	/**
	 * Restores the {@link module:engine/model/document~Document#selection document selection} state after a batch was undone.
	 *
	 * @protected
	 * @param {Array.<module:engine/model/range~Range>} ranges Ranges to be restored.
	 * @param {Boolean} isBackward A flag describing whether the restored range was selected forward or backward.
	 * @param {Array.<module:engine/model/operation/operation~Operation>} operations Operations which has been applied
	 * since selection has been stored.
	 */
	_restoreSelection( ranges, isBackward, operations ) {
		const model = this.editor.model;
		const document = model.document;

		// This will keep the transformed selection ranges.
		const selectionRanges = [];

		// Transform all ranges from the restored selection.
		const transformedRangeGroups = ranges.map( range => range.getTransformedByOperations( operations ) );
		const allRanges = transformedRangeGroups.flat();

		for ( const rangeGroup of transformedRangeGroups ) {
			// While transforming there could appear ranges that are contained by other ranges, we shall ignore them.
			const transformed = rangeGroup
				.filter( range => range.root != document.graveyard )
				.filter( range => !isRangeContainedByAnyOtherRange( range, allRanges ) );

			// All the transformed ranges ended up in graveyard.
			if ( !transformed.length ) {
				continue;
			}

			// After the range got transformed, we have an array of ranges. Some of those
			// ranges may be "touching" -- they can be next to each other and could be merged.
			normalizeRanges( transformed );

			// For each `range` from `ranges`, we take only one transformed range.
			// This is because we want to prevent situation where single-range selection
			// got transformed to multi-range selection.
			selectionRanges.push( transformed[ 0 ] );
		}

		// @if CK_DEBUG_ENGINE // console.log( `Restored selection by undo: ${ selectionRanges.join( ', ' ) }` );

		// `selectionRanges` may be empty if all ranges ended up in graveyard. If that is the case, do not restore selection.
		if ( selectionRanges.length ) {
			model.change( writer => {
				writer.setSelection( selectionRanges, { backward: isBackward } );
			} );
		}
	}

	/**
	 * Undoes a batch by reversing that batch, transforming reversed batch and finally applying it.
	 * This is a helper method for {@link #execute}.
	 *
	 * @protected
	 * @param {module:engine/model/batch~Batch} batchToUndo The batch to be undone.
	 * @param {module:engine/model/batch~Batch} undoingBatch The batch that will contain undoing changes.
	 */
	_undo( batchToUndo, undoingBatch ) {
		const model = this.editor.model;
		const document = model.document;

		// All changes done by the command execution will be saved as one batch.
		this._createdBatches.add( undoingBatch );

		const operationsToUndo = batchToUndo.operations.slice().filter( operation => operation.isDocumentOperation );
		operationsToUndo.reverse();

		// We will process each operation from `batchToUndo`, in reverse order. If there were operations A, B and C in undone batch,
		// we need to revert them in reverse order, so first C' (reversed C), then B', then A'.
		for ( const operationToUndo of operationsToUndo ) {
			const nextBaseVersion = operationToUndo.baseVersion + 1;
			const historyOperations = Array.from( document.history.getOperations( nextBaseVersion ) );

			const transformedSets = transformSets(
				[ operationToUndo.getReversed() ],
				historyOperations,
				{
					useRelations: true,
					document: this.editor.model.document,
					padWithNoOps: false,
					forceWeakRemove: true
				}
			);

			const reversedOperations = transformedSets.operationsA;

			// After reversed operation has been transformed by all history operations, apply it.
			for ( const operation of reversedOperations ) {
				// Before applying, add the operation to the `undoingBatch`.
				undoingBatch.addOperation( operation );
				model.applyOperation( operation );

				document.history.setOperationAsUndone( operationToUndo, operation );
			}
		}
	}
}

// Normalizes list of ranges by joining intersecting or "touching" ranges.
//
// @param {Array.<module:engine/model/range~Range>} ranges
//
function normalizeRanges( ranges ) {
	ranges.sort( ( a, b ) => a.start.isBefore( b.start ) ? -1 : 1 );

	for ( let i = 1; i < ranges.length; i++ ) {
		const previousRange = ranges[ i - 1 ];
		const joinedRange = previousRange.getJoined( ranges[ i ], true );

		if ( joinedRange ) {
			// Replace the ranges on the list with the new joined range.
			i--;
			ranges.splice( i, 2, joinedRange );
		}
	}
}

function isRangeContainedByAnyOtherRange( range, ranges ) {
	return ranges.some( otherRange => otherRange !== range && otherRange.containsRange( range, true ) );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The undo command stores {@link module:engine/model/batch~Batch batches} applied to the
 * {@link module:engine/model/document~Document document} and is able to undo a batch by reversing it and transforming by
 * batches from {@link module:engine/model/document~Document#history history} that happened after the reversed batch.
 *
 * The undo command also takes care of restoring the {@link module:engine/model/document~Document#selection document selection}.
 *
 * @extends module:undo/basecommand~BaseCommand
 */
class UndoCommand extends BaseCommand {
	/**
	 * Executes the command. This method reverts a {@link module:engine/model/batch~Batch batch} added to the command's stack, transforms
	 * and applies the reverted version on the {@link module:engine/model/document~Document document} and removes the batch from the stack.
	 * Then, it restores the {@link module:engine/model/document~Document#selection document selection}.
	 *
	 * @fires execute
	 * @fires revert
	 * @param {module:engine/model/batch~Batch} [batch] A batch that should be undone. If not set, the last added batch will be undone.
	 */
	execute( batch = null ) {
		// If batch is not given, set `batchIndex` to the last index in command stack.
		const batchIndex = batch ? this._stack.findIndex( a => a.batch == batch ) : this._stack.length - 1;

		const item = this._stack.splice( batchIndex, 1 )[ 0 ];
		const undoingBatch = this.editor.model.createBatch( { isUndo: true } );

		// All changes has to be done in one `enqueueChange` callback so other listeners will not
		// step between consecutive operations, or won't do changes to the document before selection is properly restored.
		this.editor.model.enqueueChange( undoingBatch, () => {
			this._undo( item.batch, undoingBatch );

			const operations = this.editor.model.document.history.getOperations( item.batch.baseVersion );
			this._restoreSelection( item.selection.ranges, item.selection.isBackward, operations );

			this.fire( 'revert', item.batch, undoingBatch );
		} );

		this.refresh();
	}
}

/**
 * Fired when execution of the command reverts some batch.
 *
 * @event revert
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The redo command stores {@link module:engine/model/batch~Batch batches} that were used to undo a batch by
 * {@link module:undo/undocommand~UndoCommand}. It is able to redo a previously undone batch by reversing the undoing
 * batches created by `UndoCommand`. The reversed batch is transformed by all the batches from
 * {@link module:engine/model/document~Document#history history} that happened after the reversed undo batch.
 *
 * The redo command also takes care of restoring the {@link module:engine/model/document~Document#selection document selection}.
 *
 * @extends module:undo/basecommand~BaseCommand
 */
class RedoCommand extends BaseCommand {
	/**
	 * Executes the command. This method reverts the last {@link module:engine/model/batch~Batch batch} added to
	 * the command's stack, applies the reverted and transformed version on the
	 * {@link module:engine/model/document~Document document} and removes the batch from the stack.
	 * Then, it restores the {@link module:engine/model/document~Document#selection document selection}.
	 *
	 * @fires execute
	 */
	execute() {
		const item = this._stack.pop();
		const redoingBatch = this.editor.model.createBatch( { isUndo: true } );

		// All changes have to be done in one `enqueueChange` callback so other listeners will not step between consecutive
		// operations, or won't do changes to the document before selection is properly restored.
		this.editor.model.enqueueChange( redoingBatch, () => {
			const lastOperation = item.batch.operations[ item.batch.operations.length - 1 ];
			const nextBaseVersion = lastOperation.baseVersion + 1;
			const operations = this.editor.model.document.history.getOperations( nextBaseVersion );

			this._restoreSelection( item.selection.ranges, item.selection.isBackward, operations );
			this._undo( item.batch, redoingBatch );
		} );

		this.refresh();
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The undo engine feature.
 *
 * It introduces the `'undo'` and `'redo'` commands to the editor.
 *
 * @extends module:core/plugin~Plugin
 */
class UndoEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'UndoEditing';
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		/**
		 * The command that manages undo {@link module:engine/model/batch~Batch batches} stack (history).
		 * Created and registered during the {@link #init feature initialization}.
		 *
		 * @private
		 * @member {module:undo/undocommand~UndoCommand} #_undoCommand
		 */

		/**
		 * The command that manages redo {@link module:engine/model/batch~Batch batches} stack (history).
		 * Created and registered during the {@link #init feature initialization}.
		 *
		 * @private
		 * @member {module:undo/undocommand~UndoCommand} #_redoCommand
		 */

		/**
		 * Keeps track of which batches were registered in undo.
		 *
		 * @private
		 * @member {WeakSet.<module:engine/model/batch~Batch>}
		 */
		this._batchRegistry = new WeakSet();
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		// Create commands.
		this._undoCommand = new UndoCommand( editor );
		this._redoCommand = new RedoCommand( editor );

		// Register command to the editor.
		editor.commands.add( 'undo', this._undoCommand );
		editor.commands.add( 'redo', this._redoCommand );

		this.listenTo( editor.model, 'applyOperation', ( evt, args ) => {
			const operation = args[ 0 ];

			// Do not register batch if the operation is not a document operation.
			// This prevents from creating empty undo steps, where all operations where non-document operations.
			// Non-document operations creates and alters content in detached tree fragments (for example, document fragments).
			// Most of time this is preparing data before it is inserted into actual tree (for example during copy & paste).
			// Such operations should not be reversed.
			if ( !operation.isDocumentOperation ) {
				return;
			}

			const batch = operation.batch;

			const isRedoBatch = this._redoCommand._createdBatches.has( batch );
			const isUndoBatch = this._undoCommand._createdBatches.has( batch );
			const wasProcessed = this._batchRegistry.has( batch );

			// Skip the batch if it was already processed.
			if ( wasProcessed ) {
				return;
			}

			// Add the batch to the registry so it will not be processed again.
			this._batchRegistry.add( batch );

			if ( !batch.isUndoable ) {
				return;
			}

			if ( isRedoBatch ) {
				// If this batch comes from `redoCommand`, add it to the `undoCommand` stack.
				this._undoCommand.addBatch( batch );
			} else if ( !isUndoBatch ) {
				// If the batch comes neither  from `redoCommand` nor from `undoCommand` then it is a new, regular batch.
				// Add the batch to the `undoCommand` stack and clear the `redoCommand` stack.
				this._undoCommand.addBatch( batch );
				this._redoCommand.clearStack();
			}
		}, { priority: 'highest' } );

		this.listenTo( this._undoCommand, 'revert', ( evt, undoneBatch, undoingBatch ) => {
			this._redoCommand.addBatch( undoingBatch );
		} );

		editor.keystrokes.set( 'CTRL+Z', 'undo' );
		editor.keystrokes.set( 'CTRL+Y', 'redo' );
		editor.keystrokes.set( 'CTRL+SHIFT+Z', 'redo' );
	}
}var undoIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m5.042 9.367 2.189 1.837a.75.75 0 0 1-.965 1.149l-3.788-3.18a.747.747 0 0 1-.21-.284.75.75 0 0 1 .17-.945L6.23 4.762a.75.75 0 1 1 .964 1.15L4.863 7.866h8.917A.75.75 0 0 1 14 7.9a4 4 0 1 1-1.477 7.718l.344-1.489a2.5 2.5 0 1 0 1.094-4.73l.008-.032H5.042z\"/></svg>";
var redoIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m14.958 9.367-2.189 1.837a.75.75 0 0 0 .965 1.149l3.788-3.18a.747.747 0 0 0 .21-.284.75.75 0 0 0-.17-.945L13.77 4.762a.75.75 0 1 0-.964 1.15l2.331 1.955H6.22A.75.75 0 0 0 6 7.9a4 4 0 1 0 1.477 7.718l-.344-1.489A2.5 2.5 0 1 1 6.039 9.4l-.008-.032h8.927z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The undo UI feature. It introduces the `'undo'` and `'redo'` buttons to the editor.
 *
 * @extends module:core/plugin~Plugin
 */
class UndoUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'UndoUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const locale = editor.locale;
		const t = editor.t;

		const localizedUndoIcon = locale.uiLanguageDirection == 'ltr' ? undoIcon : redoIcon;
		const localizedRedoIcon = locale.uiLanguageDirection == 'ltr' ? redoIcon : undoIcon;

		this._addButton( 'undo', t( 'Undo' ), 'CTRL+Z', localizedUndoIcon );
		this._addButton( 'redo', t( 'Redo' ), 'CTRL+Y', localizedRedoIcon );
	}

	/**
	 * Creates a button for the specified command.
	 *
	 * @private
	 * @param {String} name Command name.
	 * @param {String} label Button label.
	 * @param {String} keystroke Command keystroke.
	 * @param {String} Icon Source of the icon.
	 */
	_addButton( name, label, keystroke, Icon ) {
		const editor = this.editor;

		editor.ui.componentFactory.add( name, locale => {
			const command = editor.commands.get( name );
			const view = new ButtonView( locale );

			view.set( {
				label,
				icon: Icon,
				keystroke,
				tooltip: true
			} );

			view.bind( 'isEnabled' ).to( command, 'isEnabled' );

			this.listenTo( view, 'execute', () => {
				editor.execute( name );
				editor.editing.view.focus();
			} );

			return view;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The undo feature.
 *
 * This is a "glue" plugin which loads the {@link module:undo/undoediting~UndoEditing undo editing feature}
 * and {@link module:undo/undoui~UndoUI undo UI feature}.
 *
 * Below is the explanation of the undo mechanism working together with {@link module:engine/model/history~History History}:
 *
 * Whenever a {@link module:engine/model/operation/operation~Operation operation} is applied to the
 * {@link module:engine/model/document~Document document}, it is saved to `History` as is.
 * The {@link module:engine/model/batch~Batch batch} that owns that operation is also saved, in
 * {@link module:undo/undocommand~UndoCommand}, together with the selection that was present in the document before the
 * operation was applied. A batch is saved instead of the operation because changes are undone batch-by-batch, not operation-by-operation
 * and a batch is seen as one undo step.
 *
 * After some changes happen to the document, the `History` and `UndoCommand` stack can be represented as follows:
 *
 *		    History                            Undo stack
 *		==============             ==================================
 *		[operation A1]                         [batch A]
 *		[operation B1]                         [batch B]
 *		[operation B2]                         [batch C]
 *		[operation C1]
 *		[operation C2]
 *		[operation B3]
 *		[operation C3]
 *
 * Where operations starting with the same letter are from same batch.
 *
 * Undoing a batch means that a set of operations which will reverse the effects of that batch needs to be generated.
 * For example, if a batch added several letters, undoing the batch should remove them. It is important to apply undoing
 * operations in the reversed order, so if a batch has operation `X`, `Y`, `Z`, reversed operations `Zr`, `Yr` and `Xr`
 * need to be applied. Otherwise reversed operation `Xr` would operate on a wrong document state, because operation `X`
 * does not know that operations `Y` and `Z` happened.
 *
 * After operations from an undone batch got {@link module:engine/model/operation/operation~Operation#getReversed reversed},
 * one needs to make sure if they are ready to be applied. In the scenario above, operation `C3` is the last operation and `C3r`
 * bases on up-to-date document state, so it can be applied to the document.
 *
 *		     History                             Undo stack
 *		=================             ==================================
 *		[ operation A1  ]                      [  batch A  ]
 *		[ operation B1  ]                      [  batch B  ]
 *		[ operation B2  ]             [   processing undoing batch C   ]
 *		[ operation C1  ]
 *		[ operation C2  ]
 *		[ operation B3  ]
 *		[ operation C3  ]
 *		[ operation C3r ]
 *
 * Next is operation `C2`, reversed to `C2r`. `C2r` bases on `C2`, so it bases on the wrong document state. It needs to be
 * transformed by operations from history that happened after it, so it "knows" about them. Let us assume that `C2' = C2r * B3 * C3 * C3r`,
 * where `*` means "transformed by". Rest of operations from that batch are processed in the same fashion.
 *
 *		     History                             Undo stack                                      Redo stack
 *		=================             ==================================             ==================================
 *		[ operation A1  ]                      [  batch A  ]                                    [ batch Cr ]
 *		[ operation B1  ]                      [  batch B  ]
 *		[ operation B2  ]
 *		[ operation C1  ]
 *		[ operation C2  ]
 *		[ operation B3  ]
 *		[ operation C3  ]
 *		[ operation C3r ]
 *		[ operation C2' ]
 *		[ operation C1' ]
 *
 * Selective undo works on the same basis, however, instead of undoing the last batch in the undo stack, any batch can be undone.
 * The same algorithm applies: operations from a batch (i.e. `A1`) are reversed and then transformed by operations stored in history.
 *
 * Redo also is very similar to undo. It has its own stack that is filled with undoing (reversed batches). Operations from
 * batch that is re-done are reversed-back, transformed in proper order and applied to the document.
 *
 *		     History                             Undo stack                                      Redo stack
 *		=================             ==================================             ==================================
 *		[ operation A1  ]                      [  batch A  ]
 *		[ operation B1  ]                      [  batch B  ]
 *		[ operation B2  ]                      [ batch Crr ]
 *		[ operation C1  ]
 *		[ operation C2  ]
 *		[ operation B3  ]
 *		[ operation C3  ]
 *		[ operation C3r ]
 *		[ operation C2' ]
 *		[ operation C1' ]
 *		[ operation C1'r]
 *		[ operation C2'r]
 *		[ operation C3rr]
 *
 * @extends module:core/plugin~Plugin
 */
class Undo extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ UndoEditing, UndoUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Undo';
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A plugin including all essential editing features. It represents a set of features that enables similar functionalities
 * to a `<textarea>` element.
 *
 * It includes:
 *
 * * {@link module:clipboard/clipboard~Clipboard},
 * * {@link module:enter/enter~Enter},
 * * {@link module:select-all/selectall~SelectAll},
 * * {@link module:enter/shiftenter~ShiftEnter},
 * * {@link module:typing/typing~Typing},
 * * {@link module:undo/undo~Undo}.
 *
 * This plugin set does not define any block-level containers (such as {@link module:paragraph/paragraph~Paragraph}).
 * If your editor is supposed to handle block content, make sure to include it.
 *
 * @extends module:core/plugin~Plugin
 */
class Essentials extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ Clipboard, Enter, SelectAll, ShiftEnter, Typing, Undo ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Essentials';
	}
}var css_248z$t = ".ck-vertical-form .ck-button:after{bottom:-1px;content:\"\";position:absolute;right:-1px;top:-1px;width:0;z-index:1}.ck-vertical-form .ck-button:focus:after{display:none}@media screen and (max-width:600px){.ck.ck-responsive-form .ck-button:after{bottom:-1px;content:\"\";position:absolute;right:-1px;top:-1px;width:0;z-index:1}.ck.ck-responsive-form .ck-button:focus:after{display:none}}.ck-vertical-form>.ck-button:nth-last-child(2):after{border-right:1px solid var(--ck-color-base-border)}.ck.ck-responsive-form{padding:var(--ck-spacing-large)}.ck.ck-responsive-form:focus{outline:none}[dir=ltr] .ck.ck-responsive-form>:not(:first-child),[dir=rtl] .ck.ck-responsive-form>:not(:last-child){margin-left:var(--ck-spacing-standard)}@media screen and (max-width:600px){.ck.ck-responsive-form{padding:0;width:calc(var(--ck-input-width)*.8)}.ck.ck-responsive-form .ck-labeled-field-view{margin:var(--ck-spacing-large) var(--ck-spacing-large) 0}.ck.ck-responsive-form .ck-labeled-field-view .ck-input-text{min-width:0;width:100%}.ck.ck-responsive-form .ck-labeled-field-view .ck-labeled-field-view__error{white-space:normal}.ck.ck-responsive-form>.ck-button:nth-last-child(2):after{border-right:1px solid var(--ck-color-base-border)}.ck.ck-responsive-form>.ck-button:last-child,.ck.ck-responsive-form>.ck-button:nth-last-child(2){border-radius:0;margin-top:var(--ck-spacing-large);padding:var(--ck-spacing-standard)}.ck.ck-responsive-form>.ck-button:last-child:not(:focus),.ck.ck-responsive-form>.ck-button:nth-last-child(2):not(:focus){border-top:1px solid var(--ck-color-base-border)}[dir=ltr] .ck.ck-responsive-form>.ck-button:last-child,[dir=ltr] .ck.ck-responsive-form>.ck-button:nth-last-child(2),[dir=rtl] .ck.ck-responsive-form>.ck-button:last-child,[dir=rtl] .ck.ck-responsive-form>.ck-button:nth-last-child(2){margin-left:0}[dir=rtl] .ck.ck-responsive-form>.ck-button:last-child:last-of-type,[dir=rtl] .ck.ck-responsive-form>.ck-button:nth-last-child(2):last-of-type{border-right:1px solid var(--ck-color-base-border)}}";
styleInject(css_248z$t);var css_248z$s = ".ck.ck-find-and-replace-form{max-width:100%}.ck.ck-find-and-replace-form fieldset{display:flex}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find .ck-results-counter{position:absolute}.ck.ck-find-and-replace-form{width:400px}.ck.ck-find-and-replace-form:focus{outline:none}.ck.ck-find-and-replace-form fieldset{align-content:stretch;align-items:center;border:0;flex-direction:row;flex-wrap:nowrap;margin:0;padding:var(--ck-spacing-large)}.ck.ck-find-and-replace-form fieldset>.ck-button{flex:0 0 auto}[dir=ltr] .ck.ck-find-and-replace-form fieldset>*+*{margin-left:var(--ck-spacing-standard)}[dir=rtl] .ck.ck-find-and-replace-form fieldset>*+*{margin-right:var(--ck-spacing-standard)}.ck.ck-find-and-replace-form fieldset .ck-labeled-field-view{flex:1 1 auto}.ck.ck-find-and-replace-form fieldset .ck-labeled-field-view .ck-input{min-width:50px;width:100%}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find{align-items:flex-start}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find>.ck-button-find{font-weight:700}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find>.ck-button-find .ck-button__label{padding-left:var(--ck-spacing-large);padding-right:var(--ck-spacing-large)}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find>.ck-button-prev>.ck-icon{transform:rotate(90deg)}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find>.ck-button-next>.ck-icon{transform:rotate(-90deg)}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find .ck-results-counter{top:50%;transform:translateY(-50%)}[dir=ltr] .ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find .ck-results-counter{right:var(--ck-spacing-standard)}[dir=rtl] .ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find .ck-results-counter{left:var(--ck-spacing-standard)}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find .ck-results-counter{color:var(--ck-color-base-border)}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__replace{flex-wrap:wrap;justify-content:flex-end;margin-top:calc(var(--ck-spacing-large)*-1)}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__replace>.ck-labeled-field-view{margin-bottom:var(--ck-spacing-large)}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__replace>.ck-options-dropdown{margin-left:0;margin-right:auto}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__replace>.ck-labeled-field-view,.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__replace>.ck-labeled-field-view .ck-input{width:100%}@media screen and (max-width:600px){.ck.ck-find-and-replace-form{width:300px}.ck.ck-find-and-replace-form fieldset{flex-wrap:wrap}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find .ck-labeled-field-view{flex:1 0 auto;margin-bottom:var(--ck-spacing-standard);width:100%}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find>.ck-button{text-align:center}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find>.ck-button:first-of-type{flex:1 1 auto}[dir=ltr] .ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find>.ck-button:first-of-type{margin-left:0}[dir=rtl] .ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find>.ck-button:first-of-type{margin-right:0}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__find>.ck-button:first-of-type .ck-button__label{text-align:center;width:100%}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__replace>:not(.ck-labeled-field-view){flex:1 1 auto}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__replace>.ck-dropdown:not(.ck-labeled-field-view){flex-grow:0}.ck.ck-find-and-replace-form fieldset.ck-find-and-replace-form__replace>.ck-button:not(.ck-labeled-field-view)>.ck-button__label{text-align:center;width:100%}}";
styleInject(css_248z$s);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The find and replace form view class.
 *
 * See {@link module:find-and-replace/ui/findandreplaceformview~FindAndReplaceFormView}.
 *
 * @extends module:ui/view~View
 */
class FindAndReplaceFormView extends View {
	/**
	 * Creates a view of find and replace form.
	 *
	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
	 */
	constructor( locale ) {
		super( locale );

		const t = locale.t;

		/**
		 * Stores the number of matched search results.
		 *
		 * @readonly
		 * @observable
		 * @member {Number} #matchCount
		 */
		this.set( 'matchCount', 0 );

		/**
		 * The offset of currently highlighted search result in {@link #matchCount matched results}.
		 *
		 * @readonly
		 * @observable
		 * @member {Number|null} #highlightOffset
		 */
		this.set( 'highlightOffset', 0 );

		/**
		 * `true` when the search params (find text, options) has been changed by the user since
		 * the last time find was executed. `false` otherwise.
		 *
		 * @readonly
		 * @observable
		 * @member {Boolean} #isDirty
		 */
		this.set( 'isDirty', false );

		/**
		 * A live object with the aggregated `isEnabled` states of editor commands related to find and
		 * replace. For instance, it may look as follows:
		 *
		 *		{
		 *			findNext: true,
		 *			findPrevious: true,
		 *			replace: false,
		 *			replaceAll: false
		 *		}
		 *
		 * @protected
		 * @readonly
		 * @observable
		 * @member {Object} #_areCommandsEnabled
		 */
		this.set( '_areCommandsEnabled', {} );

		/**
		 * The content of the counter label displaying the index of the current highlighted match
		 * on top of the find input, for instance "3 of 50".
		 *
		 * @protected
		 * @readonly
		 * @observable
		 * @member {String} #_resultsCounterText
		 */
		this.set( '_resultsCounterText', '' );

		/**
		 * The flag reflecting the state of the "Match case" switch button in the search options
		 * dropdown.
		 *
		 * @protected
		 * @readonly
		 * @observable
		 * @member {Boolean} #_matchCase
		 */
		this.set( '_matchCase', false );

		/**
		 * The flag reflecting the state of the "Whole words only" switch button in the search options
		 * dropdown.
		 *
		 * @protected
		 * @readonly
		 * @observable
		 * @member {Boolean} #_wholeWordsOnly
		 */
		this.set( '_wholeWordsOnly', false );

		/**
		 * This flag is set `true` when some matches were found and the user didn't change the search
		 * params (text to find, options) yet. This is only possible immediately after hitting the "Find" button.
		 * `false` when there were no matches (see {@link #matchCount}) or the user changed the params (see {@link #isDirty}).
		 *
		 * It is used to control the enabled state of the replace UI (input and buttons); replacing text is only possible
		 * if this flag is `true`.
		 *
		 * @protected
		 * @readonly
		 * @observable
		 * @member {Boolean} #_searchResultsFound
		 */
		this.bind( '_searchResultsFound' ).to(
			this, 'matchCount',
			this, 'isDirty',
			( matchCount, isDirty ) => {
				return matchCount > 0 && !isDirty;
			}
		);

		/**
		 * The find in text input view that stores the searched string.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
		 */
		this._findInputView = this._createInputField( t( 'Find in text…' ) );

		/**
		 * The replace input view.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
		 */
		this._replaceInputView = this._createInputField( t( 'Replace with…' ) );

		/**
		 * The find button view that initializes the search process.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this._findButtonView = this._createButton( {
			label: t( 'Find' ),
			class: 'ck-button-find ck-button-action',
			withText: true
		} );

		/**
		 * The find previous button view.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this._findPrevButtonView = this._createButton( {
			label: t( 'Previous result' ),
			class: 'ck-button-prev',
			icon: previousArrow,
			keystroke: 'Shift+F3',
			tooltip: true
		} );

		/**
		 * The find next button view.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this._findNextButtonView = this._createButton( {
			label: t( 'Next result' ),
			class: 'ck-button-next',
			icon: previousArrow,
			keystroke: 'F3',
			tooltip: true
		} );

		/**
		 * The find options dropdown.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/dropdown/dropdownview~DropdownView}
		 */
		this._optionsDropdown = this._createOptionsDropdown();

		/**
		 * The replace button view.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this._replaceButtonView = this._createButton( {
			label: t( 'Replace' ),
			class: 'ck-button-replace',
			withText: true
		} );

		/**
		 * The replace all button view.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this._replaceAllButtonView = this._createButton( {
			label: t( 'Replace all' ),
			class: 'ck-button-replaceall',
			withText: true
		} );

		/**
		 * The fieldset aggregating the find UI.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/view/view~View}
		 */
		this._findFieldsetView = this._createFindFieldset();

		/**
		 * The fieldset aggregating the replace UI.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/view/view~View}
		 */
		this._replaceFieldsetView = this._createReplaceFieldset();

		/**
		 * Tracks information about the DOM focus in the form.
		 *
		 * @readonly
		 * @protected
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this._focusTracker = new FocusTracker();

		/**
		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
		 *
		 * @readonly
		 * @protected
		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
		 */
		this._keystrokes = new KeystrokeHandler();

		/**
		 * A collection of views that can be focused in the form.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this._focusables = new ViewCollection();

		/**
		 * Helps cycling over {@link #_focusables} in the form.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/focuscycler~FocusCycler}
		 */
		this._focusCycler = new FocusCycler( {
			focusables: this._focusables,
			focusTracker: this._focusTracker,
			keystrokeHandler: this._keystrokes,
			actions: {
				// Navigate form fields backwards using the <kbd>Shift</kbd> + <kbd>Tab</kbd> keystroke.
				focusPrevious: 'shift + tab',

				// Navigate form fields forwards using the <kbd>Tab</kbd> key.
				focusNext: 'tab'
			}
		} );

		this.setTemplate( {
			tag: 'form',
			attributes: {
				class: [
					'ck',
					'ck-find-and-replace-form'
				],

				tabindex: '-1'
			},
			children: [
				new FormHeaderView( locale, {
					label: t( 'Find and replace' )
				} ),
				this._findFieldsetView,
				this._replaceFieldsetView
			]
		} );

		injectCssTransitionDisabler( this );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		submitHandler( { view: this } );

		this._initFocusCycling();
		this._initKeystrokeHandling();
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this._focusTracker.destroy();
		this._keystrokes.destroy();
	}

	/**
	 * Focuses the fist {@link #_focusables} in the form.
	 */
	focus() {
		this._focusCycler.focusFirst();
	}

	/**
	 * Resets the form before re-appearing.
	 *
	 * It clears error messages, hides the match counter and disables the replace feature
	 * until the next hit of the "Find" button.
	 *
	 * **Note**: It does not reset inputs and options, though. This way the form works better in editors with
	 * disappearing toolbar (e.g. BalloonEditor): hiding the toolbar by accident (together with the find and replace UI)
	 * does not require filling the entire form again.
	 */
	reset() {
		this._findInputView.errorText = null;
		this.isDirty = true;
	}

	/**
	 * Returns the value of the find input.
	 *
	 * @protected
	 * @returns {String}
	 */
	get _textToFind() {
		return this._findInputView.fieldView.element.value;
	}

	/**
	 * Returns the value of the replace input.
	 *
	 * @protected
	 * @returns {String}
	 */
	get _textToReplace() {
		return this._replaceInputView.fieldView.element.value;
	}

	/**
	 * Configures and returns the `<fieldset>` aggregating all find controls.
	 *
	 * @private
	 * @returns {module:ui/view~View}
	 */
	_createFindFieldset() {
		const locale = this.locale;
		const fieldsetView = new View( locale );

		// Typing in the find field invalidates all previous results (the form is "dirty").
		this._findInputView.fieldView.on( 'input', () => {
			this.isDirty = true;
		} );

		this._findButtonView.on( 'execute', this._onFindButtonExecute.bind( this ) );

		// Pressing prev/next buttons fires related event on the form.
		this._findPrevButtonView.delegate( 'execute' ).to( this, 'findPrevious' );
		this._findNextButtonView.delegate( 'execute' ).to( this, 'findNext' );

		// Prev/next buttons will be disabled when related editor command gets disabled.
		this._findPrevButtonView.bind( 'isEnabled' ).to( this, '_areCommandsEnabled', ( { findPrevious } ) => findPrevious );
		this._findNextButtonView.bind( 'isEnabled' ).to( this, '_areCommandsEnabled', ( { findNext } ) => findNext );

		this._injectFindResultsCounter();

		fieldsetView.setTemplate( {
			tag: 'fieldset',
			attributes: {
				class: [ 'ck', 'ck-find-and-replace-form__find' ]
			},
			children: [
				this._findInputView,
				this._findButtonView,
				this._findPrevButtonView,
				this._findNextButtonView
			]
		} );

		return fieldsetView;
	}

	/**
	 * The action performed when the {@link #_findButtonView} is pressed.
	 *
	 * @private
	 */
	_onFindButtonExecute() {
		// When hitting "Find" in an empty input, an error should be displayed.
		// Also, if the form was "dirty", it should remain so.
		if ( !this._textToFind ) {
			const t = this.t;

			this._findInputView.errorText = t( 'Text to find must not be empty.' );

			return;
		}

		// Hitting "Find" automatically clears the dirty state.
		this.isDirty = false;

		this.fire( 'findNext', {
			searchText: this._textToFind,
			matchCase: this._matchCase,
			wholeWords: this._wholeWordsOnly
		} );
	}

	/**
	 * Configures an injects the find results counter displaying a "N of M" label of the {@link #_findInputView}.
	 *
	 * @private
	 */
	_injectFindResultsCounter() {
		const locale = this.locale;
		const t = locale.t;
		const bind = this.bindTemplate;
		const resultsCounterView = new View( locale );

		this.bind( '_resultsCounterText' ).to( this, 'highlightOffset', this, 'matchCount',
			( highlightOffset, matchCount ) => t( '%0 of %1', [ highlightOffset, matchCount ] )
		);

		resultsCounterView.setTemplate( {
			tag: 'span',
			attributes: {
				class: [
					'ck',
					'ck-results-counter',
					// The counter only makes sense when the field text corresponds to search results in the editing.
					bind.if( 'isDirty', 'ck-hidden' )
				]
			},
			children: [
				{
					text: bind.to( '_resultsCounterText' )
				}
			]
		} );

		// The whole idea is that when the text of the counter changes, its width also increases/decreases and
		// it consumes more or less space over the input. The input, on the other hand, should adjust it's right
		// padding so its *entire* text always remains visible and available to the user.
		const updateFindInputPadding = () => {
			const inputElement = this._findInputView.fieldView.element;

			// Don't adjust the padding if the input (also: counter) were not rendered or not inserted into DOM yet.
			if ( !inputElement || !isVisible( inputElement ) ) {
				return;
			}

			const counterWidth = new Rect( resultsCounterView.element ).width;
			const paddingPropertyName = locale.uiLanguageDirection === 'ltr' ? 'paddingRight' : 'paddingLeft';

			if ( !counterWidth ) {
				inputElement.style[ paddingPropertyName ] = null;
			} else {
				inputElement.style[ paddingPropertyName ] = `calc( 2 * var(--ck-spacing-standard) + ${ counterWidth }px )`;
			}
		};

		// Adjust the input padding when the text of the counter changes, for instance "1 of 200" is narrower than "123 of 200".
		// Using "low" priority to let the text be set by the template binding first.
		this.on( 'change:_resultsCounterText', updateFindInputPadding, { priority: 'low' } );

		// Adjust the input padding when the counter shows or hides. When hidden, there should be no padding. When it shows, the
		// padding should be set according to the text of the counter.
		// Using "low" priority to let the text be set by the template binding first.
		this.on( 'change:isDirty', updateFindInputPadding, { priority: 'low' } );

		// Put the counter element next to the <input> in the find field.
		this._findInputView.template.children[ 0 ].children.push( resultsCounterView );
	}

	/**
	 * Configures and returns the `<fieldset>` aggregating all replace controls.
	 *
	 * @private
	 * @returns {module:ui/view~View}
	 */
	_createReplaceFieldset() {
		const locale = this.locale;
		const t = locale.t;
		const fieldsetView = new View( locale );

		this._replaceButtonView.bind( 'isEnabled' ).to(
			this, '_areCommandsEnabled',
			this, '_searchResultsFound',
			( { replace }, resultsFound ) => replace && resultsFound );

		this._replaceAllButtonView.bind( 'isEnabled' ).to(
			this, '_areCommandsEnabled',
			this, '_searchResultsFound',
			( { replaceAll }, resultsFound ) => replaceAll && resultsFound );

		this._replaceInputView.bind( 'isEnabled' ).to(
			this, '_areCommandsEnabled',
			this, '_searchResultsFound',
			( { replace }, resultsFound ) => replace && resultsFound );

		this._replaceInputView.bind( 'infoText' ).to(
			this._replaceInputView, 'isEnabled',
			this._replaceInputView, 'isFocused',
			( isEnabled, isFocused ) => {
				if ( isEnabled || !isFocused ) {
					return '';
				}

				return t( 'Tip: Find some text first in order to replace it.' );
			} );

		this._replaceButtonView.on( 'execute', () => {
			this.fire( 'replace', {
				searchText: this._textToFind,
				replaceText: this._textToReplace
			} );
		} );

		this._replaceAllButtonView.on( 'execute', () => {
			this.fire( 'replaceAll', {
				searchText: this._textToFind,
				replaceText: this._textToReplace
			} );

			this.focus();
		} );

		fieldsetView.setTemplate( {
			tag: 'fieldset',
			attributes: {
				class: [ 'ck', 'ck-find-and-replace-form__replace' ]
			},
			children: [
				this._replaceInputView,
				this._optionsDropdown,
				this._replaceButtonView,
				this._replaceAllButtonView
			]
		} );

		return fieldsetView;
	}

	/**
	 * Creates, configures and returns and instance of a dropdown allowing users to narrow
	 * the search criteria down. The dropdown has a list with switch buttons for each option.
	 *
	 * @private
	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
	 */
	_createOptionsDropdown() {
		const locale = this.locale;
		const t = locale.t;
		const dropdownView = createDropdown( locale );

		dropdownView.class = 'ck-options-dropdown';

		dropdownView.buttonView.set( {
			withText: false,
			label: t( 'Show options' ),
			icon: icons.cog,
			tooltip: true
		} );

		const matchCaseModel = new Model( {
			withText: true,
			label: t( 'Match case' ),

			// A dummy read-only prop to make it easy to tell which switch was toggled.
			_isMatchCaseSwitch: true
		} );

		const wholeWordsOnlyModel = new Model( {
			withText: true,
			label: t( 'Whole words only' )
		} );

		// Let the switches be controlled by form's observable properties.
		matchCaseModel.bind( 'isOn' ).to( this, '_matchCase' );
		wholeWordsOnlyModel.bind( 'isOn' ).to( this, '_wholeWordsOnly' );

		// Update the state of the form when a switch is toggled.
		dropdownView.on( 'execute', evt => {
			if ( evt.source._isMatchCaseSwitch ) {
				this._matchCase = !this._matchCase;
			} else {
				this._wholeWordsOnly = !this._wholeWordsOnly;
			}

			// Toggling a switch makes the form dirty because this changes search criteria
			// just like typing text of the find input.
			this.isDirty = true;
		} );

		addListToDropdown( dropdownView, new Collection( [
			{ type: 'switchbutton', model: matchCaseModel },
			{ type: 'switchbutton', model: wholeWordsOnlyModel }
		] ) );

		return dropdownView;
	}

	/**
	 * Initializes the {@link #_focusables} and {@link #_focusTracker} to allow navigation
	 * using <kbd>Tab</kbd> and <kbd>Shift</kbd>+<kbd>Tab</kbd> keystrokes in the right order.
	 *
	 * @private
	 */
	_initFocusCycling() {
		const childViews = [
			this._findInputView,
			this._findButtonView,
			this._findPrevButtonView,
			this._findNextButtonView,
			this._replaceInputView,
			this._optionsDropdown,
			this._replaceButtonView,
			this._replaceAllButtonView
		];

		childViews.forEach( v => {
			// Register the view as focusable.
			this._focusables.add( v );

			// Register the view in the focus tracker.
			this._focusTracker.add( v.element );
		} );
	}

	/**
	 * Initializes the keystroke handling in the form.
	 *
	 * @private
	 */
	_initKeystrokeHandling() {
		const stopPropagation = data => data.stopPropagation();
		const stopPropagationAndPreventDefault = data => {
			data.stopPropagation();
			data.preventDefault();
		};

		// Start listening for the keystrokes coming from #element.
		this._keystrokes.listenTo( this.element );

		// Find the next result upon F3.
		this._keystrokes.set( 'f3', event => {
			stopPropagationAndPreventDefault( event );

			this._findNextButtonView.fire( 'execute' );
		} );

		// Find the previous result upon F3.
		this._keystrokes.set( 'shift+f3', event => {
			stopPropagationAndPreventDefault( event );

			this._findPrevButtonView.fire( 'execute' );
		} );

		// Find or replace upon pressing Enter in the find and replace fields.
		this._keystrokes.set( 'enter', event => {
			const target = event.target;

			if ( target === this._findInputView.fieldView.element ) {
				if ( this._areCommandsEnabled.findNext ) {
					this._findNextButtonView.fire( 'execute' );
				} else {
					this._findButtonView.fire( 'execute' );
				}
				stopPropagationAndPreventDefault( event );
			} else if ( target === this._replaceInputView.fieldView.element && !this.isDirty ) {
				this._replaceButtonView.fire( 'execute' );
				stopPropagationAndPreventDefault( event );
			}
		} );

		// Find previous upon pressing Shift+Enter in the find field.
		this._keystrokes.set( 'shift+enter', event => {
			const target = event.target;

			if ( target !== this._findInputView.fieldView.element ) {
				return;
			}

			if ( this._areCommandsEnabled.findPrevious ) {
				this._findPrevButtonView.fire( 'execute' );
			} else {
				this._findButtonView.fire( 'execute' );
			}

			stopPropagationAndPreventDefault( event );
		} );

		// Since the form is in the dropdown panel which is a child of the toolbar, the toolbar's
		// keystroke handler would take over the key management in the URL input.
		// We need to prevent this ASAP. Otherwise, the basic caret movement using the arrow keys will be impossible.
		this._keystrokes.set( 'arrowright', stopPropagation );
		this._keystrokes.set( 'arrowleft', stopPropagation );
		this._keystrokes.set( 'arrowup', stopPropagation );
		this._keystrokes.set( 'arrowdown', stopPropagation );

		// Intercept the `selectstart` event, which is blocked by default because of the default behavior
		// of the DropdownView#panelView. This blocking prevents the native select all on Ctrl+A.
		this.listenTo( this._findInputView.element, 'selectstart', ( evt, domEvt ) => {
			domEvt.stopPropagation();
		}, { priority: 'high' } );

		this.listenTo( this._replaceInputView.element, 'selectstart', ( evt, domEvt ) => {
			domEvt.stopPropagation();
		}, { priority: 'high' } );
	}

	/**
	 * Creates a button view.
	 *
	 * @private
	 * @param {Object} options The properties of the `ButtonView`.
	 * @returns {module:ui/button/buttonview~ButtonView} The button view instance.
	 */
	_createButton( options ) {
		const button = new ButtonView( this.locale );

		button.set( options );

		return button;
	}

	/**
	 * Creates a labeled input view.
	 *
	 * @private
	 * @param {String} label The input label.
	 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView} The labeled input view instance.
	 */
	_createInputField( label ) {
		const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText );

		labeledInput.label = label;

		return labeledInput;
	}
}

/**
 * Fired when the find next button is triggered.
 *
 * @event findNext
 * @param {String} searchText Search text.
 */

/**
 * Fired when the find previous button is triggered.
 *
 * @event findPrevious
 * @param {String} searchText Search text.
 */

/**
 * Fired when the replace button is triggered.
 *
 * @event replace
 * @param {String} replaceText Replacement text.
 */

/**
 * Fired when the replaceAll button is triggered.
 *
 * @event replaceAll
 * @param {String} replaceText Replacement text.
 */var loupeIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m12.87 13.786 1.532-1.286 3.857 4.596a1 1 0 1 1-1.532 1.286l-3.857-4.596z\"/><path d=\"M16.004 8.5a6.5 6.5 0 0 1-9.216 5.905c-1.154-.53-.863-1.415-.663-1.615.194-.194.564-.592 1.635-.141a4.5 4.5 0 0 0 5.89-5.904l-.104-.227 1.332-1.331c.045-.046.196-.041.224.007a6.47 6.47 0 0 1 .902 3.306zm-3.4-5.715c.562.305.742 1.106.354 1.494-.388.388-.995.414-1.476.178a4.5 4.5 0 0 0-6.086 5.882l.114.236-1.348 1.349c-.038.037-.17.022-.198-.023a6.5 6.5 0 0 1 5.54-9.9 6.469 6.469 0 0 1 3.1.784z\"/><path d=\"M4.001 11.93.948 8.877a.2.2 0 0 1 .141-.341h6.106a.2.2 0 0 1 .141.341L4.283 11.93a.2.2 0 0 1-.282 0zm11.083-6.789 3.053 3.053a.2.2 0 0 1-.14.342H11.89a.2.2 0 0 1-.14-.342l3.052-3.053a.2.2 0 0 1 .282 0z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The default find and replace UI.
 *
 * It registers the `'findAndReplace'` UI button in the editor's {@link module:ui/componentfactory~ComponentFactory component factory}.
 * that uses the {@link module:find-and-replace/findandreplace~FindAndReplace FindAndReplace} plugin API.
 *
 * @extends module:core/plugin~Plugin
 */
class FindAndReplaceUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'FindAndReplaceUI';
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		/**
		 * A reference to the find and replace form view.
		 *
		 * @member {module:find-and-replace/ui/findandreplaceformview~FindAndReplaceFormView} #formView
		 */
		this.formView = null;
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		// Register the toolbar dropdown component.
		editor.ui.componentFactory.add( 'findAndReplace', locale => {
			const dropdown = createDropdown( locale );
			const formView = this.formView = new FindAndReplaceFormView( editor.locale );

			// Dropdown should be disabled when in source editing mode. See #10001.
			dropdown.bind( 'isEnabled' ).to( editor.commands.get( 'find' ) );
			dropdown.panelView.children.add( formView );

			// Every time a dropdown is opened, the search text field should get focused and selected for better UX.
			// Note: Using the low priority here to make sure the following listener starts working after
			// the default action of the drop-down is executed (i.e. the panel showed up). Otherwise,
			// the invisible form/input cannot be focused/selected.
			//
			// Each time a dropdown is closed, move the focus back to the find and replace toolbar button
			// and let the find and replace editing feature know that all search results can be invalidated
			// and no longer should be marked in the content.
			dropdown.on( 'change:isOpen', ( event, name, isOpen ) => {
				if ( isOpen ) {
					formView.disableCssTransitions();

					formView.reset();
					formView._findInputView.fieldView.select();
					formView.focus();

					formView.enableCssTransitions();
				} else {
					formView.focus();

					this.fire( 'searchReseted' );
				}
			}, { priority: 'low' } );

			this._setupDropdownButton( dropdown );
			this._setupFormView( formView );

			return dropdown;
		} );
	}

	/**
	 * Sets up the find and replace button.
	 *
	 * @private
	 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdown
	 */
	_setupDropdownButton( dropdown ) {
		const editor = this.editor;
		const t = editor.locale.t;

		dropdown.buttonView.set( {
			icon: loupeIcon,
			label: t( 'Find and replace' ),
			keystroke: 'CTRL+F',
			tooltip: true
		} );

		editor.keystrokes.set( 'Ctrl+F', ( data, cancelEvent ) => {
			dropdown.isOpen = true;
			cancelEvent();
		} );
	}

	/**
	 * Sets up the form view for the find and replace.
	 *
	 * @private
	 * @param {module:find-and-replace/ui/findandreplaceformview~FindAndReplaceFormView} formView A related form view.
	 */
	_setupFormView( formView ) {
		const editor = this.editor;
		const commands = editor.commands;
		const findAndReplaceEditing = this.editor.plugins.get( 'FindAndReplaceEditing' );
		const editingState = findAndReplaceEditing.state;
		const sortMapping = { before: -1, same: 0, after: 1 };

		// Let the form know which result is being highlighted.
		formView.bind( 'highlightOffset' ).to( editingState, 'highlightedResult', highlightedResult => {
			if ( !highlightedResult ) {
				return 0;
			}

			return Array.from( editingState.results )
				.sort( ( a, b ) => sortMapping[ a.marker.getStart().compareWith( b.marker.getStart() ) ] )
				.indexOf( highlightedResult ) + 1;
		} );

		// Let the form know how many results were found in total.
		formView.listenTo( editingState.results, 'change', () => {
			formView.matchCount = editingState.results.length;
		} );

		// Command states are used to enable/disable individual form controls.
		// To keep things simple, instead of binding 4 individual observables, there's only one that combines every
		// commands' isEnabled state. Yes, it will change more often but this simplifies the structure of the form.
		formView.bind( '_areCommandsEnabled' ).to(
			commands.get( 'findNext' ), 'isEnabled',
			commands.get( 'findPrevious' ), 'isEnabled',
			commands.get( 'replace' ), 'isEnabled',
			commands.get( 'replaceAll' ), 'isEnabled',
			( findNext, findPrevious, replace, replaceAll ) => ( { findNext, findPrevious, replace, replaceAll } )
		);

		// The UI plugin works as an interface between the form and the editing part of the feature.
		formView.delegate( 'findNext', 'findPrevious', 'replace', 'replaceAll' ).to( this );

		// Let the feature know that search results are no longer relevant because the user changed the searched phrase
		// (or options) but didn't hit the "Find" button yet (e.g. still typing).
		formView.on( 'change:isDirty', ( evt, data, isDirty ) => {
			if ( isDirty ) {
				this.fire( 'searchReseted' );
			}
		} );
	}
}

/**
 * Fired when the find next button is triggered.
 *
 * @event findNext
 * @param {String} searchText Search text.
 */

/**
 * Fired when the find previous button is triggered.
 *
 * @event findPrevious
 * @param {String} searchText Search text.
 */

/**
 * Fired when the replace button is triggered.
 *
 * @event replace
 * @param {String} replaceText Replacement text.
 */

/**
 * Fired when the replaceAll button is triggered.
 *
 * @event replaceAll
 * @param {String} replaceText Replacement text.
 */

/**
 * Fired when the UI was reset and the search results marked in the editing root should be invalidated,
 * for instance, because the user changed the searched phrase (or options) but didn't hit
 * the "Find" button yet.
 *
 * @event searchReseted
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Executes findCallback and updates search results list.
 *
 * @param {module:engine/model/range~Range} range The model range to scan for matches.
 * @param {module:engine/model/model~Model} model The model.
 * @param {Function} findCallback The callback that should return `true` if provided text matches the search term.
 * @param {module:utils/collection~Collection} [startResults] An optional collection of find matches that the function should
 * start with. This would be a collection returned by a previous `updateFindResultFromRange()` call.
 * @returns {module:utils/collection~Collection} A collection of objects describing find match.
 *
 * An example structure:
 *
 * ```js
 * {
 *	id: resultId,
 *	label: foundItem.label,
 *	marker
 *	}
 * ```
 */
function updateFindResultFromRange( range, model, findCallback, startResults ) {
	const results = startResults || new Collection();

	model.change( writer => {
		[ ...range ].forEach( ( { type, item } ) => {
			if ( type === 'elementStart' ) {
				if ( model.schema.checkChild( item, '$text' ) ) {
					const foundItems = findCallback( {
						item,
						text: rangeToText( model.createRangeIn( item ) )
					} );

					if ( !foundItems ) {
						return;
					}

					foundItems.forEach( foundItem => {
						const resultId = `findResult:${ uid() }`;
						const marker = writer.addMarker( resultId, {
							usingOperation: false,
							affectsData: false,
							range: writer.createRange(
								writer.createPositionAt( item, foundItem.start ),
								writer.createPositionAt( item, foundItem.end )
							)
						} );

						const index = findInsertIndex( results, marker );

						results.add(
							{
								id: resultId,
								label: foundItem.label,
								marker
							},
							index
						);
					} );
				}
			}
		} );
	} );

	return results;
}

/**
 * Returns text representation of a range. The returned text length should be the same as range length.
 * In order to achieve this this function will replace inline elements (text-line) as new line character ("\n").
 *
 * @param {module:engine/model/range~Range} range The model range.
 * @returns {String} The text content of the provided range.
 */
function rangeToText( range ) {
	return Array.from( range.getItems() ).reduce( ( rangeText, node ) => {
		// Trim text to a last occurrence of an inline element and update range start.
		if ( !( node.is( 'text' ) || node.is( 'textProxy' ) ) ) {
			// Editor has only one inline element defined in schema: `<softBreak>` which is treated as new line character in blocks.
			// Special handling might be needed for other inline elements (inline widgets).
			return `${ rangeText }\n`;
		}

		return rangeText + node.data;
	}, '' );
}

// Finds the appropriate index in the resultsList Collection.
function findInsertIndex( resultsList, markerToInsert ) {
	const result = resultsList.find( ( { marker } ) => {
		return markerToInsert.getStart().isBefore( marker.getStart() );
	} );

	return result ? resultsList.getIndex( result ) : resultsList.length;
}

// Maps RegExp match result to find result.
function regexpMatchToFindResult( matchResult ) {
	const lastGroupIndex = matchResult.length - 1;

	let startOffset = matchResult.index;

	// Searches with match all flag have an extra matching group with empty string or white space matched before the word.
	// If the search term starts with the space already, there is no extra group even with match all flag on.
	if ( matchResult.length === 3 ) {
		startOffset += matchResult[ 1 ].length;
	}

	return {
		label: matchResult[ lastGroupIndex ],
		start: startOffset,
		end: startOffset + matchResult[ lastGroupIndex ].length
	};
}

/**
 * Creates a text matching callback for a specified search term and matching options.
 *
 * @param {String} searchTerm The search term.
 * @param {Object} [options] Matching options.
 * @param {Boolean} [options.matchCase=false] If set to `true` letter casing will be ignored.
 * @param {Boolean} [options.wholeWords=false] If set to `true` only whole words that match `callbackOrText` will be matched.
 * @returns {Function}
 */
function findByTextCallback( searchTerm, options ) {
	let flags = 'gu';

	if ( !options.matchCase ) {
		flags += 'i';
	}

	let regExpQuery = `(${ escapeRegExp( searchTerm ) })`;

	if ( options.wholeWords ) {
		const nonLetterGroup = '[^a-zA-Z\u00C0-\u024F\u1E00-\u1EFF]';

		if ( !new RegExp( '^' + nonLetterGroup ).test( searchTerm ) ) {
			regExpQuery = `(^|${ nonLetterGroup }|_)${ regExpQuery }`;
		}

		if ( !new RegExp( nonLetterGroup + '$' ).test( searchTerm ) ) {
			regExpQuery = `${ regExpQuery }(?=_|${ nonLetterGroup }|$)`;
		}
	}

	const regExp = new RegExp( regExpQuery, flags );

	function findCallback( { text } ) {
		const matches = [ ...text.matchAll( regExp ) ];

		return matches.map( regexpMatchToFindResult );
	}

	return findCallback;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The find command. It is used by the {@link module:find-and-replace/findandreplace~FindAndReplace find and replace feature}.
 *
 * @extends module:core/command~Command
 */
class FindCommand extends Command {
	/**
	 * Creates a new `FindCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor The editor on which this command will be used.
	 * @param {module:find-and-replace/findandreplacestate~FindAndReplaceState} state An object to hold plugin state.
	 */
	constructor( editor, state ) {
		super( editor );

		// The find command is always enabled.
		this.isEnabled = true;

		// It does not affect data so should be enabled in read-only mode.
		this.affectsData = false;

		/**
		 * The find and replace state object used for command operations.
		 *
		 * @private
		 * @member {module:find-and-replace/findandreplacestate~FindAndReplaceState} #_state
		 */
		this._state = state;
	}

	/**
	 * Executes the command.
	 *
	 * @param {Function|String} callbackOrText
	 * @param {Object} [options]
	 * @param {Boolean} [options.matchCase=false] If set to `true`, the letter case will be matched.
	 * @param {Boolean} [options.wholeWords=false] If set to `true`, only whole words that match `callbackOrText` will be matched.
	 *
	 * @fires execute
	 */
	execute( callbackOrText, { matchCase, wholeWords } = {} ) {
		const { editor } = this;
		const { model } = editor;

		let findCallback;

		// Allow to execute `find()` on a plugin with a keyword only.
		if ( typeof callbackOrText === 'string' ) {
			findCallback = findByTextCallback( callbackOrText, { matchCase, wholeWords } );

			this._state.searchText = callbackOrText;
		} else {
			findCallback = callbackOrText;
		}

		// Initial search is done on all nodes in all roots inside the content.
		const results = model.document.getRootNames()
			.reduce( ( ( currentResults, rootName ) => updateFindResultFromRange(
				model.createRangeIn( model.document.getRoot( rootName ) ),
				model,
				findCallback,
				currentResults
			) ), null );

		this._state.clear( model );
		this._state.results.addMany( Array.from( results ) );
		this._state.highlightedResult = results.get( 0 );

		if ( typeof callbackOrText === 'string' ) {
			this._state.searchText = callbackOrText;
		}

		this._state.matchCase = !!matchCase;
		this._state.matchWholeWords = !!wholeWords;

		return {
			results,
			findCallback
		};
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The replace command. It is used by the {@link module:find-and-replace/findandreplace~FindAndReplace find and replace feature}.
 *
 * @extends module:core/command~Command
 */
class ReplaceCommand extends Command {
	/**
	 * Creates a new `ReplaceCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
	 * @param {module:find-and-replace/findandreplacestate~FindAndReplaceState} state An object to hold plugin state.
	 */
	constructor( editor, state ) {
		super( editor );

		// The replace command is always enabled.
		this.isEnabled = true;

		/**
		 * The find and replace state object used for command operations.
		 *
		 * @protected
		 * @member {module:find-and-replace/findandreplacestate~FindAndReplaceState} #_state
		 */
		this._state = state;
	}

	/**
	 * Replace a given find result by a string or a callback.
	 *
	 * @param {String} replacementText
	 * @param {Object} result A single result from the find command.
	 *
	 * @fires module:core/command~Command#event:execute
	 */
	execute( replacementText, result ) {
		const { model } = this.editor;

		model.change( writer => {
			const range = result.marker.getRange();

			// Don't replace a result (marker) that found its way into the $graveyard (e.g. removed by collaborators).
			if ( range.root.rootName === '$graveyard' ) {
				this._state.results.remove( result );

				return;
			}

			let textAttributes = {};

			for ( const item of range.getItems() ) {
				if ( item.is( '$text' ) || item.is( '$textProxy' ) ) {
					textAttributes = item.getAttributes();
					break;
				}
			}

			model.insertContent( writer.createText( replacementText, textAttributes ), range );

			if ( this._state.results.has( result ) ) {
				this._state.results.remove( result );
			}
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The replace all command. It is used by the {@link module:find-and-replace/findandreplace~FindAndReplace find and replace feature}.
 *
 * @extends module:find-and-replace/replacecommand~ReplaceCommand
 */
class ReplaceAllCommand extends ReplaceCommand {
	/**
	 * Replaces all the occurrences of `textToReplace` with a given `newText` string.
	 *
	 * ```js
	 *	replaceAllCommand.execute( 'replaceAll', 'new text replacement', 'text to replace' );
	 * ```
	 *
	 * Alternatively you can call it from editor instance:
	 *
	 * ```js
	 *	editor.execute( 'replaceAll', 'new text', 'old text' );
	 * ```
	 *
	 * @param {String} newText Text that will be inserted to the editor for each match.
	 * @param {String|module:utils/collection~Collection} textToReplace Text to be replaced or a collection of matches
	 * as returned by the find command.
	 *
	 * @fires module:core/command~Command#event:execute
	 */
	execute( newText, textToReplace ) {
		const { editor } = this;
		const { model } = editor;

		const results = textToReplace instanceof Collection ?
			textToReplace : model.document.getRootNames()
				.reduce( ( ( currentResults, rootName ) => updateFindResultFromRange(
					model.createRangeIn( model.document.getRoot( rootName ) ),
					model,
					findByTextCallback( textToReplace, this._state ),
					currentResults
				) ), null );

		if ( results.length ) {
			model.change( () => {
				[ ...results ].forEach( searchResult => {
					// Just reuse logic from the replace command to replace a single match.
					super.execute( newText, searchResult );
				} );
			} );
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The find next command. Moves the highlight to the next search result.
 *
 * It is used by the {@link module:find-and-replace/findandreplace~FindAndReplace find and replace feature}.
 *
 * @extends module:core/command~Command
 */
class FindNextCommand extends Command {
	/**
	 * Creates a new `FindNextCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor The editor on which this command will be used.
	 * @param {module:find-and-replace/findandreplacestate~FindAndReplaceState} state An object to hold plugin state.
	 */
	constructor( editor, state ) {
		super( editor );

		// It does not affect data so should be enabled in read-only mode.
		this.affectsData = false;

		/**
		 * The find and replace state object used for command operations.
		 *
		 * @protected
		 * @member {module:find-and-replace/findandreplacestate~FindAndReplaceState} #_state
		 */
		this._state = state;

		this.isEnabled = false;

		this.listenTo( this._state.results, 'change', () => {
			this.isEnabled = this._state.results.length > 1;
		} );
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		this.isEnabled = this._state.results.length > 1;
	}

	/**
	 * @inheritDoc
	 */
	execute() {
		const results = this._state.results;
		const currentIndex = results.getIndex( this._state.highlightedResult );
		const nextIndex = currentIndex + 1 >= results.length ?
			0 : currentIndex + 1;

		this._state.highlightedResult = this._state.results.get( nextIndex );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The find previous command. Moves the highlight to the previous search result.
 *
 * It is used by the {@link module:find-and-replace/findandreplace~FindAndReplace find and replace feature}.
 *
 * @extends module:find-and-replace/findnextcommand~FindNextCommand
 */
class FindPreviousCommand extends FindNextCommand {
	/**
	 * @inheritDoc
	 */
	execute() {
		const results = this._state.results;
		const currentIndex = results.getIndex( this._state.highlightedResult );
		const previousIndex = currentIndex - 1 < 0 ?
			this._state.results.length - 1 : currentIndex - 1;

		this._state.highlightedResult = this._state.results.get( previousIndex );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The object storing find and replace plugin state for a given editor instance.
 *
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class FindAndReplaceState {
	/**
	 * Creates an instance of the state.
	 *
	 * @param {module:engine/model/model~Model} model
	 */
	constructor( model ) {
		/**
		 * A collection of find matches.
		 *
		 * @protected
		 * @observable
		 * @member {module:utils/collection~Collection} #results
		 */
		this.set( 'results', new Collection() );

		/**
		 * Currently highlighted search result in {@link #results matched results}.
		 *
		 * @readonly
		 * @observable
		 * @member {Object|null} #highlightedResult
		 */
		this.set( 'highlightedResult', null );

		/**
		 * Searched text value.
		 *
		 * @readonly
		 * @observable
		 * @member {String} #searchText
		 */
		this.set( 'searchText', '' );

		/**
		 * Replace text value.
		 *
		 * @readonly
		 * @observable
		 * @member {String} #replaceText
		 */
		this.set( 'replaceText', '' );

		/**
		 * Indicates whether the matchCase checkbox has been checked.
		 *
		 * @readonly
		 * @observable
		 * @member {Boolean} #matchCase
		 */
		this.set( 'matchCase', false );

		/**
		 * Indicates whether the matchWholeWords checkbox has been checked.
		 *
		 * @readonly
		 * @observable
		 * @member {Boolean} #matchWholeWords
		 */
		this.set( 'matchWholeWords', false );

		this.results.on( 'change', ( eventInfo, { removed, index } ) => {
			removed = Array.from( removed );

			if ( removed.length ) {
				let highlightedResultRemoved = false;

				model.change( writer => {
					for ( const removedResult of removed ) {
						if ( this.highlightedResult === removedResult ) {
							highlightedResultRemoved = true;
						}

						if ( model.markers.has( removedResult.marker.name ) ) {
							writer.removeMarker( removedResult.marker );
						}
					}
				} );

				if ( highlightedResultRemoved ) {
					const nextHighlightedIndex = index >= this.results.length ? 0 : index;
					this.highlightedResult = this.results.get( nextHighlightedIndex );
				}
			}
		} );
	}

	/**
	 * Cleans the state up and removes markers from the model.
	 *
	 * @param {module:engine/model/model~Model} model
	 */
	clear( model ) {
		this.searchText = '';

		model.change( writer => {
			if ( this.highlightedResult ) {
				const oldMatchId = this.highlightedResult.marker.name.split( ':' )[ 1 ];
				const oldMarker = model.markers.get( `findResultHighlighted:${ oldMatchId }` );

				if ( oldMarker ) {
					writer.removeMarker( oldMarker );
				}
			}

			[ ...this.results ].forEach( ( { marker } ) => {
				writer.removeMarker( marker );
			} );
		} );

		this.results.clear();
	}
}

mix( FindAndReplaceState, ObservableMixin );var css_248z$r = ".ck-find-result{background:#ff0;color:var(--ck-color-text)}.ck-find-result_selected{background:#ff9633}";
styleInject(css_248z$r);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const HIGHLIGHT_CLASS$1 = 'ck-find-result_selected';

// Reacts to document changes in order to update search list.
function onDocumentChange( results, model, searchCallback ) {
	const changedNodes = new Set();
	const removedMarkers = new Set();

	const changes = model.document.differ.getChanges();

	// Get nodes in which changes happened to re-run a search callback on them.
	changes.forEach( change => {
		if ( change.name === '$text' || model.schema.isInline( change.position.nodeAfter ) ) {
			changedNodes.add( change.position.parent );

			[ ...model.markers.getMarkersAtPosition( change.position ) ].forEach( markerAtChange => {
				removedMarkers.add( markerAtChange.name );
			} );
		} else if ( change.type === 'insert' ) {
			changedNodes.add( change.position.nodeAfter );
		}
	} );

	// Get markers from removed nodes also.
	model.document.differ.getChangedMarkers().forEach( ( { name, data: { newRange } } ) => {
		if ( newRange && newRange.start.root.rootName === '$graveyard' ) {
			removedMarkers.add( name );
		}
	} );

	// Get markers from the updated nodes and remove all (search will be re-run on these nodes).
	changedNodes.forEach( node => {
		const markersInNode = [ ...model.markers.getMarkersIntersectingRange( model.createRangeIn( node ) ) ];

		markersInNode.forEach( marker => removedMarkers.add( marker.name ) );
	} );

	// Remove results & markers from the changed part of content.
	model.change( writer => {
		removedMarkers.forEach( markerName => {
			// Remove the result first - in order to prevent rendering a removed marker.
			if ( results.has( markerName ) ) {
				results.remove( markerName );
			}

			writer.removeMarker( markerName );
		} );
	} );

	// Run search callback again on updated nodes.
	changedNodes.forEach( nodeToCheck => {
		updateFindResultFromRange( model.createRangeOn( nodeToCheck ), model, searchCallback, results );
	} );
}

/**
 * Implements the editing part for find and replace plugin. For example conversion, commands etc.
 *
 * @extends module:core/plugin~Plugin
 */
class FindAndReplaceEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'FindAndReplaceEditing';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		/**
		 * The collection of currently highlighted search results.
		 *
		 * @private
		 * @member {module:utils/collection~Collection} #_activeResults
		 */
		this._activeResults = null;

		/**
		 * An object storing the find and replace state within a given editor instance.
		 *
		 * @member {module:find-and-replace/findandreplacestate~FindAndReplaceState} #state
		 */
		this.state = new FindAndReplaceState( this.editor.model );

		this._defineConverters();
		this._defineCommands();

		this.listenTo( this.state, 'change:highlightedResult', ( eventInfo, name, newValue, oldValue ) => {
			const { model } = this.editor;

			model.change( writer => {
				if ( oldValue ) {
					const oldMatchId = oldValue.marker.name.split( ':' )[ 1 ];
					const oldMarker = model.markers.get( `findResultHighlighted:${ oldMatchId }` );

					if ( oldMarker ) {
						writer.removeMarker( oldMarker );
					}
				}

				if ( newValue ) {
					const newMatchId = newValue.marker.name.split( ':' )[ 1 ];
					writer.addMarker( `findResultHighlighted:${ newMatchId }`, {
						usingOperation: false,
						affectsData: false,
						range: newValue.marker.getRange()
					} );
				}
			} );
		} );

		const debouncedScrollListener = debounce( scrollToHighlightedResult.bind( this ), 32 );
		// Debounce scroll as highlight might be changed very frequently, e.g. when there's a replace all command.
		this.listenTo( this.state, 'change:highlightedResult', debouncedScrollListener, { priority: 'low' } );

		// It's possible that the editor will get destroyed before debounced call kicks in.
		// This would result with accessing a view three that is no longer in DOM.
		this.listenTo( this.editor, 'destroy', debouncedScrollListener.cancel );

		/* istanbul ignore next */
		function scrollToHighlightedResult( eventInfo, name, newValue ) {
			if ( newValue ) {
				const domConverter = this.editor.editing.view.domConverter;
				const viewRange = this.editor.editing.mapper.toViewRange( newValue.marker.getRange() );

				scrollViewportToShowTarget( {
					target: domConverter.viewRangeToDom( viewRange ),
					viewportOffset: 40
				} );
			}
		}
	}

	/**
	 * Initiate a search.
	 *
	 * @param {Function|String} callbackOrText
	 * @returns {module:utils/collection~Collection}
	 */
	find( callbackOrText ) {
		const { editor } = this;
		const { model } = editor;

		const { findCallback, results } = editor.execute( 'find', callbackOrText );

		this._activeResults = results;

		// @todo: handle this listener, another copy is in findcommand.js file.
		this.listenTo( model.document, 'change:data', () => onDocumentChange( this._activeResults, model, findCallback ) );

		return this._activeResults;
	}

	/**
	 * Stops active results from updating, and clears out the results.
	 */
	stop() {
		if ( !this._activeResults ) {
			return;
		}

		this.stopListening( this.editor.model.document );

		this.state.clear( this.editor.model );

		this._activeResults = null;
	}

	/**
	 * Sets up the commands.
	 *
	 * @private
	 */
	_defineCommands() {
		this.editor.commands.add( 'find', new FindCommand( this.editor, this.state ) );
		this.editor.commands.add( 'findNext', new FindNextCommand( this.editor, this.state ) );
		this.editor.commands.add( 'findPrevious', new FindPreviousCommand( this.editor, this.state ) );
		this.editor.commands.add( 'replace', new ReplaceCommand( this.editor, this.state ) );
		this.editor.commands.add( 'replaceAll', new ReplaceAllCommand( this.editor, this.state ) );
	}

	/**
	 * Sets up the marker downcast converters for search results highlighting.
	 *
	 * @private
	 */
	_defineConverters() {
		const { editor } = this;

		// Setup the marker highlighting conversion.
		editor.conversion.for( 'editingDowncast' ).markerToHighlight( {
			model: 'findResult',
			view: ( { markerName } ) => {
				const [ , id ] = markerName.split( ':' );

				// Marker removal from the view has a bug: https://github.com/ckeditor/ckeditor5/issues/7499
				// A minimal option is to return a new object for each converted marker...
				return {
					name: 'span',
					classes: [ 'ck-find-result' ],
					attributes: {
						// ...however, adding a unique attribute should be future-proof..
						'data-find-result': id
					}
				};
			}
		} );

		editor.conversion.for( 'editingDowncast' ).markerToHighlight( {
			model: 'findResultHighlighted',
			view: ( { markerName } ) => {
				const [ , id ] = markerName.split( ':' );

				// Marker removal from the view has a bug: https://github.com/ckeditor/ckeditor5/issues/7499
				// A minimal option is to return a new object for each converted marker...
				return {
					name: 'span',
					classes: [ HIGHLIGHT_CLASS$1 ],
					attributes: {
						// ...however, adding a unique attribute should be future-proof..
						'data-find-result': id
					}
				};
			}
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The find and replace plugin.
 *
 * For a detailed overview, check the {@glink features/find-and-replace Find and replace feature documentation}.
 *
 * This is a "glue" plugin which loads the following plugins:
 *
 * * The {@link module:find-and-replace/findandreplaceediting~FindAndReplaceEditing find and replace editing feature},
 * * The {@link module:find-and-replace/findandreplaceui~FindAndReplaceUI find and replace UI feature}
 *
 * @extends module:core/plugin~Plugin
 */
class FindAndReplace extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ FindAndReplaceEditing, FindAndReplaceUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'FindAndReplace';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const ui = this.editor.plugins.get( 'FindAndReplaceUI' );
		const findAndReplaceEditing = this.editor.plugins.get( 'FindAndReplaceEditing' );
		const state = findAndReplaceEditing.state;

		ui.on( 'findNext', ( event, data ) => {
			// Data is contained only for the "find" button.
			if ( data ) {
				state.searchText = data.searchText;
				this.editor.execute( 'find', data.searchText, data );
			} else {
				// Find next arrow button press.
				this.editor.execute( 'findNext' );
			}
		} );

		ui.on( 'findPrevious', ( event, data ) => {
			if ( data && state.searchText !== data.searchText ) {
				this.editor.execute( 'find', data.searchText );
			} else {
				// Subsequent calls.
				this.editor.execute( 'findPrevious' );
			}
		} );

		ui.on( 'replace', ( event, data ) => {
			if ( state.searchText !== data.searchText ) {
				this.editor.execute( 'find', data.searchText );
			}

			const highlightedResult = state.highlightedResult;

			if ( highlightedResult ) {
				this.editor.execute( 'replace', data.replaceText, highlightedResult );
			}
		} );

		ui.on( 'replaceAll', ( event, data ) => {
			// The state hadn't been yet built for this search text.
			if ( state.searchText !== data.searchText ) {
				this.editor.execute( 'find', data.searchText );
			}

			this.editor.execute( 'replaceAll', data.replaceText, state.results );
		} );

		// Reset the state when the user invalidated last search results, for instance,
		// by starting typing another search query or changing options.
		ui.on( 'searchReseted', () => {
			state.clear( this.editor.model );
			findAndReplaceEditing.stop();
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The paragraph command.
 *
 * @extends module:core/command~Command
 */
class ParagraphCommand extends Command {
	/**
	 * The value of the command. Indicates whether the selection start is placed in a paragraph.
	 *
	 * @readonly
	 * @observable
	 * @member {Boolean} #value
	 */

	/**
	 * @inheritDoc
	 */
	refresh() {
		const model = this.editor.model;
		const document = model.document;
		const block = first( document.selection.getSelectedBlocks() );

		this.value = !!block && block.is( 'element', 'paragraph' );
		this.isEnabled = !!block && checkCanBecomeParagraph( block, model.schema );
	}

	/**
	 * Executes the command. All the blocks (see {@link module:engine/model/schema~Schema}) in the selection
	 * will be turned to paragraphs.
	 *
	 * @fires execute
	 * @param {Object} [options] Options for the executed command.
	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} [options.selection]
	 * The selection that the command should be applied to.
	 * By default, if not provided, the command is applied to the {@link module:engine/model/document~Document#selection}.
	 */
	execute( options = {} ) {
		const model = this.editor.model;
		const document = model.document;

		model.change( writer => {
			const blocks = ( options.selection || document.selection ).getSelectedBlocks();

			for ( const block of blocks ) {
				if ( !block.is( 'element', 'paragraph' ) && checkCanBecomeParagraph( block, model.schema ) ) {
					writer.rename( block, 'paragraph' );
				}
			}
		} );
	}
}

// Checks whether the given block can be replaced by a paragraph.
//
// @private
// @param {module:engine/model/element~Element} block A block to be tested.
// @param {module:engine/model/schema~Schema} schema The schema of the document.
// @returns {Boolean}
function checkCanBecomeParagraph( block, schema ) {
	return schema.checkChild( block.parent, 'paragraph' ) && !schema.isObject( block );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The insert paragraph command. It inserts a new paragraph at a specific
 * {@link module:engine/model/position~Position document position}.
 *
 *		// Insert a new paragraph before an element in the document.
 *		editor.execute( 'insertParagraph', {
 *			position: editor.model.createPositionBefore( element )
 *		} );
 *
 * If a paragraph is disallowed in the context of the specific position, the command
 * will attempt to split position ancestors to find a place where it is possible
 * to insert a paragraph.
 *
 * **Note**: This command moves the selection to the inserted paragraph.
 *
 * @extends module:core/command~Command
 */
class InsertParagraphCommand extends Command {
	/**
	 * Executes the command.
	 *
	 * @param {Object} options Options for the executed command.
	 * @param {module:engine/model/position~Position} options.position The model position at which
	 * the new paragraph will be inserted.
	 * @param {Object} attributes Attributes keys and values to set on a inserted paragraph
	 * @fires execute
	 */
	execute( options ) {
		const model = this.editor.model;
		const attributes = options.attributes;

		let position = options.position;

		model.change( writer => {
			const paragraph = writer.createElement( 'paragraph' );

			if ( attributes ) {
				model.schema.setAllowedAttributes( paragraph, attributes, writer );
			}

			if ( !model.schema.checkChild( position.parent, paragraph ) ) {
				const allowedParent = model.schema.findAllowedParent( position, paragraph );

				// It could be there's no ancestor limit that would allow paragraph.
				// In theory, "paragraph" could be disallowed even in the "$root".
				if ( !allowedParent ) {
					return;
				}

				position = writer.split( position, allowedParent ).position;
			}

			model.insertContent( paragraph, position );

			writer.setSelection( paragraph, 'in' );
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The paragraph feature for the editor.
 *
 * It introduces the `<paragraph>` element in the model which renders as a `<p>` element in the DOM and data.
 *
 * It also brings two editors commands:
 *
 * * The {@link module:paragraph/paragraphcommand~ParagraphCommand `'paragraph'`} command that converts all
 * blocks in the model selection into paragraphs.
 * * The {@link module:paragraph/insertparagraphcommand~InsertParagraphCommand `'insertParagraph'`} command
 * that inserts a new paragraph at a specified location in the model.
 *
 * @extends module:core/plugin~Plugin
 */
class Paragraph extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Paragraph';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const model = editor.model;

		editor.commands.add( 'paragraph', new ParagraphCommand( editor ) );
		editor.commands.add( 'insertParagraph', new InsertParagraphCommand( editor ) );

		// Schema.
		model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );

		editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );

		// Conversion for paragraph-like elements which has not been converted by any plugin.
		editor.conversion.for( 'upcast' ).elementToElement( {
			model: ( viewElement, { writer } ) => {
				if ( !Paragraph.paragraphLikeElements.has( viewElement.name ) ) {
					return null;
				}

				// Do not auto-paragraph empty elements.
				if ( viewElement.isEmpty ) {
					return null;
				}

				return writer.createElement( 'paragraph' );
			},
			view: /.+/,
			converterPriority: 'low'
		} );
	}
}

/**
 * A list of element names which should be treated by the autoparagraphing algorithms as
 * paragraph-like. This means that e.g. the following content:
 *
 *		<h1>Foo</h1>
 *		<table>
 *			<tr>
 *				<td>X</td>
 *				<td>
 *					<ul>
 *						<li>Y</li>
 *						<li>Z</li>
 *					</ul>
 *				</td>
 *			</tr>
 *		</table>
 *
 * contains five paragraph-like elements: `<h1>`, two `<td>`s and two `<li>`s.
 * Hence, if none of the features is going to convert those elements the above content will be automatically handled
 * by the paragraph feature and converted to:
 *
 *		<p>Foo</p>
 *		<p>X</p>
 *		<p>Y</p>
 *		<p>Z</p>
 *
 * Note: The `<td>` containing two `<li>` elements was ignored as the innermost paragraph-like elements
 * have a priority upon conversion.
 *
 * @member {Set.<String>} module:paragraph/paragraph~Paragraph.paragraphLikeElements
 */
Paragraph.paragraphLikeElements = new Set( [
	'blockquote',
	'dd',
	'div',
	'dt',
	'h1',
	'h2',
	'h3',
	'h4',
	'h5',
	'h6',
	'li',
	'p',
	'td',
	'th'
] );/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The heading command. It is used by the {@link module:heading/heading~Heading heading feature} to apply headings.
 *
 * @extends module:core/command~Command
 */
class HeadingCommand extends Command {
	/**
	 * Creates an instance of the command.
	 *
	 * @param {module:core/editor/editor~Editor} editor Editor instance.
	 * @param {Array.<String>} modelElements Names of the element which this command can apply in the model.
	 */
	constructor( editor, modelElements ) {
		super( editor );

		/**
		 * If the selection starts in a heading (which {@link #modelElements is supported by this command})
		 * the value is set to the name of that heading model element.
		 * It is  set to `false` otherwise.
		 *
		 * @observable
		 * @readonly
		 * @member {Boolean|String} #value
		 */

		/**
		 * Set of defined model's elements names that this command support.
		 * See {@link module:heading/heading~HeadingOption}.
		 *
		 * @readonly
		 * @member {Array.<String>}
		 */
		this.modelElements = modelElements;
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		const block = first( this.editor.model.document.selection.getSelectedBlocks() );

		this.value = !!block && this.modelElements.includes( block.name ) && block.name;
		this.isEnabled = !!block && this.modelElements.some( heading => checkCanBecomeHeading( block, heading, this.editor.model.schema ) );
	}

	/**
	 * Executes the command. Applies the heading to the selected blocks or, if the first selected
	 * block is a heading already, turns selected headings (of this level only) to paragraphs.
	 *
	 * @param {Object} options
	 * @param {String} options.value Name of the element which this command will apply in the model.
	 * @fires execute
	 */
	execute( options ) {
		const model = this.editor.model;
		const document = model.document;

		const modelElement = options.value;

		model.change( writer => {
			const blocks = Array.from( document.selection.getSelectedBlocks() )
				.filter( block => {
					return checkCanBecomeHeading( block, modelElement, model.schema );
				} );

			for ( const block of blocks ) {
				if ( !block.is( 'element', modelElement ) ) {
					writer.rename( block, modelElement );
				}
			}
		} );
	}
}

// Checks whether the given block can be replaced by a specific heading.
//
// @private
// @param {module:engine/model/element~Element} block A block to be tested.
// @param {module:heading/headingcommand~HeadingCommand#modelElement} heading Command element name in the model.
// @param {module:engine/model/schema~Schema} schema The schema of the document.
// @returns {Boolean}
function checkCanBecomeHeading( block, heading, schema ) {
	return schema.checkChild( block.parent, heading ) && !schema.isObject( block );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const defaultModelElement = 'paragraph';

/**
 * The headings engine feature. It handles switching between block formats &ndash; headings and paragraph.
 * This class represents the engine part of the heading feature. See also {@link module:heading/heading~Heading}.
 * It introduces `heading1`-`headingN` commands which allow to convert paragraphs into headings.
 *
 * @extends module:core/plugin~Plugin
 */
class HeadingEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'HeadingEditing';
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		editor.config.define( 'heading', {
			options: [
				{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
				{ model: 'heading1', view: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
				{ model: 'heading2', view: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
				{ model: 'heading3', view: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
			]
		} );
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ Paragraph ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const options = editor.config.get( 'heading.options' );

		const modelElements = [];

		for ( const option of options ) {
			// Skip paragraph - it is defined in required Paragraph feature.
			if ( option.model !== defaultModelElement ) {
				// Schema.
				editor.model.schema.register( option.model, {
					inheritAllFrom: '$block'
				} );

				editor.conversion.elementToElement( option );

				modelElements.push( option.model );
			}
		}

		this._addDefaultH1Conversion( editor );

		// Register the heading command for this option.
		editor.commands.add( 'heading', new HeadingCommand( editor, modelElements ) );
	}

	/**
	 * @inheritDoc
	 */
	afterInit() {
		// If the enter command is added to the editor, alter its behavior.
		// Enter at the end of a heading element should create a paragraph.
		const editor = this.editor;
		const enterCommand = editor.commands.get( 'enter' );
		const options = editor.config.get( 'heading.options' );

		if ( enterCommand ) {
			this.listenTo( enterCommand, 'afterExecute', ( evt, data ) => {
				const positionParent = editor.model.document.selection.getFirstPosition().parent;
				const isHeading = options.some( option => positionParent.is( 'element', option.model ) );

				if ( isHeading && !positionParent.is( 'element', defaultModelElement ) && positionParent.childCount === 0 ) {
					data.writer.rename( positionParent, defaultModelElement );
				}
			} );
		}
	}

	/**
	 * Adds default conversion for `h1` -> `heading1` with a low priority.
	 *
	 * @private
	 * @param {module:core/editor/editor~Editor} editor Editor instance on which to add the `h1` conversion.
	 */
	_addDefaultH1Conversion( editor ) {
		editor.conversion.for( 'upcast' ).elementToElement( {
			model: 'heading1',
			view: 'h1',
			// With a `low` priority, `paragraph` plugin autoparagraphing mechanism is executed. Make sure
			// this listener is called before it. If not, `h1` will be transformed into a paragraph.
			converterPriority: priorities.get( 'low' ) + 1
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module heading/utils
 */

/**
 * Returns heading options as defined in `config.heading.options` but processed to consider
 * the editor localization, i.e. to display {@link module:heading/heading~HeadingOption}
 * in the correct language.
 *
 * Note: The reason behind this method is that there is no way to use {@link module:utils/locale~Locale#t}
 * when the user configuration is defined because the editor does not exist yet.
 *
 * @param {module:core/editor/editor~Editor} editor
 * @returns {Array.<module:heading/heading~HeadingOption>}.
 */
function getLocalizedOptions( editor ) {
	const t = editor.t;
	const localizedTitles = {
		Paragraph: t( 'Paragraph' ),
		'Heading 1': t( 'Heading 1' ),
		'Heading 2': t( 'Heading 2' ),
		'Heading 3': t( 'Heading 3' ),
		'Heading 4': t( 'Heading 4' ),
		'Heading 5': t( 'Heading 5' ),
		'Heading 6': t( 'Heading 6' )
	};

	return editor.config.get( 'heading.options' ).map( option => {
		const title = localizedTitles[ option.title ];

		if ( title && title != option.title ) {
			option.title = title;
		}

		return option;
	} );
}var css_248z$q = ".ck.ck-heading_heading1{font-size:20px}.ck.ck-heading_heading2{font-size:17px}.ck.ck-heading_heading3{font-size:14px}.ck[class*=ck-heading_heading]{font-weight:700}.ck.ck-dropdown.ck-heading-dropdown .ck-dropdown__button .ck-button__label{width:8em}.ck.ck-dropdown.ck-heading-dropdown .ck-dropdown__panel .ck-list__item{min-width:18em}";
styleInject(css_248z$q);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The headings UI feature. It introduces the `headings` dropdown.
 *
 * @extends module:core/plugin~Plugin
 */
class HeadingUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'HeadingUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;
		const options = getLocalizedOptions( editor );
		const defaultTitle = t( 'Choose heading' );
		const dropdownTooltip = t( 'Heading' );

		// Register UI component.
		editor.ui.componentFactory.add( 'heading', locale => {
			const titles = {};
			const itemDefinitions = new Collection();

			const headingCommand = editor.commands.get( 'heading' );
			const paragraphCommand = editor.commands.get( 'paragraph' );

			const commands = [ headingCommand ];

			for ( const option of options ) {
				const def = {
					type: 'button',
					model: new Model( {
						label: option.title,
						class: option.class,
						withText: true
					} )
				};

				if ( option.model === 'paragraph' ) {
					def.model.bind( 'isOn' ).to( paragraphCommand, 'value' );
					def.model.set( 'commandName', 'paragraph' );
					commands.push( paragraphCommand );
				} else {
					def.model.bind( 'isOn' ).to( headingCommand, 'value', value => value === option.model );
					def.model.set( {
						commandName: 'heading',
						commandValue: option.model
					} );
				}

				// Add the option to the collection.
				itemDefinitions.add( def );

				titles[ option.model ] = option.title;
			}

			const dropdownView = createDropdown( locale );
			addListToDropdown( dropdownView, itemDefinitions );

			dropdownView.buttonView.set( {
				isOn: false,
				withText: true,
				tooltip: dropdownTooltip
			} );

			dropdownView.extendTemplate( {
				attributes: {
					class: [
						'ck-heading-dropdown'
					]
				}
			} );

			dropdownView.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( ...areEnabled ) => {
				return areEnabled.some( isEnabled => isEnabled );
			} );

			dropdownView.buttonView.bind( 'label' ).to( headingCommand, 'value', paragraphCommand, 'value', ( value, para ) => {
				const whichModel = value || para && 'paragraph';
				// If none of the commands is active, display default title.
				return titles[ whichModel ] ? titles[ whichModel ] : defaultTitle;
			} );

			// Execute command when an item from the dropdown is selected.
			this.listenTo( dropdownView, 'execute', evt => {
				editor.execute( evt.source.commandName, evt.source.commandValue ? { value: evt.source.commandValue } : undefined );
				editor.editing.view.focus();
			} );

			return dropdownView;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The headings feature.
 *
 * For a detailed overview, check the {@glink features/headings Headings feature documentation}
 * and the {@glink api/heading package page}.
 *
 * This is a "glue" plugin which loads the {@link module:heading/headingediting~HeadingEditing heading editing feature}
 * and {@link module:heading/headingui~HeadingUI heading UI feature}.
 *
 * @extends module:core/plugin~Plugin
 */
class Heading extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ HeadingEditing, HeadingUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Heading';
	}
}

/**
 * The configuration of the heading feature. Introduced by the {@link module:heading/headingediting~HeadingEditing} feature.
 *
 * Read more in {@link module:heading/heading~HeadingConfig}.
 *
 * @member {module:heading/heading~HeadingConfig} module:core/editor/editorconfig~EditorConfig#heading
 */

/**
 * The configuration of the heading feature.
 * The option is used by the {@link module:heading/headingediting~HeadingEditing} feature.
 *
 *		ClassicEditor
 *			.create( {
 * 				heading: ... // Heading feature config.
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
 *
 * @interface HeadingConfig
 */

/**
 * The available heading options.
 *
 * The default value is:
 *
 *		const headingConfig = {
 *			options: [
 *				{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
 *				{ model: 'heading1', view: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
 *				{ model: 'heading2', view: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
 *				{ model: 'heading3', view: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
 *			]
 *		};
 *
 * It defines 3 levels of headings. In the editor model they will use `heading1`, `heading2`, and `heading3` elements.
 * Their respective view elements (so the elements output by the editor) will be: `h2`, `h3`, and `h4`. This means that
 * if you choose "Heading 1" in the headings dropdown the editor will turn the current block to `<heading1>` in the model
 * which will result in rendering (and outputting to data) the `<h2>` element.
 *
 * The `title` and `class` properties will be used by the `headings` dropdown to render available options.
 * Usually, the first option in the headings dropdown is the "Paragraph" option, hence it's also defined on the list.
 * However, you don't need to define its view representation because it's handled by
 * the {@link module:paragraph/paragraph~Paragraph} feature (which is required by
 * the {@link module:heading/headingediting~HeadingEditing} feature).
 *
 * You can **read more** about configuring heading levels and **see more examples** in
 * the {@glink features/headings Headings} guide.
 *
 * Note: In the model you should always start from `heading1`, regardless of how the headings are represented in the view.
 * That's assumption is used by features like {@link module:autoformat/autoformat~Autoformat} to know which element
 * they should use when applying the first level heading.
 *
 * The defined headings are also available as values passed to the `'heading'` command under their model names.
 * For example, the below code will apply `<heading1>` to the current selection:
 *
 *		editor.execute( 'heading', { value: 'heading1' } );
 *
 * @member {Array.<module:heading/heading~HeadingOption>} module:heading/heading~HeadingConfig#options
 */

/**
 * Heading option descriptor.
 *
 * @typedef {Object} module:heading/heading~HeadingOption
 * @property {String} model Name of the model element to convert.
 * @property {module:engine/view/elementdefinition~ElementDefinition} view Definition of a view element to convert from/to.
 * @property {String} title The user-readable title of the option.
 * @property {String} class The class which will be added to the dropdown item representing this option.
 * @property {String} [icon] Icon used by {@link module:heading/headingbuttonsui~HeadingButtonsUI}. It can be omitted when using
 * the default configuration.
 * @extends module:engine/conversion/conversion~ConverterDefinition
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The indent editing feature.
 *
 * This plugin registers the `'indent'` and `'outdent'` commands.
 *
 * **Note**: In order for the commands to work, at least one of the compatible features is required. Read more in the
 * {@link module:indent/indent~Indent indent feature} API documentation.
 *
 * @extends module:core/plugin~Plugin
 */
class IndentEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'IndentEditing';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		editor.commands.add( 'indent', new MultiCommand( editor ) );
		editor.commands.add( 'outdent', new MultiCommand( editor ) );
	}
}var indentIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2 3.75c0 .414.336.75.75.75h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75zm5 6c0 .414.336.75.75.75h9.5a.75.75 0 1 0 0-1.5h-9.5a.75.75 0 0 0-.75.75zM2.75 16.5h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 1 0 0 1.5zM1.632 6.95 5.02 9.358a.4.4 0 0 1-.013.661l-3.39 2.207A.4.4 0 0 1 1 11.892V7.275a.4.4 0 0 1 .632-.326z\"/></svg>";
var outdentIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2 3.75c0 .414.336.75.75.75h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 0 0-.75.75zm5 6c0 .414.336.75.75.75h9.5a.75.75 0 1 0 0-1.5h-9.5a.75.75 0 0 0-.75.75zM2.75 16.5h14.5a.75.75 0 1 0 0-1.5H2.75a.75.75 0 1 0 0 1.5zm1.618-9.55L.98 9.358a.4.4 0 0 0 .013.661l3.39 2.207A.4.4 0 0 0 5 11.892V7.275a.4.4 0 0 0-.632-.326z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The indent UI feature.
 *
 * This plugin registers the `'indent'` and `'outdent'` buttons.
 *
 * **Note**: In order for the commands to work, at least one of the compatible features is required. Read more in
 * the {@link module:indent/indent~Indent indent feature} API documentation.
 *
 * @extends module:core/plugin~Plugin
 */
class IndentUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'IndentUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const locale = editor.locale;
		const t = editor.t;

		const localizedIndentIcon = locale.uiLanguageDirection == 'ltr' ? indentIcon : outdentIcon;
		const localizedOutdentIcon = locale.uiLanguageDirection == 'ltr' ? outdentIcon : indentIcon;

		this._defineButton( 'indent', t( 'Increase indent' ), localizedIndentIcon );
		this._defineButton( 'outdent', t( 'Decrease indent' ), localizedOutdentIcon );
	}

	/**
	 * Defines a UI button.
	 *
	 * @param {String} commandName
	 * @param {String} label
	 * @param {String} icon
	 * @private
	 */
	_defineButton( commandName, label, icon ) {
		const editor = this.editor;

		editor.ui.componentFactory.add( commandName, locale => {
			const command = editor.commands.get( commandName );
			const view = new ButtonView( locale );

			view.set( {
				label,
				icon,
				tooltip: true
			} );

			view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

			this.listenTo( view, 'execute', () => {
				editor.execute( commandName );
				editor.editing.view.focus();
			} );

			return view;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The indent feature.
 *
 * This plugin acts as a single entry point plugin for other features that implement indentation of elements like lists or paragraphs.
 *
 * The compatible features are:
 *
 * * The {@link module:list/list~List} or {@link module:list/list/listediting~ListEditing} feature for list indentation.
 * * The {@link module:indent/indentblock~IndentBlock} feature for block indentation.
 *
 * This is a "glue" plugin that loads the following plugins:
 *
 * * The {@link module:indent/indentediting~IndentEditing indent editing feature}.
 * * The {@link module:indent/indentui~IndentUI indent UI feature}.
 *
 * The dependent plugins register the `'indent'` and `'outdent'` commands and introduce the `'indent'` and `'outdent'` buttons
 * that allow to increase or decrease text indentation of supported elements.
 *
 * **Note**: In order for the commands and buttons to work, at least one of compatible features is required.
 *
 * @extends module:core/plugin~Plugin
 */
class Indent extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Indent';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ IndentEditing, IndentUI ];
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Helper class that ties together all {@link module:link/link~LinkDecoratorAutomaticDefinition} and provides
 * the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement downcast dispatchers} for them.
 */
class AutomaticDecorators {
	constructor() {
		/**
		 * Stores the definition of {@link module:link/link~LinkDecoratorAutomaticDefinition automatic decorators}.
		 * This data is used as a source for a downcast dispatcher to create a proper conversion to output data.
		 *
		 * @private
		 * @type {Set}
		 */
		this._definitions = new Set();
	}

	/**
	 * Gives information about the number of decorators stored in the {@link module:link/utils~AutomaticDecorators} instance.
	 *
	 * @readonly
	 * @protected
	 * @type {Number}
	 */
	get length() {
		return this._definitions.size;
	}

	/**
	 * Adds automatic decorator objects or an array with them to be used during downcasting.
	 *
	 * @param {module:link/link~LinkDecoratorAutomaticDefinition|Array.<module:link/link~LinkDecoratorAutomaticDefinition>} item
	 * A configuration object of automatic rules for decorating links. It might also be an array of such objects.
	 */
	add( item ) {
		if ( Array.isArray( item ) ) {
			item.forEach( item => this._definitions.add( item ) );
		} else {
			this._definitions.add( item );
		}
	}

	/**
	 * Provides the conversion helper used in the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add} method.
	 *
	 * @returns {Function} A dispatcher function used as conversion helper
	 * in {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add}.
	 */
	getDispatcher() {
		return dispatcher => {
			dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
				// There is only test as this behavior decorates links and
				// it is run before dispatcher which actually consumes this node.
				// This allows on writing own dispatcher with highest priority,
				// which blocks both native converter and this additional decoration.
				if ( !conversionApi.consumable.test( data.item, 'attribute:linkHref' ) ) {
					return;
				}

				// Automatic decorators for block links are handled e.g. in LinkImageEditing.
				if ( !( data.item.is( 'selection' ) || conversionApi.schema.isInline( data.item ) ) ) {
					return;
				}

				const viewWriter = conversionApi.writer;
				const viewSelection = viewWriter.document.selection;

				for ( const item of this._definitions ) {
					const viewElement = viewWriter.createAttributeElement( 'a', item.attributes, {
						priority: 5
					} );

					if ( item.classes ) {
						viewWriter.addClass( item.classes, viewElement );
					}

					for ( const key in item.styles ) {
						viewWriter.setStyle( key, item.styles[ key ], viewElement );
					}

					viewWriter.setCustomProperty( 'link', true, viewElement );
					if ( item.callback( data.attributeNewValue ) ) {
						if ( data.item.is( 'selection' ) ) {
							viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
						} else {
							viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
						}
					} else {
						viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
					}
				}
			}, { priority: 'high' } );
		};
	}

	/**
	 * Provides the conversion helper used in the {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add} method
	 * when linking images.
	 *
	 * @returns {Function} A dispatcher function used as conversion helper
	 * in {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add}.
	 */
	getDispatcherForLinkedImage() {
		return dispatcher => {
			dispatcher.on( 'attribute:linkHref:imageBlock', ( evt, data, { writer, mapper } ) => {
				const viewFigure = mapper.toViewElement( data.item );
				const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );

				for ( const item of this._definitions ) {
					const attributes = toMap( item.attributes );

					if ( item.callback( data.attributeNewValue ) ) {
						for ( const [ key, val ] of attributes ) {
							// Left for backward compatibility. Since v30 decorator should
							// accept `classes` and `styles` separately from `attributes`.
							if ( key === 'class' ) {
								writer.addClass( val, linkInImage );
							} else {
								writer.setAttribute( key, val, linkInImage );
							}
						}

						if ( item.classes ) {
							writer.addClass( item.classes, linkInImage );
						}

						for ( const key in item.styles ) {
							writer.setStyle( key, item.styles[ key ], linkInImage );
						}
					} else {
						for ( const [ key, val ] of attributes ) {
							if ( key === 'class' ) {
								writer.removeClass( val, linkInImage );
							} else {
								writer.removeAttribute( key, linkInImage );
							}
						}

						if ( item.classes ) {
							writer.removeClass( item.classes, linkInImage );
						}

						for ( const key in item.styles ) {
							writer.removeStyle( key, linkInImage );
						}
					}
				}
			} );
		};
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const ATTRIBUTE_WHITESPACES = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g; // eslint-disable-line no-control-regex
const SAFE_URL = /^(?:(?:https?|ftps?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.:-]|$))/i;

// Simplified email test - should be run over previously found URL.
const EMAIL_REG_EXP = /^[\S]+@((?![-_])(?:[-\w\u00a1-\uffff]{0,63}[^-_]\.))+(?:[a-z\u00a1-\uffff]{2,})$/i;

// The regex checks for the protocol syntax ('xxxx://' or 'xxxx:')
// or non-word characters at the beginning of the link ('/', '#' etc.).
const PROTOCOL_REG_EXP = /^((\w+:(\/{2,})?)|(\W))/i;

/**
 * A keystroke used by the {@link module:link/linkui~LinkUI link UI feature}.
 */
const LINK_KEYSTROKE = 'Ctrl+K';

/**
 * Returns `true` if a given view node is the link element.
 *
 * @param {module:engine/view/node~Node} node
 * @returns {Boolean}
 */
function isLinkElement( node ) {
	return node.is( 'attributeElement' ) && !!node.getCustomProperty( 'link' );
}

/**
 * Creates a link {@link module:engine/view/attributeelement~AttributeElement} with the provided `href` attribute.
 *
 * @param {String} href
 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
 * @returns {module:engine/view/attributeelement~AttributeElement}
 */
function createLinkElement( href, { writer } ) {
	// Priority 5 - https://github.com/ckeditor/ckeditor5-link/issues/121.
	const linkElement = writer.createAttributeElement( 'a', { href }, { priority: 5 } );
	writer.setCustomProperty( 'link', true, linkElement );

	return linkElement;
}

/**
 * Returns a safe URL based on a given value.
 *
 * A URL is considered safe if it is safe for the user (does not contain any malicious code).
 *
 * If a URL is considered unsafe, a simple `"#"` is returned.
 *
 * @protected
 * @param {*} url
 * @returns {String} Safe URL.
 */
function ensureSafeUrl( url ) {
	url = String( url );

	return isSafeUrl( url ) ? url : '#';
}

// Checks whether the given URL is safe for the user (does not contain any malicious code).
//
// @param {String} url URL to check.
function isSafeUrl( url ) {
	const normalizedUrl = url.replace( ATTRIBUTE_WHITESPACES, '' );

	return normalizedUrl.match( SAFE_URL );
}

/**
 * Returns the {@link module:link/link~LinkConfig#decorators `config.link.decorators`} configuration processed
 * to respect the locale of the editor, i.e. to display the {@link module:link/link~LinkDecoratorManualDefinition label}
 * in the correct language.
 *
 * **Note**: Only the few most commonly used labels are translated automatically. Other labels should be manually
 * translated in the {@link module:link/link~LinkConfig#decorators `config.link.decorators`} configuration.
 *
 * @param {module:utils/locale~Locale#t} t shorthand for {@link module:utils/locale~Locale#t Locale#t}
 * @param {Array.<module:link/link~LinkDecoratorDefinition>} The decorator reference
 * where the label values should be localized.
 * @returns {Array.<module:link/link~LinkDecoratorDefinition>}
 */
function getLocalizedDecorators( t, decorators ) {
	const localizedDecoratorsLabels = {
		'Open in a new tab': t( 'Open in a new tab' ),
		'Downloadable': t( 'Downloadable' )
	};

	decorators.forEach( decorator => {
		if ( decorator.label && localizedDecoratorsLabels[ decorator.label ] ) {
			decorator.label = localizedDecoratorsLabels[ decorator.label ];
		}
		return decorator;
	} );

	return decorators;
}

/**
 * Converts an object with defined decorators to a normalized array of decorators. The `id` key is added for each decorator and
 * is used as the attribute's name in the model.
 *
 * @param {Object.<String, module:link/link~LinkDecoratorDefinition>} decorators
 * @returns {Array.<module:link/link~LinkDecoratorDefinition>}
 */
function normalizeDecorators( decorators ) {
	const retArray = [];

	if ( decorators ) {
		for ( const [ key, value ] of Object.entries( decorators ) ) {
			const decorator = Object.assign(
				{},
				value,
				{ id: `link${ upperFirst$1( key ) }` }
			);
			retArray.push( decorator );
		}
	}

	return retArray;
}

/**
 * Returns `true` if the specified `element` can be linked (the element allows the `linkHref` attribute).
 *
 * @params {module:engine/model/element~Element|null} element
 * @params {module:engine/model/schema~Schema} schema
 * @returns {Boolean}
 */
function isLinkableElement( element, schema ) {
	if ( !element ) {
		return false;
	}

	return schema.checkAttribute( element.name, 'linkHref' );
}

/**
 * Returns `true` if the specified `value` is an email.
 *
 * @params {String} value
 * @returns {Boolean}
 */
function isEmail( value ) {
	return EMAIL_REG_EXP.test( value );
}

/**
 * Adds the protocol prefix to the specified `link` when:
 *
 * * it does not contain it already, and there is a {@link module:link/link~LinkConfig#defaultProtocol `defaultProtocol` }
 * configuration value provided,
 * * or the link is an email address.
 *
 *
 * @params {String} link
 * @params {String} defaultProtocol
 * @returns {Boolean}
 */
function addLinkProtocolIfApplicable( link, defaultProtocol ) {
	const protocol = isEmail( link ) ? 'mailto:' : defaultProtocol;
	const isProtocolNeeded = !!protocol && !PROTOCOL_REG_EXP.test( link );

	return link && isProtocolNeeded ? protocol + link : link;
}

/**
 * Opens the link in a new browser tab.
 *
 * @param {String} link
 */
function openLink( link ) {
	window.open( link, '_blank', 'noopener' );
}var utils=/*#__PURE__*/Object.freeze({__proto__:null,LINK_KEYSTROKE:LINK_KEYSTROKE,isLinkElement:isLinkElement,createLinkElement:createLinkElement,ensureSafeUrl:ensureSafeUrl,getLocalizedDecorators:getLocalizedDecorators,normalizeDecorators:normalizeDecorators,isLinkableElement:isLinkableElement,isEmail:isEmail,addLinkProtocolIfApplicable:addLinkProtocolIfApplicable,openLink:openLink});/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The link command. It is used by the {@link module:link/link~Link link feature}.
 *
 * @extends module:core/command~Command
 */
class LinkCommand extends Command {
	/**
	 * The value of the `'linkHref'` attribute if the start of the selection is located in a node with this attribute.
	 *
	 * @observable
	 * @readonly
	 * @member {Object|undefined} #value
	 */

	constructor( editor ) {
		super( editor );

		/**
		 * A collection of {@link module:link/utils~ManualDecorator manual decorators}
		 * corresponding to the {@link module:link/link~LinkConfig#decorators decorator configuration}.
		 *
		 * You can consider it a model with states of manual decorators added to the currently selected link.
		 *
		 * @readonly
		 * @type {module:utils/collection~Collection}
		 */
		this.manualDecorators = new Collection();

		/**
		 * An instance of the helper that ties together all {@link module:link/link~LinkDecoratorAutomaticDefinition}
		 * that are used by the {@glink features/link link} and the {@glink features/images/images-linking linking images} features.
		 *
		 * @readonly
		 * @type {module:link/utils~AutomaticDecorators}
		 */
		this.automaticDecorators = new AutomaticDecorators();
	}

	/**
	 * Synchronizes the state of {@link #manualDecorators} with the currently present elements in the model.
	 */
	restoreManualDecoratorStates() {
		for ( const manualDecorator of this.manualDecorators ) {
			manualDecorator.value = this._getDecoratorStateFromModel( manualDecorator.id );
		}
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		const model = this.editor.model;
		const selection = model.document.selection;
		const selectedElement = selection.getSelectedElement() || first( selection.getSelectedBlocks() );

		// A check for any integration that allows linking elements (e.g. `LinkImage`).
		// Currently the selection reads attributes from text nodes only. See #7429 and #7465.
		if ( isLinkableElement( selectedElement, model.schema ) ) {
			this.value = selectedElement.getAttribute( 'linkHref' );
			this.isEnabled = model.schema.checkAttribute( selectedElement, 'linkHref' );
		} else {
			this.value = selection.getAttribute( 'linkHref' );
			this.isEnabled = model.schema.checkAttributeInSelection( selection, 'linkHref' );
		}

		for ( const manualDecorator of this.manualDecorators ) {
			manualDecorator.value = this._getDecoratorStateFromModel( manualDecorator.id );
		}
	}

	/**
	 * Executes the command.
	 *
	 * When the selection is non-collapsed, the `linkHref` attribute will be applied to nodes inside the selection, but only to
	 * those nodes where the `linkHref` attribute is allowed (disallowed nodes will be omitted).
	 *
	 * When the selection is collapsed and is not inside the text with the `linkHref` attribute, a
	 * new {@link module:engine/model/text~Text text node} with the `linkHref` attribute will be inserted in place of the caret, but
	 * only if such element is allowed in this place. The `_data` of the inserted text will equal the `href` parameter.
	 * The selection will be updated to wrap the just inserted text node.
	 *
	 * When the selection is collapsed and inside the text with the `linkHref` attribute, the attribute value will be updated.
	 *
	 * # Decorators and model attribute management
	 *
	 * There is an optional argument to this command that applies or removes model
	 * {@glink framework/guides/architecture/editing-engine#text-attributes text attributes} brought by
	 * {@link module:link/utils~ManualDecorator manual link decorators}.
	 *
	 * Text attribute names in the model correspond to the entries in the {@link module:link/link~LinkConfig#decorators configuration}.
	 * For every decorator configured, a model text attribute exists with the "link" prefix. For example, a `'linkMyDecorator'` attribute
	 * corresponds to `'myDecorator'` in the configuration.
	 *
	 * To learn more about link decorators, check out the {@link module:link/link~LinkConfig#decorators `config.link.decorators`}
	 * documentation.
	 *
	 * Here is how to manage decorator attributes with the link command:
	 *
	 *		const linkCommand = editor.commands.get( 'link' );
	 *
	 *		// Adding a new decorator attribute.
	 *		linkCommand.execute( 'http://example.com', {
	 *			linkIsExternal: true
	 *		} );
	 *
	 *		// Removing a decorator attribute from the selection.
	 *		linkCommand.execute( 'http://example.com', {
	 *			linkIsExternal: false
	 *		} );
	 *
	 *		// Adding multiple decorator attributes at the same time.
	 *		linkCommand.execute( 'http://example.com', {
	 *			linkIsExternal: true,
	 *			linkIsDownloadable: true,
	 *		} );
	 *
	 *		// Removing and adding decorator attributes at the same time.
	 *		linkCommand.execute( 'http://example.com', {
	 *			linkIsExternal: false,
	 *			linkFoo: true,
	 *			linkIsDownloadable: false,
	 *		} );
	 *
	 * **Note**: If the decorator attribute name is not specified, its state remains untouched.
	 *
	 * **Note**: {@link module:link/unlinkcommand~UnlinkCommand#execute `UnlinkCommand#execute()`} removes all
	 * decorator attributes.
	 *
	 * @fires execute
	 * @param {String} href Link destination.
	 * @param {Object} [manualDecoratorIds={}] The information about manual decorator attributes to be applied or removed upon execution.
	 */
	execute( href, manualDecoratorIds = {} ) {
		const model = this.editor.model;
		const selection = model.document.selection;
		// Stores information about manual decorators to turn them on/off when command is applied.
		const truthyManualDecorators = [];
		const falsyManualDecorators = [];

		for ( const name in manualDecoratorIds ) {
			if ( manualDecoratorIds[ name ] ) {
				truthyManualDecorators.push( name );
			} else {
				falsyManualDecorators.push( name );
			}
		}

		model.change( writer => {
			// If selection is collapsed then update selected link or insert new one at the place of caret.
			if ( selection.isCollapsed ) {
				const position = selection.getFirstPosition();

				// When selection is inside text with `linkHref` attribute.
				if ( selection.hasAttribute( 'linkHref' ) ) {
					// Then update `linkHref` value.
					const linkRange = findAttributeRange( position, 'linkHref', selection.getAttribute( 'linkHref' ), model );

					writer.setAttribute( 'linkHref', href, linkRange );

					truthyManualDecorators.forEach( item => {
						writer.setAttribute( item, true, linkRange );
					} );

					falsyManualDecorators.forEach( item => {
						writer.removeAttribute( item, linkRange );
					} );

					// Put the selection at the end of the updated link.
					writer.setSelection( writer.createPositionAfter( linkRange.end.nodeBefore ) );
				}
				// If not then insert text node with `linkHref` attribute in place of caret.
				// However, since selection is collapsed, attribute value will be used as data for text node.
				// So, if `href` is empty, do not create text node.
				else if ( href !== '' ) {
					const attributes = toMap( selection.getAttributes() );

					attributes.set( 'linkHref', href );

					truthyManualDecorators.forEach( item => {
						attributes.set( item, true );
					} );

					const { end: positionAfter } = model.insertContent( writer.createText( href, attributes ), position );

					// Put the selection at the end of the inserted link.
					// Using end of range returned from insertContent in case nodes with the same attributes got merged.
					writer.setSelection( positionAfter );
				}

				// Remove the `linkHref` attribute and all link decorators from the selection.
				// It stops adding a new content into the link element.
				[ 'linkHref', ...truthyManualDecorators, ...falsyManualDecorators ].forEach( item => {
					writer.removeSelectionAttribute( item );
				} );
			} else {
				// If selection has non-collapsed ranges, we change attribute on nodes inside those ranges
				// omitting nodes where the `linkHref` attribute is disallowed.
				const ranges = model.schema.getValidRanges( selection.getRanges(), 'linkHref' );

				// But for the first, check whether the `linkHref` attribute is allowed on selected blocks (e.g. the "image" element).
				const allowedRanges = [];

				for ( const element of selection.getSelectedBlocks() ) {
					if ( model.schema.checkAttribute( element, 'linkHref' ) ) {
						allowedRanges.push( writer.createRangeOn( element ) );
					}
				}

				// Ranges that accept the `linkHref` attribute. Since we will iterate over `allowedRanges`, let's clone it.
				const rangesToUpdate = allowedRanges.slice();

				// For all selection ranges we want to check whether given range is inside an element that accepts the `linkHref` attribute.
				// If so, we don't want to propagate applying the attribute to its children.
				for ( const range of ranges ) {
					if ( this._isRangeToUpdate( range, allowedRanges ) ) {
						rangesToUpdate.push( range );
					}
				}

				for ( const range of rangesToUpdate ) {
					writer.setAttribute( 'linkHref', href, range );

					truthyManualDecorators.forEach( item => {
						writer.setAttribute( item, true, range );
					} );

					falsyManualDecorators.forEach( item => {
						writer.removeAttribute( item, range );
					} );
				}
			}
		} );
	}

	/**
	 * Provides information whether a decorator with a given name is present in the currently processed selection.
	 *
	 * @private
	 * @param {String} decoratorName The name of the manual decorator used in the model
	 * @returns {Boolean} The information whether a given decorator is currently present in the selection.
	 */
	_getDecoratorStateFromModel( decoratorName ) {
		const model = this.editor.model;
		const selection = model.document.selection;
		const selectedElement = selection.getSelectedElement();

		// A check for the `LinkImage` plugin. If the selection contains an element, get values from the element.
		// Currently the selection reads attributes from text nodes only. See #7429 and #7465.
		if ( isLinkableElement( selectedElement, model.schema ) ) {
			return selectedElement.getAttribute( decoratorName );
		}

		return selection.getAttribute( decoratorName );
	}

	/**
	 * Checks whether specified `range` is inside an element that accepts the `linkHref` attribute.
	 *
	 * @private
	 * @param {module:engine/view/range~Range} range A range to check.
	 * @param {Array.<module:engine/view/range~Range>} allowedRanges An array of ranges created on elements where the attribute is accepted.
	 * @returns {Boolean}
	 */
	_isRangeToUpdate( range, allowedRanges ) {
		for ( const allowedRange of allowedRanges ) {
			// A range is inside an element that will have the `linkHref` attribute. Do not modify its nodes.
			if ( allowedRange.containsRange( range ) ) {
				return false;
			}
		}

		return true;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The unlink command. It is used by the {@link module:link/link~Link link plugin}.
 *
 * @extends module:core/command~Command
 */
class UnlinkCommand extends Command {
	/**
	 * @inheritDoc
	 */
	refresh() {
		const model = this.editor.model;
		const selection = model.document.selection;
		const selectedElement = selection.getSelectedElement();

		// A check for any integration that allows linking elements (e.g. `LinkImage`).
		// Currently the selection reads attributes from text nodes only. See #7429 and #7465.
		if ( isLinkableElement( selectedElement, model.schema ) ) {
			this.isEnabled = model.schema.checkAttribute( selectedElement, 'linkHref' );
		} else {
			this.isEnabled = model.schema.checkAttributeInSelection( selection, 'linkHref' );
		}
	}

	/**
	 * Executes the command.
	 *
	 * When the selection is collapsed, it removes the `linkHref` attribute from each node with the same `linkHref` attribute value.
	 * When the selection is non-collapsed, it removes the `linkHref` attribute from each node in selected ranges.
	 *
	 * # Decorators
	 *
	 * If {@link module:link/link~LinkConfig#decorators `config.link.decorators`} is specified,
	 * all configured decorators are removed together with the `linkHref` attribute.
	 *
	 * @fires execute
	 */
	execute() {
		const editor = this.editor;
		const model = this.editor.model;
		const selection = model.document.selection;
		const linkCommand = editor.commands.get( 'link' );

		model.change( writer => {
			// Get ranges to unlink.
			const rangesToUnlink = selection.isCollapsed ?
				[ findAttributeRange(
					selection.getFirstPosition(),
					'linkHref',
					selection.getAttribute( 'linkHref' ),
					model
				) ] :
				model.schema.getValidRanges( selection.getRanges(), 'linkHref' );

			// Remove `linkHref` attribute from specified ranges.
			for ( const range of rangesToUnlink ) {
				writer.removeAttribute( 'linkHref', range );
				// If there are registered custom attributes, then remove them during unlink.
				if ( linkCommand ) {
					for ( const manualDecorator of linkCommand.manualDecorators ) {
						writer.removeAttribute( manualDecorator.id, range );
					}
				}
			}
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Helper class that stores manual decorators with observable {@link module:link/utils~ManualDecorator#value}
 * to support integration with the UI state. An instance of this class is a model with the state of individual manual decorators.
 * These decorators are kept as collections in {@link module:link/linkcommand~LinkCommand#manualDecorators}.
 *
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class ManualDecorator {
	/**
	 * Creates a new instance of {@link module:link/utils~ManualDecorator}.
	 *
	 * @param {Object} config
	 * @param {String} config.id The name of the attribute used in the model that represents a given manual decorator.
	 * For example: `'linkIsExternal'`.
	 * @param {String} config.label The label used in the user interface to toggle the manual decorator.
	 * @param {Object} config.attributes A set of attributes added to output data when the decorator is active for a specific link.
	 * Attributes should keep the format of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}.
	 * @param {Boolean} [config.defaultValue] Controls whether the decorator is "on" by default.
	 */
	constructor( { id, label, attributes, classes, styles, defaultValue } ) {
		/**
		 * An ID of a manual decorator which is the name of the attribute in the model, for example: 'linkManualDecorator0'.
		 *
		 * @type {String}
		 */
		this.id = id;

		/**
		 * The value of the current manual decorator. It reflects its state from the UI.
		 *
		 * @observable
		 * @member {Boolean} module:link/utils~ManualDecorator#value
		 */
		this.set( 'value' );

		/**
		 * The default value of manual decorator.
		 *
		 * @type {Boolean}
		 */
		this.defaultValue = defaultValue;

		/**
		 * The label used in the user interface to toggle the manual decorator.
		 *
		 * @type {String}
		 */
		this.label = label;

		/**
		 * A set of attributes added to downcasted data when the decorator is activated for a specific link.
		 * Attributes should be added in a form of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}.
		 *
		 * @type {Object}
		 */
		this.attributes = attributes;

		/**
		 * A set of classes added to downcasted data when the decorator is activated for a specific link.
		 * Classes should be added in a form of classes defined in {@link module:engine/view/elementdefinition~ElementDefinition}.
		 *
		 * @type {Object}
		 */
		this.classes = classes;

		/**
		 * A set of styles added to downcasted data when the decorator is activated for a specific link.
		 * Styles should be added in a form of styles defined in {@link module:engine/view/elementdefinition~ElementDefinition}.
		 *
		 * @type {Object}
		 */
		this.styles = styles;
	}

	/**
	 * Returns {@link module:engine/view/matcher~MatcherPattern} with decorator attributes.
	 *
	 * @protected
	 * @returns {module:engine/view/matcher~MatcherPattern}
	 */
	_createPattern() {
		return {
			attributes: this.attributes,
			classes: this.classes,
			styles: this.styles
		};
	}
}

mix( ManualDecorator, ObservableMixin );var css_248z$p = ".ck .ck-link_selected{background:var(--ck-color-link-selected-background)}.ck .ck-link_selected span.image-inline{outline:var(--ck-widget-outline-thickness) solid var(--ck-color-link-selected-background)}.ck .ck-fake-link-selection{background:var(--ck-color-link-fake-selection)}.ck .ck-fake-link-selection_collapsed{border-right:1px solid var(--ck-color-base-text);height:100%;margin-right:-1px;outline:1px solid hsla(0,0%,100%,.5)}";
styleInject(css_248z$p);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const HIGHLIGHT_CLASS = 'ck-link_selected';
const DECORATOR_AUTOMATIC = 'automatic';
const DECORATOR_MANUAL = 'manual';
const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//;

/**
 * The link engine feature.
 *
 * It introduces the `linkHref="url"` attribute in the model which renders to the view as a `<a href="url">` element
 * as well as `'link'` and `'unlink'` commands.
 *
 * @extends module:core/plugin~Plugin
 */
class LinkEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'LinkEditing';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		// Clipboard is required for handling cut and paste events while typing over the link.
		return [ TwoStepCaretMovement, Input, ClipboardPipeline ];
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		editor.config.define( 'link', {
			addTargetToExternalLinks: false
		} );
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		// Allow link attribute on all inline nodes.
		editor.model.schema.extend( '$text', { allowAttributes: 'linkHref' } );

		editor.conversion.for( 'dataDowncast' )
			.attributeToElement( { model: 'linkHref', view: createLinkElement } );

		editor.conversion.for( 'editingDowncast' )
			.attributeToElement( { model: 'linkHref', view: ( href, conversionApi ) => {
				return createLinkElement( ensureSafeUrl( href ), conversionApi );
			} } );

		editor.conversion.for( 'upcast' )
			.elementToAttribute( {
				view: {
					name: 'a',
					attributes: {
						href: true
					}
				},
				model: {
					key: 'linkHref',
					value: viewElement => viewElement.getAttribute( 'href' )
				}
			} );

		// Create linking commands.
		editor.commands.add( 'link', new LinkCommand( editor ) );
		editor.commands.add( 'unlink', new UnlinkCommand( editor ) );

		const linkDecorators = getLocalizedDecorators( editor.t, normalizeDecorators( editor.config.get( 'link.decorators' ) ) );

		this._enableAutomaticDecorators( linkDecorators.filter( item => item.mode === DECORATOR_AUTOMATIC ) );
		this._enableManualDecorators( linkDecorators.filter( item => item.mode === DECORATOR_MANUAL ) );

		// Enable two-step caret movement for `linkHref` attribute.
		const twoStepCaretMovementPlugin = editor.plugins.get( TwoStepCaretMovement );
		twoStepCaretMovementPlugin.registerAttribute( 'linkHref' );

		// Setup highlight over selected link.
		inlineHighlight( editor, 'linkHref', 'a', HIGHLIGHT_CLASS );

		// Handle link following by CTRL+click or ALT+ENTER
		this._enableLinkOpen();

		// Change the attributes of the selection in certain situations after the link was inserted into the document.
		this._enableInsertContentSelectionAttributesFixer();

		// Handle a click at the beginning/end of a link element.
		this._enableClickingAfterLink();

		// Handle typing over the link.
		this._enableTypingOverLink();

		// Handle removing the content after the link element.
		this._handleDeleteContentAfterLink();
	}

	/**
	 * Processes an array of configured {@link module:link/link~LinkDecoratorAutomaticDefinition automatic decorators}
	 * and registers a {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher}
	 * for each one of them. Downcast dispatchers are obtained using the
	 * {@link module:link/utils~AutomaticDecorators#getDispatcher} method.
	 *
	 * **Note**: This method also activates the automatic external link decorator if enabled with
	 * {@link module:link/link~LinkConfig#addTargetToExternalLinks `config.link.addTargetToExternalLinks`}.
	 *
	 * @private
	 * @param {Array.<module:link/link~LinkDecoratorAutomaticDefinition>} automaticDecoratorDefinitions
	 */
	_enableAutomaticDecorators( automaticDecoratorDefinitions ) {
		const editor = this.editor;
		// Store automatic decorators in the command instance as we do the same with manual decorators.
		// Thanks to that, `LinkImageEditing` plugin can re-use the same definitions.
		const command = editor.commands.get( 'link' );
		const automaticDecorators = command.automaticDecorators;

		// Adds a default decorator for external links.
		if ( editor.config.get( 'link.addTargetToExternalLinks' ) ) {
			automaticDecorators.add( {
				id: 'linkIsExternal',
				mode: DECORATOR_AUTOMATIC,
				callback: url => EXTERNAL_LINKS_REGEXP.test( url ),
				attributes: {
					target: '_blank',
					rel: 'noopener noreferrer'
				}
			} );
		}

		automaticDecorators.add( automaticDecoratorDefinitions );

		if ( automaticDecorators.length ) {
			editor.conversion.for( 'downcast' ).add( automaticDecorators.getDispatcher() );
		}
	}

	/**
	 * Processes an array of configured {@link module:link/link~LinkDecoratorManualDefinition manual decorators},
	 * transforms them into {@link module:link/utils~ManualDecorator} instances and stores them in the
	 * {@link module:link/linkcommand~LinkCommand#manualDecorators} collection (a model for manual decorators state).
	 *
	 * Also registers an {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement attribute-to-element}
	 * converter for each manual decorator and extends the {@link module:engine/model/schema~Schema model's schema}
	 * with adequate model attributes.
	 *
	 * @private
	 * @param {Array.<module:link/link~LinkDecoratorManualDefinition>} manualDecoratorDefinitions
	 */
	_enableManualDecorators( manualDecoratorDefinitions ) {
		if ( !manualDecoratorDefinitions.length ) {
			return;
		}

		const editor = this.editor;
		const command = editor.commands.get( 'link' );
		const manualDecorators = command.manualDecorators;

		manualDecoratorDefinitions.forEach( decorator => {
			editor.model.schema.extend( '$text', { allowAttributes: decorator.id } );

			// Keeps reference to manual decorator to decode its name to attributes during downcast.
			decorator = new ManualDecorator( decorator );

			manualDecorators.add( decorator );

			editor.conversion.for( 'downcast' ).attributeToElement( {
				model: decorator.id,
				view: ( manualDecoratorValue, { writer, schema }, { item } ) => {
					// Manual decorators for block links are handled e.g. in LinkImageEditing.
					if ( !( item.is( 'selection' ) || schema.isInline( item ) ) ) {
						return;
					}

					if ( manualDecoratorValue ) {
						const element = writer.createAttributeElement( 'a', decorator.attributes, { priority: 5 } );

						if ( decorator.classes ) {
							writer.addClass( decorator.classes, element );
						}

						for ( const key in decorator.styles ) {
							writer.setStyle( key, decorator.styles[ key ], element );
						}

						writer.setCustomProperty( 'link', true, element );

						return element;
					}
				}
			} );

			editor.conversion.for( 'upcast' ).elementToAttribute( {
				view: {
					name: 'a',
					...decorator._createPattern()
				},
				model: {
					key: decorator.id
				}
			} );
		} );
	}

	/**
	 * Attaches handlers for {@link module:engine/view/document~Document#event:enter} and
	 * {@link module:engine/view/document~Document#event:click} to enable link following.
	 *
	 * @private
	 */
	_enableLinkOpen() {
		const editor = this.editor;
		const view = editor.editing.view;
		const viewDocument = view.document;
		const modelDocument = editor.model.document;

		this.listenTo( viewDocument, 'click', ( evt, data ) => {
			const shouldOpen = env.isMac ? data.domEvent.metaKey : data.domEvent.ctrlKey;

			if ( !shouldOpen ) {
				return;
			}

			let clickedElement = data.domTarget;

			if ( clickedElement.tagName.toLowerCase() != 'a' ) {
				clickedElement = clickedElement.closest( 'a' );
			}

			if ( !clickedElement ) {
				return;
			}

			const url = clickedElement.getAttribute( 'href' );

			if ( !url ) {
				return;
			}

			evt.stop();
			data.preventDefault();

			openLink( url );
		}, { context: '$capture' } );

		this.listenTo( viewDocument, 'enter', ( evt, data ) => {
			const selection = modelDocument.selection;

			const selectedElement = selection.getSelectedElement();

			const url = selectedElement ?
				selectedElement.getAttribute( 'linkHref' ) :
				selection.getAttribute( 'linkHref' );

			const shouldOpen = url && data.domEvent.altKey;

			if ( !shouldOpen ) {
				return;
			}

			evt.stop();

			openLink( url );
		}, { context: 'a' } );
	}

	/**
	 * Starts listening to {@link module:engine/model/model~Model#event:insertContent} and corrects the model
	 * selection attributes if the selection is at the end of a link after inserting the content.
	 *
	 * The purpose of this action is to improve the overall UX because the user is no longer "trapped" by the
	 * `linkHref` attribute of the selection and they can type a "clean" (`linkHref`–less) text right away.
	 *
	 * See https://github.com/ckeditor/ckeditor5/issues/6053.
	 *
	 * @private
	 */
	_enableInsertContentSelectionAttributesFixer() {
		const editor = this.editor;
		const model = editor.model;
		const selection = model.document.selection;

		this.listenTo( model, 'insertContent', () => {
			const nodeBefore = selection.anchor.nodeBefore;
			const nodeAfter = selection.anchor.nodeAfter;

			// NOTE: ↰ and ↱ represent the gravity of the selection.

			// The only truly valid case is:
			//
			//		                                 ↰
			//		...<$text linkHref="foo">INSERTED[]</$text>
			//
			// If the selection is not "trapped" by the `linkHref` attribute after inserting, there's nothing
			// to fix there.
			if ( !selection.hasAttribute( 'linkHref' ) ) {
				return;
			}

			// Filter out the following case where a link with the same href (e.g. <a href="foo">INSERTED</a>) is inserted
			// in the middle of an existing link:
			//
			// Before insertion:
			//		                       ↰
			//		<$text linkHref="foo">l[]ink</$text>
			//
			// Expected after insertion:
			//		                               ↰
			//		<$text linkHref="foo">lINSERTED[]ink</$text>
			//
			if ( !nodeBefore ) {
				return;
			}

			// Filter out the following case where the selection has the "linkHref" attribute because the
			// gravity is overridden and some text with another attribute (e.g. <b>INSERTED</b>) is inserted:
			//
			// Before insertion:
			//
			//		                       ↱
			//		<$text linkHref="foo">[]link</$text>
			//
			// Expected after insertion:
			//
			//		                                                          ↱
			//		<$text bold="true">INSERTED</$text><$text linkHref="foo">[]link</$text>
			//
			if ( !nodeBefore.hasAttribute( 'linkHref' ) ) {
				return;
			}

			// Filter out the following case where a link is a inserted in the middle (or before) another link
			// (different URLs, so they will not merge). In this (let's say weird) case, we can leave the selection
			// attributes as they are because the user will end up writing in one link or another anyway.
			//
			// Before insertion:
			//
			//		                       ↰
			//		<$text linkHref="foo">l[]ink</$text>
			//
			// Expected after insertion:
			//
			//		                                                             ↰
			//		<$text linkHref="foo">l</$text><$text linkHref="bar">INSERTED[]</$text><$text linkHref="foo">ink</$text>
			//
			if ( nodeAfter && nodeAfter.hasAttribute( 'linkHref' ) ) {
				return;
			}

			model.change( writer => {
				removeLinkAttributesFromSelection( writer, getLinkAttributesAllowedOnText( model.schema ) );
			} );
		}, { priority: 'low' } );
	}

	/**
	 * Starts listening to {@link module:engine/view/document~Document#event:mousedown} and
	 * {@link module:engine/view/document~Document#event:selectionChange} and puts the selection before/after a link node
	 * if clicked at the beginning/ending of the link.
	 *
	 * The purpose of this action is to allow typing around the link node directly after a click.
	 *
	 * See https://github.com/ckeditor/ckeditor5/issues/1016.
	 *
	 * @private
	 */
	_enableClickingAfterLink() {
		const editor = this.editor;
		const model = editor.model;

		editor.editing.view.addObserver( MouseObserver );

		let clicked = false;

		// Detect the click.
		this.listenTo( editor.editing.view.document, 'mousedown', () => {
			clicked = true;
		} );

		// When the selection has changed...
		this.listenTo( editor.editing.view.document, 'selectionChange', () => {
			if ( !clicked ) {
				return;
			}

			// ...and it was caused by the click...
			clicked = false;

			const selection = model.document.selection;

			// ...and no text is selected...
			if ( !selection.isCollapsed ) {
				return;
			}

			// ...and clicked text is the link...
			if ( !selection.hasAttribute( 'linkHref' ) ) {
				return;
			}

			const position = selection.getFirstPosition();
			const linkRange = findAttributeRange( position, 'linkHref', selection.getAttribute( 'linkHref' ), model );

			// ...check whether clicked start/end boundary of the link.
			// If so, remove the `linkHref` attribute.
			if ( position.isTouching( linkRange.start ) || position.isTouching( linkRange.end ) ) {
				model.change( writer => {
					removeLinkAttributesFromSelection( writer, getLinkAttributesAllowedOnText( model.schema ) );
				} );
			}
		} );
	}

	/**
	 * Starts listening to {@link module:engine/model/model~Model#deleteContent} and {@link module:engine/model/model~Model#insertContent}
	 * and checks whether typing over the link. If so, attributes of removed text are preserved and applied to the inserted text.
	 *
	 * The purpose of this action is to allow modifying a text without loosing the `linkHref` attribute (and other).
	 *
	 * See https://github.com/ckeditor/ckeditor5/issues/4762.
	 *
	 * @private
	 */
	_enableTypingOverLink() {
		const editor = this.editor;
		const view = editor.editing.view;

		// Selection attributes when started typing over the link.
		let selectionAttributes;

		// Whether pressed `Backspace` or `Delete`. If so, attributes should not be preserved.
		let deletedContent;

		// Detect pressing `Backspace` / `Delete`.
		this.listenTo( view.document, 'delete', () => {
			deletedContent = true;
		}, { priority: 'high' } );

		// Listening to `model#deleteContent` allows detecting whether selected content was a link.
		// If so, before removing the element, we will copy its attributes.
		this.listenTo( editor.model, 'deleteContent', () => {
			const selection = editor.model.document.selection;

			// Copy attributes only if anything is selected.
			if ( selection.isCollapsed ) {
				return;
			}

			// When the content was deleted, do not preserve attributes.
			if ( deletedContent ) {
				deletedContent = false;

				return;
			}

			// Enabled only when typing.
			if ( !isTyping( editor ) ) {
				return;
			}

			if ( shouldCopyAttributes( editor.model ) ) {
				selectionAttributes = selection.getAttributes();
			}
		}, { priority: 'high' } );

		// Listening to `model#insertContent` allows detecting the content insertion.
		// We want to apply attributes that were removed while typing over the link.
		this.listenTo( editor.model, 'insertContent', ( evt, [ element ] ) => {
			deletedContent = false;

			// Enabled only when typing.
			if ( !isTyping( editor ) ) {
				return;
			}

			if ( !selectionAttributes ) {
				return;
			}

			editor.model.change( writer => {
				for ( const [ attribute, value ] of selectionAttributes ) {
					writer.setAttribute( attribute, value, element );
				}
			} );

			selectionAttributes = null;
		}, { priority: 'high' } );
	}

	/**
	 * Starts listening to {@link module:engine/model/model~Model#deleteContent} and checks whether
	 * removing a content right after the "linkHref" attribute.
	 *
	 * If so, the selection should not preserve the `linkHref` attribute. However, if
	 * the {@link module:typing/twostepcaretmovement~TwoStepCaretMovement} plugin is active and
	 * the selection has the "linkHref" attribute due to overriden gravity (at the end), the `linkHref` attribute should stay untouched.
	 *
	 * The purpose of this action is to allow removing the link text and keep the selection outside the link.
	 *
	 * See https://github.com/ckeditor/ckeditor5/issues/7521.
	 *
	 * @private
	 */
	_handleDeleteContentAfterLink() {
		const editor = this.editor;
		const model = editor.model;
		const selection = model.document.selection;
		const view = editor.editing.view;

		// A flag whether attributes `linkHref` attribute should be preserved.
		let shouldPreserveAttributes = false;

		// A flag whether the `Backspace` key was pressed.
		let hasBackspacePressed = false;

		// Detect pressing `Backspace`.
		this.listenTo( view.document, 'delete', ( evt, data ) => {
			hasBackspacePressed = data.domEvent.keyCode === keyCodes.backspace;
		}, { priority: 'high' } );

		// Before removing the content, check whether the selection is inside a link or at the end of link but with 2-SCM enabled.
		// If so, we want to preserve link attributes.
		this.listenTo( model, 'deleteContent', () => {
			// Reset the state.
			shouldPreserveAttributes = false;

			const position = selection.getFirstPosition();
			const linkHref = selection.getAttribute( 'linkHref' );

			if ( !linkHref ) {
				return;
			}

			const linkRange = findAttributeRange( position, 'linkHref', linkHref, model );

			// Preserve `linkHref` attribute if the selection is in the middle of the link or
			// the selection is at the end of the link and 2-SCM is activated.
			shouldPreserveAttributes = linkRange.containsPosition( position ) || linkRange.end.isEqual( position );
		}, { priority: 'high' } );

		// After removing the content, check whether the current selection should preserve the `linkHref` attribute.
		this.listenTo( model, 'deleteContent', () => {
			// If didn't press `Backspace`.
			if ( !hasBackspacePressed ) {
				return;
			}

			hasBackspacePressed = false;

			// Disable the mechanism if inside a link (`<$text url="foo">F[]oo</$text>` or <$text url="foo">Foo[]</$text>`).
			if ( shouldPreserveAttributes ) {
				return;
			}

			// Use `model.enqueueChange()` in order to execute the callback at the end of the changes process.
			editor.model.enqueueChange( writer => {
				removeLinkAttributesFromSelection( writer, getLinkAttributesAllowedOnText( model.schema ) );
			} );
		}, { priority: 'low' } );
	}
}

// Make the selection free of link-related model attributes.
// All link-related model attributes start with "link". That includes not only "linkHref"
// but also all decorator attributes (they have dynamic names), or even custom plugins.
//
// @param {module:engine/model/writer~Writer} writer
// @param {Array.<String>} linkAttributes
function removeLinkAttributesFromSelection( writer, linkAttributes ) {
	writer.removeSelectionAttribute( 'linkHref' );

	for ( const attribute of linkAttributes ) {
		writer.removeSelectionAttribute( attribute );
	}
}

// Checks whether selection's attributes should be copied to the new inserted text.
//
// @param {module:engine/model/model~Model} model
// @returns {Boolean}
function shouldCopyAttributes( model ) {
	const selection = model.document.selection;
	const firstPosition = selection.getFirstPosition();
	const lastPosition = selection.getLastPosition();
	const nodeAtFirstPosition = firstPosition.nodeAfter;

	// The text link node does not exist...
	if ( !nodeAtFirstPosition ) {
		return false;
	}

	// ...or it isn't the text node...
	if ( !nodeAtFirstPosition.is( '$text' ) ) {
		return false;
	}

	// ...or isn't the link.
	if ( !nodeAtFirstPosition.hasAttribute( 'linkHref' ) ) {
		return false;
	}

	// `textNode` = the position is inside the link element.
	// `nodeBefore` = the position is at the end of the link element.
	const nodeAtLastPosition = lastPosition.textNode || lastPosition.nodeBefore;

	// If both references the same node selection contains a single text node.
	if ( nodeAtFirstPosition === nodeAtLastPosition ) {
		return true;
	}

	// If nodes are not equal, maybe the link nodes has defined additional attributes inside.
	// First, we need to find the entire link range.
	const linkRange = findAttributeRange( firstPosition, 'linkHref', nodeAtFirstPosition.getAttribute( 'linkHref' ), model );

	// Then we can check whether selected range is inside the found link range. If so, attributes should be preserved.
	return linkRange.containsRange( model.createRange( firstPosition, lastPosition ), true );
}

// Checks whether provided changes were caused by typing.
//
// @params {module:core/editor/editor~Editor} editor
// @returns {Boolean}
function isTyping( editor ) {
	const currentBatch = editor.model.change( writer => writer.batch );

	return currentBatch.isTyping;
}

// Returns an array containing names of the attributes allowed on `$text` that describes the link item.
//
// @param {module:engine/model/schema~Schema} schema
// @returns {Array.<String>}
function getLinkAttributesAllowedOnText( schema ) {
	const textAttributes = schema.getDefinition( '$text' ).allowAttributes;

	return textAttributes.filter( attribute => attribute.startsWith( 'link' ) );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Widget toolbar repository plugin. A central point for registering widget toolbars. This plugin handles the whole
 * toolbar rendering process and exposes a concise API.
 *
 * To add a toolbar for your widget use the {@link ~WidgetToolbarRepository#register `WidgetToolbarRepository#register()`} method.
 *
 * The following example comes from the {@link module:image/imagetoolbar~ImageToolbar} plugin:
 *
 * 		class ImageToolbar extends Plugin {
 *			static get requires() {
 *				return [ WidgetToolbarRepository ];
 *			}
 *
 *			afterInit() {
 *				const editor = this.editor;
 *				const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
 *
 *				widgetToolbarRepository.register( 'image', {
 *					items: editor.config.get( 'image.toolbar' ),
 *					getRelatedElement: getClosestSelectedImageWidget
 *				} );
 *			}
 *		}
 */
class WidgetToolbarRepository extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ ContextualBalloon ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'WidgetToolbarRepository';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		// Disables the default balloon toolbar for all widgets.
		if ( editor.plugins.has( 'BalloonToolbar' ) ) {
			const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );

			this.listenTo( balloonToolbar, 'show', evt => {
				if ( isWidgetSelected( editor.editing.view.document.selection ) ) {
					evt.stop();
				}
			}, { priority: 'high' } );
		}

		/**
		 * A map of toolbar definitions.
		 *
		 * @protected
		 * @member {Map.<String,module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition>} #_toolbarDefinitions
		 */
		this._toolbarDefinitions = new Map();

		/**
		 * @private
		 */
		this._balloon = this.editor.plugins.get( 'ContextualBalloon' );

		this.on( 'change:isEnabled', () => {
			this._updateToolbarsVisibility();
		} );

		this.listenTo( editor.ui, 'update', () => {
			this._updateToolbarsVisibility();
		} );

		// UI#update is not fired after focus is back in editor, we need to check if balloon panel should be visible.
		this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => {
			this._updateToolbarsVisibility();
		}, { priority: 'low' } );
	}

	destroy() {
		super.destroy();

		for ( const toolbarConfig of this._toolbarDefinitions.values() ) {
			toolbarConfig.view.destroy();
		}
	}

	/**
	 * Registers toolbar in the WidgetToolbarRepository. It renders it in the `ContextualBalloon` based on the value of the invoked
	 * `getRelatedElement` function. Toolbar items are gathered from `items` array.
	 * The balloon's CSS class is by default `ck-toolbar-container` and may be override with the `balloonClassName` option.
	 *
	 * Note: This method should be called in the {@link module:core/plugin~PluginInterface#afterInit `Plugin#afterInit()`}
	 * callback (or later) to make sure that the given toolbar items were already registered by other plugins.
	 *
	 * @param {String} toolbarId An id for the toolbar. Used to
	 * @param {Object} options
	 * @param {String} [options.ariaLabel] Label used by assistive technologies to describe this toolbar element.
	 * @param {Array.<String>} options.items Array of toolbar items.
	 * @param {Function} options.getRelatedElement Callback which returns an element the toolbar should be attached to.
	 * @param {String} [options.balloonClassName='ck-toolbar-container'] CSS class for the widget balloon.
	 */
	register( toolbarId, { ariaLabel, items, getRelatedElement, balloonClassName = 'ck-toolbar-container' } ) {
		// Trying to register a toolbar without any item.
		if ( !items.length ) {
			/**
			 * When {@link #register registering} a new widget toolbar, you need to provide a non-empty array with
			 * the items that will be inserted into the toolbar.
			 *
			 * If you see this error when integrating the editor, you likely forgot to configure one of the widget toolbars.
			 *
			 * See for instance:
			 *
			 * * {@link module:table/table~TableConfig#contentToolbar `config.table.contentToolbar`}
			 * * {@link module:image/image~ImageConfig#toolbar `config.image.toolbar`}
			 *
			 * @error widget-toolbar-no-items
			 * @param {String} toolbarId The id of the toolbar that has not been configured correctly.
			 */
			logWarning( 'widget-toolbar-no-items', { toolbarId } );

			return;
		}

		const editor = this.editor;
		const t = editor.t;
		const toolbarView = new ToolbarView( editor.locale );

		toolbarView.ariaLabel = ariaLabel || t( 'Widget toolbar' );

		if ( this._toolbarDefinitions.has( toolbarId ) ) {
			/**
			 * Toolbar with the given id was already added.
			 *
			 * @error widget-toolbar-duplicated
			 * @param toolbarId Toolbar id.
			 */
			throw new CKEditorError( 'widget-toolbar-duplicated', this, { toolbarId } );
		}

		toolbarView.fillFromConfig( items, editor.ui.componentFactory );

		const toolbarDefinition = {
			view: toolbarView,
			getRelatedElement,
			balloonClassName
		};

		// Register the toolbar so it becomes available for Alt+F10 and Esc navigation.
		editor.ui.addToolbar( toolbarView, {
			isContextual: true,
			beforeFocus: () => {
				const relatedElement = getRelatedElement( editor.editing.view.document.selection );

				if ( relatedElement ) {
					this._showToolbar( toolbarDefinition, relatedElement );
				}
			},
			afterBlur: () => {
				this._hideToolbar( toolbarDefinition );
			}
		} );

		this._toolbarDefinitions.set( toolbarId, toolbarDefinition );
	}

	/**
	 * Iterates over stored toolbars and makes them visible or hidden.
	 *
	 * @private
	 */
	_updateToolbarsVisibility() {
		let maxRelatedElementDepth = 0;
		let deepestRelatedElement = null;
		let deepestToolbarDefinition = null;

		for ( const definition of this._toolbarDefinitions.values() ) {
			const relatedElement = definition.getRelatedElement( this.editor.editing.view.document.selection );

			if ( !this.isEnabled || !relatedElement ) {
				if ( this._isToolbarInBalloon( definition ) ) {
					this._hideToolbar( definition );
				}
			} else if ( !this.editor.ui.focusTracker.isFocused ) {
				if ( this._isToolbarVisible( definition ) ) {
					this._hideToolbar( definition );
				}
			} else {
				const relatedElementDepth = relatedElement.getAncestors().length;

				// Many toolbars can express willingness to be displayed but they do not know about
				// each other. Figure out which toolbar is deepest in the view tree to decide which
				// should be displayed. For instance, if a selected image is inside a table cell, display
				// the ImageToolbar rather than the TableToolbar (#60).
				if ( relatedElementDepth > maxRelatedElementDepth ) {
					maxRelatedElementDepth = relatedElementDepth;
					deepestRelatedElement = relatedElement;
					deepestToolbarDefinition = definition;
				}
			}
		}

		if ( deepestToolbarDefinition ) {
			this._showToolbar( deepestToolbarDefinition, deepestRelatedElement );
		}
	}

	/**
	 * Hides the given toolbar.
	 *
	 * @private
	 * @param {module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition} toolbarDefinition
	 */
	_hideToolbar( toolbarDefinition ) {
		this._balloon.remove( toolbarDefinition.view );
		this.stopListening( this._balloon, 'change:visibleView' );
	}

	/**
	 * Shows up the toolbar if the toolbar is not visible.
	 * Otherwise, repositions the toolbar's balloon when toolbar's view is the most top view in balloon stack.
	 *
	 * It might happen here that the toolbar's view is under another view. Then do nothing as the other toolbar view
	 * should be still visible after the {@link module:core/editor/editorui~EditorUI#event:update}.
	 *
	 * @private
	 * @param {module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition} toolbarDefinition
	 * @param {module:engine/view/element~Element} relatedElement
	 */
	_showToolbar( toolbarDefinition, relatedElement ) {
		if ( this._isToolbarVisible( toolbarDefinition ) ) {
			repositionContextualBalloon$1( this.editor, relatedElement );
		} else if ( !this._isToolbarInBalloon( toolbarDefinition ) ) {
			this._balloon.add( {
				view: toolbarDefinition.view,
				position: getBalloonPositionData( this.editor, relatedElement ),
				balloonClassName: toolbarDefinition.balloonClassName
			} );

			// Update toolbar position each time stack with toolbar view is switched to visible.
			// This is in a case target element has changed when toolbar was in invisible stack
			// e.g. target image was wrapped by a block quote.
			// See https://github.com/ckeditor/ckeditor5-widget/issues/92.
			this.listenTo( this._balloon, 'change:visibleView', () => {
				for ( const definition of this._toolbarDefinitions.values() ) {
					if ( this._isToolbarVisible( definition ) ) {
						const relatedElement = definition.getRelatedElement( this.editor.editing.view.document.selection );
						repositionContextualBalloon$1( this.editor, relatedElement );
					}
				}
			} );
		}
	}

	/**
	 * @private
	 * @param {Object} toolbar
	 * @returns {Boolean}
	 */
	_isToolbarVisible( toolbar ) {
		return this._balloon.visibleView === toolbar.view;
	}

	/**
	 * @private
	 * @param {Object} toolbar
	 * @returns {Boolean}
	 */
	_isToolbarInBalloon( toolbar ) {
		return this._balloon.hasView( toolbar.view );
	}
}

function repositionContextualBalloon$1( editor, relatedElement ) {
	const balloon = editor.plugins.get( 'ContextualBalloon' );
	const position = getBalloonPositionData( editor, relatedElement );

	balloon.updatePosition( position );
}

function getBalloonPositionData( editor, relatedElement ) {
	const editingView = editor.editing.view;
	const defaultPositions = BalloonPanelView.defaultPositions;

	return {
		target: editingView.domConverter.mapViewToDom( relatedElement ),
		positions: [
			defaultPositions.northArrowSouth,
			defaultPositions.northArrowSouthWest,
			defaultPositions.northArrowSouthEast,
			defaultPositions.southArrowNorth,
			defaultPositions.southArrowNorthWest,
			defaultPositions.southArrowNorthEast,
			defaultPositions.viewportStickyNorth
		]
	};
}

function isWidgetSelected( selection ) {
	const viewElement = selection.getSelectedElement();

	return !!( viewElement && isWidget( viewElement ) );
}

/**
 * The toolbar definition object used by the toolbar repository to manage toolbars.
 * It contains information necessary to display the toolbar in the
 * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} and
 * update it during its life (display) cycle.
 *
 * @typedef {Object} module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition
 *
 * @property {module:ui/view~View} view The UI view of the toolbar.
 * @property {Function} getRelatedElement A function that returns an engine {@link module:engine/view/view~View}
 * element the toolbar is to be attached to. For instance, an image widget or a table widget (or `null` when
 * there is no such element). The function accepts an instance of {@link module:engine/view/selection~Selection}.
 * @property {String} balloonClassName CSS class for the widget balloon when a toolbar is displayed.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Stores the internal state of a single resizable object.
 *
 */
class ResizeState {
	/**
	 * @param {module:widget/widgetresize~ResizerOptions} options Resizer options.
	 */
	constructor( options ) {
		/**
		 * The original width (pixels) of the resized object when the resize process was started.
		 *
		 * @readonly
		 * @member {Number} #originalWidth
		 */

		/**
		 * The original height (pixels) of the resized object when the resize process was started.
		 *
		 * @readonly
		 * @member {Number} #originalHeight
		 */

		/**
		 * The original width (percents) of the resized object when the resize process was started.
		 *
		 * @readonly
		 * @member {Number} #originalWidthPercents
		 */

		/**
		 * The position of the handle that initiated the resizing. E.g. `"top-left"`, `"bottom-right"` etc. or `null`
		 * if unknown.
		 *
		 * @readonly
		 * @observable
		 * @member {String|null} #activeHandlePosition
		 */
		this.set( 'activeHandlePosition', null );

		/**
		 * The width (percents) proposed, but not committed yet, in the current resize process.
		 *
		 * @readonly
		 * @observable
		 * @member {Number|null} #proposedWidthPercents
		 */
		this.set( 'proposedWidthPercents', null );

		/**
		 * The width (pixels) proposed, but not committed yet, in the current resize process.
		 *
		 * @readonly
		 * @observable
		 * @member {Number|null} #proposedWidthPixels
		 */
		this.set( 'proposedWidth', null );

		/**
		 * The height (pixels) proposed, but not committed yet, in the current resize process.
		 *
		 * @readonly
		 * @observable
		 * @member {Number|null} #proposedHeightPixels
		 */
		this.set( 'proposedHeight', null );

		this.set( 'proposedHandleHostWidth', null );
		this.set( 'proposedHandleHostHeight', null );

		/**
		 * A width to height ratio of the resized image.
		 *
		 * @readonly
		 * @member {Number} #aspectRatio
		 */

		/**
		 * @private
		 * @type {module:widget/widgetresize~ResizerOptions}
		 */
		this._options = options;

		/**
		 * The reference point of the resizer where the dragging started. It is used to measure the distance the user cursor
		 * traveled, so how much the image should be enlarged.
		 * This information is only known after the DOM was rendered, so it will be updated later.
		 *
		 * @private
		 * @type {Object}
		 */
		this._referenceCoordinates = null;
	}

	/**
	 *
	 * @param {HTMLElement} domResizeHandle The handle used to calculate the reference point.
	 * @param {HTMLElement} domHandleHost
	 * @param {HTMLElement} domResizeHost
	 */
	begin( domResizeHandle, domHandleHost, domResizeHost ) {
		const clientRect = new Rect( domHandleHost );

		this.activeHandlePosition = getHandlePosition( domResizeHandle );

		this._referenceCoordinates = getAbsoluteBoundaryPoint( domHandleHost, getOppositePosition( this.activeHandlePosition ) );

		this.originalWidth = clientRect.width;
		this.originalHeight = clientRect.height;

		this.aspectRatio = clientRect.width / clientRect.height;

		const widthStyle = domResizeHost.style.width;

		if ( widthStyle && widthStyle.match( /^\d+(\.\d*)?%$/ ) ) {
			this.originalWidthPercents = parseFloat( widthStyle );
		} else {
			this.originalWidthPercents = calculateHostPercentageWidth( domResizeHost, clientRect );
		}
	}

	update( newSize ) {
		this.proposedWidth = newSize.width;
		this.proposedHeight = newSize.height;
		this.proposedWidthPercents = newSize.widthPercents;

		this.proposedHandleHostWidth = newSize.handleHostWidth;
		this.proposedHandleHostHeight = newSize.handleHostHeight;
	}
}

mix( ResizeState, ObservableMixin );

// Calculates a relative width of a `domResizeHost` compared to it's parent in percents.
//
// @private
// @param {HTMLElement} domResizeHost
// @param {module:utils/dom/rect~Rect} resizeHostRect
// @returns {Number}
function calculateHostPercentageWidth( domResizeHost, resizeHostRect ) {
	const domResizeHostParent = domResizeHost.parentElement;
	// Need to use computed style as it properly excludes parent's paddings from the returned value.
	const parentWidth = parseFloat( domResizeHostParent.ownerDocument.defaultView.getComputedStyle( domResizeHostParent ).width );

	return resizeHostRect.width / parentWidth * 100;
}

// Returns coordinates of the top-left corner of an element, relative to the document's top-left corner.
//
// @private
// @param {HTMLElement} element
// @param {String} resizerPosition The position of the resize handle, e.g. `"top-left"`, `"bottom-right"`.
// @returns {Object} return
// @returns {Number} return.x
// @returns {Number} return.y
function getAbsoluteBoundaryPoint( element, resizerPosition ) {
	const elementRect = new Rect( element );
	const positionParts = resizerPosition.split( '-' );
	const ret = {
		x: positionParts[ 1 ] == 'right' ? elementRect.right : elementRect.left,
		y: positionParts[ 0 ] == 'bottom' ? elementRect.bottom : elementRect.top
	};

	ret.x += element.ownerDocument.defaultView.scrollX;
	ret.y += element.ownerDocument.defaultView.scrollY;

	return ret;
}

// @private
// @param {String} resizerPosition The expected resizer position, like `"top-left"`, `"bottom-right"`.
// @returns {String} A prefixed HTML class name for the resizer element.
function getResizerHandleClass( resizerPosition ) {
	return `ck-widget__resizer__handle-${ resizerPosition }`;
}

// Determines the position of a given resize handle.
//
// @private
// @param {HTMLElement} domHandle Handle used to calculate the reference point.
// @returns {String|undefined} Returns a string like `"top-left"` or `undefined` if not matched.
function getHandlePosition( domHandle ) {
	const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ];

	for ( const position of resizerPositions ) {
		if ( domHandle.classList.contains( getResizerHandleClass( position ) ) ) {
			return position;
		}
	}
}

// @private
// @param {String} position Like `"top-left"`.
// @returns {String} Inverted `position`, e.g. it returns `"bottom-right"` if `"top-left"` was given as `position`.
function getOppositePosition( position ) {
	const parts = position.split( '-' );
	const replacements = {
		top: 'bottom',
		bottom: 'top',
		left: 'right',
		right: 'left'
	};

	return `${ replacements[ parts[ 0 ] ] }-${ replacements[ parts[ 1 ] ] }`;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A view displaying the proposed new element size during the resizing.
 *
 * @protected
 * @extends {module:ui/view~View}
 */
class SizeView extends View {
	constructor() {
		super();

		/**
		 * The visibility of the view defined based on the existence of the host proposed dimensions.
		 *
		 * @private
		 * @observable
		 * @readonly
		 * @member {Boolean} #_isVisible
		 */

		/**
		 * The text that will be displayed in the `SizeView` child.
		 * It can be formatted as the pixel values (e.g. 10x20) or the percentage value (e.g. 10%).
		 *
		 * @private
		 * @observable
		 * @readonly
		 * @member {Boolean} #_label
		 */

		/**
		 * The position of the view defined based on the host size and active handle position.
		 *
		 * @private
		 * @observable
		 * @readonly
		 * @member {String} #_viewPosition
		 */

		const bind = this.bindTemplate;

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-size-view',
					bind.to( '_viewPosition', value => value ? `ck-orientation-${ value }` : '' )
				],
				style: {
					display: bind.if( '_isVisible', 'none', visible => !visible )
				}
			},
			children: [ {
				text: bind.to( '_label' )
			} ]
		} );
	}

	/**
	 * A method used for binding the `SizeView` instance properties to the `ResizeState` instance observable properties.
	 *
	 * @protected
	 * @param {module:widget/widgetresize~ResizerOptions} options
	 * An object defining the resizer options, used for setting the proper size label.
	 * @param {module:widget/widgetresize/resizerstate~ResizeState} resizeState
	 * The `ResizeState` class instance, used for keeping the `SizeView` state up to date.
	 */
	_bindToState( options, resizeState ) {
		this.bind( '_isVisible' ).to( resizeState, 'proposedWidth', resizeState, 'proposedHeight', ( width, height ) =>
			width !== null && height !== null );

		this.bind( '_label' ).to(
			resizeState, 'proposedHandleHostWidth',
			resizeState, 'proposedHandleHostHeight',
			resizeState, 'proposedWidthPercents',
			( width, height, widthPercents ) => {
				if ( options.unit === 'px' ) {
					return `${ width }×${ height }`;
				} else {
					return `${ widthPercents }%`;
				}
			}
		);

		this.bind( '_viewPosition' ).to(
			resizeState, 'activeHandlePosition',
			resizeState, 'proposedHandleHostWidth',
			resizeState, 'proposedHandleHostHeight',
			// If the widget is too small to contain the size label, display the label above.
			( position, width, height ) => width < 50 || height < 50 ? 'above-center' : position
		);
	}

	/**
	 * A method used for cleaning up. It removes the bindings and hides the view.
	 *
	 * @protected
	 */
	_dismiss() {
		this.unbind();
		this._isVisible = false;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Represents a resizer for a single resizable object.
 *
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class Resizer {
	/**
	 * @param {module:widget/widgetresize~ResizerOptions} options Resizer options.
	 */
	constructor( options ) {
		/**
		 * Stores the state of the resizable host geometry, such as the original width, the currently proposed height, etc.
		 *
		 * Note that a new state is created for each resize transaction.
		 *
		 * @readonly
		 * @member {module:widget/widgetresize/resizerstate~ResizerState} #state
		 */

		/**
		 * A view displaying the proposed new element size during the resizing.
		 *
		 * @protected
		 * @readonly
		 * @member {module:widget/widgetresize/sizeview~SizeView} #_sizeView
		 */

		/**
		 * Options passed to the {@link #constructor}.
		 *
		 * @private
		 * @type {module:widget/widgetresize~ResizerOptions}
		 */
		this._options = options;

		/**
		 * A wrapper that is controlled by the resizer. This is usually a widget element.
		 *
		 * @private
		 * @type {module:engine/view/element~Element|null}
		 */
		this._viewResizerWrapper = null;

		/**
		 * The width of the resized {@link module:widget/widgetresize~ResizerOptions#viewElement viewElement} before the resizing started.
		 *
		 * @private
		 * @member {Number|String|undefined} #_initialViewWidth
		 */

		/**
		 * @observable
		 */
		this.set( 'isEnabled', true );

		this.decorate( 'begin' );
		this.decorate( 'cancel' );
		this.decorate( 'commit' );
		this.decorate( 'updateSize' );

		this.on( 'commit', event => {
			// State might not be initialized yet. In this case, prevent further handling and make sure that the resizer is
			// cleaned up (#5195).
			if ( !this.state.proposedWidth && !this.state.proposedWidthPercents ) {
				this._cleanup();
				event.stop();
			}
		}, { priority: 'high' } );

		this.on( 'change:isEnabled', () => {
			// We should redraw the resize handles when the plugin is enabled again.
			// Otherwise they won't show up.
			if ( this.isEnabled ) {
				this.redraw();
			}
		} );
	}

	/**
	 * Attaches the resizer to the DOM.
	 */
	attach() {
		const that = this;
		const widgetElement = this._options.viewElement;
		const editingView = this._options.editor.editing.view;

		editingView.change( writer => {
			const viewResizerWrapper = writer.createUIElement( 'div', {
				class: 'ck ck-reset_all ck-widget__resizer'
			}, function( domDocument ) {
				const domElement = this.toDomElement( domDocument );

				that._appendHandles( domElement );
				that._appendSizeUI( domElement );

				that.on( 'change:isEnabled', ( evt, propName, newValue ) => {
					domElement.style.display = newValue ? '' : 'none';
				} );

				domElement.style.display = that.isEnabled ? '' : 'none';

				return domElement;
			} );

			// Append the resizer wrapper to the widget's wrapper.
			writer.insert( writer.createPositionAt( widgetElement, 'end' ), viewResizerWrapper );
			writer.addClass( 'ck-widget_with-resizer', widgetElement );

			this._viewResizerWrapper = viewResizerWrapper;
		} );
	}

	/**
	 * Starts the resizing process.
	 *
	 * Creates a new {@link #state} for the current process.
	 *
	 * @fires begin
	 * @param {HTMLElement} domResizeHandle Clicked handle.
	 */
	begin( domResizeHandle ) {
		this.state = new ResizeState( this._options );

		this._sizeView._bindToState( this._options, this.state );

		this._initialViewWidth = this._options.viewElement.getStyle( 'width' );

		this.state.begin( domResizeHandle, this._getHandleHost(), this._getResizeHost() );
	}

	/**
	 * Updates the proposed size based on `domEventData`.
	 *
	 * @fires updateSize
	 * @param {Event} domEventData
	 */
	updateSize( domEventData ) {
		const newSize = this._proposeNewSize( domEventData );
		const editingView = this._options.editor.editing.view;

		editingView.change( writer => {
			const unit = this._options.unit || '%';
			const newWidth = ( unit === '%' ? newSize.widthPercents : newSize.width ) + unit;

			writer.setStyle( 'width', newWidth, this._options.viewElement );
		} );

		// Get an actual image width, and:
		// * reflect this size to the resize wrapper
		// * apply this **real** size to the state
		const domHandleHost = this._getHandleHost();
		const domHandleHostRect = new Rect( domHandleHost );

		newSize.handleHostWidth = Math.round( domHandleHostRect.width );
		newSize.handleHostHeight = Math.round( domHandleHostRect.height );

		// Handle max-width limitation.
		const domResizeHostRect = new Rect( domHandleHost );

		newSize.width = Math.round( domResizeHostRect.width );
		newSize.height = Math.round( domResizeHostRect.height );

		this.redraw( domHandleHostRect );

		this.state.update( newSize );
	}

	/**
	 * Applies the geometry proposed with the resizer.
	 *
	 * @fires commit
	 */
	commit() {
		const unit = this._options.unit || '%';
		const newValue = ( unit === '%' ? this.state.proposedWidthPercents : this.state.proposedWidth ) + unit;

		// Both cleanup and onCommit callback are very likely to make view changes. Ensure that it is made in a single step.
		this._options.editor.editing.view.change( () => {
			this._cleanup();
			this._options.onCommit( newValue );
		} );
	}

	/**
	 * Cancels and rejects the proposed resize dimensions, hiding the UI.
	 *
	 * @fires cancel
	 */
	cancel() {
		this._cleanup();
	}

	/**
	 * Destroys the resizer.
	 */
	destroy() {
		this.cancel();
	}

	/**
	 * Redraws the resizer.
	 *
	 * @param {module:utils/dom/rect~Rect} [handleHostRect] Handle host rectangle might be given to improve performance.
	 */
	redraw( handleHostRect ) {
		const domWrapper = this._domResizerWrapper;

		// Refresh only if resizer exists in the DOM.
		if ( !existsInDom( domWrapper ) ) {
			return;
		}

		const widgetWrapper = domWrapper.parentElement;
		const handleHost = this._getHandleHost();
		const resizerWrapper = this._viewResizerWrapper;
		const currentDimensions = [
			resizerWrapper.getStyle( 'width' ),
			resizerWrapper.getStyle( 'height' ),
			resizerWrapper.getStyle( 'left' ),
			resizerWrapper.getStyle( 'top' )
		];
		let newDimensions;

		if ( widgetWrapper.isSameNode( handleHost ) ) {
			const clientRect = handleHostRect || new Rect( handleHost );

			newDimensions = [
				clientRect.width + 'px',
				clientRect.height + 'px',
				undefined,
				undefined
			];
		}
		// In case a resizing host is not a widget wrapper, we need to compensate
		// for any additional offsets the resize host might have. E.g. wrapper padding
		// or simply another editable. By doing that the border and resizers are shown
		// only around the resize host.
		else {
			newDimensions = [
				handleHost.offsetWidth + 'px',
				handleHost.offsetHeight + 'px',
				handleHost.offsetLeft + 'px',
				handleHost.offsetTop + 'px'
			];
		}

		// Make changes to the view only if the resizer should actually get new dimensions.
		// Otherwise, if View#change() was always called, this would cause EditorUI#update
		// loops because the WidgetResize plugin listens to EditorUI#update and updates
		// the resizer.
		// https://github.com/ckeditor/ckeditor5/issues/7633
		if ( compareArrays( currentDimensions, newDimensions ) !== 'same' ) {
			this._options.editor.editing.view.change( writer => {
				writer.setStyle( {
					width: newDimensions[ 0 ],
					height: newDimensions[ 1 ],
					left: newDimensions[ 2 ],
					top: newDimensions[ 3 ]
				}, resizerWrapper );
			} );
		}
	}

	containsHandle( domElement ) {
		return this._domResizerWrapper.contains( domElement );
	}

	static isResizeHandle( domElement ) {
		return domElement.classList.contains( 'ck-widget__resizer__handle' );
	}

	/**
	 * Cleans up the context state.
	 *
	 * @protected
	 */
	_cleanup() {
		this._sizeView._dismiss();

		const editingView = this._options.editor.editing.view;

		editingView.change( writer => {
			writer.setStyle( 'width', this._initialViewWidth, this._options.viewElement );
		} );
	}

	/**
	 * Calculates the proposed size as the resize handles are dragged.
	 *
	 * @private
	 * @param {Event} domEventData Event data that caused the size update request. It should be used to calculate the proposed size.
	 * @returns {Object} return
	 * @returns {Number} return.width Proposed width.
	 * @returns {Number} return.height Proposed height.
	 */
	_proposeNewSize( domEventData ) {
		const state = this.state;
		const currentCoordinates = extractCoordinates( domEventData );
		const isCentered = this._options.isCentered ? this._options.isCentered( this ) : true;

		// Enlargement defines how much the resize host has changed in a given axis. Naturally it could be a negative number
		// meaning that it has been shrunk.
		//
		// +----------------+--+
		// |                |  |
		// |       img      |  |
		// |  /handle host  |  |
		// +----------------+  | ^
		// |                   | | - enlarge y
		// +-------------------+ v
		// 					<-->
		// 					 enlarge x
		const enlargement = {
			x: state._referenceCoordinates.x - ( currentCoordinates.x + state.originalWidth ),
			y: ( currentCoordinates.y - state.originalHeight ) - state._referenceCoordinates.y
		};

		if ( isCentered && state.activeHandlePosition.endsWith( '-right' ) ) {
			enlargement.x = currentCoordinates.x - ( state._referenceCoordinates.x + state.originalWidth );
		}

		// Objects needs to be resized twice as much in horizontal axis if centered, since enlargement is counted from
		// one resized corner to your cursor. It needs to be duplicated to compensate for the other side too.
		if ( isCentered ) {
			enlargement.x *= 2;
		}

		// const resizeHost = this._getResizeHost();

		// The size proposed by the user. It does not consider the aspect ratio.
		const proposedSize = {
			width: Math.abs( state.originalWidth + enlargement.x ),
			height: Math.abs( state.originalHeight + enlargement.y )
		};

		// Dominant determination must take the ratio into account.
		proposedSize.dominant = proposedSize.width / state.aspectRatio > proposedSize.height ? 'width' : 'height';
		proposedSize.max = proposedSize[ proposedSize.dominant ];

		// Proposed size, respecting the aspect ratio.
		const targetSize = {
			width: proposedSize.width,
			height: proposedSize.height
		};

		if ( proposedSize.dominant == 'width' ) {
			targetSize.height = targetSize.width / state.aspectRatio;
		} else {
			targetSize.width = targetSize.height * state.aspectRatio;
		}

		return {
			width: Math.round( targetSize.width ),
			height: Math.round( targetSize.height ),
			widthPercents: Math.min( Math.round( state.originalWidthPercents / state.originalWidth * targetSize.width * 100 ) / 100, 100 )
		};
	}

	/**
	 * Obtains the resize host.
	 *
	 * Resize host is an object that receives dimensions which are the result of resizing.
	 *
	 * @protected
	 * @returns {HTMLElement}
	 */
	_getResizeHost() {
		const widgetWrapper = this._domResizerWrapper.parentElement;

		return this._options.getResizeHost( widgetWrapper );
	}

	/**
	 * Obtains the handle host.
	 *
	 * Handle host is an object that the handles are aligned to.
	 *
	 * Handle host will not always be an entire widget itself. Take an image as an example. The image widget
	 * contains an image and a caption. Only the image should be surrounded with handles.
	 *
	 * @protected
	 * @returns {HTMLElement}
	 */
	_getHandleHost() {
		const widgetWrapper = this._domResizerWrapper.parentElement;

		return this._options.getHandleHost( widgetWrapper );
	}

	/**
	 * DOM container of the entire resize UI.
	 *
	 * Note that this property will have a value only after the element bound with the resizer is rendered
	 * (otherwise `null`).
	 *
	 * @private
	 * @member {HTMLElement|null}
	 */
	get _domResizerWrapper() {
		return this._options.editor.editing.view.domConverter.mapViewToDom( this._viewResizerWrapper );
	}

	/**
	 * Renders the resize handles in the DOM.
	 *
	 * @private
	 * @param {HTMLElement} domElement The resizer wrapper.
	 */
	_appendHandles( domElement ) {
		const resizerPositions = [ 'top-left', 'top-right', 'bottom-right', 'bottom-left' ];

		for ( const currentPosition of resizerPositions ) {
			domElement.appendChild( ( new Template( {
				tag: 'div',
				attributes: {
					class: `ck-widget__resizer__handle ${ getResizerClass( currentPosition ) }`
				}
			} ).render() ) );
		}
	}

	/**
	 * Sets up the {@link #_sizeView} property and adds it to the passed `domElement`.
	 *
	 * @private
	 * @param {HTMLElement} domElement
	 */
	_appendSizeUI( domElement ) {
		this._sizeView = new SizeView();

		// Make sure icon#element is rendered before passing to appendChild().
		this._sizeView.render();

		domElement.appendChild( this._sizeView.element );
	}

	/**
	 * @event begin
	 */

	/**
	 * @event updateSize
	 */

	/**
	 * @event commit
	 */

	/**
	 * @event cancel
	 */
}

mix( Resizer, ObservableMixin );

// @private
// @param {String} resizerPosition Expected resizer position like `"top-left"`, `"bottom-right"`.
// @returns {String} A prefixed HTML class name for the resizer element
function getResizerClass( resizerPosition ) {
	return `ck-widget__resizer__handle-${ resizerPosition }`;
}

function extractCoordinates( event ) {
	return {
		x: event.pageX,
		y: event.pageY
	};
}

function existsInDom( element ) {
	return element && element.ownerDocument && element.ownerDocument.contains( element );
}var css_248z$o = ".ck .ck-widget_with-resizer{position:relative}.ck .ck-widget__resizer{display:none;left:0;pointer-events:none;position:absolute;top:0}.ck-focused .ck-widget_with-resizer.ck-widget_selected>.ck-widget__resizer{display:block}.ck .ck-widget__resizer__handle{pointer-events:all;position:absolute}.ck .ck-widget__resizer__handle.ck-widget__resizer__handle-bottom-right,.ck .ck-widget__resizer__handle.ck-widget__resizer__handle-top-left{cursor:nwse-resize}.ck .ck-widget__resizer__handle.ck-widget__resizer__handle-bottom-left,.ck .ck-widget__resizer__handle.ck-widget__resizer__handle-top-right{cursor:nesw-resize}:root{--ck-resizer-size:10px;--ck-resizer-offset:calc(var(--ck-resizer-size)/-2 - 2px);--ck-resizer-border-width:1px}.ck .ck-widget__resizer{outline:1px solid var(--ck-color-resizer)}.ck .ck-widget__resizer__handle{background:var(--ck-color-focus-border);border:var(--ck-resizer-border-width) solid #fff;border-radius:var(--ck-resizer-border-radius);height:var(--ck-resizer-size);width:var(--ck-resizer-size)}.ck .ck-widget__resizer__handle.ck-widget__resizer__handle-top-left{left:var(--ck-resizer-offset);top:var(--ck-resizer-offset)}.ck .ck-widget__resizer__handle.ck-widget__resizer__handle-top-right{right:var(--ck-resizer-offset);top:var(--ck-resizer-offset)}.ck .ck-widget__resizer__handle.ck-widget__resizer__handle-bottom-right{bottom:var(--ck-resizer-offset);right:var(--ck-resizer-offset)}.ck .ck-widget__resizer__handle.ck-widget__resizer__handle-bottom-left{bottom:var(--ck-resizer-offset);left:var(--ck-resizer-offset)}";
styleInject(css_248z$o);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The widget resize feature plugin.
 *
 * Use the {@link module:widget/widgetresize~WidgetResize#attachTo} method to create a resizer for the specified widget.
 *
 * @extends module:core/plugin~Plugin
 * @mixes module:utils/observablemixin~ObservableMixin
 */
class WidgetResize extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'WidgetResize';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editing = this.editor.editing;
		const domDocument = global$2.window.document;

		/**
		 * The currently visible resizer.
		 *
		 * @observable
		 * @member {module:widget/widgetresize/resizer~Resizer|null} #visibleResizer
		 */
		this.set( 'visibleResizer', null );

		/**
		 * References an active resizer.
		 *
		 * Active resizer means a resizer which handle is actively used by the end user.
		 *
		 * @protected
		 * @observable
		 * @member {module:widget/widgetresize/resizer~Resizer|null} #_activeResizer
		 */
		this.set( '_activeResizer', null );

		/**
		 * A map of resizers created using this plugin instance.
		 *
		 * @protected
		 * @type {Map.<module:engine/view/containerelement~ContainerElement, module:widget/widgetresize/resizer~Resizer>}
		 */
		this._resizers = new Map();

		editing.view.addObserver( MouseObserver );

		this._observer = Object.create( DomEmitterMixin );

		this.listenTo( editing.view.document, 'mousedown', this._mouseDownListener.bind( this ), { priority: 'high' } );

		this._observer.listenTo( domDocument, 'mousemove', this._mouseMoveListener.bind( this ) );
		this._observer.listenTo( domDocument, 'mouseup', this._mouseUpListener.bind( this ) );

		const redrawFocusedResizer = () => {
			if ( this.visibleResizer ) {
				this.visibleResizer.redraw();
			}
		};

		this._redrawFocusedResizerThrottled = throttle( redrawFocusedResizer, 200 );

		// Redraws occurring upon a change of visible resizer must not be throttled, as it is crucial for the initial
		// render. Without it the resizer frame would be misaligned with resizing host for a fraction of second.
		this.on( 'change:visibleResizer', redrawFocusedResizer );

		// Redrawing on any change of the UI of the editor (including content changes).
		this.editor.ui.on( 'update', this._redrawFocusedResizerThrottled );

		// Remove view widget-resizer mappings for widgets that have been removed from the document.
		// https://github.com/ckeditor/ckeditor5/issues/10156
		// https://github.com/ckeditor/ckeditor5/issues/10266
		this.editor.model.document.on( 'change', () => {
			for ( const [ viewElement, resizer ] of this._resizers ) {
				if ( !viewElement.isAttached() ) {
					this._resizers.delete( viewElement );
					resizer.destroy();
				}
			}
		}, { priority: 'lowest' } );

		// Resizers need to be redrawn upon window resize, because new window might shrink resize host.
		this._observer.listenTo( global$2.window, 'resize', this._redrawFocusedResizerThrottled );

		const viewSelection = this.editor.editing.view.document.selection;

		viewSelection.on( 'change', () => {
			const selectedElement = viewSelection.getSelectedElement();

			this.visibleResizer = this.getResizerByViewElement( selectedElement ) || null;
		} );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		this._observer.stopListening();

		for ( const resizer of this._resizers.values() ) {
			resizer.destroy();
		}

		this._redrawFocusedResizerThrottled.cancel();
	}

	/**
	 * @param {module:widget/widgetresize~ResizerOptions} [options] Resizer options.
	 * @returns {module:widget/widgetresize/resizer~Resizer}
	 */
	attachTo( options ) {
		const resizer = new Resizer( options );
		const plugins = this.editor.plugins;

		resizer.attach();

		if ( plugins.has( 'WidgetToolbarRepository' ) ) {
			// Hiding widget toolbar to improve the performance
			// (https://github.com/ckeditor/ckeditor5-widget/pull/112#issuecomment-564528765).
			const widgetToolbarRepository = plugins.get( 'WidgetToolbarRepository' );

			resizer.on( 'begin', () => {
				widgetToolbarRepository.forceDisabled( 'resize' );
			}, { priority: 'lowest' } );

			resizer.on( 'cancel', () => {
				widgetToolbarRepository.clearForceDisabled( 'resize' );
			}, { priority: 'highest' } );

			resizer.on( 'commit', () => {
				widgetToolbarRepository.clearForceDisabled( 'resize' );
			}, { priority: 'highest' } );
		}

		this._resizers.set( options.viewElement, resizer );

		const viewSelection = this.editor.editing.view.document.selection;
		const selectedElement = viewSelection.getSelectedElement();

		// If the element the resizer is created for is currently focused, it should become visible.
		if ( this.getResizerByViewElement( selectedElement ) == resizer ) {
			this.visibleResizer = resizer;
		}

		return resizer;
	}

	/**
	 * Returns a resizer created for a given view element (widget element).
	 *
	 * @param {module:engine/view/containerelement~ContainerElement} viewElement View element associated with the resizer.
	 * @returns {module:widget/widgetresize/resizer~Resizer|undefined}
	 */
	getResizerByViewElement( viewElement ) {
		return this._resizers.get( viewElement );
	}

	/**
	 * Returns a resizer that contains a given resize handle.
	 *
	 * @protected
	 * @param {HTMLElement} domResizeHandle
	 * @returns {module:widget/widgetresize/resizer~Resizer}
	 */
	_getResizerByHandle( domResizeHandle ) {
		for ( const resizer of this._resizers.values() ) {
			if ( resizer.containsHandle( domResizeHandle ) ) {
				return resizer;
			}
		}
	}

	/**
	 * @protected
	 * @param {module:utils/eventinfo~EventInfo} event
	 * @param {Event} domEventData Native DOM event.
	 */
	_mouseDownListener( event, domEventData ) {
		const resizeHandle = domEventData.domTarget;

		if ( !Resizer.isResizeHandle( resizeHandle ) ) {
			return;
		}

		this._activeResizer = this._getResizerByHandle( resizeHandle );

		if ( this._activeResizer ) {
			this._activeResizer.begin( resizeHandle );

			// Do not call other events when resizing. See: #6755.
			event.stop();
			domEventData.preventDefault();
		}
	}

	/**
	 * @protected
	 * @param {module:utils/eventinfo~EventInfo} event
	 * @param {Event} domEventData Native DOM event.
	 */
	_mouseMoveListener( event, domEventData ) {
		if ( this._activeResizer ) {
			this._activeResizer.updateSize( domEventData );
		}
	}

	/**
	 * @protected
	 */
	_mouseUpListener() {
		if ( this._activeResizer ) {
			this._activeResizer.commit();
			this._activeResizer = null;
		}
	}
}

mix( WidgetResize, ObservableMixin );

/**
 * Interface describing a resizer. It allows to specify the resizing host, custom logic for calculating aspect ratio, etc.
 *
 * @interface ResizerOptions
 */

/**
 * Editor instance associated with the resizer.
 *
 * @member {module:core/editor/editor~Editor} module:widget/widgetresize~ResizerOptions#editor
 */

/**
 * @member {module:engine/model/element~Element} module:widget/widgetresize~ResizerOptions#modelElement
 */

/**
 * A view of an element to be resized. Typically it's the main widget's view instance.
 *
 * @member {module:engine/view/containerelement~ContainerElement} module:widget/widgetresize~ResizerOptions#viewElement
 */

/**
 * A callback to be executed once the resizing process is done.
 *
 * It receives a `Number` (`newValue`) as a parameter.
 *
 * For example, {@link module:image/imageresize~ImageResize} uses it to execute the resize image command
 * which puts the new value into the model.
 *
 * ```js
 * {
 *	editor,
 *	modelElement: data.item,
 *	viewElement: widget,
 *
 *	onCommit( newValue ) {
 *		editor.execute( 'resizeImage', { width: newValue } );
 *	}
 * };
 * ```
 *
 *
 * @member {Function} module:widget/widgetresize~ResizerOptions#onCommit
 */

/**
 * @member {Function} module:widget/widgetresize~ResizerOptions#getResizeHost
 */

/**
 * @member {Function} module:widget/widgetresize~ResizerOptions#isCentered
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */var index$2=/*#__PURE__*/Object.freeze({__proto__:null,Widget:Widget,WidgetToolbarRepository:WidgetToolbarRepository,WidgetResize:WidgetResize,WidgetTypeAround:WidgetTypeAround,WIDGET_CLASS_NAME:WIDGET_CLASS_NAME,WIDGET_SELECTED_CLASS_NAME:WIDGET_SELECTED_CLASS_NAME,isWidget:isWidget,toWidget:toWidget,setHighlightHandling:setHighlightHandling,setLabel:setLabel,getLabel:getLabel,toWidgetEditable:toWidgetEditable,findOptimalInsertionRange:findOptimalInsertionRange,viewToModelPositionOutsideModelElement:viewToModelPositionOutsideModelElement});var css_248z$n = ".ck.ck-link-form{display:flex}.ck.ck-link-form .ck-label{display:none}@media screen and (max-width:600px){.ck.ck-link-form{flex-wrap:wrap}.ck.ck-link-form .ck-labeled-field-view{flex-basis:100%}.ck.ck-link-form .ck-button{flex-basis:50%}}.ck.ck-link-form_layout-vertical{display:block}.ck.ck-link-form_layout-vertical .ck-button.ck-button-cancel,.ck.ck-link-form_layout-vertical .ck-button.ck-button-save{margin-top:var(--ck-spacing-medium)}.ck.ck-link-form_layout-vertical{min-width:var(--ck-input-width);padding:0}.ck.ck-link-form_layout-vertical .ck-labeled-field-view{margin:var(--ck-spacing-large) var(--ck-spacing-large) var(--ck-spacing-small)}.ck.ck-link-form_layout-vertical .ck-labeled-field-view .ck-input-text{min-width:0;width:100%}.ck.ck-link-form_layout-vertical>.ck-button{border-radius:0;margin:0;padding:var(--ck-spacing-standard);width:50%}.ck.ck-link-form_layout-vertical>.ck-button:not(:focus){border-top:1px solid var(--ck-color-base-border)}[dir=ltr] .ck.ck-link-form_layout-vertical>.ck-button,[dir=rtl] .ck.ck-link-form_layout-vertical>.ck-button{margin-left:0}[dir=rtl] .ck.ck-link-form_layout-vertical>.ck-button:last-of-type{border-right:1px solid var(--ck-color-base-border)}.ck.ck-link-form_layout-vertical .ck.ck-list{margin:var(--ck-spacing-standard) var(--ck-spacing-large)}.ck.ck-link-form_layout-vertical .ck.ck-list .ck-button.ck-switchbutton{padding:0;width:100%}.ck.ck-link-form_layout-vertical .ck.ck-list .ck-button.ck-switchbutton:hover{background:none}";
styleInject(css_248z$n);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The link form view controller class.
 *
 * See {@link module:link/ui/linkformview~LinkFormView}.
 *
 * @extends module:ui/view~View
 */
class LinkFormView extends View {
	/**
	 * Creates an instance of the {@link module:link/ui/linkformview~LinkFormView} class.
	 *
	 * Also see {@link #render}.
	 *
	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
	 * @param {module:link/linkcommand~LinkCommand} linkCommand Reference to {@link module:link/linkcommand~LinkCommand}.
	 * @param {String} [protocol] A value of a protocol to be displayed in the input's placeholder.
	 */
	constructor( locale, linkCommand ) {
		super( locale );

		const t = locale.t;

		/**
		 * Tracks information about DOM focus in the form.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
		 *
		 * @readonly
		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
		 */
		this.keystrokes = new KeystrokeHandler();

		/**
		 * The URL input view.
		 *
		 * @member {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
		 */
		this.urlInputView = this._createUrlInput();

		/**
		 * The Save button view.
		 *
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this.saveButtonView = this._createButton( t( 'Save' ), icons.check, 'ck-button-save' );
		this.saveButtonView.type = 'submit';

		/**
		 * The Cancel button view.
		 *
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this.cancelButtonView = this._createButton( t( 'Cancel' ), icons.cancel, 'ck-button-cancel', 'cancel' );

		/**
		 * A collection of {@link module:ui/button/switchbuttonview~SwitchButtonView},
		 * which corresponds to {@link module:link/linkcommand~LinkCommand#manualDecorators manual decorators}
		 * configured in the editor.
		 *
		 * @private
		 * @readonly
		 * @type {module:ui/viewcollection~ViewCollection}
		 */
		this._manualDecoratorSwitches = this._createManualDecoratorSwitches( linkCommand );

		/**
		 * A collection of child views in the form.
		 *
		 * @readonly
		 * @type {module:ui/viewcollection~ViewCollection}
		 */
		this.children = this._createFormChildren( linkCommand.manualDecorators );

		/**
		 * A collection of views that can be focused in the form.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this._focusables = new ViewCollection();

		/**
		 * Helps cycling over {@link #_focusables} in the form.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/focuscycler~FocusCycler}
		 */
		this._focusCycler = new FocusCycler( {
			focusables: this._focusables,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				// Navigate form fields backwards using the Shift + Tab keystroke.
				focusPrevious: 'shift + tab',

				// Navigate form fields forwards using the Tab key.
				focusNext: 'tab'
			}
		} );

		const classList = [ 'ck', 'ck-link-form', 'ck-responsive-form' ];

		if ( linkCommand.manualDecorators.length ) {
			classList.push( 'ck-link-form_layout-vertical', 'ck-vertical-form' );
		}

		this.setTemplate( {
			tag: 'form',

			attributes: {
				class: classList,

				// https://github.com/ckeditor/ckeditor5-link/issues/90
				tabindex: '-1'
			},

			children: this.children
		} );

		injectCssTransitionDisabler( this );
	}

	/**
	 * Obtains the state of the {@link module:ui/button/switchbuttonview~SwitchButtonView switch buttons} representing
	 * {@link module:link/linkcommand~LinkCommand#manualDecorators manual link decorators}
	 * in the {@link module:link/ui/linkformview~LinkFormView}.
	 *
	 * @returns {Object.<String,Boolean>} Key-value pairs, where the key is the name of the decorator and the value is
	 * its state.
	 */
	getDecoratorSwitchesState() {
		return Array.from( this._manualDecoratorSwitches ).reduce( ( accumulator, switchButton ) => {
			accumulator[ switchButton.name ] = switchButton.isOn;
			return accumulator;
		}, {} );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		submitHandler( {
			view: this
		} );

		const childViews = [
			this.urlInputView,
			...this._manualDecoratorSwitches,
			this.saveButtonView,
			this.cancelButtonView
		];

		childViews.forEach( v => {
			// Register the view as focusable.
			this._focusables.add( v );

			// Register the view in the focus tracker.
			this.focusTracker.add( v.element );
		} );

		// Start listening for the keystrokes coming from #element.
		this.keystrokes.listenTo( this.element );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}

	/**
	 * Focuses the fist {@link #_focusables} in the form.
	 */
	focus() {
		this._focusCycler.focusFirst();
	}

	/**
	 * Creates a labeled input view.
	 *
	 * @private
	 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView} Labeled field view instance.
	 */
	_createUrlInput() {
		const t = this.locale.t;
		const labeledInput = new LabeledFieldView( this.locale, createLabeledInputText );

		labeledInput.label = t( 'Link URL' );

		return labeledInput;
	}

	/**
	 * Creates a button view.
	 *
	 * @private
	 * @param {String} label The button label.
	 * @param {String} icon The button icon.
	 * @param {String} className The additional button CSS class name.
	 * @param {String} [eventName] An event name that the `ButtonView#execute` event will be delegated to.
	 * @returns {module:ui/button/buttonview~ButtonView} The button view instance.
	 */
	_createButton( label, icon, className, eventName ) {
		const button = new ButtonView( this.locale );

		button.set( {
			label,
			icon,
			tooltip: true
		} );

		button.extendTemplate( {
			attributes: {
				class: className
			}
		} );

		if ( eventName ) {
			button.delegate( 'execute' ).to( this, eventName );
		}

		return button;
	}

	/**
	 * Populates {@link module:ui/viewcollection~ViewCollection} of {@link module:ui/button/switchbuttonview~SwitchButtonView}
	 * made based on {@link module:link/linkcommand~LinkCommand#manualDecorators}.
	 *
	 * @private
	 * @param {module:link/linkcommand~LinkCommand} linkCommand A reference to the link command.
	 * @returns {module:ui/viewcollection~ViewCollection} of switch buttons.
	 */
	_createManualDecoratorSwitches( linkCommand ) {
		const switches = this.createCollection();

		for ( const manualDecorator of linkCommand.manualDecorators ) {
			const switchButton = new SwitchButtonView( this.locale );

			switchButton.set( {
				name: manualDecorator.id,
				label: manualDecorator.label,
				withText: true
			} );

			switchButton.bind( 'isOn' ).toMany( [ manualDecorator, linkCommand ], 'value', ( decoratorValue, commandValue ) => {
				return commandValue === undefined && decoratorValue === undefined ? manualDecorator.defaultValue : decoratorValue;
			} );

			switchButton.on( 'execute', () => {
				manualDecorator.set( 'value', !switchButton.isOn );
			} );

			switches.add( switchButton );
		}

		return switches;
	}

	/**
	 * Populates the {@link #children} collection of the form.
	 *
	 * If {@link module:link/linkcommand~LinkCommand#manualDecorators manual decorators} are configured in the editor, it creates an
	 * additional `View` wrapping all {@link #_manualDecoratorSwitches} switch buttons corresponding
	 * to these decorators.
	 *
	 * @private
	 * @param {module:utils/collection~Collection} manualDecorators A reference to
	 * the collection of manual decorators stored in the link command.
	 * @returns {module:ui/viewcollection~ViewCollection} The children of link form view.
	 */
	_createFormChildren( manualDecorators ) {
		const children = this.createCollection();

		children.add( this.urlInputView );

		if ( manualDecorators.length ) {
			const additionalButtonsView = new View();

			additionalButtonsView.setTemplate( {
				tag: 'ul',
				children: this._manualDecoratorSwitches.map( switchButton => ( {
					tag: 'li',
					children: [ switchButton ],
					attributes: {
						class: [
							'ck',
							'ck-list__item'
						]
					}
				} ) ),
				attributes: {
					class: [
						'ck',
						'ck-reset',
						'ck-list'
					]
				}
			} );
			children.add( additionalButtonsView );
		}

		children.add( this.saveButtonView );
		children.add( this.cancelButtonView );

		return children;
	}
}

/**
 * Fired when the form view is submitted (when one of the children triggered the submit event),
 * for example with a click on {@link #saveButtonView}.
 *
 * @event submit
 */

/**
 * Fired when the form view is canceled, for example with a click on {@link #cancelButtonView}.
 *
 * @event cancel
 */var css_248z$m = ".ck.ck-link-actions{display:flex;flex-direction:row;flex-wrap:nowrap}.ck.ck-link-actions .ck-link-actions__preview{display:inline-block}.ck.ck-link-actions .ck-link-actions__preview .ck-button__label{overflow:hidden}@media screen and (max-width:600px){.ck.ck-link-actions{flex-wrap:wrap}.ck.ck-link-actions .ck-link-actions__preview{flex-basis:100%}.ck.ck-link-actions .ck-button:not(.ck-link-actions__preview){flex-basis:50%}}.ck.ck-link-actions .ck-button.ck-link-actions__preview{padding-left:0;padding-right:0}.ck.ck-link-actions .ck-button.ck-link-actions__preview .ck-button__label{color:var(--ck-color-link-default);cursor:pointer;max-width:var(--ck-input-width);min-width:3em;padding:0 var(--ck-spacing-medium);text-align:center;text-overflow:ellipsis}.ck.ck-link-actions .ck-button.ck-link-actions__preview .ck-button__label:hover{text-decoration:underline}.ck.ck-link-actions .ck-button.ck-link-actions__preview,.ck.ck-link-actions .ck-button.ck-link-actions__preview:active,.ck.ck-link-actions .ck-button.ck-link-actions__preview:focus,.ck.ck-link-actions .ck-button.ck-link-actions__preview:hover{background:none}.ck.ck-link-actions .ck-button.ck-link-actions__preview:active{box-shadow:none}.ck.ck-link-actions .ck-button.ck-link-actions__preview:focus .ck-button__label{text-decoration:underline}[dir=ltr] .ck.ck-link-actions .ck-button:not(:first-child),[dir=rtl] .ck.ck-link-actions .ck-button:not(:last-child){margin-left:var(--ck-spacing-standard)}@media screen and (max-width:600px){.ck.ck-link-actions .ck-button.ck-link-actions__preview{margin:var(--ck-spacing-standard) var(--ck-spacing-standard) 0}.ck.ck-link-actions .ck-button.ck-link-actions__preview .ck-button__label{max-width:100%;min-width:0}[dir=ltr] .ck.ck-link-actions .ck-button:not(.ck-link-actions__preview),[dir=rtl] .ck.ck-link-actions .ck-button:not(.ck-link-actions__preview){margin-left:0}}";
styleInject(css_248z$m);var unlinkIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m11.077 15 .991-1.416a.75.75 0 1 1 1.229.86l-1.148 1.64a.748.748 0 0 1-.217.206 5.251 5.251 0 0 1-8.503-5.955.741.741 0 0 1 .12-.274l1.147-1.639a.75.75 0 1 1 1.228.86L4.933 10.7l.006.003a3.75 3.75 0 0 0 6.132 4.294l.006.004zm5.494-5.335a.748.748 0 0 1-.12.274l-1.147 1.639a.75.75 0 1 1-1.228-.86l.86-1.23a3.75 3.75 0 0 0-6.144-4.301l-.86 1.229a.75.75 0 0 1-1.229-.86l1.148-1.64a.748.748 0 0 1 .217-.206 5.251 5.251 0 0 1 8.503 5.955zm-4.563-2.532a.75.75 0 0 1 .184 1.045l-3.155 4.505a.75.75 0 1 1-1.229-.86l3.155-4.506a.75.75 0 0 1 1.045-.184zm4.919 10.562-1.414 1.414a.75.75 0 1 1-1.06-1.06l1.414-1.415-1.415-1.414a.75.75 0 0 1 1.061-1.06l1.414 1.414 1.414-1.415a.75.75 0 0 1 1.061 1.061l-1.414 1.414 1.414 1.415a.75.75 0 0 1-1.06 1.06l-1.415-1.414z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The link actions view class. This view displays the link preview, allows
 * unlinking or editing the link.
 *
 * @extends module:ui/view~View
 */
class LinkActionsView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		const t = locale.t;

		/**
		 * Tracks information about DOM focus in the actions.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
		 *
		 * @readonly
		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
		 */
		this.keystrokes = new KeystrokeHandler();

		/**
		 * The href preview view.
		 *
		 * @member {module:ui/view~View}
		 */
		this.previewButtonView = this._createPreviewButton();

		/**
		 * The unlink button view.
		 *
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this.unlinkButtonView = this._createButton( t( 'Unlink' ), unlinkIcon, 'unlink' );

		/**
		 * The edit link button view.
		 *
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this.editButtonView = this._createButton( t( 'Edit link' ), icons.pencil, 'edit' );

		/**
		 * The value of the "href" attribute of the link to use in the {@link #previewButtonView}.
		 *
		 * @observable
		 * @member {String}
		 */
		this.set( 'href' );

		/**
		 * A collection of views that can be focused in the view.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this._focusables = new ViewCollection();

		/**
		 * Helps cycling over {@link #_focusables} in the view.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/focuscycler~FocusCycler}
		 */
		this._focusCycler = new FocusCycler( {
			focusables: this._focusables,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				// Navigate fields backwards using the Shift + Tab keystroke.
				focusPrevious: 'shift + tab',

				// Navigate fields forwards using the Tab key.
				focusNext: 'tab'
			}
		} );

		this.setTemplate( {
			tag: 'div',

			attributes: {
				class: [
					'ck',
					'ck-link-actions',
					'ck-responsive-form'
				],

				// https://github.com/ckeditor/ckeditor5-link/issues/90
				tabindex: '-1'
			},

			children: [
				this.previewButtonView,
				this.editButtonView,
				this.unlinkButtonView
			]
		} );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		const childViews = [
			this.previewButtonView,
			this.editButtonView,
			this.unlinkButtonView
		];

		childViews.forEach( v => {
			// Register the view as focusable.
			this._focusables.add( v );

			// Register the view in the focus tracker.
			this.focusTracker.add( v.element );
		} );

		// Start listening for the keystrokes coming from #element.
		this.keystrokes.listenTo( this.element );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}

	/**
	 * Focuses the fist {@link #_focusables} in the actions.
	 */
	focus() {
		this._focusCycler.focusFirst();
	}

	/**
	 * Creates a button view.
	 *
	 * @private
	 * @param {String} label The button label.
	 * @param {String} icon The button icon.
	 * @param {String} [eventName] An event name that the `ButtonView#execute` event will be delegated to.
	 * @returns {module:ui/button/buttonview~ButtonView} The button view instance.
	 */
	_createButton( label, icon, eventName ) {
		const button = new ButtonView( this.locale );

		button.set( {
			label,
			icon,
			tooltip: true
		} );

		button.delegate( 'execute' ).to( this, eventName );

		return button;
	}

	/**
	 * Creates a link href preview button.
	 *
	 * @private
	 * @returns {module:ui/button/buttonview~ButtonView} The button view instance.
	 */
	_createPreviewButton() {
		const button = new ButtonView( this.locale );
		const bind = this.bindTemplate;
		const t = this.t;

		button.set( {
			withText: true,
			tooltip: t( 'Open link in new tab' )
		} );

		button.extendTemplate( {
			attributes: {
				class: [
					'ck',
					'ck-link-actions__preview'
				],
				href: bind.to( 'href', href => href && ensureSafeUrl( href ) ),
				target: '_blank',
				rel: 'noopener noreferrer'
			}
		} );

		button.bind( 'label' ).to( this, 'href', href => {
			return href || t( 'This link has no URL' );
		} );

		button.bind( 'isEnabled' ).to( this, 'href', href => !!href );

		button.template.tag = 'a';
		button.template.eventListeners = {};

		return button;
	}
}

/**
 * Fired when the {@link #editButtonView} is clicked.
 *
 * @event edit
 */

/**
 * Fired when the {@link #unlinkButtonView} is clicked.
 *
 * @event unlink
 */var linkIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m11.077 15 .991-1.416a.75.75 0 1 1 1.229.86l-1.148 1.64a.748.748 0 0 1-.217.206 5.251 5.251 0 0 1-8.503-5.955.741.741 0 0 1 .12-.274l1.147-1.639a.75.75 0 1 1 1.228.86L4.933 10.7l.006.003a3.75 3.75 0 0 0 6.132 4.294l.006.004zm5.494-5.335a.748.748 0 0 1-.12.274l-1.147 1.639a.75.75 0 1 1-1.228-.86l.86-1.23a3.75 3.75 0 0 0-6.144-4.301l-.86 1.229a.75.75 0 0 1-1.229-.86l1.148-1.64a.748.748 0 0 1 .217-.206 5.251 5.251 0 0 1 8.503 5.955zm-4.563-2.532a.75.75 0 0 1 .184 1.045l-3.155 4.505a.75.75 0 1 1-1.229-.86l3.155-4.506a.75.75 0 0 1 1.045-.184z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const VISUAL_SELECTION_MARKER_NAME = 'link-ui';

/**
 * The link UI plugin. It introduces the `'link'` and `'unlink'` buttons and support for the <kbd>Ctrl+K</kbd> keystroke.
 *
 * It uses the
 * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
 *
 * @extends module:core/plugin~Plugin
 */
class LinkUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ ContextualBalloon ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'LinkUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		editor.editing.view.addObserver( ClickObserver );

		/**
		 * The actions view displayed inside of the balloon.
		 *
		 * @member {module:link/ui/linkactionsview~LinkActionsView}
		 */
		this.actionsView = this._createActionsView();

		/**
		 * The form view displayed inside the balloon.
		 *
		 * @member {module:link/ui/linkformview~LinkFormView}
		 */
		this.formView = this._createFormView();

		/**
		 * The contextual balloon plugin instance.
		 *
		 * @private
		 * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
		 */
		this._balloon = editor.plugins.get( ContextualBalloon );

		// Create toolbar buttons.
		this._createToolbarLinkButton();

		// Attach lifecycle actions to the the balloon.
		this._enableUserBalloonInteractions();

		// Renders a fake visual selection marker on an expanded selection.
		editor.conversion.for( 'editingDowncast' ).markerToHighlight( {
			model: VISUAL_SELECTION_MARKER_NAME,
			view: {
				classes: [ 'ck-fake-link-selection' ]
			}
		} );

		// Renders a fake visual selection marker on a collapsed selection.
		editor.conversion.for( 'editingDowncast' ).markerToElement( {
			model: VISUAL_SELECTION_MARKER_NAME,
			view: {
				name: 'span',
				classes: [ 'ck-fake-link-selection', 'ck-fake-link-selection_collapsed' ]
			}
		} );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		// Destroy created UI components as they are not automatically destroyed (see ckeditor5#1341).
		this.formView.destroy();
	}

	/**
	 * Creates the {@link module:link/ui/linkactionsview~LinkActionsView} instance.
	 *
	 * @private
	 * @returns {module:link/ui/linkactionsview~LinkActionsView} The link actions view instance.
	 */
	_createActionsView() {
		const editor = this.editor;
		const actionsView = new LinkActionsView( editor.locale );
		const linkCommand = editor.commands.get( 'link' );
		const unlinkCommand = editor.commands.get( 'unlink' );

		actionsView.bind( 'href' ).to( linkCommand, 'value' );
		actionsView.editButtonView.bind( 'isEnabled' ).to( linkCommand );
		actionsView.unlinkButtonView.bind( 'isEnabled' ).to( unlinkCommand );

		// Execute unlink command after clicking on the "Edit" button.
		this.listenTo( actionsView, 'edit', () => {
			this._addFormView();
		} );

		// Execute unlink command after clicking on the "Unlink" button.
		this.listenTo( actionsView, 'unlink', () => {
			editor.execute( 'unlink' );
			this._hideUI();
		} );

		// Close the panel on esc key press when the **actions have focus**.
		actionsView.keystrokes.set( 'Esc', ( data, cancel ) => {
			this._hideUI();
			cancel();
		} );

		// Open the form view on Ctrl+K when the **actions have focus**..
		actionsView.keystrokes.set( LINK_KEYSTROKE, ( data, cancel ) => {
			this._addFormView();
			cancel();
		} );

		return actionsView;
	}

	/**
	 * Creates the {@link module:link/ui/linkformview~LinkFormView} instance.
	 *
	 * @private
	 * @returns {module:link/ui/linkformview~LinkFormView} The link form view instance.
	 */
	_createFormView() {
		const editor = this.editor;
		const linkCommand = editor.commands.get( 'link' );
		const defaultProtocol = editor.config.get( 'link.defaultProtocol' );

		const formView = new LinkFormView( editor.locale, linkCommand );

		formView.urlInputView.fieldView.bind( 'value' ).to( linkCommand, 'value' );

		// Form elements should be read-only when corresponding commands are disabled.
		formView.urlInputView.bind( 'isReadOnly' ).to( linkCommand, 'isEnabled', value => !value );
		formView.saveButtonView.bind( 'isEnabled' ).to( linkCommand );

		// Execute link command after clicking the "Save" button.
		this.listenTo( formView, 'submit', () => {
			const { value } = formView.urlInputView.fieldView.element;
			const parsedUrl = addLinkProtocolIfApplicable( value, defaultProtocol );
			editor.execute( 'link', parsedUrl, formView.getDecoratorSwitchesState() );
			this._closeFormView();
		} );

		// Hide the panel after clicking the "Cancel" button.
		this.listenTo( formView, 'cancel', () => {
			this._closeFormView();
		} );

		// Close the panel on esc key press when the **form has focus**.
		formView.keystrokes.set( 'Esc', ( data, cancel ) => {
			this._closeFormView();
			cancel();
		} );

		return formView;
	}

	/**
	 * Creates a toolbar Link button. Clicking this button will show
	 * a {@link #_balloon} attached to the selection.
	 *
	 * @private
	 */
	_createToolbarLinkButton() {
		const editor = this.editor;
		const linkCommand = editor.commands.get( 'link' );
		const t = editor.t;

		// Handle the `Ctrl+K` keystroke and show the panel.
		editor.keystrokes.set( LINK_KEYSTROKE, ( keyEvtData, cancel ) => {
			// Prevent focusing the search bar in FF, Chrome and Edge. See https://github.com/ckeditor/ckeditor5/issues/4811.
			cancel();

			if ( linkCommand.isEnabled ) {
				this._showUI( true );
			}
		} );

		editor.ui.componentFactory.add( 'link', locale => {
			const button = new ButtonView( locale );

			button.isEnabled = true;
			button.label = t( 'Link' );
			button.icon = linkIcon;
			button.keystroke = LINK_KEYSTROKE;
			button.tooltip = true;
			button.isToggleable = true;

			// Bind button to the command.
			button.bind( 'isEnabled' ).to( linkCommand, 'isEnabled' );
			button.bind( 'isOn' ).to( linkCommand, 'value', value => !!value );

			// Show the panel on button click.
			this.listenTo( button, 'execute', () => this._showUI( true ) );

			return button;
		} );
	}

	/**
	 * Attaches actions that control whether the balloon panel containing the
	 * {@link #formView} is visible or not.
	 *
	 * @private
	 */
	_enableUserBalloonInteractions() {
		const viewDocument = this.editor.editing.view.document;

		// Handle click on view document and show panel when selection is placed inside the link element.
		// Keep panel open until selection will be inside the same link element.
		this.listenTo( viewDocument, 'click', () => {
			const parentLink = this._getSelectedLinkElement();

			if ( parentLink ) {
				// Then show panel but keep focus inside editor editable.
				this._showUI();
			}
		} );

		// Focus the form if the balloon is visible and the Tab key has been pressed.
		this.editor.keystrokes.set( 'Tab', ( data, cancel ) => {
			if ( this._areActionsVisible && !this.actionsView.focusTracker.isFocused ) {
				this.actionsView.focus();
				cancel();
			}
		}, {
			// Use the high priority because the link UI navigation is more important
			// than other feature's actions, e.g. list indentation.
			// https://github.com/ckeditor/ckeditor5-link/issues/146
			priority: 'high'
		} );

		// Close the panel on the Esc key press when the editable has focus and the balloon is visible.
		this.editor.keystrokes.set( 'Esc', ( data, cancel ) => {
			if ( this._isUIVisible ) {
				this._hideUI();
				cancel();
			}
		} );

		// Close on click outside of balloon panel element.
		clickOutsideHandler( {
			emitter: this.formView,
			activator: () => this._isUIInPanel,
			contextElements: [ this._balloon.view.element ],
			callback: () => this._hideUI()
		} );
	}

	/**
	 * Adds the {@link #actionsView} to the {@link #_balloon}.
	 *
	 * @protected
	 */
	_addActionsView() {
		if ( this._areActionsInPanel ) {
			return;
		}

		this._balloon.add( {
			view: this.actionsView,
			position: this._getBalloonPositionData()
		} );
	}

	/**
	 * Adds the {@link #formView} to the {@link #_balloon}.
	 *
	 * @protected
	 */
	_addFormView() {
		if ( this._isFormInPanel ) {
			return;
		}

		const editor = this.editor;
		const linkCommand = editor.commands.get( 'link' );

		this.formView.disableCssTransitions();

		this._balloon.add( {
			view: this.formView,
			position: this._getBalloonPositionData()
		} );

		// Select input when form view is currently visible.
		if ( this._balloon.visibleView === this.formView ) {
			this.formView.urlInputView.fieldView.select();
		}

		this.formView.enableCssTransitions();

		// Make sure that each time the panel shows up, the URL field remains in sync with the value of
		// the command. If the user typed in the input, then canceled the balloon (`urlInputView.fieldView#value` stays
		// unaltered) and re-opened it without changing the value of the link command (e.g. because they
		// clicked the same link), they would see the old value instead of the actual value of the command.
		// https://github.com/ckeditor/ckeditor5-link/issues/78
		// https://github.com/ckeditor/ckeditor5-link/issues/123
		this.formView.urlInputView.fieldView.element.value = linkCommand.value || '';
	}

	/**
	 * Closes the form view. Decides whether the balloon should be hidden completely or if the action view should be shown. This is
	 * decided upon the link command value (which has a value if the document selection is in the link).
	 *
	 * Additionally, if any {@link module:link/link~LinkConfig#decorators} are defined in the editor configuration, the state of
	 * switch buttons responsible for manual decorator handling is restored.
	 *
	 * @private
	 */
	_closeFormView() {
		const linkCommand = this.editor.commands.get( 'link' );

		// Restore manual decorator states to represent the current model state. This case is important to reset the switch buttons
		// when the user cancels the editing form.
		linkCommand.restoreManualDecoratorStates();

		if ( linkCommand.value !== undefined ) {
			this._removeFormView();
		} else {
			this._hideUI();
		}
	}

	/**
	 * Removes the {@link #formView} from the {@link #_balloon}.
	 *
	 * @protected
	 */
	_removeFormView() {
		if ( this._isFormInPanel ) {
			// Blur the input element before removing it from DOM to prevent issues in some browsers.
			// See https://github.com/ckeditor/ckeditor5/issues/1501.
			this.formView.saveButtonView.focus();

			this._balloon.remove( this.formView );

			// Because the form has an input which has focus, the focus must be brought back
			// to the editor. Otherwise, it would be lost.
			this.editor.editing.view.focus();

			this._hideFakeVisualSelection();
		}
	}

	/**
	 * Shows the correct UI type. It is either {@link #formView} or {@link #actionsView}.
	 *
	 * @param {Boolean} forceVisible
	 * @private
	 */
	_showUI( forceVisible = false ) {
		// When there's no link under the selection, go straight to the editing UI.
		if ( !this._getSelectedLinkElement() ) {
			// Show visual selection on a text without a link when the contextual balloon is displayed.
			// See https://github.com/ckeditor/ckeditor5/issues/4721.
			this._showFakeVisualSelection();

			this._addActionsView();

			// Be sure panel with link is visible.
			if ( forceVisible ) {
				this._balloon.showStack( 'main' );
			}

			this._addFormView();
		}
		// If there's a link under the selection...
		else {
			// Go to the editing UI if actions are already visible.
			if ( this._areActionsVisible ) {
				this._addFormView();
			}
			// Otherwise display just the actions UI.
			else {
				this._addActionsView();
			}

			// Be sure panel with link is visible.
			if ( forceVisible ) {
				this._balloon.showStack( 'main' );
			}
		}

		// Begin responding to ui#update once the UI is added.
		this._startUpdatingUI();
	}

	/**
	 * Removes the {@link #formView} from the {@link #_balloon}.
	 *
	 * See {@link #_addFormView}, {@link #_addActionsView}.
	 *
	 * @protected
	 */
	_hideUI() {
		if ( !this._isUIInPanel ) {
			return;
		}

		const editor = this.editor;

		this.stopListening( editor.ui, 'update' );
		this.stopListening( this._balloon, 'change:visibleView' );

		// Make sure the focus always gets back to the editable _before_ removing the focused form view.
		// Doing otherwise causes issues in some browsers. See https://github.com/ckeditor/ckeditor5-link/issues/193.
		editor.editing.view.focus();

		// Remove form first because it's on top of the stack.
		this._removeFormView();

		// Then remove the actions view because it's beneath the form.
		this._balloon.remove( this.actionsView );

		this._hideFakeVisualSelection();
	}

	/**
	 * Makes the UI react to the {@link module:core/editor/editorui~EditorUI#event:update} event to
	 * reposition itself when the editor UI should be refreshed.
	 *
	 * See: {@link #_hideUI} to learn when the UI stops reacting to the `update` event.
	 *
	 * @protected
	 */
	_startUpdatingUI() {
		const editor = this.editor;
		const viewDocument = editor.editing.view.document;

		let prevSelectedLink = this._getSelectedLinkElement();
		let prevSelectionParent = getSelectionParent();

		const update = () => {
			const selectedLink = this._getSelectedLinkElement();
			const selectionParent = getSelectionParent();

			// Hide the panel if:
			//
			// * the selection went out of the EXISTING link element. E.g. user moved the caret out
			//   of the link,
			// * the selection went to a different parent when creating a NEW link. E.g. someone
			//   else modified the document.
			// * the selection has expanded (e.g. displaying link actions then pressing SHIFT+Right arrow).
			//
			// Note: #_getSelectedLinkElement will return a link for a non-collapsed selection only
			// when fully selected.
			if ( ( prevSelectedLink && !selectedLink ) ||
				( !prevSelectedLink && selectionParent !== prevSelectionParent ) ) {
				this._hideUI();
			}
			// Update the position of the panel when:
			//  * link panel is in the visible stack
			//  * the selection remains in the original link element,
			//  * there was no link element in the first place, i.e. creating a new link
			else if ( this._isUIVisible ) {
				// If still in a link element, simply update the position of the balloon.
				// If there was no link (e.g. inserting one), the balloon must be moved
				// to the new position in the editing view (a new native DOM range).
				this._balloon.updatePosition( this._getBalloonPositionData() );
			}

			prevSelectedLink = selectedLink;
			prevSelectionParent = selectionParent;
		};

		function getSelectionParent() {
			return viewDocument.selection.focus.getAncestors()
				.reverse()
				.find( node => node.is( 'element' ) );
		}

		this.listenTo( editor.ui, 'update', update );
		this.listenTo( this._balloon, 'change:visibleView', update );
	}

	/**
	 * Returns `true` when {@link #formView} is in the {@link #_balloon}.
	 *
	 * @readonly
	 * @protected
	 * @type {Boolean}
	 */
	get _isFormInPanel() {
		return this._balloon.hasView( this.formView );
	}

	/**
	 * Returns `true` when {@link #actionsView} is in the {@link #_balloon}.
	 *
	 * @readonly
	 * @protected
	 * @type {Boolean}
	 */
	get _areActionsInPanel() {
		return this._balloon.hasView( this.actionsView );
	}

	/**
	 * Returns `true` when {@link #actionsView} is in the {@link #_balloon} and it is
	 * currently visible.
	 *
	 * @readonly
	 * @protected
	 * @type {Boolean}
	 */
	get _areActionsVisible() {
		return this._balloon.visibleView === this.actionsView;
	}

	/**
	 * Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon}.
	 *
	 * @readonly
	 * @protected
	 * @type {Boolean}
	 */
	get _isUIInPanel() {
		return this._isFormInPanel || this._areActionsInPanel;
	}

	/**
	 * Returns `true` when {@link #actionsView} or {@link #formView} is in the {@link #_balloon} and it is
	 * currently visible.
	 *
	 * @readonly
	 * @protected
	 * @type {Boolean}
	 */
	get _isUIVisible() {
		const visibleView = this._balloon.visibleView;

		return visibleView == this.formView || this._areActionsVisible;
	}

	/**
	 * Returns positioning options for the {@link #_balloon}. They control the way the balloon is attached
	 * to the target element or selection.
	 *
	 * If the selection is collapsed and inside a link element, the panel will be attached to the
	 * entire link element. Otherwise, it will be attached to the selection.
	 *
	 * @private
	 * @returns {module:utils/dom/position~Options}
	 */
	_getBalloonPositionData() {
		const view = this.editor.editing.view;
		const model = this.editor.model;
		const viewDocument = view.document;
		let target = null;

		if ( model.markers.has( VISUAL_SELECTION_MARKER_NAME ) ) {
			// There are cases when we highlight selection using a marker (#7705, #4721).
			const markerViewElements = Array.from( this.editor.editing.mapper.markerNameToElements( VISUAL_SELECTION_MARKER_NAME ) );
			const newRange = view.createRange(
				view.createPositionBefore( markerViewElements[ 0 ] ),
				view.createPositionAfter( markerViewElements[ markerViewElements.length - 1 ] )
			);

			target = view.domConverter.viewRangeToDom( newRange );
		} else {
			// Make sure the target is calculated on demand at the last moment because a cached DOM range
			// (which is very fragile) can desynchronize with the state of the editing view if there was
			// any rendering done in the meantime. This can happen, for instance, when an inline widget
			// gets unlinked.
			target = () => {
				const targetLink = this._getSelectedLinkElement();

				return targetLink ?
					// When selection is inside link element, then attach panel to this element.
					view.domConverter.mapViewToDom( targetLink ) :
					// Otherwise attach panel to the selection.
					view.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() );
			};
		}

		return { target };
	}

	/**
	 * Returns the link {@link module:engine/view/attributeelement~AttributeElement} under
	 * the {@link module:engine/view/document~Document editing view's} selection or `null`
	 * if there is none.
	 *
	 * **Note**: For a non–collapsed selection, the link element is returned when **fully**
	 * selected and the **only** element within the selection boundaries, or when
	 * a linked widget is selected.
	 *
	 * @private
	 * @returns {module:engine/view/attributeelement~AttributeElement|null}
	 */
	_getSelectedLinkElement() {
		const view = this.editor.editing.view;
		const selection = view.document.selection;
		const selectedElement = selection.getSelectedElement();

		// The selection is collapsed or some widget is selected (especially inline widget).
		if ( selection.isCollapsed || selectedElement && isWidget( selectedElement ) ) {
			return findLinkElementAncestor( selection.getFirstPosition() );
		} else {
			// The range for fully selected link is usually anchored in adjacent text nodes.
			// Trim it to get closer to the actual link element.
			const range = selection.getFirstRange().getTrimmed();
			const startLink = findLinkElementAncestor( range.start );
			const endLink = findLinkElementAncestor( range.end );

			if ( !startLink || startLink != endLink ) {
				return null;
			}

			// Check if the link element is fully selected.
			if ( view.createRangeIn( startLink ).getTrimmed().isEqual( range ) ) {
				return startLink;
			} else {
				return null;
			}
		}
	}

	/**
	 * Displays a fake visual selection when the contextual balloon is displayed.
	 *
	 * This adds a 'link-ui' marker into the document that is rendered as a highlight on selected text fragment.
	 *
	 * @private
	 */
	_showFakeVisualSelection() {
		const model = this.editor.model;

		model.change( writer => {
			const range = model.document.selection.getFirstRange();

			if ( model.markers.has( VISUAL_SELECTION_MARKER_NAME ) ) {
				writer.updateMarker( VISUAL_SELECTION_MARKER_NAME, { range } );
			} else {
				if ( range.start.isAtEnd ) {
					const startPosition = range.start.getLastMatchingPosition(
						( { item } ) => !model.schema.isContent( item ),
						{ boundaries: range }
					);

					writer.addMarker( VISUAL_SELECTION_MARKER_NAME, {
						usingOperation: false,
						affectsData: false,
						range: writer.createRange( startPosition, range.end )
					} );
				} else {
					writer.addMarker( VISUAL_SELECTION_MARKER_NAME, {
						usingOperation: false,
						affectsData: false,
						range
					} );
				}
			}
		} );
	}

	/**
	 * Hides the fake visual selection created in {@link #_showFakeVisualSelection}.
	 *
	 * @private
	 */
	_hideFakeVisualSelection() {
		const model = this.editor.model;

		if ( model.markers.has( VISUAL_SELECTION_MARKER_NAME ) ) {
			model.change( writer => {
				writer.removeMarker( VISUAL_SELECTION_MARKER_NAME );
			} );
		}
	}
}

// Returns a link element if there's one among the ancestors of the provided `Position`.
//
// @private
// @param {module:engine/view/position~Position} View position to analyze.
// @returns {module:engine/view/attributeelement~AttributeElement|null} Link element at the position or null.
function findLinkElementAncestor( position ) {
	return position.getAncestors().find( ancestor => isLinkElement( ancestor ) );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const MIN_LINK_LENGTH_WITH_SPACE_AT_END = 4; // Ie: "t.co " (length 5).

// This was a tweak from https://gist.github.com/dperini/729294.
const URL_REG_EXP = new RegExp(
	// Group 1: Line start or after a space.
	'(^|\\s)' +
	// Group 2: Detected URL (or e-mail).
	'(' +
		// Protocol identifier or short syntax "//"
		// a. Full form http://user@foo.bar.baz:8080/foo/bar.html#baz?foo=bar
		'(' +
			'(?:(?:(?:https?|ftp):)?\\/\\/)' +
			// BasicAuth using user:pass (optional)
			'(?:\\S+(?::\\S*)?@)?' +
			'(?:' +
				// IP address dotted notation octets
				// excludes loopback network 0.0.0.0
				// excludes reserved space >= 224.0.0.0
				// excludes network & broadcast addresses
				// (first & last IP address of each class)
				'(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' +
				'(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' +
				'(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' +
				'|' +
				'(' +
					// Do not allow `www.foo` - see https://github.com/ckeditor/ckeditor5/issues/8050.
					'((?!www\\.)|(www\\.))' +
					// Host & domain names.
					'(?![-_])(?:[-_a-z0-9\\u00a1-\\uffff]{1,63}\\.)+' +
					// TLD identifier name.
					'(?:[a-z\\u00a1-\\uffff]{2,63})' +
				')' +
			')' +
			// port number (optional)
			'(?::\\d{2,5})?' +
			// resource path (optional)
			'(?:[/?#]\\S*)?' +
		')' +
		'|' +
		// b. Short form (either www.example.com or example@example.com)
		'(' +
			'(www.|(\\S+@))' +
			// Host & domain names.
			'((?![-_])(?:[-_a-z0-9\\u00a1-\\uffff]{1,63}\\.))+' +
			// TLD identifier name.
			'(?:[a-z\\u00a1-\\uffff]{2,63})' +
		')' +
	')$', 'i' );

const URL_GROUP_IN_MATCH = 2;

/**
 * The autolink plugin.
 *
 * @extends module:core/plugin~Plugin
 */
class AutoLink extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ Delete ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'AutoLink';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const selection = editor.model.document.selection;

		selection.on( 'change:range', () => {
			// Disable plugin when selection is inside a code block.
			this.isEnabled = !selection.anchor.parent.is( 'element', 'codeBlock' );
		} );

		this._enableTypingHandling();
	}

	/**
	 * @inheritDoc
	 */
	afterInit() {
		this._enableEnterHandling();
		this._enableShiftEnterHandling();
	}

	/**
	 * Enables autolinking on typing.
	 *
	 * @private
	 */
	_enableTypingHandling() {
		const editor = this.editor;

		const watcher = new TextWatcher( editor.model, text => {
			// 1. Detect <kbd>Space</kbd> after a text with a potential link.
			if ( !isSingleSpaceAtTheEnd( text ) ) {
				return;
			}

			// 2. Check text before last typed <kbd>Space</kbd>.
			const url = getUrlAtTextEnd( text.substr( 0, text.length - 1 ) );

			if ( url ) {
				return { url };
			}
		} );

		watcher.on( 'matched:data', ( evt, data ) => {
			const { batch, range, url } = data;

			if ( !batch.isTyping ) {
				return;
			}

			const linkEnd = range.end.getShiftedBy( -1 ); // Executed after a space character.
			const linkStart = linkEnd.getShiftedBy( -url.length );

			const linkRange = editor.model.createRange( linkStart, linkEnd );

			this._applyAutoLink( url, linkRange );
		} );

		watcher.bind( 'isEnabled' ).to( this );
	}

	/**
	 * Enables autolinking on the <kbd>Enter</kbd> key.
	 *
	 * @private
	 */
	_enableEnterHandling() {
		const editor = this.editor;
		const model = editor.model;
		const enterCommand = editor.commands.get( 'enter' );

		if ( !enterCommand ) {
			return;
		}

		enterCommand.on( 'execute', () => {
			const position = model.document.selection.getFirstPosition();

			if ( !position.parent.previousSibling ) {
				return;
			}

			const rangeToCheck = model.createRangeIn( position.parent.previousSibling );

			this._checkAndApplyAutoLinkOnRange( rangeToCheck );
		} );
	}

	/**
	 * Enables autolinking on the <kbd>Shift</kbd>+<kbd>Enter</kbd> keyboard shortcut.
	 *
	 * @private
	 */
	_enableShiftEnterHandling() {
		const editor = this.editor;
		const model = editor.model;

		const shiftEnterCommand = editor.commands.get( 'shiftEnter' );

		if ( !shiftEnterCommand ) {
			return;
		}

		shiftEnterCommand.on( 'execute', () => {
			const position = model.document.selection.getFirstPosition();

			const rangeToCheck = model.createRange(
				model.createPositionAt( position.parent, 0 ),
				position.getShiftedBy( -1 )
			);

			this._checkAndApplyAutoLinkOnRange( rangeToCheck );
		} );
	}

	/**
	 * Checks if the passed range contains a linkable text.
	 *
	 * @param {module:engine/model/range~Range} rangeToCheck
	 * @private
	 */
	_checkAndApplyAutoLinkOnRange( rangeToCheck ) {
		const model = this.editor.model;
		const { text, range } = getLastTextLine( rangeToCheck, model );

		const url = getUrlAtTextEnd( text );

		if ( url ) {
			const linkRange = model.createRange(
				range.end.getShiftedBy( -url.length ),
				range.end
			);

			this._applyAutoLink( url, linkRange );
		}
	}

	/**
	 * Applies a link on a given range.
	 *
	 * @param {String} url The URL to link.
	 * @param {module:engine/model/range~Range} range The text range to apply the link attribute to.
	 * @private
	 */
	_applyAutoLink( link, range ) {
		const model = this.editor.model;
		const deletePlugin = this.editor.plugins.get( 'Delete' );

		if ( !this.isEnabled || !isLinkAllowedOnRange( range, model ) ) {
			return;
		}

		// Enqueue change to make undo step.
		model.enqueueChange( writer => {
			const defaultProtocol = this.editor.config.get( 'link.defaultProtocol' );
			const parsedUrl = addLinkProtocolIfApplicable( link, defaultProtocol );
			writer.setAttribute( 'linkHref', parsedUrl, range );

			model.enqueueChange( () => {
				deletePlugin.requestUndoOnBackspace();
			} );
		} );
	}
}

// Check if text should be evaluated by the plugin in order to reduce number of RegExp checks on whole text.
function isSingleSpaceAtTheEnd( text ) {
	return text.length > MIN_LINK_LENGTH_WITH_SPACE_AT_END && text[ text.length - 1 ] === ' ' && text[ text.length - 2 ] !== ' ';
}

function getUrlAtTextEnd( text ) {
	const match = URL_REG_EXP.exec( text );

	return match ? match[ URL_GROUP_IN_MATCH ] : null;
}

function isLinkAllowedOnRange( range, model ) {
	return model.schema.checkAttributeInSelection( model.createSelection( range ), 'linkHref' );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The link plugin.
 *
 * This is a "glue" plugin that loads the {@link module:link/linkediting~LinkEditing link editing feature}
 * and {@link module:link/linkui~LinkUI link UI feature}.
 *
 * @extends module:core/plugin~Plugin
 */
class Link extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ LinkEditing, LinkUI, AutoLink ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Link';
	}
}

/**
 * The configuration of the {@link module:link/link~Link} feature.
 *
 * Read more in {@link module:link/link~LinkConfig}.
 *
 * @member {module:link/link~LinkConfig} module:core/editor/editorconfig~EditorConfig#link
 */

/**
 * The configuration of the {@link module:link/link~Link link feature}.
 *
 *		ClassicEditor
 *			.create( editorElement, {
 * 				link:  ... // Link feature configuration.
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
 * @interface LinkConfig
 */

/**
 * When set, the editor will add the given protocol to the link when the user creates a link without one.
 * For example, when the user is creating a link and types `ckeditor.com` in the link form input, during link submission
 * the editor will automatically add the `http://` protocol, so the link will look as follows: `http://ckeditor.com`.
 *
 * The feature also provides email address auto-detection. When you submit `hello@example.com`,
 * the plugin will automatically change it to `mailto:hello@example.com`.
 *
 * 		ClassicEditor
 *			.create( editorElement, {
 * 				link: {
 * 					defaultProtocol: 'http://'
 * 				}
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * **NOTE:** If no configuration is provided, the editor will not auto-fix the links.
 *
 * @member {String} module:link/link~LinkConfig#defaultProtocol
 */

/**
 * When set to `true`, the `target="blank"` and `rel="noopener noreferrer"` attributes are automatically added to all external links
 * in the editor. "External links" are all links in the editor content starting with `http`, `https`, or `//`.
 *
 *		ClassicEditor
 *			.create( editorElement, {
 *				link: {
 *					addTargetToExternalLinks: true
 *				}
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * Internally, this option activates a predefined {@link module:link/link~LinkConfig#decorators automatic link decorator}
 * that extends all external links with the `target` and `rel` attributes.
 *
 * **Note**: To control the `target` and `rel` attributes of specific links in the edited content, a dedicated
 * {@link module:link/link~LinkDecoratorManualDefinition manual} decorator must be defined in the
 * {@link module:link/link~LinkConfig#decorators `config.link.decorators`} array. In such scenario,
 * the `config.link.addTargetToExternalLinks` option should remain `undefined` or `false` to not interfere with the manual decorator.
 *
 * It is possible to add other {@link module:link/link~LinkDecoratorAutomaticDefinition automatic}
 * or {@link module:link/link~LinkDecoratorManualDefinition manual} link decorators when this option is active.
 *
 * More information about decorators can be found in the {@link module:link/link~LinkConfig#decorators decorators configuration}
 * reference.
 *
 * @default false
 * @member {Boolean} module:link/link~LinkConfig#addTargetToExternalLinks
 */

/**
 * Decorators provide an easy way to configure and manage additional link attributes in the editor content. There are
 * two types of link decorators:
 *
 * * {@link module:link/link~LinkDecoratorAutomaticDefinition Automatic} &ndash; They match links against pre–defined rules and
 * manage their attributes based on the results.
 * * {@link module:link/link~LinkDecoratorManualDefinition Manual} &ndash; They allow users to control link attributes individually,
 *  using the editor UI.
 *
 * Link decorators are defined as objects with key-value pairs, where the key is the name provided for a given decorator and the
 * value is the decorator definition.
 *
 * The name of the decorator also corresponds to the {@glink framework/guides/architecture/editing-engine#text-attributes text attribute}
 * in the model. For instance, the `isExternal` decorator below is represented as a `linkIsExternal` attribute in the model.
 *
 *		ClassicEditor
 *			.create( editorElement, {
 *				link: {
 *					decorators: {
 *						isExternal: {
 *							mode: 'automatic',
 *							callback: url => url.startsWith( 'http://' ),
 *							attributes: {
 *								target: '_blank',
 *								rel: 'noopener noreferrer'
 *							}
 *						},
 *						isDownloadable: {
 *							mode: 'manual',
 *							label: 'Downloadable',
 *							attributes: {
 *								download: 'file.png',
 *							}
 *						},
 *						// ...
 *					}
 *				}
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * To learn more about the configuration syntax, check out the {@link module:link/link~LinkDecoratorAutomaticDefinition automatic}
 * and {@link module:link/link~LinkDecoratorManualDefinition manual} decorator option reference.
 *
 * **Warning:** Currently, link decorators work independently of one another and no conflict resolution mechanism exists.
 * For example, configuring the `target` attribute using both an automatic and a manual decorator at the same time could end up with
 * quirky results. The same applies if multiple manual or automatic decorators were defined for the same attribute.
 *
 * **Note**: Since the `target` attribute management for external links is a common use case, there is a predefined automatic decorator
 * dedicated for that purpose which can be enabled by turning a single option on. Check out the
 * {@link module:link/link~LinkConfig#addTargetToExternalLinks `config.link.addTargetToExternalLinks`}
 * configuration description to learn more.
 *
 * See also the {@glink features/link#custom-link-attributes-decorators link feature guide} for more information.
 *
 * @member {Object.<String, module:link/link~LinkDecoratorDefinition>} module:link/link~LinkConfig#decorators
 */

/**
 * A link decorator definition. Two types implement this defition:
 *
 * * {@link module:link/link~LinkDecoratorManualDefinition}
 * * {@link module:link/link~LinkDecoratorAutomaticDefinition}
 *
 * Refer to their document for more information about available options or to the
 * {@glink features/link#custom-link-attributes-decorators link feature guide} for general information.
 *
 * @interface LinkDecoratorDefinition
 */

/**
 * Link decorator type.
 *
 * Check out the {@glink features/link#custom-link-attributes-decorators link feature guide} for more information.
 *
 * @member {'manual'|'automatic'} module:link/link~LinkDecoratorDefinition#mode
 */

/**
 * Describes an automatic {@link module:link/link~LinkConfig#decorators link decorator}. This decorator type matches
 * all links in the editor content against a function that decides whether the link should receive a pre–defined set of attributes.
 *
 * It takes an object with key-value pairs of attributes and a callback function that must return a Boolean value based on the link's
 * `href` (URL). When the callback returns `true`, attributes are applied to the link.
 *
 * For example, to add the `target="_blank"` attribute to all links in the editor starting with `http://`, the
 * configuration could look like this:
 *
 *		{
 *			mode: 'automatic',
 *			callback: url => url.startsWith( 'http://' ),
 *			attributes: {
 *				target: '_blank'
 *			}
 *		}
 *
 * **Note**: Since the `target` attribute management for external links is a common use case, there is a predefined automatic decorator
 * dedicated for that purpose that can be enabled by turning a single option on. Check out the
 * {@link module:link/link~LinkConfig#addTargetToExternalLinks `config.link.addTargetToExternalLinks`}
 * configuration description to learn more.
 *
 * @typedef {Object} module:link/link~LinkDecoratorAutomaticDefinition
 * @property {'automatic'} mode Link decorator type. It is `'automatic'` for all automatic decorators.
 * @property {Function} callback Takes a `url` as a parameter and returns `true` if the `attributes` should be applied to the link.
 * @property {Object} [attributes] Key-value pairs used as link attributes added to the output during the
 * {@glink framework/guides/architecture/editing-engine#conversion downcasting}.
 * Attributes should follow the {@link module:engine/view/elementdefinition~ElementDefinition} syntax.
 * @property {Object} [styles] Key-value pairs used as link styles added to the output during the
 * {@glink framework/guides/architecture/editing-engine#conversion downcasting}.
 * Styles should follow the {@link module:engine/view/elementdefinition~ElementDefinition} syntax.
 * @property {String|Array.<String>} [classes] Class names used as link classes added to the output during the
 * {@glink framework/guides/architecture/editing-engine#conversion downcasting}.
 * Classes should follow the {@link module:engine/view/elementdefinition~ElementDefinition} syntax.
 */

/**
 * Describes a manual {@link module:link/link~LinkConfig#decorators link decorator}. This decorator type is represented in
 * the link feature's {@link module:link/linkui user interface} as a switch that the user can use to control the presence
 * of a predefined set of attributes.
 *
 * For instance, to allow the users to manually control the presence of the `target="_blank"` and
 * `rel="noopener noreferrer"` attributes on specific links, the decorator could look as follows:
 *
 *		{
 *			mode: 'manual',
 *			label: 'Open in a new tab',
 *			defaultValue: true,
 *			attributes: {
 *				target: '_blank',
 *				rel: 'noopener noreferrer'
 *			}
 *		}
 *
 * @typedef {Object} module:link/link~LinkDecoratorManualDefinition
 * @property {'manual'} mode Link decorator type. It is `'manual'` for all manual decorators.
 * @property {String} label The label of the UI button that the user can use to control the presence of link attributes.
 * @property {Object} [attributes] Key-value pairs used as link attributes added to the output during the
 * {@glink framework/guides/architecture/editing-engine#conversion downcasting}.
 * Attributes should follow the {@link module:engine/view/elementdefinition~ElementDefinition} syntax.
 * @property {Object} [styles] Key-value pairs used as link styles added to the output during the
 * {@glink framework/guides/architecture/editing-engine#conversion downcasting}.
 * Styles should follow the {@link module:engine/view/elementdefinition~ElementDefinition} syntax.
 * @property {String|Array.<String>} [classes] Class names used as link classes added to the output during the
 * {@glink framework/guides/architecture/editing-engine#conversion downcasting}.
 * Classes should follow the {@link module:engine/view/elementdefinition~ElementDefinition} syntax.
 * @property {Boolean} [defaultValue] Controls whether the decorator is "on" by default.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The list command. It is used by the {@link module:list/list~List list feature}.
 *
 * @extends module:core/command~Command
 */
class ListCommand extends Command {
	/**
	 * Creates an instance of the command.
	 *
	 * @param {module:core/editor/editor~Editor} editor The editor instance.
	 * @param {'numbered'|'bulleted'} type List type that will be handled by this command.
	 */
	constructor( editor, type ) {
		super( editor );

		/**
		 * The type of the list created by the command.
		 *
		 * @readonly
		 * @member {'numbered'|'bulleted'|'todo'}
		 */
		this.type = type;

		/**
		 * A flag indicating whether the command is active, which means that the selection starts in a list of the same type.
		 *
		 * @observable
		 * @readonly
		 * @member {Boolean} #value
		 */
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		this.value = this._getValue();
		this.isEnabled = this._checkEnabled();
	}

	/**
	 * Executes the list command.
	 *
	 * @fires execute
	 * @param {Object} [options] Command options.
	 * @param {Boolean} [options.forceValue] If set, it will force the command behavior. If `true`, the command will try to convert the
	 * selected items and potentially the neighbor elements to the proper list items. If set to `false`, it will convert selected elements
	 * to paragraphs. If not set, the command will toggle selected elements to list items or paragraphs, depending on the selection.
	 */
	execute( options = {} ) {
		const model = this.editor.model;
		const document = model.document;
		const blocks = Array.from( document.selection.getSelectedBlocks() )
			.filter( block => checkCanBecomeListItem( block, model.schema ) );

		// Whether we are turning off some items.
		const turnOff = options.forceValue !== undefined ? !options.forceValue : this.value;

		// If we are turning off items, we are going to rename them to paragraphs.

		model.change( writer => {
			// If part of a list got turned off, we need to handle (outdent) all of sub-items of the last turned-off item.
			// To be sure that model is all the time in a good state, we first fix items below turned-off item.
			if ( turnOff ) {
				// Start from the model item that is just after the last turned-off item.
				let next = blocks[ blocks.length - 1 ].nextSibling;
				let currentIndent = Number.POSITIVE_INFINITY;
				let changes = [];

				// Correct indent of all items after the last turned off item.
				// Rules that should be followed:
				// 1. All direct sub-items of turned-off item should become indent 0, because the first item after it
				//    will be the first item of a new list. Other items are at the same level, so should have same 0 index.
				// 2. All items with indent lower than indent of turned-off item should become indent 0, because they
				//    should not end up as a child of any of list items that they were not children of before.
				// 3. All other items should have their indent changed relatively to it's parent.
				//
				// For example:
				// 1  * --------
				// 2     * --------
				// 3        * --------			<-- this is turned off.
				// 4           * --------		<-- this has to become indent = 0, because it will be first item on a new list.
				// 5              * --------	<-- this should be still be a child of item above, so indent = 1.
				// 6        * --------			<-- this has to become indent = 0, because it should not be a child of any of items above.
				// 7           * --------		<-- this should be still be a child of item above, so indent = 1.
				// 8     * --------				<-- this has to become indent = 0.
				// 9        * --------			<-- this should still be a child of item above, so indent = 1.
				// 10          * --------		<-- this should still be a child of item above, so indent = 2.
				// 11          * --------		<-- this should still be at the same level as item above, so indent = 2.
				// 12 * --------				<-- this and all below are left unchanged.
				// 13    * --------
				// 14       * --------
				//
				// After turning off 3 the list becomes:
				//
				// 1  * --------
				// 2     * --------
				//
				// 3  --------
				//
				// 4  * --------
				// 5     * --------
				// 6  * --------
				// 7     * --------
				// 8  * --------
				// 9     * --------
				// 10       * --------
				// 11       * --------
				// 12 * --------
				// 13    * --------
				// 14       * --------
				//
				// Thanks to this algorithm no lists are mismatched and no items get unexpected children/parent, while
				// those parent-child connection which are possible to maintain are still maintained. It's worth noting
				// that this is the same effect that we would be get by multiple use of outdent command. However doing
				// it like this is much more efficient because it's less operation (less memory usage, easier OT) and
				// less conversion (faster).
				while ( next && next.name == 'listItem' && next.getAttribute( 'listIndent' ) !== 0 ) {
					// Check each next list item, as long as its indent is bigger than 0.
					// If the indent is 0 we are not going to change anything anyway.
					const indent = next.getAttribute( 'listIndent' );

					// We check if that's item indent is lower as current relative indent.
					if ( indent < currentIndent ) {
						// If it is, current relative indent becomes that indent.
						currentIndent = indent;
					}

					// Fix indent relatively to current relative indent.
					// Note, that if we just changed the current relative indent, the newIndent will be equal to 0.
					const newIndent = indent - currentIndent;

					// Save the entry in changes array. We do not apply it at the moment, because we will need to
					// reverse the changes so the last item is changed first.
					// This is to keep model in correct state all the time.
					changes.push( { element: next, listIndent: newIndent } );

					// Find next item.
					next = next.nextSibling;
				}

				changes = changes.reverse();

				for ( const item of changes ) {
					writer.setAttribute( 'listIndent', item.listIndent, item.element );
				}
			}

			// If we are turning on, we might change some items that are already `listItem`s but with different type.
			// Changing one nested list item to other type should also trigger changing all its siblings so the
			// whole nested list is of the same type.
			// Example (assume changing to numbered list):
			// * ------				<-- do not fix, top level item
			//   * ------			<-- fix, because latter list item of this item's list is changed
			//      * ------		<-- do not fix, item is not affected (different list)
			//   * ------			<-- fix, because latter list item of this item's list is changed
			//      * ------		<-- fix, because latter list item of this item's list is changed
			//      * ---[--		<-- already in selection
			//   * ------			<-- already in selection
			//   * ------			<-- already in selection
			// * ------				<-- already in selection, but does not cause other list items to change because is top-level
			//   * ---]--			<-- already in selection
			//   * ------			<-- fix, because preceding list item of this item's list is changed
			//      * ------		<-- do not fix, item is not affected (different list)
			// * ------				<-- do not fix, top level item
			if ( !turnOff ) {
				// Find lowest indent among selected items. This will be indicator what is the indent of
				// top-most list affected by the command.
				let lowestIndent = Number.POSITIVE_INFINITY;

				for ( const item of blocks ) {
					if ( item.is( 'element', 'listItem' ) && item.getAttribute( 'listIndent' ) < lowestIndent ) {
						lowestIndent = item.getAttribute( 'listIndent' );
					}
				}

				// Do not execute the fix for top-level lists.
				lowestIndent = lowestIndent === 0 ? 1 : lowestIndent;

				// Fix types of list items that are "before" the selected blocks.
				_fixType( blocks, true, lowestIndent );

				// Fix types of list items that are "after" the selected blocks.
				_fixType( blocks, false, lowestIndent );
			}

			// Phew! Now it will be easier :).
			// For each block element that was in the selection, we will either: turn it to list item,
			// turn it to paragraph, or change it's type. Or leave it as it is.
			// Do it in reverse as there might be multiple blocks (same as with changing indents).
			for ( const element of blocks.reverse() ) {
				if ( turnOff && element.name == 'listItem' ) {
					// We are turning off and the element is a `listItem` - it should be converted to `paragraph`.
					// List item specific attributes are removed by post fixer.
					writer.rename( element, 'paragraph' );
				} else if ( !turnOff && element.name != 'listItem' ) {
					// We are turning on and the element is not a `listItem` - it should be converted to `listItem`.
					// The order of operations is important to keep model in correct state.
					writer.setAttributes( { listType: this.type, listIndent: 0 }, element );
					writer.rename( element, 'listItem' );
				} else if ( !turnOff && element.name == 'listItem' && element.getAttribute( 'listType' ) != this.type ) {
					// We are turning on and the element is a `listItem` but has different type - change it's type and
					// type of it's all siblings that have same indent.
					writer.setAttribute( 'listType', this.type, element );
				}
			}

			/**
			 * Event fired by the {@link #execute} method.
			 *
			 * It allows to execute an action after executing the {@link ~ListCommand#execute} method, for example adjusting
			 * attributes of changed blocks.
			 *
			 * @protected
			 * @event _executeCleanup
			 */
			this.fire( '_executeCleanup', blocks );
		} );
	}

	/**
	 * Checks the command's {@link #value}.
	 *
	 * @private
	 * @returns {Boolean} The current value.
	 */
	_getValue() {
		// Check whether closest `listItem` ancestor of the position has a correct type.
		const listItem = first( this.editor.model.document.selection.getSelectedBlocks() );

		return !!listItem && listItem.is( 'element', 'listItem' ) && listItem.getAttribute( 'listType' ) == this.type;
	}

	/**
	 * Checks whether the command can be enabled in the current context.
	 *
	 * @private
	 * @returns {Boolean} Whether the command should be enabled.
	 */
	_checkEnabled() {
		// If command value is true it means that we are in list item, so the command should be enabled.
		if ( this.value ) {
			return true;
		}

		const selection = this.editor.model.document.selection;
		const schema = this.editor.model.schema;

		const firstBlock = first( selection.getSelectedBlocks() );

		if ( !firstBlock ) {
			return false;
		}

		// Otherwise, check if list item can be inserted at the position start.
		return checkCanBecomeListItem( firstBlock, schema );
	}
}

// Helper function used when one or more list item have their type changed. Fixes type of other list items
// that are affected by the change (are in same lists) but are not directly in selection. The function got extracted
// not to duplicated code, as same fix has to be performed before and after selection.
//
// @param {Array.<module:engine/model/node~Node>} blocks Blocks that are in selection.
// @param {Boolean} isBackward Specified whether fix will be applied for blocks before first selected block (`true`)
// or blocks after last selected block (`false`).
// @param {Number} lowestIndent Lowest indent among selected blocks.
function _fixType( blocks, isBackward, lowestIndent ) {
	// We need to check previous sibling of first changed item and next siblings of last changed item.
	const startingItem = isBackward ? blocks[ 0 ] : blocks[ blocks.length - 1 ];

	if ( startingItem.is( 'element', 'listItem' ) ) {
		let item = startingItem[ isBackward ? 'previousSibling' : 'nextSibling' ];
		// During processing items, keeps the lowest indent of already processed items.
		// This saves us from changing too many items.
		// Following example is for going forward as it is easier to read, however same applies to going backward.
		// * ------
		//   * ------
		//     * --[---
		//   * ------		<-- `lowestIndent` should be 1
		//     * --]---		<-- `startingItem`, `currentIndent` = 2, `lowestIndent` == 1
		//     * ------		<-- should be fixed, `indent` == 2 == `currentIndent`
		//   * ------		<-- should be fixed, set `currentIndent` to 1, `indent` == 1 == `currentIndent`
		//     * ------		<-- should not be fixed, item is in different list, `indent` = 2, `indent` != `currentIndent`
		//   * ------		<-- should be fixed, `indent` == 1 == `currentIndent`
		// * ------			<-- break loop (`indent` < `lowestIndent`)
		let currentIndent = startingItem.getAttribute( 'listIndent' );

		// Look back until a list item with indent lower than reference `lowestIndent`.
		// That would be the parent of nested sublist which contains item having `lowestIndent`.
		while ( item && item.is( 'element', 'listItem' ) && item.getAttribute( 'listIndent' ) >= lowestIndent ) {
			if ( currentIndent > item.getAttribute( 'listIndent' ) ) {
				currentIndent = item.getAttribute( 'listIndent' );
			}

			// Found an item that is in the same nested sublist.
			if ( item.getAttribute( 'listIndent' ) == currentIndent ) {
				// Just add the item to selected blocks like it was selected by the user.
				blocks[ isBackward ? 'unshift' : 'push' ]( item );
			}

			item = item[ isBackward ? 'previousSibling' : 'nextSibling' ];
		}
	}
}

// Checks whether the given block can be replaced by a listItem.
//
// @private
// @param {module:engine/model/element~Element} block A block to be tested.
// @param {module:engine/model/schema~Schema} schema The schema of the document.
// @returns {Boolean}
function checkCanBecomeListItem( block, schema ) {
	return schema.checkChild( block.parent, 'listItem' ) && !schema.isObject( block );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The list indent command. It is used by the {@link module:list/list~List list feature}.
 *
 * @extends module:core/command~Command
 */
class IndentCommand extends Command {
	/**
	 * Creates an instance of the command.
	 *
	 * @param {module:core/editor/editor~Editor} editor The editor instance.
	 * @param {'forward'|'backward'} indentDirection The direction of indent. If it is equal to `backward`, the command
	 * will outdent a list item.
	 */
	constructor( editor, indentDirection ) {
		super( editor );

		/**
		 * Determines by how much the command will change the list item's indent attribute.
		 *
		 * @readonly
		 * @private
		 * @member {Number}
		 */
		this._indentBy = indentDirection == 'forward' ? 1 : -1;
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		this.isEnabled = this._checkEnabled();
	}

	/**
	 * Indents or outdents (depending on the {@link #constructor}'s `indentDirection` parameter) selected list items.
	 *
	 * @fires execute
	 * @fires _executeCleanup
	 */
	execute() {
		const model = this.editor.model;
		const doc = model.document;
		let itemsToChange = Array.from( doc.selection.getSelectedBlocks() );

		model.change( writer => {
			const lastItem = itemsToChange[ itemsToChange.length - 1 ];

			// Indenting a list item should also indent all the items that are already sub-items of indented item.
			let next = lastItem.nextSibling;

			// Check all items after last indented item, as long as their indent is bigger than indent of that item.
			while ( next && next.name == 'listItem' && next.getAttribute( 'listIndent' ) > lastItem.getAttribute( 'listIndent' ) ) {
				itemsToChange.push( next );

				next = next.nextSibling;
			}

			// We need to be sure to keep model in correct state after each small change, because converters
			// bases on that state and assumes that model is correct.
			// Because of that, if the command outdents items, we will outdent them starting from the last item, as
			// it is safer.
			if ( this._indentBy < 0 ) {
				itemsToChange = itemsToChange.reverse();
			}

			for ( const item of itemsToChange ) {
				const indent = item.getAttribute( 'listIndent' ) + this._indentBy;

				// If indent is lower than 0, it means that the item got outdented when it was not indented.
				// This means that we need to convert that list item to paragraph.
				if ( indent < 0 ) {
					// To keep the model as correct as possible, first rename listItem, then remove attributes,
					// as listItem without attributes is very incorrect and will cause problems in converters.
					// No need to remove attributes, will be removed by post fixer.
					writer.rename( item, 'paragraph' );
				}
				// If indent is >= 0, change the attribute value.
				else {
					writer.setAttribute( 'listIndent', indent, item );
				}
			}

			/**
			 * Event fired by the {@link #execute} method.
			 *
			 * It allows to execute an action after executing the {@link ~IndentCommand#execute} method, for example adjusting
			 * attributes of changed list items.
			 *
			 * @protected
			 * @event _executeCleanup
			 */
			this.fire( '_executeCleanup', itemsToChange );
		} );
	}

	/**
	 * Checks whether the command can be enabled in the current context.
	 *
	 * @private
	 * @returns {Boolean} Whether the command should be enabled.
	 */
	_checkEnabled() {
		// Check whether any of position's ancestor is a list item.
		const listItem = first( this.editor.model.document.selection.getSelectedBlocks() );

		// If selection is not in a list item, the command is disabled.
		if ( !listItem || !listItem.is( 'element', 'listItem' ) ) {
			return false;
		}

		if ( this._indentBy > 0 ) {
			// Cannot indent first item in it's list. Check if before `listItem` is a list item that is in same list.
			// To be in the same list, the item has to have same attributes and cannot be "split" by an item with lower indent.
			const indent = listItem.getAttribute( 'listIndent' );
			const type = listItem.getAttribute( 'listType' );

			let prev = listItem.previousSibling;

			while ( prev && prev.is( 'element', 'listItem' ) && prev.getAttribute( 'listIndent' ) >= indent ) {
				if ( prev.getAttribute( 'listIndent' ) == indent ) {
					// The item is on the same level.
					// If it has same type, it means that we found a preceding sibling from the same list.
					// If it does not have same type, it means that `listItem` is on different list (this can happen only
					// on top level lists, though).
					return prev.getAttribute( 'listType' ) == type;
				}

				prev = prev.previousSibling;
			}

			// Could not find similar list item, this means that `listItem` is first in its list.
			return false;
		}

		// If we are outdenting it is enough to be in list item. Every list item can always be outdented.
		return true;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Creates a list item {@link module:engine/view/containerelement~ContainerElement}.
 *
 * @param {module:engine/view/downcastwriter~DowncastWriter} writer The writer instance.
 * @returns {module:engine/view/containerelement~ContainerElement}
 */
function createViewListItemElement( writer ) {
	const viewItem = writer.createContainerElement( 'li' );

	viewItem.getFillerOffset = getListItemFillerOffset;

	return viewItem;
}

/**
 * Helper function that creates a `<ul><li></li></ul>` or (`<ol>`) structure out of the given `modelItem` model `listItem` element.
 * Then, it binds the created view list item (`<li>`) with the model `listItem` element.
 * The function then returns the created view list item (`<li>`).
 *
 * @param {module:engine/model/item~Item} modelItem Model list item.
 * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface.
 * @returns {module:engine/view/containerelement~ContainerElement} View list element.
 */
function generateLiInUl( modelItem, conversionApi ) {
	const mapper = conversionApi.mapper;
	const viewWriter = conversionApi.writer;
	const listType = modelItem.getAttribute( 'listType' ) == 'numbered' ? 'ol' : 'ul';
	const viewItem = createViewListItemElement( viewWriter );

	const viewList = viewWriter.createContainerElement( listType, null );

	viewWriter.insert( viewWriter.createPositionAt( viewList, 0 ), viewItem );

	mapper.bindElements( modelItem, viewItem );

	return viewItem;
}

/**
 * Helper function that inserts a view list at a correct place and merges it with its siblings.
 * It takes a model list item element (`modelItem`) and a corresponding view list item element (`injectedItem`). The view list item
 * should be in a view list element (`<ul>` or `<ol>`) and should be its only child.
 * See comments below to better understand the algorithm.
 *
 * @param {module:engine/view/item~Item} modelItem Model list item.
 * @param {module:engine/view/containerelement~ContainerElement} injectedItem
 * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface.
 * @param {module:engine/model/model~Model} model The model instance.
 */
function injectViewList( modelItem, injectedItem, conversionApi, model ) {
	const injectedList = injectedItem.parent;
	const mapper = conversionApi.mapper;
	const viewWriter = conversionApi.writer;

	// The position where the view list will be inserted.
	let insertPosition = mapper.toViewPosition( model.createPositionBefore( modelItem ) );

	// 1. Find the previous list item that has the same or smaller indent. Basically we are looking for the first model item
	// that is a "parent" or "sibling" of the injected model item.
	// If there is no such list item, it means that the injected list item is the first item in "its list".
	const refItem = getSiblingListItem( modelItem.previousSibling, {
		sameIndent: true,
		smallerIndent: true,
		listIndent: modelItem.getAttribute( 'listIndent' )
	} );
	const prevItem = modelItem.previousSibling;

	if ( refItem && refItem.getAttribute( 'listIndent' ) == modelItem.getAttribute( 'listIndent' ) ) {
		// There is a list item with the same indent - we found the same-level sibling.
		// Break the list after it. The inserted view item will be added in the broken space.
		const viewItem = mapper.toViewElement( refItem );
		insertPosition = viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) );
	} else {
		// There is no list item with the same indent. Check the previous model item.
		if ( prevItem && prevItem.name == 'listItem' ) {
			// If it is a list item, it has to have a lower indent.
			// It means that the inserted item should be added to it as its nested item.
			insertPosition = mapper.toViewPosition( model.createPositionAt( prevItem, 'end' ) );

			// There could be some not mapped elements (eg. span in to-do list) but we need to insert
			// a nested list directly inside the li element.
			const mappedViewAncestor = mapper.findMappedViewAncestor( insertPosition );
			const nestedList = findNestedList( mappedViewAncestor );

			// If there already is some nested list, then use it's position.
			if ( nestedList ) {
				insertPosition = viewWriter.createPositionBefore( nestedList );
			} else {
				// Else just put new list on the end of list item content.
				insertPosition = viewWriter.createPositionAt( mappedViewAncestor, 'end' );
			}
		} else {
			// The previous item is not a list item (or does not exist at all).
			// Just map the position and insert the view item at the mapped position.
			insertPosition = mapper.toViewPosition( model.createPositionBefore( modelItem ) );
		}
	}

	insertPosition = positionAfterUiElements( insertPosition );

	// Insert the view item.
	viewWriter.insert( insertPosition, injectedList );

	// 2. Handle possible children of the injected model item.
	if ( prevItem && prevItem.name == 'listItem' ) {
		const prevView = mapper.toViewElement( prevItem );

		const walkerBoundaries = viewWriter.createRange( viewWriter.createPositionAt( prevView, 0 ), insertPosition );
		const walker = walkerBoundaries.getWalker( { ignoreElementEnd: true } );

		for ( const value of walker ) {
			if ( value.item.is( 'element', 'li' ) ) {
				const breakPosition = viewWriter.breakContainer( viewWriter.createPositionBefore( value.item ) );
				const viewList = value.item.parent;

				const targetPosition = viewWriter.createPositionAt( injectedItem, 'end' );
				mergeViewLists( viewWriter, targetPosition.nodeBefore, targetPosition.nodeAfter );
				viewWriter.move( viewWriter.createRangeOn( viewList ), targetPosition );

				walker.position = breakPosition;
			}
		}
	} else {
		const nextViewList = injectedList.nextSibling;

		if ( nextViewList && ( nextViewList.is( 'element', 'ul' ) || nextViewList.is( 'element', 'ol' ) ) ) {
			let lastSubChild = null;

			for ( const child of nextViewList.getChildren() ) {
				const modelChild = mapper.toModelElement( child );

				if ( modelChild && modelChild.getAttribute( 'listIndent' ) > modelItem.getAttribute( 'listIndent' ) ) {
					lastSubChild = child;
				} else {
					break;
				}
			}

			if ( lastSubChild ) {
				viewWriter.breakContainer( viewWriter.createPositionAfter( lastSubChild ) );
				viewWriter.move( viewWriter.createRangeOn( lastSubChild.parent ), viewWriter.createPositionAt( injectedItem, 'end' ) );
			}
		}
	}

	// Merge the inserted view list with its possible neighbor lists.
	mergeViewLists( viewWriter, injectedList, injectedList.nextSibling );
	mergeViewLists( viewWriter, injectedList.previousSibling, injectedList );
}

/**
 * Helper function that takes two parameters that are expected to be view list elements, and merges them.
 * The merge happens only if both parameters are list elements of the same type (the same element name and the same class attributes).
 *
 * @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter The writer instance.
 * @param {module:engine/view/item~Item} firstList The first element to compare.
 * @param {module:engine/view/item~Item} secondList The second element to compare.
 * @returns {module:engine/view/position~Position|null} The position after merge or `null` when there was no merge.
 */
function mergeViewLists( viewWriter, firstList, secondList ) {
	// Check if two lists are going to be merged.
	if ( !firstList || !secondList || ( firstList.name != 'ul' && firstList.name != 'ol' ) ) {
		return null;
	}

	// Both parameters are list elements, so compare types now.
	if ( firstList.name != secondList.name || firstList.getAttribute( 'class' ) !== secondList.getAttribute( 'class' ) ) {
		return null;
	}

	return viewWriter.mergeContainers( viewWriter.createPositionAfter( firstList ) );
}

/**
 * Helper function that for a given `view.Position`, returns a `view.Position` that is after all `view.UIElement`s that
 * are after the given position.
 *
 * For example:
 * `<container:p>foo^<ui:span></ui:span><ui:span></ui:span>bar</container:p>`
 * For position ^, the position before "bar" will be returned.
 *
 * @param {module:engine/view/position~Position} viewPosition
 * @returns {module:engine/view/position~Position}
 */
function positionAfterUiElements( viewPosition ) {
	return viewPosition.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
}

/**
 * Helper function that searches for a previous list item sibling of a given model item that meets the given criteria
 * passed by the options object.
 *
 * @param {module:engine/model/item~Item} modelItem
 * @param {Object} options Search criteria.
 * @param {Boolean} [options.sameIndent=false] Whether the sought sibling should have the same indentation.
 * @param {Boolean} [options.smallerIndent=false] Whether the sought sibling should have a smaller indentation.
 * @param {Number} [options.listIndent] The reference indentation.
 * @param {'forward'|'backward'} [options.direction='backward'] Walking direction.
 * @returns {module:engine/model/item~Item|null}
 */
function getSiblingListItem( modelItem, options ) {
	const sameIndent = !!options.sameIndent;
	const smallerIndent = !!options.smallerIndent;
	const indent = options.listIndent;

	let item = modelItem;

	while ( item && item.name == 'listItem' ) {
		const itemIndent = item.getAttribute( 'listIndent' );

		if ( ( sameIndent && indent == itemIndent ) || ( smallerIndent && indent > itemIndent ) ) {
			return item;
		}

		if ( options.direction === 'forward' ) {
			item = item.nextSibling;
		} else {
			item = item.previousSibling;
		}
	}

	return null;
}

/**
 * Helper method for creating a UI button and linking it with an appropriate command.
 *
 * @private
 * @param {module:core/editor/editor~Editor} editor The editor instance to which the UI component will be added.
 * @param {String} commandName The name of the command.
 * @param {String} label The button label.
 * @param {String} icon The source of the icon.
 */
function createUIComponent( editor, commandName, label, icon ) {
	editor.ui.componentFactory.add( commandName, locale => {
		const command = editor.commands.get( commandName );
		const buttonView = new ButtonView( locale );

		buttonView.set( {
			label,
			icon,
			tooltip: true,
			isToggleable: true
		} );

		// Bind button model to command.
		buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

		// Execute command.
		buttonView.on( 'execute', () => {
			editor.execute( commandName );
			editor.editing.view.focus();
		} );

		return buttonView;
	} );
}

/**
 * Returns a first list view element that is direct child of the given view element.
 *
 * @param {module:engine/view/element~Element} viewElement
 * @return {module:engine/view/element~Element|null}
 */
function findNestedList( viewElement ) {
	for ( const node of viewElement.getChildren() ) {
		if ( node.name == 'ul' || node.name == 'ol' ) {
			return node;
		}
	}

	return null;
}

// Implementation of getFillerOffset for view list item element.
//
// @returns {Number|null} Block filler offset or `null` if block filler is not needed.
function getListItemFillerOffset() {
	const hasOnlyLists = !this.isEmpty && ( this.getChild( 0 ).name == 'ul' || this.getChild( 0 ).name == 'ol' );

	if ( this.isEmpty || hasOnlyLists ) {
		return 0;
	}

	return getFillerOffset$5.call( this );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A model-to-view converter for the `listItem` model element insertion.
 *
 * It creates a `<ul><li></li><ul>` (or `<ol>`) view structure out of a `listItem` model element, inserts it at the correct
 * position, and merges the list with surrounding lists (if available).
 *
 * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert
 * @param {module:engine/model/model~Model} model Model instance.
 * @returns {Function} Returns a conversion callback.
 */
function modelViewInsertion( model ) {
	return ( evt, data, conversionApi ) => {
		const consumable = conversionApi.consumable;

		if ( !consumable.test( data.item, 'insert' ) ||
			!consumable.test( data.item, 'attribute:listType' ) ||
			!consumable.test( data.item, 'attribute:listIndent' )
		) {
			return;
		}

		consumable.consume( data.item, 'insert' );
		consumable.consume( data.item, 'attribute:listType' );
		consumable.consume( data.item, 'attribute:listIndent' );

		const modelItem = data.item;
		const viewItem = generateLiInUl( modelItem, conversionApi );

		injectViewList( modelItem, viewItem, conversionApi, model );
	};
}

/**
 * A model-to-view converter for the `listItem` model element removal.
 *
 * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove
 * @param {module:engine/model/model~Model} model Model instance.
 * @returns {Function} Returns a conversion callback.
 */
function modelViewRemove( model ) {
	return ( evt, data, conversionApi ) => {
		const viewPosition = conversionApi.mapper.toViewPosition( data.position );
		const viewStart = viewPosition.getLastMatchingPosition( value => !value.item.is( 'element', 'li' ) );
		const viewItem = viewStart.nodeAfter;
		const viewWriter = conversionApi.writer;

		// 1. Break the container after and before the list item.
		// This will create a view list with one view list item - the one to remove.
		viewWriter.breakContainer( viewWriter.createPositionBefore( viewItem ) );
		viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) );

		// 2. Remove the list with the item to remove.
		const viewList = viewItem.parent;
		const viewListPrev = viewList.previousSibling;
		const removeRange = viewWriter.createRangeOn( viewList );
		const removed = viewWriter.remove( removeRange );

		// 3. Merge the whole created by breaking and removing the list.
		if ( viewListPrev && viewListPrev.nextSibling ) {
			mergeViewLists( viewWriter, viewListPrev, viewListPrev.nextSibling );
		}

		// 4. Bring back nested list that was in the removed <li>.
		const modelItem = conversionApi.mapper.toModelElement( viewItem );

		hoistNestedLists( modelItem.getAttribute( 'listIndent' ) + 1, data.position, removeRange.start, viewItem, conversionApi, model );

		// 5. Unbind removed view item and all children.
		for ( const child of viewWriter.createRangeIn( removed ).getItems() ) {
			conversionApi.mapper.unbindViewElement( child );
		}

		evt.stop();
	};
}

/**
 * A model-to-view converter for the `type` attribute change on the `listItem` model element.
 *
 * This change means that the `<li>` element parent changes from `<ul>` to `<ol>` (or vice versa). This is accomplished
 * by breaking view elements and changing their name. The next {@link module:list/list/converters~modelViewMergeAfterChangeType}
 * converter will attempt to merge split nodes.
 *
 * Splitting this conversion into 2 steps makes it possible to add an additional conversion in the middle.
 * Check {@link module:list/todolist/todolistconverters~modelViewChangeType} to see an example of it.
 *
 * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
 * @param {Object} data Additional information about the change.
 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
 */
function modelViewChangeType( evt, data, conversionApi ) {
	if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
		return;
	}

	const viewItem = conversionApi.mapper.toViewElement( data.item );
	const viewWriter = conversionApi.writer;

	// Break the container after and before the list item.
	// This will create a view list with one view list item -- the one that changed type.
	viewWriter.breakContainer( viewWriter.createPositionBefore( viewItem ) );
	viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) );

	// Change name of the view list that holds the changed view item.
	// We cannot just change name property, because that would not render properly.
	const viewList = viewItem.parent;
	const listName = data.attributeNewValue == 'numbered' ? 'ol' : 'ul';

	viewWriter.rename( listName, viewList );
}

/**
 * A model-to-view converter that attempts to merge nodes split by {@link module:list/list/converters~modelViewChangeType}.
 *
 * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
 * @param {Object} data Additional information about the change.
 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
 */
function modelViewMergeAfterChangeType( evt, data, conversionApi ) {
	conversionApi.consumable.consume( data.item, evt.name );

	const viewItem = conversionApi.mapper.toViewElement( data.item );
	const viewList = viewItem.parent;
	const viewWriter = conversionApi.writer;

	// Merge the changed view list with other lists, if possible.
	mergeViewLists( viewWriter, viewList, viewList.nextSibling );
	mergeViewLists( viewWriter, viewList.previousSibling, viewList );
}

/**
 * A model-to-view converter for the `listIndent` attribute change on the `listItem` model element.
 *
 * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute
 * @param {module:engine/model/model~Model} model Model instance.
 * @returns {Function} Returns a conversion callback.
 */
function modelViewChangeIndent( model ) {
	return ( evt, data, conversionApi ) => {
		if ( !conversionApi.consumable.consume( data.item, 'attribute:listIndent' ) ) {
			return;
		}

		const viewItem = conversionApi.mapper.toViewElement( data.item );
		const viewWriter = conversionApi.writer;

		// 1. Break the container after and before the list item.
		// This will create a view list with one view list item -- the one that changed type.
		viewWriter.breakContainer( viewWriter.createPositionBefore( viewItem ) );
		viewWriter.breakContainer( viewWriter.createPositionAfter( viewItem ) );

		// 2. Extract view list with changed view list item and merge "hole" possibly created by breaking and removing elements.
		const viewList = viewItem.parent;
		const viewListPrev = viewList.previousSibling;
		const removeRange = viewWriter.createRangeOn( viewList );
		viewWriter.remove( removeRange );

		if ( viewListPrev && viewListPrev.nextSibling ) {
			mergeViewLists( viewWriter, viewListPrev, viewListPrev.nextSibling );
		}

		// 3. Bring back nested list that was in the removed <li>.
		hoistNestedLists( data.attributeOldValue + 1, data.range.start, removeRange.start, viewItem, conversionApi, model );

		// 4. Inject view list like it is newly inserted.
		injectViewList( data.item, viewItem, conversionApi, model );

		// 5. Consume insertion of children inside the item. They are already handled by re-building the item in view.
		for ( const child of data.item.getChildren() ) {
			conversionApi.consumable.consume( child, 'insert' );
		}
	};
}

/**
 * A special model-to-view converter introduced by the {@link module:list/list~List list feature}. This converter is fired for
 * insert change of every model item, and should be fired before the actual converter. The converter checks whether the inserted
 * model item is a non-`listItem` element. If it is, and it is inserted inside a view list, the converter breaks the
 * list so the model element is inserted to the view parent element corresponding to its model parent element.
 *
 * The converter prevents such situations:
 *
 *		// Model:                        // View:
 *		<listItem>foo</listItem>         <ul>
 *		<listItem>bar</listItem>             <li>foo</li>
 *		                                     <li>bar</li>
 *		                                 </ul>
 *
 *		// After change:                 // Correct view guaranteed by this converter:
 *		<listItem>foo</listItem>         <ul><li>foo</li></ul><p>xxx</p><ul><li>bar</li></ul>
 *		<paragraph>xxx</paragraph>       // Instead of this wrong view state:
 *		<listItem>bar</listItem>         <ul><li>foo</li><p>xxx</p><li>bar</li></ul>
 *
 * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert
 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
 * @param {Object} data Additional information about the change.
 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
 */
function modelViewSplitOnInsert( evt, data, conversionApi ) {
	if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
		return;
	}

	if ( data.item.name != 'listItem' ) {
		let viewPosition = conversionApi.mapper.toViewPosition( data.range.start );

		const viewWriter = conversionApi.writer;
		const lists = [];

		// Break multiple ULs/OLs if there are.
		//
		// Imagine following list:
		//
		// 1 --------
		//   1.1 --------
		//     1.1.1 --------
		//     1.1.2 --------
		//     1.1.3 --------
		//       1.1.3.1 --------
		//   1.2 --------
		//     1.2.1 --------
		// 2 --------
		//
		// Insert paragraph after item 1.1.1:
		//
		// 1 --------
		//   1.1 --------
		//     1.1.1 --------
		//
		// Lorem ipsum.
		//
		//     1.1.2 --------
		//     1.1.3 --------
		//       1.1.3.1 --------
		//   1.2 --------
		//     1.2.1 --------
		// 2 --------
		//
		// In this case 1.1.2 has to become beginning of a new list.
		// We need to break list before 1.1.2 (obvious), then we need to break list also before 1.2.
		// Then we need to move those broken pieces one after another and merge:
		//
		// 1 --------
		//   1.1 --------
		//     1.1.1 --------
		//
		// Lorem ipsum.
		//
		// 1.1.2 --------
		//   1.1.3 --------
		//     1.1.3.1 --------
		// 1.2 --------
		//   1.2.1 --------
		// 2 --------
		//
		while ( viewPosition.parent.name == 'ul' || viewPosition.parent.name == 'ol' ) {
			viewPosition = viewWriter.breakContainer( viewPosition );

			if ( viewPosition.parent.name != 'li' ) {
				break;
			}

			// Remove lists that are after inserted element.
			// They will be brought back later, below the inserted element.
			const removeStart = viewPosition;
			const removeEnd = viewWriter.createPositionAt( viewPosition.parent, 'end' );

			// Don't remove if there is nothing to remove.
			if ( !removeStart.isEqual( removeEnd ) ) {
				const removed = viewWriter.remove( viewWriter.createRange( removeStart, removeEnd ) );
				lists.push( removed );
			}

			viewPosition = viewWriter.createPositionAfter( viewPosition.parent );
		}

		// Bring back removed lists.
		if ( lists.length > 0 ) {
			for ( let i = 0; i < lists.length; i++ ) {
				const previousList = viewPosition.nodeBefore;
				const insertedRange = viewWriter.insert( viewPosition, lists[ i ] );
				viewPosition = insertedRange.end;

				// Don't merge first list! We want a split in that place (this is why this converter is introduced).
				if ( i > 0 ) {
					const mergePos = mergeViewLists( viewWriter, previousList, previousList.nextSibling );

					// If `mergePos` is in `previousList` it means that the lists got merged.
					// In this case, we need to fix insert position.
					if ( mergePos && mergePos.parent == previousList ) {
						viewPosition.offset--;
					}
				}
			}

			// Merge last inserted list with element after it.
			mergeViewLists( viewWriter, viewPosition.nodeBefore, viewPosition.nodeAfter );
		}
	}
}

/**
 * A special model-to-view converter introduced by the {@link module:list/list~List list feature}. This converter takes care of
 * merging view lists after something is removed or moved from near them.
 *
 * Example:
 *
 *		// Model:                        // View:
 *		<listItem>foo</listItem>         <ul><li>foo</li></ul>
 *		<paragraph>xxx</paragraph>       <p>xxx</p>
 *		<listItem>bar</listItem>         <ul><li>bar</li></ul>
 *
 *		// After change:                 // Correct view guaranteed by this converter:
 *		<listItem>foo</listItem>         <ul>
 *		<listItem>bar</listItem>             <li>foo</li>
 *		                                     <li>bar</li>
 *		                                 </ul>
 *
 * @see module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove
 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
 * @param {Object} data Additional information about the change.
 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface.
 */
function modelViewMergeAfter( evt, data, conversionApi ) {
	const viewPosition = conversionApi.mapper.toViewPosition( data.position );
	const viewItemPrev = viewPosition.nodeBefore;
	const viewItemNext = viewPosition.nodeAfter;

	// Merge lists if something (remove, move) was done from inside of list.
	// Merging will be done only if both items are view lists of the same type.
	// The check is done inside the helper function.
	mergeViewLists( conversionApi.writer, viewItemPrev, viewItemNext );
}

/**
 * A view-to-model converter that converts the `<li>` view elements into the `listItem` model elements.
 *
 * To set correct values of the `listType` and `listIndent` attributes the converter:
 * * checks `<li>`'s parent,
 * * stores and increases the `conversionApi.store.indent` value when `<li>`'s sub-items are converted.
 *
 * @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
 * @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
 * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface to be used by the callback.
 */
function viewModelConverter( evt, data, conversionApi ) {
	if ( conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
		const writer = conversionApi.writer;

		// 1. Create `listItem` model element.
		const listItem = writer.createElement( 'listItem' );

		// 2. Handle `listItem` model element attributes.
		const indent = getIndent( data.viewItem );

		writer.setAttribute( 'listIndent', indent, listItem );

		// Set 'bulleted' as default. If this item is pasted into a context,
		const type = data.viewItem.parent && data.viewItem.parent.name == 'ol' ? 'numbered' : 'bulleted';
		writer.setAttribute( 'listType', type, listItem );

		if ( !conversionApi.safeInsert( listItem, data.modelCursor ) ) {
			return;
		}

		const nextPosition = viewToModelListItemChildrenConverter( listItem, data.viewItem.getChildren(), conversionApi );

		// Result range starts before the first item and ends after the last.
		data.modelRange = writer.createRange( data.modelCursor, nextPosition );

		conversionApi.updateConversionResult( listItem, data );
	}
}

/**
 * A view-to-model converter for the `<ul>` and `<ol>` view elements that cleans the input view of garbage.
 * This is mostly to clean whitespaces from between the `<li>` view elements inside the view list element, however, also
 * incorrect data can be cleared if the view was incorrect.
 *
 * @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
 * @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
 * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface to be used by the callback.
 */
function cleanList( evt, data, conversionApi ) {
	if ( conversionApi.consumable.test( data.viewItem, { name: true } ) ) {
		// Caching children because when we start removing them iterating fails.
		const children = Array.from( data.viewItem.getChildren() );

		for ( const child of children ) {
			const isWrongElement = !( child.is( 'element', 'li' ) || isList$1( child ) );

			if ( isWrongElement ) {
				child._remove();
			}
		}
	}
}

/**
 * A view-to-model converter for the `<li>` elements that cleans whitespace formatting from the input view.
 *
 * @see module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
 * @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
 * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface to be used by the callback.
 */
function cleanListItem( evt, data, conversionApi ) {
	if ( conversionApi.consumable.test( data.viewItem, { name: true } ) ) {
		if ( data.viewItem.childCount === 0 ) {
			return;
		}

		const children = [ ...data.viewItem.getChildren() ];

		let foundList = false;

		for ( const child of children ) {
			if ( foundList && !isList$1( child ) ) {
				child._remove();
			}

			if ( isList$1( child ) ) {
				// If this is a <ul> or <ol>, do not process it, just mark that we already visited list element.
				foundList = true;
			}
		}
	}
}

/**
 * Returns a callback for model position to view position mapping for {@link module:engine/conversion/mapper~Mapper}. The callback fixes
 * positions between the `listItem` elements that would be incorrectly mapped because of how list items are represented in the model
 * and in the view.
 *
 * @see module:engine/conversion/mapper~Mapper#event:modelToViewPosition
 * @param {module:engine/view/view~View} view A view instance.
 * @returns {Function}
 */
function modelToViewPosition( view ) {
	return ( evt, data ) => {
		if ( data.isPhantom ) {
			return;
		}

		const modelItem = data.modelPosition.nodeBefore;

		if ( modelItem && modelItem.is( 'element', 'listItem' ) ) {
			const viewItem = data.mapper.toViewElement( modelItem );
			const topmostViewList = viewItem.getAncestors().find( isList$1 );
			const walker = view.createPositionAt( viewItem, 0 ).getWalker();

			for ( const value of walker ) {
				if ( value.type == 'elementStart' && value.item.is( 'element', 'li' ) ) {
					data.viewPosition = value.previousPosition;

					break;
				} else if ( value.type == 'elementEnd' && value.item == topmostViewList ) {
					data.viewPosition = value.nextPosition;

					break;
				}
			}
		}
	};
}

/**
 * The callback for view position to model position mapping for {@link module:engine/conversion/mapper~Mapper}. The callback fixes
 * positions between the `<li>` elements that would be incorrectly mapped because of how list items are represented in the model
 * and in the view.
 *
 * @see module:engine/conversion/mapper~Mapper#event:viewToModelPosition
 * @param {module:engine/model/model~Model} model Model instance.
 * @returns {Function} Returns a conversion callback.
 */
function viewToModelPosition( model ) {
	return ( evt, data ) => {
		const viewPos = data.viewPosition;
		const viewParent = viewPos.parent;
		const mapper = data.mapper;

		if ( viewParent.name == 'ul' || viewParent.name == 'ol' ) {
			// Position is directly in <ul> or <ol>.
			if ( !viewPos.isAtEnd ) {
				// If position is not at the end, it must be before <li>.
				// Get that <li>, map it to `listItem` and set model position before that `listItem`.
				const modelNode = mapper.toModelElement( viewPos.nodeAfter );

				data.modelPosition = model.createPositionBefore( modelNode );
			} else {
				// Position is at the end of <ul> or <ol>, so there is no <li> after it to be mapped.
				// There is <li> before the position, but we cannot just map it to `listItem` and set model position after it,
				// because that <li> may contain nested items.
				// We will check "model length" of that <li>, in other words - how many `listItem`s are in that <li>.
				const modelNode = mapper.toModelElement( viewPos.nodeBefore );
				const modelLength = mapper.getModelLength( viewPos.nodeBefore );

				// Then we get model position before mapped `listItem` and shift it accordingly.
				data.modelPosition = model.createPositionBefore( modelNode ).getShiftedBy( modelLength );
			}

			evt.stop();
		} else if (
			viewParent.name == 'li' &&
			viewPos.nodeBefore &&
			( viewPos.nodeBefore.name == 'ul' || viewPos.nodeBefore.name == 'ol' )
		) {
			// In most cases when view position is in <li> it is in text and this is a correct position.
			// However, if position is after <ul> or <ol> we have to fix it -- because in model <ul>/<ol> are not in the `listItem`.
			const modelNode = mapper.toModelElement( viewParent );

			// Check all <ul>s and <ol>s that are in the <li> but before mapped position.
			// Get model length of those elements and then add it to the offset of `listItem` mapped to the original <li>.
			let modelLength = 1; // Starts from 1 because the original <li> has to be counted in too.
			let viewList = viewPos.nodeBefore;

			while ( viewList && isList$1( viewList ) ) {
				modelLength += mapper.getModelLength( viewList );

				viewList = viewList.previousSibling;
			}

			data.modelPosition = model.createPositionBefore( modelNode ).getShiftedBy( modelLength );

			evt.stop();
		}
	};
}

/**
 * Post-fixer that reacts to changes on document and fixes incorrect model states.
 *
 * In the example below, there is a correct list structure.
 * Then the middle element is removed so the list structure will become incorrect:
 *
 *		<listItem listType="bulleted" listIndent=0>Item 1</listItem>
 *		<listItem listType="bulleted" listIndent=1>Item 2</listItem>   <--- this is removed.
 *		<listItem listType="bulleted" listIndent=2>Item 3</listItem>
 *
 * The list structure after the middle element is removed:
 *
 * 		<listItem listType="bulleted" listIndent=0>Item 1</listItem>
 *		<listItem listType="bulleted" listIndent=2>Item 3</listItem>
 *
 * Should become:
 *
 *		<listItem listType="bulleted" listIndent=0>Item 1</listItem>
 *		<listItem listType="bulleted" listIndent=1>Item 3</listItem>   <--- note that indent got post-fixed.
 *
 * @param {module:engine/model/model~Model} model The data model.
 * @param {module:engine/model/writer~Writer} writer The writer to do changes with.
 * @returns {Boolean} `true` if any change has been applied, `false` otherwise.
 */
function modelChangePostFixer( model, writer ) {
	const changes = model.document.differ.getChanges();
	const itemToListHead = new Map();

	let applied = false;

	for ( const entry of changes ) {
		if ( entry.type == 'insert' && entry.name == 'listItem' ) {
			_addListToFix( entry.position );
		} else if ( entry.type == 'insert' && entry.name != 'listItem' ) {
			if ( entry.name != '$text' ) {
				// In case of renamed element.
				const item = entry.position.nodeAfter;

				if ( item.hasAttribute( 'listIndent' ) ) {
					writer.removeAttribute( 'listIndent', item );

					applied = true;
				}

				if ( item.hasAttribute( 'listType' ) ) {
					writer.removeAttribute( 'listType', item );

					applied = true;
				}

				if ( item.hasAttribute( 'listStyle' ) ) {
					writer.removeAttribute( 'listStyle', item );

					applied = true;
				}

				if ( item.hasAttribute( 'listReversed' ) ) {
					writer.removeAttribute( 'listReversed', item );

					applied = true;
				}

				if ( item.hasAttribute( 'listStart' ) ) {
					writer.removeAttribute( 'listStart', item );

					applied = true;
				}

				for ( const innerItem of Array.from( model.createRangeIn( item ) ).filter( e => e.item.is( 'element', 'listItem' ) ) ) {
					_addListToFix( innerItem.previousPosition );
				}
			}

			const posAfter = entry.position.getShiftedBy( entry.length );

			_addListToFix( posAfter );
		} else if ( entry.type == 'remove' && entry.name == 'listItem' ) {
			_addListToFix( entry.position );
		} else if ( entry.type == 'attribute' && entry.attributeKey == 'listIndent' ) {
			_addListToFix( entry.range.start );
		} else if ( entry.type == 'attribute' && entry.attributeKey == 'listType' ) {
			_addListToFix( entry.range.start );
		}
	}

	for ( const listHead of itemToListHead.values() ) {
		_fixListIndents( listHead );
		_fixListTypes( listHead );
	}

	return applied;

	function _addListToFix( position ) {
		const previousNode = position.nodeBefore;

		if ( !previousNode || !previousNode.is( 'element', 'listItem' ) ) {
			const item = position.nodeAfter;

			if ( item && item.is( 'element', 'listItem' ) ) {
				itemToListHead.set( item, item );
			}
		} else {
			let listHead = previousNode;

			if ( itemToListHead.has( listHead ) ) {
				return;
			}

			for (
				// Cache previousSibling and reuse for performance reasons. See #6581.
				let previousSibling = listHead.previousSibling;
				previousSibling && previousSibling.is( 'element', 'listItem' );
				previousSibling = listHead.previousSibling
			) {
				listHead = previousSibling;

				if ( itemToListHead.has( listHead ) ) {
					return;
				}
			}

			itemToListHead.set( previousNode, listHead );
		}
	}

	function _fixListIndents( item ) {
		let maxIndent = 0;
		let fixBy = null;

		while ( item && item.is( 'element', 'listItem' ) ) {
			const itemIndent = item.getAttribute( 'listIndent' );

			if ( itemIndent > maxIndent ) {
				let newIndent;

				if ( fixBy === null ) {
					fixBy = itemIndent - maxIndent;
					newIndent = maxIndent;
				} else {
					if ( fixBy > itemIndent ) {
						fixBy = itemIndent;
					}

					newIndent = itemIndent - fixBy;
				}

				writer.setAttribute( 'listIndent', newIndent, item );

				applied = true;
			} else {
				fixBy = null;
				maxIndent = item.getAttribute( 'listIndent' ) + 1;
			}

			item = item.nextSibling;
		}
	}

	function _fixListTypes( item ) {
		let typesStack = [];
		let prev = null;

		while ( item && item.is( 'element', 'listItem' ) ) {
			const itemIndent = item.getAttribute( 'listIndent' );

			if ( prev && prev.getAttribute( 'listIndent' ) > itemIndent ) {
				typesStack = typesStack.slice( 0, itemIndent + 1 );
			}

			if ( itemIndent != 0 ) {
				if ( typesStack[ itemIndent ] ) {
					const type = typesStack[ itemIndent ];

					if ( item.getAttribute( 'listType' ) != type ) {
						writer.setAttribute( 'listType', type, item );

						applied = true;
					}
				} else {
					typesStack[ itemIndent ] = item.getAttribute( 'listType' );
				}
			}

			prev = item;
			item = item.nextSibling;
		}
	}
}

/**
 * A fixer for pasted content that includes list items.
 *
 * It fixes indentation of pasted list items so the pasted items match correctly to the context they are pasted into.
 *
 * Example:
 *
 *		<listItem listType="bulleted" listIndent=0>A</listItem>
 *		<listItem listType="bulleted" listIndent=1>B^</listItem>
 *		// At ^ paste:  <listItem listType="bulleted" listIndent=4>X</listItem>
 *		//              <listItem listType="bulleted" listIndent=5>Y</listItem>
 *		<listItem listType="bulleted" listIndent=2>C</listItem>
 *
 * Should become:
 *
 *		<listItem listType="bulleted" listIndent=0>A</listItem>
 *		<listItem listType="bulleted" listIndent=1>BX</listItem>
 *		<listItem listType="bulleted" listIndent=2>Y/listItem>
 *		<listItem listType="bulleted" listIndent=2>C</listItem>
 *
 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
 * @param {Array} args Arguments of {@link module:engine/model/model~Model#insertContent}.
 */
function modelIndentPasteFixer( evt, [ content, selectable ] ) {
	// Check whether inserted content starts from a `listItem`. If it does not, it means that there are some other
	// elements before it and there is no need to fix indents, because even if we insert that content into a list,
	// that list will be broken.
	// Note: we also need to handle singular elements because inserting item with indent 0 into 0,1,[],2
	// would create incorrect model.
	let item = content.is( 'documentFragment' ) ? content.getChild( 0 ) : content;

	let selection;

	if ( !selectable ) {
		selection = this.document.selection;
	} else {
		selection = this.createSelection( selectable );
	}

	if ( item && item.is( 'element', 'listItem' ) ) {
		// Get a reference list item. Inserted list items will be fixed according to that item.
		const pos = selection.getFirstPosition();
		let refItem = null;

		if ( pos.parent.is( 'element', 'listItem' ) ) {
			refItem = pos.parent;
		} else if ( pos.nodeBefore && pos.nodeBefore.is( 'element', 'listItem' ) ) {
			refItem = pos.nodeBefore;
		}

		// If there is `refItem` it means that we do insert list items into an existing list.
		if ( refItem ) {
			// First list item in `data` has indent equal to 0 (it is a first list item). It should have indent equal
			// to the indent of reference item. We have to fix the first item and all of it's children and following siblings.
			// Indent of all those items has to be adjusted to reference item.
			const indentChange = refItem.getAttribute( 'listIndent' );

			// Fix only if there is anything to fix.
			if ( indentChange > 0 ) {
				// Adjust indent of all "first" list items in inserted data.
				while ( item && item.is( 'element', 'listItem' ) ) {
					item._setAttribute( 'listIndent', item.getAttribute( 'listIndent' ) + indentChange );

					item = item.nextSibling;
				}
			}
		}
	}
}

// Helper function that converts children of a given `<li>` view element into corresponding model elements.
// The function maintains proper order of elements if model `listItem` is split during the conversion
// due to block children conversion.
//
// @param {module:engine/model/element~Element} listItemModel List item model element to which converted children will be inserted.
// @param {Iterable.<module:engine/view/node~Node>} viewChildren View elements which will be converted.
// @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion interface to be used by the callback.
// @returns {module:engine/model/position~Position} Position on which next elements should be inserted after children conversion.
function viewToModelListItemChildrenConverter( listItemModel, viewChildren, conversionApi ) {
	const { writer, schema } = conversionApi;

	// A position after the last inserted `listItem`.
	let nextPosition = writer.createPositionAfter( listItemModel );

	// Check all children of the converted `<li>`. At this point we assume there are no "whitespace" view text nodes
	// in view list, between view list items. This should be handled by `<ul>` and `<ol>` converters.
	for ( const child of viewChildren ) {
		if ( child.name == 'ul' || child.name == 'ol' ) {
			// If the children is a list, we will insert its conversion result after currently handled `listItem`.
			// Then, next insertion position will be set after all the new list items (and maybe other elements if
			// something split list item).
			//
			// If this is a list, we expect that some `listItem`s and possibly other blocks will be inserted, however `.modelCursor`
			// should be set after last `listItem` (or block). This is why it feels safe to use it as `nextPosition`
			nextPosition = conversionApi.convertItem( child, nextPosition ).modelCursor;
		} else {
			// If this is not a list, try inserting content at the end of the currently handled `listItem`.
			const result = conversionApi.convertItem( child, writer.createPositionAt( listItemModel, 'end' ) );

			// It may end up that the current `listItem` becomes split (if that content cannot be inside `listItem`). For example:
			//
			// <li><p>Foo</p></li>
			//
			// will be converted to:
			//
			// <listItem></listItem><paragraph>Foo</paragraph><listItem></listItem>
			//
			const convertedChild = result.modelRange.start.nodeAfter;
			const wasSplit = convertedChild && convertedChild.is( 'element' ) && !schema.checkChild( listItemModel, convertedChild.name );

			if ( wasSplit ) {
				// As `lastListItem` got split, we need to update it to the second part of the split `listItem` element.
				//
				// `modelCursor` should be set to a position where the conversion should continue. There are multiple possible scenarios
				// that may happen. Usually, `modelCursor` (marked as `#` below) would point to the second list item after conversion:
				//
				//		`<li><p>Foo</p></li>` -> `<listItem></listItem><paragraph>Foo</paragraph><listItem>#</listItem>`
				//
				// However, in some cases, like auto-paragraphing, the position is placed at the end of the block element:
				//
				//		`<li><div>Foo</div></li>` -> `<listItem></listItem><paragraph>Foo#</paragraph><listItem></listItem>`
				//
				// or after an element if another element broken auto-paragraphed element:
				//
				//		`<li><div><h2>Foo</h2></div></li>` -> `<listItem></listItem><heading1>Foo</heading1>#<listItem></listItem>`
				//
				// We need to check for such cases and use proper list item and position based on it.
				//
				if ( result.modelCursor.parent.is( 'element', 'listItem' ) ) {
					// (1).
					listItemModel = result.modelCursor.parent;
				} else {
					// (2), (3).
					listItemModel = findNextListItem( result.modelCursor );
				}

				nextPosition = writer.createPositionAfter( listItemModel );
			}
		}
	}

	return nextPosition;
}

// Helper function that seeks for a next list item starting from given `startPosition`.
function findNextListItem( startPosition ) {
	const treeWalker = new TreeWalker( { startPosition } );

	let value;

	do {
		value = treeWalker.next();
	} while ( !value.value.item.is( 'element', 'listItem' ) );

	return value.value.item;
}

// Helper function that takes all children of given `viewRemovedItem` and moves them in a correct place, according
// to other given parameters.
function hoistNestedLists( nextIndent, modelRemoveStartPosition, viewRemoveStartPosition, viewRemovedItem, conversionApi, model ) {
	// Find correct previous model list item element.
	// The element has to have either same or smaller indent than given reference indent.
	// This will be the model element which will get nested items (if it has smaller indent) or sibling items (if it has same indent).
	// Keep in mind that such element might not be found, if removed item was the first item.
	const prevModelItem = getSiblingListItem( modelRemoveStartPosition.nodeBefore, {
		sameIndent: true,
		smallerIndent: true,
		listIndent: nextIndent,
		foo: 'b'
	} );

	const mapper = conversionApi.mapper;
	const viewWriter = conversionApi.writer;

	// Indent of found element or `null` if the element has not been found.
	const prevIndent = prevModelItem ? prevModelItem.getAttribute( 'listIndent' ) : null;

	let insertPosition;

	if ( !prevModelItem ) {
		// If element has not been found, simply insert lists at the position where the removed item was:
		//
		// Lorem ipsum.
		// 1 --------           <--- this is removed, no previous list item, put nested items in place of removed item.
		//   1.1 --------       <--- this is reference indent.
		//     1.1.1 --------
		//     1.1.2 --------
		//   1.2 --------
		//
		// Becomes:
		//
		// Lorem ipsum.
		// 1.1 --------
		//   1.1.1 --------
		//   1.1.2 --------
		// 1.2 --------
		insertPosition = viewRemoveStartPosition;
	} else if ( prevIndent == nextIndent ) {
		// If element has been found and has same indent as reference indent it means that nested items should
		// become siblings of found element:
		//
		// 1 --------
		//   1.1 --------
		//   1.2 --------       <--- this is `prevModelItem`.
		// 2 --------           <--- this is removed, previous list item has indent same as reference indent.
		//   2.1 --------       <--- this is reference indent, this and 2.2 should become siblings of 1.2.
		//   2.2 --------
		//
		// Becomes:
		//
		// 1 --------
		//   1.1 --------
		//   1.2 --------
		//   2.1 --------
		//   2.2 --------
		const prevViewList = mapper.toViewElement( prevModelItem ).parent;
		insertPosition = viewWriter.createPositionAfter( prevViewList );
	} else {
		// If element has been found and has smaller indent as reference indent it means that nested items
		// should become nested items of found item:
		//
		// 1 --------           <--- this is `prevModelItem`.
		//   1.1 --------       <--- this is removed, previous list item has indent smaller than reference indent.
		//     1.1.1 --------   <--- this is reference indent, this and 1.1.1 should become nested items of 1.
		//     1.1.2 --------
		//   1.2 --------
		//
		// Becomes:
		//
		// 1 --------
		//   1.1.1 --------
		//   1.1.2 --------
		//   1.2 --------
		//
		// Note: in this case 1.1.1 have indent 2 while 1 have indent 0. In model that should not be possible,
		// because following item may have indent bigger only by one. But this is fixed by postfixer.
		const modelPosition = model.createPositionAt( prevModelItem, 'end' );
		insertPosition = mapper.toViewPosition( modelPosition );
	}

	insertPosition = positionAfterUiElements( insertPosition );

	// Handle multiple lists. This happens if list item has nested numbered and bulleted lists. Following lists
	// are inserted after the first list (no need to recalculate insertion position for them).
	for ( const child of [ ...viewRemovedItem.getChildren() ] ) {
		if ( isList$1( child ) ) {
			insertPosition = viewWriter.move( viewWriter.createRangeOn( child ), insertPosition ).end;

			mergeViewLists( viewWriter, child, child.nextSibling );
			mergeViewLists( viewWriter, child.previousSibling, child );
		}
	}
}

// Checks if view element is a list type (ul or ol).
//
// @param {module:engine/view/element~Element} viewElement
// @returns {Boolean}
function isList$1( viewElement ) {
	return viewElement.is( 'element', 'ol' ) || viewElement.is( 'element', 'ul' );
}

// Calculates the indent value for a list item. Handles HTML compliant and non-compliant lists.
//
// Also, fixes non HTML compliant lists indents:
//
//		before:                                     fixed list:
//		OL                                          OL
//		|-> LI (parent LIs: 0)                      |-> LI     (indent: 0)
//		    |-> OL                                  |-> OL
//		        |-> OL                                  |
//		        |   |-> OL                              |
//		        |       |-> OL                          |
//		        |           |-> LI (parent LIs: 1)      |-> LI (indent: 1)
//		        |-> LI (parent LIs: 1)                  |-> LI (indent: 1)
//
//		before:                                     fixed list:
//		OL                                          OL
//		|-> OL                                      |
//		    |-> OL                                  |
//		         |-> OL                             |
//		             |-> LI (parent LIs: 0)         |-> LI        (indent: 0)
//
//		before:                                     fixed list:
//		OL                                          OL
//		|-> LI (parent LIs: 0)                      |-> LI         (indent: 0)
//		|-> OL                                          |-> OL
//		    |-> LI (parent LIs: 0)                          |-> LI (indent: 1)
//
// @param {module:engine/view/element~Element} listItem
// @param {Object} conversionStore
// @returns {Number}
function getIndent( listItem ) {
	let indent = 0;

	let parent = listItem.parent;

	while ( parent ) {
		// Each LI in the tree will result in an increased indent for HTML compliant lists.
		if ( parent.is( 'element', 'li' ) ) {
			indent++;
		} else {
			// If however the list is nested in other list we should check previous sibling of any of the list elements...
			const previousSibling = parent.previousSibling;

			// ...because the we might need increase its indent:
			//		before:                           fixed list:
			//		OL                                OL
			//		|-> LI (parent LIs: 0)            |-> LI         (indent: 0)
			//		|-> OL                                |-> OL
			//		    |-> LI (parent LIs: 0)                |-> LI (indent: 1)
			if ( previousSibling && previousSibling.is( 'element', 'li' ) ) {
				indent++;
			}
		}

		parent = parent.parent;
	}

	return indent;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The engine of the list feature. It handles creating, editing and removing lists and list items.
 *
 * It registers the `'numberedList'`, `'bulletedList'`, `'indentList'` and `'outdentList'` commands.
 *
 * @extends module:core/plugin~Plugin
 */
class ListEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'ListEditing';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ Enter, Delete ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		// Schema.
		// Note: in case `$block` will ever be allowed in `listItem`, keep in mind that this feature
		// uses `Selection#getSelectedBlocks()` without any additional processing to obtain all selected list items.
		// If there are blocks allowed inside list item, algorithms using `getSelectedBlocks()` will have to be modified.
		editor.model.schema.register( 'listItem', {
			inheritAllFrom: '$block',
			allowAttributes: [ 'listType', 'listIndent' ]
		} );

		// Converters.
		const data = editor.data;
		const editing = editor.editing;

		editor.model.document.registerPostFixer( writer => modelChangePostFixer( editor.model, writer ) );

		editing.mapper.registerViewToModelLength( 'li', getViewListItemLength );
		data.mapper.registerViewToModelLength( 'li', getViewListItemLength );

		editing.mapper.on( 'modelToViewPosition', modelToViewPosition( editing.view ) );
		editing.mapper.on( 'viewToModelPosition', viewToModelPosition( editor.model ) );
		data.mapper.on( 'modelToViewPosition', modelToViewPosition( editing.view ) );

		editor.conversion.for( 'editingDowncast' )
			.add( dispatcher => {
				dispatcher.on( 'insert', modelViewSplitOnInsert, { priority: 'high' } );
				dispatcher.on( 'insert:listItem', modelViewInsertion( editor.model ) );
				dispatcher.on( 'attribute:listType:listItem', modelViewChangeType, { priority: 'high' } );
				dispatcher.on( 'attribute:listType:listItem', modelViewMergeAfterChangeType, { priority: 'low' } );
				dispatcher.on( 'attribute:listIndent:listItem', modelViewChangeIndent( editor.model ) );
				dispatcher.on( 'remove:listItem', modelViewRemove( editor.model ) );
				dispatcher.on( 'remove', modelViewMergeAfter, { priority: 'low' } );
			} );

		editor.conversion.for( 'dataDowncast' )
			.add( dispatcher => {
				dispatcher.on( 'insert', modelViewSplitOnInsert, { priority: 'high' } );
				dispatcher.on( 'insert:listItem', modelViewInsertion( editor.model ) );
			} );

		editor.conversion.for( 'upcast' )
			.add( dispatcher => {
				dispatcher.on( 'element:ul', cleanList, { priority: 'high' } );
				dispatcher.on( 'element:ol', cleanList, { priority: 'high' } );
				dispatcher.on( 'element:li', cleanListItem, { priority: 'high' } );
				dispatcher.on( 'element:li', viewModelConverter );
			} );

		// Fix indentation of pasted items.
		editor.model.on( 'insertContent', modelIndentPasteFixer, { priority: 'high' } );

		// Register commands for numbered and bulleted list.
		editor.commands.add( 'numberedList', new ListCommand( editor, 'numbered' ) );
		editor.commands.add( 'bulletedList', new ListCommand( editor, 'bulleted' ) );

		// Register commands for indenting.
		editor.commands.add( 'indentList', new IndentCommand( editor, 'forward' ) );
		editor.commands.add( 'outdentList', new IndentCommand( editor, 'backward' ) );

		const viewDocument = editing.view.document;

		// Overwrite default Enter key behavior.
		// If Enter key is pressed with selection collapsed in empty list item, outdent it instead of breaking it.
		this.listenTo( viewDocument, 'enter', ( evt, data ) => {
			const doc = this.editor.model.document;
			const positionParent = doc.selection.getLastPosition().parent;

			if ( doc.selection.isCollapsed && positionParent.name == 'listItem' && positionParent.isEmpty ) {
				this.editor.execute( 'outdentList' );

				data.preventDefault();
				evt.stop();
			}
		}, { context: 'li' } );

		// Overwrite default Backspace key behavior.
		// If Backspace key is pressed with selection collapsed on first position in first list item, outdent it. #83
		this.listenTo( viewDocument, 'delete', ( evt, data ) => {
			// Check conditions from those that require less computations like those immediately available.
			if ( data.direction !== 'backward' ) {
				return;
			}

			const selection = this.editor.model.document.selection;

			if ( !selection.isCollapsed ) {
				return;
			}

			const firstPosition = selection.getFirstPosition();

			if ( !firstPosition.isAtStart ) {
				return;
			}

			const positionParent = firstPosition.parent;

			if ( positionParent.name !== 'listItem' ) {
				return;
			}

			const previousIsAListItem = positionParent.previousSibling && positionParent.previousSibling.name === 'listItem';

			if ( previousIsAListItem ) {
				return;
			}

			this.editor.execute( 'outdentList' );

			data.preventDefault();
			evt.stop();
		}, { context: 'li' } );

		this.listenTo( editor.editing.view.document, 'tab', ( evt, data ) => {
			const commandName = data.shiftKey ? 'outdentList' : 'indentList';
			const command = this.editor.commands.get( commandName );

			if ( command.isEnabled ) {
				editor.execute( commandName );

				data.stopPropagation();
				data.preventDefault();
				evt.stop();
			}
		}, { context: 'li' } );
	}

	/**
	 * @inheritDoc
	 */
	afterInit() {
		const commands = this.editor.commands;

		const indent = commands.get( 'indent' );
		const outdent = commands.get( 'outdent' );

		if ( indent ) {
			indent.registerChildCommand( commands.get( 'indentList' ) );
		}

		if ( outdent ) {
			outdent.registerChildCommand( commands.get( 'outdentList' ) );
		}
	}
}

function getViewListItemLength( element ) {
	let length = 1;

	for ( const child of element.getChildren() ) {
		if ( child.name == 'ul' || child.name == 'ol' ) {
			for ( const item of child.getChildren() ) {
				length += getViewListItemLength( item );
			}
		}
	}

	return length;
}var numberedListIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M7 5.75c0 .414.336.75.75.75h9.5a.75.75 0 1 0 0-1.5h-9.5a.75.75 0 0 0-.75.75zM3.5 3v5H2V3.7H1v-1h2.5V3zM.343 17.857l2.59-3.257H2.92a.6.6 0 1 0-1.04 0H.302a2 2 0 1 1 3.995 0h-.001c-.048.405-.16.734-.333.988-.175.254-.59.692-1.244 1.312H4.3v1h-4l.043-.043zM7 14.75a.75.75 0 0 1 .75-.75h9.5a.75.75 0 1 1 0 1.5h-9.5a.75.75 0 0 1-.75-.75z\"/></svg>";
var bulletedListIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M7 5.75c0 .414.336.75.75.75h9.5a.75.75 0 1 0 0-1.5h-9.5a.75.75 0 0 0-.75.75zm-6 0C1 4.784 1.777 4 2.75 4c.966 0 1.75.777 1.75 1.75 0 .966-.777 1.75-1.75 1.75C1.784 7.5 1 6.723 1 5.75zm6 9c0 .414.336.75.75.75h9.5a.75.75 0 1 0 0-1.5h-9.5a.75.75 0 0 0-.75.75zm-6 0c0-.966.777-1.75 1.75-1.75.966 0 1.75.777 1.75 1.75 0 .966-.777 1.75-1.75 1.75-.966 0-1.75-.777-1.75-1.75z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The list UI feature. It introduces the `'numberedList'` and `'bulletedList'` buttons that
 * allow to convert paragraphs to and from list items and indent or outdent them.
 *
 * @extends module:core/plugin~Plugin
 */
class ListUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'ListUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const t = this.editor.t;

		// Create two buttons and link them with numberedList and bulletedList commands.
		createUIComponent( this.editor, 'numberedList', t( 'Numbered List' ), numberedListIcon );
		createUIComponent( this.editor, 'bulletedList', t( 'Bulleted List' ), bulletedListIcon );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The list feature.
 *
 * This is a "glue" plugin that loads the {@link module:list/list/listediting~ListEditing list editing feature}
 * and {@link module:list/list/listui~ListUI list UI feature}.
 *
 * @extends module:core/plugin~Plugin
 */
class List extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ ListEditing, ListUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'List';
	}
}

/**
 * The configuration of the {@link module:list/list~List list} feature
 * and the {@link module:list/documentlist~DocumentList document list} feature.
 *
 *		ClassicEditor
 *			.create( editorElement, {
 *				list:  ... // The list feature configuration.
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
 *
 * @interface ListConfig
 */

/**
 * The configuration of the {@link module:list/list~List} feature and the {@link module:list/documentlist~DocumentList} feature.
 *
 * Read more in {@link module:list/list~ListConfig}.
 *
 * @member {module:module:list/list~ListConfig} module:core/editor/editorconfig~EditorConfig#list
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module paste-from-office/filters/removeboldwrapper
 */

/**
 * Removes `<b>` tag wrapper added by Google Docs to a copied content.
 *
 * @param {module:engine/view/documentfragment~DocumentFragment} documentFragment element `data.content` obtained from clipboard
 * @param {module:engine/view/upcastwriter~UpcastWriter} writer
 */
function removeBoldWrapper( documentFragment, writer ) {
	for ( const child of documentFragment.getChildren() ) {
		if ( child.is( 'element', 'b' ) && child.getStyle( 'font-weight' ) === 'normal' ) {
			const childIndex = documentFragment.getChildIndex( child );

			writer.remove( child );
			writer.insertChild( childIndex, child.getChildren(), documentFragment );
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Transforms Word specific list-like elements to the semantic HTML lists.
 *
 * Lists in Word are represented by block elements with special attributes like:
 *
 *		<p class=MsoListParagraphCxSpFirst style='mso-list:l1 level1 lfo1'>...</p> // Paragraph based list.
 *		<h1 style='mso-list:l0 level1 lfo1'>...</h1> // Heading 1 based list.
 *
 * @param {module:engine/view/documentfragment~DocumentFragment} documentFragment The view structure to be transformed.
 * @param {String} stylesString Styles from which list-like elements styling will be extracted.
 */
function transformListItemLikeElementsIntoLists( documentFragment, stylesString ) {
	if ( !documentFragment.childCount ) {
		return;
	}

	const writer = new UpcastWriter( documentFragment.document );
	const itemLikeElements = findAllItemLikeElements( documentFragment, writer );

	if ( !itemLikeElements.length ) {
		return;
	}

	let currentList = null;
	let currentIndentation = 1;

	itemLikeElements.forEach( ( itemLikeElement, i ) => {
		const isDifferentList = isNewListNeeded( itemLikeElements[ i - 1 ], itemLikeElement );
		const previousItemLikeElement = isDifferentList ? null : itemLikeElements[ i - 1 ];
		const indentationDifference = getIndentationDifference( previousItemLikeElement, itemLikeElement );

		if ( isDifferentList ) {
			currentList = null;
			currentIndentation = 1;
		}

		if ( !currentList || indentationDifference !== 0 ) {
			const listStyle = detectListStyle( itemLikeElement, stylesString );

			if ( !currentList ) {
				currentList = insertNewEmptyList( listStyle, itemLikeElement.element, writer );
			} else if ( itemLikeElement.indent > currentIndentation ) {
				const lastListItem = currentList.getChild( currentList.childCount - 1 );
				const lastListItemChild = lastListItem.getChild( lastListItem.childCount - 1 );

				currentList = insertNewEmptyList( listStyle, lastListItemChild, writer );
				currentIndentation += 1;
			} else if ( itemLikeElement.indent < currentIndentation ) {
				const differentIndentation = currentIndentation - itemLikeElement.indent;

				currentList = findParentListAtLevel( currentList, differentIndentation );
				currentIndentation = parseInt( itemLikeElement.indent );
			}

			if ( itemLikeElement.indent <= currentIndentation ) {
				if ( !currentList.is( 'element', listStyle.type ) ) {
					currentList = writer.rename( listStyle.type, currentList );
				}
			}
		}

		const listItem = transformElementIntoListItem( itemLikeElement.element, writer );

		writer.appendChild( listItem, currentList );
	} );
}

/**
 * Removes paragraph wrapping content inside a list item.
 *
 * @param {module:engine/view/documentfragment~DocumentFragment} documentFragment
 * @param {module:engine/view/upcastwriter~UpcastWriter} writer
 */
function unwrapParagraphInListItem( documentFragment, writer ) {
	for ( const value of writer.createRangeIn( documentFragment ) ) {
		const element = value.item;

		if ( element.is( 'element', 'li' ) ) {
			// Google Docs allows for single paragraph inside LI.
			const firstChild = element.getChild( 0 );

			if ( firstChild && firstChild.is( 'element', 'p' ) ) {
				writer.unwrapElement( firstChild );
			}
		}
	}
}

// Finds all list-like elements in a given document fragment.
//
// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment
// in which to look for list-like nodes.
// @param {module:engine/view/upcastwriter~UpcastWriter} writer
// @returns {Array.<Object>} Array of found list-like items. Each item is an object containing:
//
//		* {module:engine/src/view/element~Element} element List-like element.
//		* {Number} id List item id parsed from `mso-list` style (see `getListItemData()` function).
//		* {Number} order List item creation order parsed from `mso-list` style (see `getListItemData()` function).
//		* {Number} indent List item indentation level parsed from `mso-list` style (see `getListItemData()` function).
function findAllItemLikeElements( documentFragment, writer ) {
	const range = writer.createRangeIn( documentFragment );

	// Matcher for finding list-like elements.
	const itemLikeElementsMatcher = new Matcher( {
		name: /^p|h\d+$/,
		styles: {
			'mso-list': /.*/
		}
	} );

	const itemLikeElements = [];

	for ( const value of range ) {
		if ( value.type === 'elementStart' && itemLikeElementsMatcher.match( value.item ) ) {
			const itemData = getListItemData( value.item );

			itemLikeElements.push( {
				element: value.item,
				id: itemData.id,
				order: itemData.order,
				indent: itemData.indent
			} );
		}
	}

	return itemLikeElements;
}

// Extracts list item style from the provided CSS.
//
// List item style is extracted from the CSS stylesheet. Each list with its specific style attribute
// value (`mso-list:l1 level1 lfo1`) has its dedicated properties in a CSS stylesheet defined with a selector like:
//
// 		@list l1:level1 { ... }
//
// It contains `mso-level-number-format` property which defines list numbering/bullet style. If this property
// is not defined it means default `decimal` numbering.
//
// Here CSS string representation is used as `mso-level-number-format` property is an invalid CSS property
// and will be removed during CSS parsing.
//
// @param {Object} listLikeItem List-like item for which list style will be searched for. Usually
// a result of `findAllItemLikeElements()` function.
// @param {String} stylesString CSS stylesheet.
// @returns {Object} result
// @returns {String} result.type List type, could be `ul` or `ol`.
// @returns {Number} result.startIndex List start index, valid only for ordered lists.
// @returns {String|null} result.style List style, for example: `decimal`, `lower-roman`, etc. It is extracted
// directly from Word stylesheet and adjusted to represent proper values for the CSS `list-style-type` property.
// If it cannot be adjusted, the `null` value is returned.
function detectListStyle( listLikeItem, stylesString ) {
	const listStyleRegexp = new RegExp( `@list l${ listLikeItem.id }:level${ listLikeItem.indent }\\s*({[^}]*)`, 'gi' );
	const listStyleTypeRegex = /mso-level-number-format:([^;]{0,100});/gi;
	const listStartIndexRegex = /mso-level-start-at:\s{0,100}([0-9]{0,10})\s{0,100};/gi;

	const listStyleMatch = listStyleRegexp.exec( stylesString );

	let listStyleType = 'decimal'; // Decimal is default one.
	let type = 'ol'; // <ol> is default list.
	let startIndex = null;

	if ( listStyleMatch && listStyleMatch[ 1 ] ) {
		const listStyleTypeMatch = listStyleTypeRegex.exec( listStyleMatch[ 1 ] );

		if ( listStyleTypeMatch && listStyleTypeMatch[ 1 ] ) {
			listStyleType = listStyleTypeMatch[ 1 ].trim();
			type = listStyleType !== 'bullet' && listStyleType !== 'image' ? 'ol' : 'ul';
		}

		// Styles for the numbered lists are always defined in the Word CSS stylesheet.
		// Unordered lists MAY contain a value for the Word CSS definition `mso-level-text` but sometimes
		// this tag is missing. And because of that, we cannot depend on that. We need to predict the list style value
		// based on the list style marker element.
		if ( listStyleType === 'bullet' ) {
			const bulletedStyle = findBulletedListStyle( listLikeItem.element );

			if ( bulletedStyle ) {
				listStyleType = bulletedStyle;
			}
		} else {
			const listStartIndexMatch = listStartIndexRegex.exec( listStyleMatch[ 1 ] );

			if ( listStartIndexMatch && listStartIndexMatch[ 1 ] ) {
				startIndex = parseInt( listStartIndexMatch[ 1 ] );
			}
		}
	}

	return {
		type,
		startIndex,
		style: mapListStyleDefinition( listStyleType )
	};
}

// Tries to extract the `list-style-type` value based on the marker element for bulleted list.
//
// @param {module:engine/view/element~Element} element
// @returns {String|null}
function findBulletedListStyle( element ) {
	const listMarkerElement = findListMarkerNode( element );

	if ( !listMarkerElement ) {
		return null;
	}

	const listMarker = listMarkerElement._data;

	if ( listMarker === 'o' ) {
		return 'circle';
	} else if ( listMarker === '·' ) {
		return 'disc';
	}
	// Word returns '§' instead of '■' for the square list style.
	else if ( listMarker === '§' ) {
		return 'square';
	}

	return null;
}

// Tries to find a text node that represents the marker element (list-style-type).
//
// @param {module:engine/view/element~Element} element
// @returns {module:engine/view/text~Text|null}
function findListMarkerNode( element ) {
	// If the first child is a text node, it is the data for the element.
	// The list-style marker is not present here.
	if ( element.getChild( 0 ).is( '$text' ) ) {
		return null;
	}

	for ( const childNode of element.getChildren() ) {
		// The list-style marker will be inside the `<span>` element. Let's ignore all non-span elements.
		// It may happen that the `<a>` element is added as the first child. Most probably, it's an anchor element.
		if ( !childNode.is( 'element', 'span' ) ) {
			continue;
		}

		const textNodeOrElement = childNode.getChild( 0 );

		// If already found the marker element, use it.
		if ( textNodeOrElement.is( '$text' ) ) {
			return textNodeOrElement;
		}

		return textNodeOrElement.getChild( 0 );
	}
}

// Parses the `list-style-type` value extracted directly from the Word CSS stylesheet and returns proper CSS definition.
//
// @param {String|null} value
// @returns {String|null}
function mapListStyleDefinition( value ) {
	if ( value.startsWith( 'arabic-leading-zero' ) ) {
		return 'decimal-leading-zero';
	}

	switch ( value ) {
		case 'alpha-upper':
			return 'upper-alpha';
		case 'alpha-lower':
			return 'lower-alpha';
		case 'roman-upper':
			return 'upper-roman';
		case 'roman-lower':
			return 'lower-roman';
		case 'circle':
		case 'disc':
		case 'square':
			return value;
		default:
			return null;
	}
}

// Creates an empty list of a given type and inserts it after a specified element.
//
// @param {Object} listStyle List style object which determines the type of newly created list.
// Usually a result of `detectListStyle()` function.
// @param {module:engine/view/element~Element} element Element after which list is inserted.
// @param {module:engine/view/upcastwriter~UpcastWriter} writer
// @returns {module:engine/view/element~Element} Newly created list element.

function insertNewEmptyList( listStyle, element, writer ) {
	const parent = element.parent;
	const list = writer.createElement( listStyle.type );
	const position = parent.getChildIndex( element ) + 1;

	writer.insertChild( position, list, parent );

	// We do not support modifying the marker for a particular list item.
	// Set the value for the `list-style-type` property directly to the list container.
	if ( listStyle.style ) {
		writer.setStyle( 'list-style-type', listStyle.style, list );
	}

	if ( listStyle.startIndex && listStyle.startIndex > 1 ) {
		writer.setAttribute( 'start', listStyle.startIndex, list );
	}

	return list;
}

// Transforms a given element into a semantic list item. As the function operates on a provided
// {module:engine/src/view/element~Element element} it will modify the view structure to which this element belongs.
//
// @param {module:engine/view/element~Element} element Element which will be transformed into a list item.
// @param {module:engine/view/upcastwriter~UpcastWriter} writer
// @returns {module:engine/view/element~Element} New element to which the given one was transformed. It is
// inserted in place of the old element (the reference to the old element is lost due to renaming).
function transformElementIntoListItem( element, writer ) {
	removeBulletElement( element, writer );

	return writer.rename( 'li', element );
}

// Extracts list item information from Word specific list-like element style:
//
//		`style="mso-list:l1 level1 lfo1"`
//
// where:
//
//		* `l1` is a list id (however it does not mean this is a continuous list - see #43),
//		* `level1` is a list item indentation level,
//		* `lfo1` is a list insertion order in a document.
//
// @param {module:engine/view/element~Element} element Element from which style data is extracted.
// @returns {Object} result
// @returns {Number} result.id Parent list id.
// @returns {Number} result.order List item creation order.
// @returns {Number} result.indent List item indentation level.
function getListItemData( element ) {
	const data = {};
	const listStyle = element.getStyle( 'mso-list' );

	if ( listStyle ) {
		const idMatch = listStyle.match( /(^|\s{1,100})l(\d+)/i );
		const orderMatch = listStyle.match( /\s{0,100}lfo(\d+)/i );
		const indentMatch = listStyle.match( /\s{0,100}level(\d+)/i );

		if ( idMatch && orderMatch && indentMatch ) {
			data.id = idMatch[ 2 ];
			data.order = orderMatch[ 1 ];
			data.indent = indentMatch[ 1 ];
		}
	}

	return data;
}

// Removes span with a numbering/bullet from a given element.
//
// @param {module:engine/view/element~Element} element
// @param {module:engine/view/upcastwriter~UpcastWriter} writer
function removeBulletElement( element, writer ) {
	// Matcher for finding `span` elements holding lists numbering/bullets.
	const bulletMatcher = new Matcher( {
		name: 'span',
		styles: {
			'mso-list': 'Ignore'
		}
	} );

	const range = writer.createRangeIn( element );

	for ( const value of range ) {
		if ( value.type === 'elementStart' && bulletMatcher.match( value.item ) ) {
			writer.remove( value.item );
		}
	}
}

// Whether the previous and current items belong to the same list. It is determined based on `item.id`
// (extracted from `mso-list` style, see #getListItemData) and a previous sibling of the current item.
//
// However, it's quite easy to change the `id` attribute for nested lists in Word. It will break the list feature while pasting.
// Let's check also the `indent` attribute. If the difference between those two elements is equal to 1, we can assume that
// the `currentItem` is a beginning of the nested list because lists in CKEditor 5 always start with the `indent=0` attribute.
// See: https://github.com/ckeditor/ckeditor5/issues/7805.
//
// @param {Object} previousItem
// @param {Object} currentItem
// @returns {Boolean}
function isNewListNeeded( previousItem, currentItem ) {
	if ( !previousItem ) {
		return true;
	}

	if ( previousItem.id !== currentItem.id ) {
		// See: https://github.com/ckeditor/ckeditor5/issues/7805.
		//
		// * List item 1.
		//     - Nested list item 1.
		if ( currentItem.indent - previousItem.indent === 1 ) {
			return false;
		}

		return true;
	}

	const previousSibling = currentItem.element.previousSibling;

	if ( !previousSibling ) {
		return true;
	}

	// Even with the same id the list does not have to be continuous (#43).
	return !isList( previousSibling );
}

function isList( element ) {
	return element.is( 'element', 'ol' ) || element.is( 'element', 'ul' );
}

// Calculates the indentation difference between two given list items (based on the indent attribute
// extracted from the `mso-list` style, see #getListItemData).
//
// @param {Object} previousItem
// @param {Object} currentItem
// @returns {Number}
function getIndentationDifference( previousItem, currentItem ) {
	return previousItem ? currentItem.indent - previousItem.indent : currentItem.indent - 1;
}

// Finds the parent list element (ul/ol) of a given list element with indentation level lower by a given value.
//
// @param {module:engine/view/element~Element} listElement List element from which to start looking for a parent list.
// @param {Number} indentationDifference Indentation difference between lists.
// @returns {module:engine/view/element~Element} Found list element with indentation level lower by a given value.
function findParentListAtLevel( listElement, indentationDifference ) {
	const ancestors = listElement.getAncestors( { parentFirst: true } );

	let parentList = null;
	let levelChange = 0;

	for ( const ancestor of ancestors ) {
		if ( ancestor.name === 'ul' || ancestor.name === 'ol' ) {
			levelChange++;
		}

		if ( levelChange === indentationDifference ) {
			parentList = ancestor;
			break;
		}
	}

	return parentList;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const googleDocsMatch = /id=("|')docs-internal-guid-[-0-9a-f]+("|')/i;

/**
 * Normalizer for the content pasted from Google Docs.
 *
 * @implements module:paste-from-office/normalizer~Normalizer
 */
class GoogleDocsNormalizer {
	/**
	 * Creates a new `GoogleDocsNormalizer` instance.
	 *
	 * @param {module:engine/view/document~Document} document View document.
	 */
	constructor( document ) {
		/**
		 * @readonly
		 * @type {module:engine/view/document~Document}
		 */
		this.document = document;
	}

	/**
	 * @inheritDoc
	 */
	isActive( htmlString ) {
		return googleDocsMatch.test( htmlString );
	}

	/**
	 * @inheritDoc
	 */
	execute( data ) {
		const writer = new UpcastWriter( this.document );
		const { body: documentFragment } = data._parsedData;

		removeBoldWrapper( documentFragment, writer );
		unwrapParagraphInListItem( documentFragment, writer );

		data.content = documentFragment;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Replaces source attribute of all `<img>` elements representing regular
 * images (not the Word shapes) with inlined base64 image representation extracted from RTF or Blob data.
 *
 * @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment on which transform images.
 * @param {String} rtfData The RTF data from which images representation will be used.
 */
function replaceImagesSourceWithBase64( documentFragment, rtfData ) {
	if ( !documentFragment.childCount ) {
		return;
	}

	const upcastWriter = new UpcastWriter();
	const shapesIds = findAllShapesIds( documentFragment, upcastWriter );

	removeAllImgElementsRepresentingShapes( shapesIds, documentFragment, upcastWriter );
	removeAllShapeElements( documentFragment, upcastWriter );

	const images = findAllImageElementsWithLocalSource( documentFragment, upcastWriter );

	if ( images.length ) {
		replaceImagesFileSourceWithInlineRepresentation( images, extractImageDataFromRtf( rtfData ), upcastWriter );
	}
}

/**
 * Converts given HEX string to base64 representation.
 *
 * @protected
 * @param {String} hexString The HEX string to be converted.
 * @returns {String} Base64 representation of a given HEX string.
 */
function _convertHexToBase64( hexString ) {
	return btoa( hexString.match( /\w{2}/g ).map( char => {
		return String.fromCharCode( parseInt( char, 16 ) );
	} ).join( '' ) );
}

// Finds all shapes (`<v:*>...</v:*>`) ids. Shapes can represent images (canvas)
// or Word shapes (which does not have RTF or Blob representation).
//
// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment
// from which to extract shape ids.
// @param {module:engine/view/upcastwriter~UpcastWriter} writer
// @returns {Array.<String>} Array of shape ids.
function findAllShapesIds( documentFragment, writer ) {
	const range = writer.createRangeIn( documentFragment );

	const shapeElementsMatcher = new Matcher( {
		name: /v:(.+)/
	} );

	const shapesIds = [];

	for ( const value of range ) {
		if ( value.type != 'elementStart' ) {
			continue;
		}

		const el = value.item;
		const prevSiblingName = el.previousSibling && el.previousSibling.name || null;

		// If shape element have 'o:gfxdata' attribute and is not directly before `<v:shapetype>` element it means it represent Word shape.
		if ( shapeElementsMatcher.match( el ) && el.getAttribute( 'o:gfxdata' ) && prevSiblingName !== 'v:shapetype' ) {
			shapesIds.push( value.item.getAttribute( 'id' ) );
		}
	}

	return shapesIds;
}

// Removes all `<img>` elements which represents Word shapes and not regular images.
//
// @param {Array.<String>} shapesIds Shape ids which will be checked against `<img>` elements.
// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment from which to remove `<img>` elements.
// @param {module:engine/view/upcastwriter~UpcastWriter} writer
function removeAllImgElementsRepresentingShapes( shapesIds, documentFragment, writer ) {
	const range = writer.createRangeIn( documentFragment );

	const imageElementsMatcher = new Matcher( {
		name: 'img'
	} );

	const imgs = [];

	for ( const value of range ) {
		if ( imageElementsMatcher.match( value.item ) ) {
			const el = value.item;
			const shapes = el.getAttribute( 'v:shapes' ) ? el.getAttribute( 'v:shapes' ).split( ' ' ) : [];

			if ( shapes.length && shapes.every( shape => shapesIds.indexOf( shape ) > -1 ) ) {
				imgs.push( el );
			// Shapes may also have empty source while content is paste in some browsers (Safari).
			} else if ( !el.getAttribute( 'src' ) ) {
				imgs.push( el );
			}
		}
	}

	for ( const img of imgs ) {
		writer.remove( img );
	}
}

// Removes all shape elements (`<v:*>...</v:*>`) so they do not pollute the output structure.
//
// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment from which to remove shape elements.
// @param {module:engine/view/upcastwriter~UpcastWriter} writer
function removeAllShapeElements( documentFragment, writer ) {
	const range = writer.createRangeIn( documentFragment );

	const shapeElementsMatcher = new Matcher( {
		name: /v:(.+)/
	} );

	const shapes = [];

	for ( const value of range ) {
		if ( value.type == 'elementStart' && shapeElementsMatcher.match( value.item ) ) {
			shapes.push( value.item );
		}
	}

	for ( const shape of shapes ) {
		writer.remove( shape );
	}
}

// Finds all `<img>` elements in a given document fragment which have source pointing to local `file://` resource.
//
// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment Document fragment in which to look for `<img>` elements.
// @param {module:engine/view/upcastwriter~UpcastWriter} writer
// @returns {Object} result All found images grouped by source type.
// @returns {Array.<module:engine/view/element~Element>} result.file Array of found `<img>` elements with `file://` source.
// @returns {Array.<module:engine/view/element~Element>} result.blob Array of found `<img>` elements with `blob:` source.
function findAllImageElementsWithLocalSource( documentFragment, writer ) {
	const range = writer.createRangeIn( documentFragment );

	const imageElementsMatcher = new Matcher( {
		name: 'img'
	} );

	const imgs = [];

	for ( const value of range ) {
		if ( imageElementsMatcher.match( value.item ) ) {
			if ( value.item.getAttribute( 'src' ).startsWith( 'file://' ) ) {
				imgs.push( value.item );
			}
		}
	}

	return imgs;
}

// Extracts all images HEX representations from a given RTF data.
//
// @param {String} rtfData The RTF data from which to extract images HEX representation.
// @returns {Array.<Object>} Array of found HEX representations. Each array item is an object containing:
//
// 		* {String} hex Image representation in HEX format.
// 		* {string} type Type of image, `image/png` or `image/jpeg`.
function extractImageDataFromRtf( rtfData ) {
	if ( !rtfData ) {
		return [];
	}

	const regexPictureHeader = /{\\pict[\s\S]+?\\bliptag-?\d+(\\blipupi-?\d+)?({\\\*\\blipuid\s?[\da-fA-F]+)?[\s}]*?/;
	const regexPicture = new RegExp( '(?:(' + regexPictureHeader.source + '))([\\da-fA-F\\s]+)\\}', 'g' );
	const images = rtfData.match( regexPicture );
	const result = [];

	if ( images ) {
		for ( const image of images ) {
			let imageType = false;

			if ( image.includes( '\\pngblip' ) ) {
				imageType = 'image/png';
			} else if ( image.includes( '\\jpegblip' ) ) {
				imageType = 'image/jpeg';
			}

			if ( imageType ) {
				result.push( {
					hex: image.replace( regexPictureHeader, '' ).replace( /[^\da-fA-F]/g, '' ),
					type: imageType
				} );
			}
		}
	}

	return result;
}

// Replaces `src` attribute value of all given images with the corresponding base64 image representation.
//
// @param {Array.<module:engine/view/element~Element>} imageElements Array of image elements which will have its source replaced.
// @param {Array.<Object>} imagesHexSources Array of images hex sources (usually the result of `extractImageDataFromRtf()` function).
// The array should be the same length as `imageElements` parameter.
// @param {module:engine/view/upcastwriter~UpcastWriter} writer
function replaceImagesFileSourceWithInlineRepresentation( imageElements, imagesHexSources, writer ) {
	// Assume there is an equal amount of image elements and images HEX sources so they can be matched accordingly based on existing order.
	if ( imageElements.length === imagesHexSources.length ) {
		for ( let i = 0; i < imageElements.length; i++ ) {
			const newSrc = `data:${ imagesHexSources[ i ].type };base64,${ _convertHexToBase64( imagesHexSources[ i ].hex ) }`;
			writer.setAttribute( 'src', newSrc, imageElements[ i ] );
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const msWordMatch1 = /<meta\s*name="?generator"?\s*content="?microsoft\s*word\s*\d+"?\/?>/i;
const msWordMatch2 = /xmlns:o="urn:schemas-microsoft-com/i;

/**
 * Normalizer for the content pasted from Microsoft Word.
 *
 * @implements module:paste-from-office/normalizer~Normalizer
 */
class MSWordNormalizer {
	/**
	 * Creates a new `MSWordNormalizer` instance.
	 *
	 * @param {module:engine/view/document~Document} document View document.
	 */
	constructor( document ) {
		/**
		 * @readonly
		 * @type {module:engine/view/document~Document}
		 */
		this.document = document;
	}

	/**
	 * @inheritDoc
	 */
	isActive( htmlString ) {
		return msWordMatch1.test( htmlString ) || msWordMatch2.test( htmlString );
	}

	/**
	 * @inheritDoc
	 */
	execute( data ) {
		const { body: documentFragment, stylesString } = data._parsedData;

		transformListItemLikeElementsIntoLists( documentFragment, stylesString );
		replaceImagesSourceWithBase64( documentFragment, data.dataTransfer.getData( 'text/rtf' ) );

		data.content = documentFragment;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module paste-from-office/filters/space
 */

/**
 * Replaces last space preceding elements closing tag with `&nbsp;`. Such operation prevents spaces from being removed
 * during further DOM/View processing (see especially {@link module:engine/view/domconverter~DomConverter#_processDataFromDomText}).
 * This method also takes into account Word specific `<o:p></o:p>` empty tags.
 * Additionally multiline sequences of spaces and new lines between tags are removed (see #39 and #40).
 *
 * @param {String} htmlString HTML string in which spacing should be normalized.
 * @returns {String} Input HTML with spaces normalized.
 */
function normalizeSpacing( htmlString ) {
	// Run normalizeSafariSpaceSpans() two times to cover nested spans.
	return normalizeSafariSpaceSpans( normalizeSafariSpaceSpans( htmlString ) )
		// Remove all \r\n from "spacerun spans" so the last replace line doesn't strip all whitespaces.
		.replace( /(<span\s+style=['"]mso-spacerun:yes['"]>[^\S\r\n]*?)[\r\n]+([^\S\r\n]*<\/span>)/g, '$1$2' )
		.replace( /<span\s+style=['"]mso-spacerun:yes['"]><\/span>/g, '' )
		.replace( / <\//g, '\u00A0</' )
		.replace( / <o:p><\/o:p>/g, '\u00A0<o:p></o:p>' )
		// Remove <o:p> block filler from empty paragraph. Safari uses \u00A0 instead of &nbsp;.
		.replace( /<o:p>(&nbsp;|\u00A0)<\/o:p>/g, '' )
		// Remove all whitespaces when they contain any \r or \n.
		.replace( />([^\S\r\n]*[\r\n]\s*)</g, '><' );
}

/**
 * Normalizes spacing in special Word `spacerun spans` (`<span style='mso-spacerun:yes'>\s+</span>`) by replacing
 * all spaces with `&nbsp; ` pairs. This prevents spaces from being removed during further DOM/View processing
 * (see especially {@link module:engine/view/domconverter~DomConverter#_processDataFromDomText}).
 *
 * @param {Document} htmlDocument Native `Document` object in which spacing should be normalized.
 */
function normalizeSpacerunSpans( htmlDocument ) {
	htmlDocument.querySelectorAll( 'span[style*=spacerun]' ).forEach( el => {
		const innerTextLength = el.innerText.length || 0;

		el.innerText = Array( innerTextLength + 1 ).join( '\u00A0 ' ).substr( 0, innerTextLength );
	} );
}

// Normalizes specific spacing generated by Safari when content pasted from Word (`<span class="Apple-converted-space"> </span>`)
// by replacing all spaces sequences longer than 1 space with `&nbsp; ` pairs. This prevents spaces from being removed during
// further DOM/View processing (see especially {@link module:engine/view/domconverter~DomConverter#_processDataFromDomText}).
//
// This function is similar to {@link module:clipboard/utils/normalizeclipboarddata normalizeClipboardData util} but uses
// regular spaces / &nbsp; sequence for replacement.
//
// @param {String} htmlString HTML string in which spacing should be normalized
// @returns {String} Input HTML with spaces normalized.
function normalizeSafariSpaceSpans( htmlString ) {
	return htmlString.replace( /<span(?: class="Apple-converted-space"|)>(\s+)<\/span>/g, ( fullMatch, spaces ) => {
		return spaces.length === 1 ? ' ' : Array( spaces.length + 1 ).join( '\u00A0 ' ).substr( 0, spaces.length );
	} );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Parses provided HTML extracting contents of `<body>` and `<style>` tags.
 *
 * @param {String} htmlString HTML string to be parsed.
 * @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor
 * @returns {Object} result
 * @returns {module:engine/view/documentfragment~DocumentFragment} result.body Parsed body
 * content as a traversable structure.
 * @returns {String} result.bodyString Entire body content as a string.
 * @returns {Array.<CSSStyleSheet>} result.styles Array of native `CSSStyleSheet` objects, each representing
 * separate `style` tag from the source HTML.
 * @returns {String} result.stylesString All `style` tags contents combined in the order of occurrence into one string.
 */
function parseHtml( htmlString, stylesProcessor ) {
	const domParser = new DOMParser();

	// Remove Word specific "if comments" so content inside is not omitted by the parser.
	htmlString = htmlString.replace( /<!--\[if gte vml 1]>/g, '' );

	const normalizedHtml = normalizeSpacing( cleanContentAfterBody( htmlString ) );

	// Parse htmlString as native Document object.
	const htmlDocument = domParser.parseFromString( normalizedHtml, 'text/html' );

	normalizeSpacerunSpans( htmlDocument );

	// Get `innerHTML` first as transforming to View modifies the source document.
	const bodyString = htmlDocument.body.innerHTML;

	// Transform document.body to View.
	const bodyView = documentToView( htmlDocument, stylesProcessor );

	// Extract stylesheets.
	const stylesObject = extractStyles( htmlDocument );

	return {
		body: bodyView,
		bodyString,
		styles: stylesObject.styles,
		stylesString: stylesObject.stylesString
	};
}

// Transforms native `Document` object into {@link module:engine/view/documentfragment~DocumentFragment}. Comments are skipped.
//
// @param {Document} htmlDocument Native `Document` object to be transformed.
// @param {module:engine/view/stylesmap~StylesProcessor} stylesProcessor
// @returns {module:engine/view/documentfragment~DocumentFragment}
function documentToView( htmlDocument, stylesProcessor ) {
	const viewDocument = new Document$1( stylesProcessor );
	const domConverter = new DomConverter( viewDocument, { renderingMode: 'data' } );
	const fragment = htmlDocument.createDocumentFragment();
	const nodes = htmlDocument.body.childNodes;

	while ( nodes.length > 0 ) {
		fragment.appendChild( nodes[ 0 ] );
	}

	return domConverter.domToView( fragment, { skipComments: true } );
}

// Extracts both `CSSStyleSheet` and string representation from all `style` elements available in a provided `htmlDocument`.
//
// @param {Document} htmlDocument Native `Document` object from which styles will be extracted.
// @returns {Object} result
// @returns {Array.<CSSStyleSheet>} result.styles Array of native `CSSStyleSheet` object, each representing
// separate `style` tag from the source object.
// @returns {String} result.stylesString All `style` tags contents combined in the order of occurrence as one string.
function extractStyles( htmlDocument ) {
	const styles = [];
	const stylesString = [];
	const styleTags = Array.from( htmlDocument.getElementsByTagName( 'style' ) );

	for ( const style of styleTags ) {
		if ( style.sheet && style.sheet.cssRules && style.sheet.cssRules.length ) {
			styles.push( style.sheet );
			stylesString.push( style.innerHTML );
		}
	}

	return {
		styles,
		stylesString: stylesString.join( ' ' )
	};
}

// Removes leftover content from between closing </body> and closing </html> tag:
//
// 		<html><body><p>Foo Bar</p></body><span>Fo</span></html> -> <html><body><p>Foo Bar</p></body></html>
//
// This function is used as specific browsers (Edge) add some random content after `body` tag when pasting from Word.
// @param {String} htmlString The HTML string to be cleaned.
// @returns {String} The HTML string with leftover content removed.
function cleanContentAfterBody( htmlString ) {
	const bodyCloseTag = '</body>';
	const htmlCloseTag = '</html>';

	const bodyCloseIndex = htmlString.indexOf( bodyCloseTag );

	if ( bodyCloseIndex < 0 ) {
		return htmlString;
	}

	const htmlCloseIndex = htmlString.indexOf( htmlCloseTag, bodyCloseIndex + bodyCloseTag.length );

	return htmlString.substring( 0, bodyCloseIndex + bodyCloseTag.length ) +
		( htmlCloseIndex >= 0 ? htmlString.substring( htmlCloseIndex ) : '' );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The Paste from Office plugin.
 *
 * This plugin handles content pasted from Office apps and transforms it (if necessary)
 * to a valid structure which can then be understood by the editor features.
 *
 * Transformation is made by a set of predefined {@link module:paste-from-office/normalizer~Normalizer normalizers}.
 * This plugin includes following normalizers:
 *   * {@link module:paste-from-office/normalizers/mswordnormalizer~MSWordNormalizer Microsoft Word normalizer}
 *   * {@link module:paste-from-office/normalizers/googledocsnormalizer~GoogleDocsNormalizer Google Docs normalizer}
 *
 * For more information about this feature check the {@glink api/paste-from-office package page}.
 *
 * @extends module:core/plugin~Plugin
 */
class PasteFromOffice extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'PasteFromOffice';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ ClipboardPipeline ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const viewDocument = editor.editing.view.document;
		const normalizers = [];

		normalizers.push( new MSWordNormalizer( viewDocument ) );
		normalizers.push( new GoogleDocsNormalizer( viewDocument ) );

		editor.plugins.get( 'ClipboardPipeline' ).on(
			'inputTransformation',
			( evt, data ) => {
				if ( data._isTransformedWithPasteFromOffice ) {
					return;
				}

				const codeBlock = editor.model.document.selection.getFirstPosition().parent;

				if ( codeBlock.is( 'element', 'codeBlock' ) ) {
					return;
				}

				const htmlString = data.dataTransfer.getData( 'text/html' );
				const activeNormalizer = normalizers.find( normalizer => normalizer.isActive( htmlString ) );

				if ( activeNormalizer ) {
					data._parsedData = parseHtml( htmlString, viewDocument.stylesProcessor );

					activeNormalizer.execute( data );

					data._isTransformedWithPasteFromOffice = true;
				}
			},
			{ priority: 'high' }
		);
	}
}var removeFormatIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M8.69 14.915c.053.052.173.083.36.093a.366.366 0 0 1 .345.485l-.003.01a.738.738 0 0 1-.697.497h-2.67a.374.374 0 0 1-.353-.496l.013-.038a.681.681 0 0 1 .644-.458c.197-.012.325-.043.386-.093a.28.28 0 0 0 .072-.11L9.592 4.5H6.269c-.359-.017-.609.013-.75.09-.142.078-.289.265-.442.563-.192.29-.516.464-.864.464H4.17a.43.43 0 0 1-.407-.569L4.46 3h13.08l-.62 2.043a.81.81 0 0 1-.775.574h-.114a.486.486 0 0 1-.486-.486c.001-.284-.054-.464-.167-.54-.112-.076-.367-.106-.766-.091h-3.28l-2.68 10.257c-.006.074.007.127.038.158zM3 17h8a.5.5 0 1 1 0 1H3a.5.5 0 1 1 0-1zm11.299 1.17a.75.75 0 1 1-1.06-1.06l1.414-1.415-1.415-1.414a.75.75 0 0 1 1.06-1.06l1.415 1.414 1.414-1.415a.75.75 0 1 1 1.06 1.06l-1.413 1.415 1.414 1.415a.75.75 0 0 1-1.06 1.06l-1.415-1.414-1.414 1.414z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const REMOVE_FORMAT = 'removeFormat';

/**
 * The remove format UI plugin. It registers the `'removeFormat'` button which can be
 * used in the toolbar.
 *
 * @extends module:core/plugin~Plugin
 */
class RemoveFormatUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'RemoveFormatUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		editor.ui.componentFactory.add( REMOVE_FORMAT, locale => {
			const command = editor.commands.get( REMOVE_FORMAT );
			const view = new ButtonView( locale );

			view.set( {
				label: t( 'Remove Format' ),
				icon: removeFormatIcon,
				tooltip: true
			} );

			view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

			// Execute the command.
			this.listenTo( view, 'execute', () => {
				editor.execute( REMOVE_FORMAT );
				editor.editing.view.focus();
			} );

			return view;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The remove format command.
 *
 * It is used by the {@link module:remove-format/removeformat~RemoveFormat remove format feature}
 * to clear the formatting in the selection.
 *
 *		editor.execute( 'removeFormat' );
 *
 * @extends module:core/command~Command
 */
class RemoveFormatCommand extends Command {
	/**
	 * @inheritDoc
	 */
	refresh() {
		const model = this.editor.model;

		this.isEnabled = !!first( this._getFormattingItems( model.document.selection, model.schema ) );
	}

	/**
	 * @inheritDoc
	 */
	execute() {
		const model = this.editor.model;
		const schema = model.schema;

		model.change( writer => {
			for ( const item of this._getFormattingItems( model.document.selection, schema ) ) {
				if ( item.is( 'selection' ) ) {
					for ( const attributeName of this._getFormattingAttributes( item, schema ) ) {
						writer.removeSelectionAttribute( attributeName );
					}
				} else {
					// Workaround for items with multiple removable attributes. See
					// https://github.com/ckeditor/ckeditor5-remove-format/pull/1#pullrequestreview-220515609
					const itemRange = writer.createRangeOn( item );

					for ( const attributeName of this._getFormattingAttributes( item, schema ) ) {
						writer.removeAttribute( attributeName, itemRange );
					}
				}
			}
		} );
	}

	/**
	 * Returns an iterable of items in a selection (including the selection itself) that have formatting model
	 * attributes to be removed by the feature.
	 *
	 * @protected
	 * @param {module:engine/model/documentselection~DocumentSelection} selection
	 * @param {module:engine/model/schema~Schema} schema The schema describing the item.
	 * @returns {Iterable.<module:engine/model/item~Item>|Iterable.<module:engine/model/documentselection~DocumentSelection>}
	 */
	* _getFormattingItems( selection, schema ) {
		const itemHasRemovableFormatting = item => {
			return !!first( this._getFormattingAttributes( item, schema ) );
		};

		// Check formatting on selected items that are not blocks.
		for ( const curRange of selection.getRanges() ) {
			for ( const item of curRange.getItems() ) {
				if ( !schema.isBlock( item ) && itemHasRemovableFormatting( item ) ) {
					yield item;
				}
			}
		}

		// Check formatting from selected blocks.
		for ( const block of selection.getSelectedBlocks() ) {
			if ( itemHasRemovableFormatting( block ) ) {
				yield block;
			}
		}

		// Finally the selection might be formatted as well, so make sure to check it.
		if ( itemHasRemovableFormatting( selection ) ) {
			yield selection;
		}
	}

	/**
	 * Returns an iterable of formatting attributes of a given model item.
	 *
	 * **Note:** Formatting items have the `isFormatting` property set to `true`.
	 *
	 * @protected
	 * @param {module:engine/model/item~Item|module:engine/model/documentselection~DocumentSelection} item
	 * @param {module:engine/model/schema~Schema} schema The schema describing the item.
	 * @returns {Iterable.<String>} The names of formatting attributes found in a given item.
	 */
	* _getFormattingAttributes( item, schema ) {
		for ( const [ attributeName ] of item.getAttributes() ) {
			const attributeProperties = schema.getAttributeProperties( attributeName );

			if ( attributeProperties && attributeProperties.isFormatting ) {
				yield attributeName;
			}
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The remove format editing plugin.
 *
 * It registers the {@link module:remove-format/removeformatcommand~RemoveFormatCommand removeFormat} command.
 *
 * @extends module:core/plugin~Plugin
 */
class RemoveFormatEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'RemoveFormatEditing';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		editor.commands.add( 'removeFormat', new RemoveFormatCommand( editor ) );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The remove format plugin.
 *
 * This is a "glue" plugin which loads the {@link module:remove-format/removeformatediting~RemoveFormatEditing}
 * and {@link module:remove-format/removeformatui~RemoveFormatUI} plugins.
 *
 * For a detailed overview, check out the {@glink features/remove-format remove format} feature documentation.
 *
 * @extends module:core/plugin~Plugin
 */
class RemoveFormat extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ RemoveFormatEditing, RemoveFormatUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'RemoveFormat';
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module table/utils/common
 */

/**
 * A common method to update the numeric value. If a value is the default one, it will be unset.
 *
 * @param {String} key An attribute key.
 * @param {*} value The new attribute value.
 * @param {module:engine/model/item~Item} item A model item on which the attribute will be set.
 * @param {module:engine/model/writer~Writer} writer
 * @param {*} defaultValue The default attribute value. If a value is lower or equal, it will be unset.
 */
function updateNumericAttribute( key, value, item, writer, defaultValue = 1 ) {
	if ( value > defaultValue ) {
		writer.setAttribute( key, value, item );
	} else {
		writer.removeAttribute( key, item );
	}
}

/**
 * A common method to create an empty table cell. It creates a proper model structure as a table cell must have at least one block inside.
 *
 * @param {module:engine/model/writer~Writer} writer The model writer.
 * @param {module:engine/model/position~Position} insertPosition The position at which the table cell should be inserted.
 * @param {Object} attributes The element attributes.
 * @returns {module:engine/model/element~Element} Created table cell.
 */
function createEmptyTableCell( writer, insertPosition, attributes = {} ) {
	const tableCell = writer.createElement( 'tableCell', attributes );

	writer.insertElement( 'paragraph', tableCell );
	writer.insert( tableCell, insertPosition );

	return tableCell;
}

/**
 * Checks if a table cell belongs to the heading column section.
 *
 * @param {module:table/tableutils~TableUtils} tableUtils
 * @param {module:engine/model/element~Element} tableCell
 * @returns {Boolean}
 */
function isHeadingColumnCell( tableUtils, tableCell ) {
	const table = tableCell.parent.parent;
	const headingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
	const { column } = tableUtils.getCellLocation( tableCell );

	return !!headingColumns && column < headingColumns;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Returns a function that converts the table view representation:
 *
 *		<figure class="table"><table>...</table></figure>
 *
 * to the model representation:
 *
 *		<table></table>
 *
 * @returns {Function}
 */
function upcastTableFigure() {
	return dispatcher => {
		dispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
			// Do not convert if this is not a "table figure".
			if ( !conversionApi.consumable.test( data.viewItem, { name: true, classes: 'table' } ) ) {
				return;
			}

			// Find an table element inside the figure element.
			const viewTable = getViewTableFromFigure( data.viewItem );

			// Do not convert if table element is absent or was already converted.
			if ( !viewTable || !conversionApi.consumable.test( viewTable, { name: true } ) ) {
				return;
			}

			// Consume the figure to prevent other converters from processing it again.
			conversionApi.consumable.consume( data.viewItem, { name: true, classes: 'table' } );

			// Convert view table to model table.
			const conversionResult = conversionApi.convertItem( viewTable, data.modelCursor );

			// Get table element from conversion result.
			const modelTable = first( conversionResult.modelRange.getItems() );

			// When table wasn't successfully converted then finish conversion.
			if ( !modelTable ) {
				// Revert consumed figure so other features can convert it.
				conversionApi.consumable.revert( data.viewItem, { name: true, classes: 'table' } );

				return;
			}

			conversionApi.convertChildren( data.viewItem, conversionApi.writer.createPositionAt( modelTable, 'end' ) );
			conversionApi.updateConversionResult( modelTable, data );
		} );
	};
}

/**
 * View table element to model table element conversion helper.
 *
 * This conversion helper converts the table element as well as table rows.
 *
 * @returns {Function} Conversion helper.
 */
function upcastTable() {
	return dispatcher => {
		dispatcher.on( 'element:table', ( evt, data, conversionApi ) => {
			const viewTable = data.viewItem;

			// When element was already consumed then skip it.
			if ( !conversionApi.consumable.test( viewTable, { name: true } ) ) {
				return;
			}

			const { rows, headingRows, headingColumns } = scanTable( viewTable );

			// Only set attributes if values is greater then 0.
			const attributes = {};

			if ( headingColumns ) {
				attributes.headingColumns = headingColumns;
			}

			if ( headingRows ) {
				attributes.headingRows = headingRows;
			}

			const table = conversionApi.writer.createElement( 'table', attributes );

			if ( !conversionApi.safeInsert( table, data.modelCursor ) ) {
				return;
			}

			conversionApi.consumable.consume( viewTable, { name: true } );

			// Upcast table rows in proper order (heading rows first).
			rows.forEach( row => conversionApi.convertItem( row, conversionApi.writer.createPositionAt( table, 'end' ) ) );

			// Convert everything else.
			conversionApi.convertChildren( viewTable, conversionApi.writer.createPositionAt( table, 'end' ) );

			// Create one row and one table cell for empty table.
			if ( table.isEmpty ) {
				const row = conversionApi.writer.createElement( 'tableRow' );
				conversionApi.writer.insert( row, conversionApi.writer.createPositionAt( table, 'end' ) );

				createEmptyTableCell( conversionApi.writer, conversionApi.writer.createPositionAt( row, 'end' ) );
			}

			conversionApi.updateConversionResult( table, data );
		} );
	};
}

/**
 * A conversion helper that skips empty <tr> elements from upcasting at the beginning of the table.
 *
 * An empty row is considered a table model error but when handling clipboard data there could be rows that contain only row-spanned cells
 * and empty TR-s are used to maintain the table structure (also {@link module:table/tablewalker~TableWalker} assumes that there are only
 * rows that have related `tableRow` elements).
 *
 * *Note:* Only the first empty rows are removed because they have no meaning and it solves the issue
 * of an improper table with all empty rows.
 *
 * @returns {Function} Conversion helper.
 */
function skipEmptyTableRow() {
	return dispatcher => {
		dispatcher.on( 'element:tr', ( evt, data ) => {
			if ( data.viewItem.isEmpty && data.modelCursor.index == 0 ) {
				evt.stop();
			}
		}, { priority: 'high' } );
	};
}

/**
 * A converter that ensures an empty paragraph is inserted in a table cell if no other content was converted.
 *
 * @returns {Function} Conversion helper.
 */
function ensureParagraphInTableCell( elementName ) {
	return dispatcher => {
		dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
			// The default converter will create a model range on converted table cell.
			if ( !data.modelRange ) {
				return;
			}

			// Ensure a paragraph in the model for empty table cells for converted table cells.
			if ( data.viewItem.isEmpty ) {
				const tableCell = data.modelRange.start.nodeAfter;
				const modelCursor = conversionApi.writer.createPositionAt( tableCell, 0 );

				conversionApi.writer.insertElement( 'paragraph', modelCursor );
			}
		}, { priority: 'low' } );
	};
}

// Get view `<table>` element from the view widget (`<figure>`).
//
// @private
// @param {module:engine/view/element~Element} figureView
// @returns {module:engine/view/element~Element}
function getViewTableFromFigure( figureView ) {
	for ( const figureChild of figureView.getChildren() ) {
		if ( figureChild.is( 'element', 'table' ) ) {
			return figureChild;
		}
	}
}

// Scans table rows and extracts required metadata from the table:
//
// headingRows    - The number of rows that go as table headers.
// headingColumns - The maximum number of row headings.
// rows           - Sorted `<tr>` elements as they should go into the model - ie. if `<thead>` is inserted after `<tbody>` in the view.
//
// @private
// @param {module:engine/view/element~Element} viewTable
// @returns {{headingRows, headingColumns, rows}}
function scanTable( viewTable ) {
	const tableMeta = {
		headingRows: 0,
		headingColumns: 0
	};

	// The `<tbody>` and `<thead>` sections in the DOM do not have to be in order `<thead>` -> `<tbody>` and there might be more than one
	// of them.
	// As the model does not have these sections, rows from different sections must be sorted.
	// For example, below is a valid HTML table:
	//
	//		<table>
	//			<tbody><tr><td>2</td></tr></tbody>
	//			<thead><tr><td>1</td></tr></thead>
	//			<tbody><tr><td>3</td></tr></tbody>
	//		</table>
	//
	// But browsers will render rows in order as: 1 as the heading and 2 and 3 as the body.
	const headRows = [];
	const bodyRows = [];

	// Currently the editor does not support more then one <thead> section.
	// Only the first <thead> from the view will be used as a heading row and the others will be converted to body rows.
	let firstTheadElement;

	for ( const tableChild of Array.from( viewTable.getChildren() ) ) {
		// Only `<thead>`, `<tbody>` & `<tfoot>` from allowed table children can have `<tr>`s.
		// The else is for future purposes (mainly `<caption>`).
		if ( tableChild.name === 'tbody' || tableChild.name === 'thead' || tableChild.name === 'tfoot' ) {
			// Save the first `<thead>` in the table as table header - all other ones will be converted to table body rows.
			if ( tableChild.name === 'thead' && !firstTheadElement ) {
				firstTheadElement = tableChild;
			}

			// There might be some extra empty text nodes between the `<tr>`s.
			// Make sure further code operates on `tr`s only. (#145)
			const trs = Array.from( tableChild.getChildren() ).filter( el => el.is( 'element', 'tr' ) );

			for ( const tr of trs ) {
				// This <tr> is a child of a first <thead> element.
				if ( tr.parent.name === 'thead' && tr.parent === firstTheadElement ) {
					tableMeta.headingRows++;
					headRows.push( tr );
				} else {
					bodyRows.push( tr );
					// For other rows check how many column headings this row has.

					const headingCols = scanRowForHeadingColumns( tr);

					if ( headingCols > tableMeta.headingColumns ) {
						tableMeta.headingColumns = headingCols;
					}
				}
			}
		}
	}

	tableMeta.rows = [ ...headRows, ...bodyRows ];

	return tableMeta;
}

// Scans a `<tr>` element and its children for metadata:
// - For heading row:
//     - Adds this row to either the heading or the body rows.
//     - Updates the number of heading rows.
// - For body rows:
//     - Calculates the number of column headings.
//
// @private
// @param {module:engine/view/element~Element} tr
// @returns {Number}
function scanRowForHeadingColumns( tr ) {
	let headingColumns = 0;
	let index = 0;

	// Filter out empty text nodes from tr children.
	const children = Array.from( tr.getChildren() )
		.filter( child => child.name === 'th' || child.name === 'td' );

	// Count starting adjacent <th> elements of a <tr>.
	while ( index < children.length && children[ index ].name === 'th' ) {
		const th = children[ index ];

		// Adjust columns calculation by the number of spanned columns.
		const colspan = parseInt( th.getAttribute( 'colspan' ) || 1 );

		headingColumns = headingColumns + colspan;
		index++;
	}

	return headingColumns;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module table/tablewalker
 */

// @if CK_DEBUG // import { CKEditorError } from 'ckeditor5/src/utils';

/**
 * The table iterator class. It allows to iterate over table cells. For each cell the iterator yields
 * {@link module:table/tablewalker~TableSlot} with proper table cell attributes.
 */
class TableWalker {
	/**
	 * Creates an instance of the table walker.
	 *
	 * The table walker iterates internally by traversing the table from row index = 0 and column index = 0.
	 * It walks row by row and column by column in order to output values defined in the constructor.
	 * By default it will output only the locations that are occupied by a cell. To include also spanned rows and columns,
	 * pass the `includeAllSlots` option to the constructor.
	 *
	 * The most important values of the iterator are column and row indexes of a cell.
	 *
	 * See {@link module:table/tablewalker~TableSlot} what values are returned by the table walker.
	 *
	 * To iterate over a given row:
	 *
	 *		const tableWalker = new TableWalker( table, { startRow: 1, endRow: 2 } );
	 *
	 *		for ( const tableSlot of tableWalker ) {
	 *			console.log( 'A cell at row', tableSlot.row, 'and column', tableSlot.column );
	 *		}
	 *
	 * For instance the code above for the following table:
	 *
	 *		+----+----+----+----+----+----+
	 *		| 00      | 02 | 03 | 04 | 05 |
	 *		|         +----+----+----+----+
	 *		|         | 12      | 14 | 15 |
	 *		|         +----+----+----+    +
	 *		|         | 22           |    |
	 *		|----+----+----+----+----+    +
	 *		| 30 | 31 | 32 | 33 | 34 |    |
	 *		+----+----+----+----+----+----+
	 *
	 * will log in the console:
	 *
	 *		'A cell at row 1 and column 2'
	 *		'A cell at row 1 and column 4'
	 *		'A cell at row 1 and column 5'
	 *		'A cell at row 2 and column 2'
	 *
	 * To also iterate over spanned cells:
	 *
	 *		const tableWalker = new TableWalker( table, { row: 1, includeAllSlots: true } );
	 *
	 *		for ( const tableSlot of tableWalker ) {
	 *			console.log( 'Slot at', tableSlot.row, 'x', tableSlot.column, ':', tableSlot.isAnchor ? 'is anchored' : 'is spanned' );
	 *		}
	 *
	 * will log in the console for the table from the previous example:
	 *
	 *		'Cell at 1 x 0 : is spanned'
	 *		'Cell at 1 x 1 : is spanned'
	 *		'Cell at 1 x 2 : is anchored'
	 *		'Cell at 1 x 3 : is spanned'
	 *		'Cell at 1 x 4 : is anchored'
	 *		'Cell at 1 x 5 : is anchored'
	 *
	 * **Note**: Option `row` is a shortcut that sets both `startRow` and `endRow` to the same row.
	 * (Use either `row` or `startRow` and `endRow` but never together). Similarly the `column` option sets both `startColumn`
	 * and `endColumn` to the same column (Use either `column` or `startColumn` and `endColumn` but never together).
	 *
	 * @constructor
	 * @param {module:engine/model/element~Element} table A table over which the walker iterates.
	 * @param {Object} [options={}] An object with configuration.
	 * @param {Number} [options.row] A row index for which this iterator will output cells.
	 * Can't be used together with `startRow` and `endRow`.
	 * @param {Number} [options.startRow=0] A row index from which this iterator should start. Can't be used together with `row`.
	 * @param {Number} [options.endRow] A row index at which this iterator should end. Can't be used together with `row`.
	 * @param {Number} [options.column] A column index for which this iterator will output cells.
	 * Can't be used together with `startColumn` and `endColumn`.
	 * @param {Number} [options.startColumn=0] A column index from which this iterator should start. Can't be used together with `column`.
	 * @param {Number} [options.endColumn] A column index at which this iterator should end. Can't be used together with `column`.
	 * @param {Boolean} [options.includeAllSlots=false] Also return values for spanned cells.
	 */
	constructor( table, options = {} ) {
		/**
		 * The walker's table element.
		 *
		 * @readonly
		 * @member {module:engine/model/element~Element}
		 * @protected
		 */
		this._table = table;

		/**
		 * A row index from which this iterator will start.
		 *
		 * @readonly
		 * @member {Number}
		 * @private
		 */
		this._startRow = options.row !== undefined ? options.row : options.startRow || 0;

		/**
		 * A row index at which this iterator will end.
		 *
		 * @readonly
		 * @member {Number}
		 * @private
		 */
		this._endRow = options.row !== undefined ? options.row : options.endRow;

		/**
		 * If set, the table walker will only output cells from a given column and following ones or cells that overlap them.
		 *
		 * @readonly
		 * @member {Number}
		 * @private
		 */
		this._startColumn = options.column !== undefined ? options.column : options.startColumn || 0;

		/**
		 * If set, the table walker will only output cells up to a given column.
		 *
		 * @readonly
		 * @member {Number}
		 * @private
		 */
		this._endColumn = options.column !== undefined ? options.column : options.endColumn;

		/**
		 * Enables output of spanned cells that are normally not yielded.
		 *
		 * @readonly
		 * @member {Boolean}
		 * @private
		 */
		this._includeAllSlots = !!options.includeAllSlots;

		/**
		 * Row indexes to skip from the iteration.
		 *
		 * @readonly
		 * @member {Set<Number>}
		 * @private
		 */
		this._skipRows = new Set();

		/**
		 * The current row index.
		 *
		 * @member {Number}
		 * @protected
		 */
		this._row = 0;

		/**
		 * The index of the current row element in the table.
		 *
		 * @type {Number}
		 * @protected
		 */
		this._rowIndex = 0;

		/**
		 * The current column index.
		 *
		 * @member {Number}
		 * @protected
		 */
		this._column = 0;

		/**
		 * The cell index in a parent row. For spanned cells when {@link #_includeAllSlots} is set to `true`,
		 * this represents the index of the next table cell.
		 *
		 * @member {Number}
		 * @protected
		 */
		this._cellIndex = 0;

		/**
		 * Holds a map of spanned cells in a table.
		 *
		 * @readonly
		 * @member {Map.<Number, Map.<Number, Object>>}
		 * @private
		 */
		this._spannedCells = new Map();

		/**
		 * Index of the next column where a cell is anchored.
		 *
		 * @member {Number}
		 * @private
		 */
		this._nextCellAtColumn = -1;
	}

	/**
	 * Iterable interface.
	 *
	 * @returns {Iterable.<module:table/tablewalker~TableSlot>}
	 */
	[ Symbol.iterator ]() {
		return this;
	}

	/**
	 * Gets the next table walker's value.
	 *
	 * @returns {module:table/tablewalker~TableSlot} The next table walker's value.
	 */
	next() {
		const row = this._table.getChild( this._rowIndex );

		// Iterator is done when there's no row (table ended) or the row is after `endRow` limit.
		if ( !row || this._isOverEndRow() ) {
			return { done: true };
		}

		// We step over current element when it is not a tableRow instance.
		if ( !row.is( 'element', 'tableRow' ) ) {
			this._rowIndex++;

			return this.next();
		}

		if ( this._isOverEndColumn() ) {
			return this._advanceToNextRow();
		}

		let outValue = null;

		const spanData = this._getSpanned();

		if ( spanData ) {
			if ( this._includeAllSlots && !this._shouldSkipSlot() ) {
				outValue = this._formatOutValue( spanData.cell, spanData.row, spanData.column );
			}
		} else {
			const cell = row.getChild( this._cellIndex );

			if ( !cell ) {
				// If there are no more cells left in row advance to the next row.
				return this._advanceToNextRow();
			}

			const colspan = parseInt( cell.getAttribute( 'colspan' ) || 1 );
			const rowspan = parseInt( cell.getAttribute( 'rowspan' ) || 1 );

			// Record this cell spans if it's not 1x1 cell.
			if ( colspan > 1 || rowspan > 1 ) {
				this._recordSpans( cell, rowspan, colspan );
			}

			if ( !this._shouldSkipSlot() ) {
				outValue = this._formatOutValue( cell );
			}

			this._nextCellAtColumn = this._column + colspan;
		}

		// Advance to the next column before returning value.
		this._column++;

		if ( this._column == this._nextCellAtColumn ) {
			this._cellIndex++;
		}

		// The current value will be returned only if current row and column are not skipped.
		return outValue || this.next();
	}

	/**
	 * Marks a row to skip in the next iteration. It will also skip cells from the current row if there are any cells from the current row
	 * to output.
	 *
	 * @param {Number} row The row index to skip.
	 */
	skipRow( row ) {
		this._skipRows.add( row );
	}

	/**
	 * Advances internal cursor to the next row.
	 *
	 * @private
	 * @returns {module:table/tablewalker~TableSlot}
	 */
	_advanceToNextRow() {
		this._row++;
		this._rowIndex++;
		this._column = 0;
		this._cellIndex = 0;
		this._nextCellAtColumn = -1;

		return this.next();
	}

	/**
	 * Checks if the current row is over {@link #_endRow}.
	 *
	 * @private
	 * @returns {Boolean}
	 */
	_isOverEndRow() {
		// If #_endRow is defined skip all rows after it.
		return this._endRow !== undefined && this._row > this._endRow;
	}

	/**
	 * Checks if the current cell is over {@link #_endColumn}
	 *
	 * @private
	 * @returns {Boolean}
	 */
	_isOverEndColumn() {
		// If #_endColumn is defined skip all cells after it.
		return this._endColumn !== undefined && this._column > this._endColumn;
	}

	/**
	 * A common method for formatting the iterator's output value.
	 *
	 * @private
	 * @param {module:engine/model/element~Element} cell The table cell to output.
	 * @param {Number} [anchorRow] The row index of a cell anchor slot.
	 * @param {Number} [anchorColumn] The column index of a cell anchor slot.
	 * @returns {{done: Boolean, value: {cell: *, row: Number, column: *, rowspan: *, colspan: *, cellIndex: Number}}}
	 */
	_formatOutValue( cell, anchorRow = this._row, anchorColumn = this._column ) {
		return {
			done: false,
			value: new TableSlot( this, cell, anchorRow, anchorColumn )
		};
	}

	/**
	 * Checks if the current slot should be skipped.
	 *
	 * @private
	 * @returns {Boolean}
	 */
	_shouldSkipSlot() {
		const rowIsMarkedAsSkipped = this._skipRows.has( this._row );
		const rowIsBeforeStartRow = this._row < this._startRow;

		const columnIsBeforeStartColumn = this._column < this._startColumn;
		const columnIsAfterEndColumn = this._endColumn !== undefined && this._column > this._endColumn;

		return rowIsMarkedAsSkipped || rowIsBeforeStartRow || columnIsBeforeStartColumn || columnIsAfterEndColumn;
	}

	/**
	 * Returns the cell element that is spanned over the current cell location.
	 *
	 * @private
	 * @returns {module:engine/model/element~Element}
	 */
	_getSpanned() {
		const rowMap = this._spannedCells.get( this._row );

		// No spans for given row.
		if ( !rowMap ) {
			return null;
		}

		// If spans for given rows has entry for column it means that this location if spanned by other cell.
		return rowMap.get( this._column ) || null;
	}

	/**
	 * Updates spanned cells map relative to the current cell location and its span dimensions.
	 *
	 * @private
	 * @param {module:engine/model/element~Element} cell A cell that is spanned.
	 * @param {Number} rowspan Cell height.
	 * @param {Number} colspan Cell width.
	 */
	_recordSpans( cell, rowspan, colspan ) {
		const data = {
			cell,
			row: this._row,
			column: this._column
		};

		for ( let rowToUpdate = this._row; rowToUpdate < this._row + rowspan; rowToUpdate++ ) {
			for ( let columnToUpdate = this._column; columnToUpdate < this._column + colspan; columnToUpdate++ ) {
				if ( rowToUpdate != this._row || columnToUpdate != this._column ) {
					this._markSpannedCell( rowToUpdate, columnToUpdate, data );
				}
			}
		}
	}

	/**
	 * Marks the cell location as spanned by another cell.
	 *
	 * @private
	 * @param {Number} row The row index of the cell location.
	 * @param {Number} column The column index of the cell location.
	 * @param {Object} data A spanned cell details (cell element, anchor row and column).
	 */
	_markSpannedCell( row, column, data ) {
		if ( !this._spannedCells.has( row ) ) {
			this._spannedCells.set( row, new Map() );
		}

		const rowSpans = this._spannedCells.get( row );

		rowSpans.set( column, data );
	}
}

/**
 * An object returned by {@link module:table/tablewalker~TableWalker} when traversing table cells.
 */
class TableSlot {
	/**
	 * Creates an instance of the table walker value.
	 *
	 * @protected
	 * @param {module:table/tablewalker~TableWalker} tableWalker The table walker instance.
	 * @param {module:engine/model/element~Element} cell The current table cell.
	 * @param {Number} anchorRow The row index of a cell anchor slot.
	 * @param {Number} anchorColumn The column index of a cell anchor slot.
	 */
	constructor( tableWalker, cell, anchorRow, anchorColumn ) {
		/**
		 * The current table cell.
		 *
		 * @readonly
		 * @member {module:engine/model/element~Element}
		 */
		this.cell = cell;

		/**
		 * The row index of a table slot.
		 *
		 * @readonly
		 * @member {Number}
		 */
		this.row = tableWalker._row;

		/**
		 * The column index of a table slot.
		 *
		 * @readonly
		 * @member {Number}
		 */
		this.column = tableWalker._column;

		/**
		 * The row index of a cell anchor slot.
		 *
		 * @readonly
		 * @member {Number}
		 */
		this.cellAnchorRow = anchorRow;

		/**
		 * The column index of a cell anchor slot.
		 *
		 * @readonly
		 * @member {Number}
		 */
		this.cellAnchorColumn = anchorColumn;

		/**
		 * The index of the current cell in the parent row.
		 *
		 * @readonly
		 * @member {Number}
		 * @private
		 */
		this._cellIndex = tableWalker._cellIndex;

		/**
		 * The index of the current row element in the table.
		 *
		 * @readonly
		 * @member {Number}
		 * @private
		 */
		this._rowIndex = tableWalker._rowIndex;

		/**
		 * The table element.
		 *
		 * @readonly
		 * @member {module:engine/model/element~Element}
		 * @private
		 */
		this._table = tableWalker._table;
	}

	/**
	 * Whether the cell is anchored in the current slot.
	 *
	 * @readonly
	 * @returns {Boolean}
	 */
	get isAnchor() {
		return this.row === this.cellAnchorRow && this.column === this.cellAnchorColumn;
	}

	/**
	 * The width of a cell defined by a `colspan` attribute. If the model attribute is not present, it is set to `1`.
	 *
	 * @readonly
	 * @returns {Number}
	 */
	get cellWidth() {
		return parseInt( this.cell.getAttribute( 'colspan' ) || 1 );
	}

	/**
	 * The height of a cell defined by a `rowspan` attribute. If the model attribute is not present, it is set to `1`.
	 *
	 * @readonly
	 * @returns {Number}
	 */
	get cellHeight() {
		return parseInt( this.cell.getAttribute( 'rowspan' ) || 1 );
	}

	/**
	 * The index of the current row element in the table.
	 *
	 * @readonly
	 * @returns {Number}
	 */
	get rowIndex() {
		return this._rowIndex;
	}

	/**
	 * Returns the {@link module:engine/model/position~Position} before the table slot.
	 *
	 * @returns {module:engine/model/position~Position}
	 */
	getPositionBefore() {
		const model = this._table.root.document.model;

		return model.createPositionAt( this._table.getChild( this.row ), this._cellIndex );
	}

	// @if CK_DEBUG // get isSpanned() { throwMissingGetterError( 'isSpanned' ); }
	// @if CK_DEBUG // get colspan() { throwMissingGetterError( 'colspan' ); }
	// @if CK_DEBUG // get rowspan() { throwMissingGetterError( 'rowspan' ); }
	// @if CK_DEBUG // get cellIndex() { throwMissingGetterError( 'cellIndex' ); }
}

/**
 * This `TableSlot`'s getter (property) was removed in CKEditor 5 v20.0.0.
 *
 * Check out the new `TableWalker`'s API in the documentation.
 *
 * @error tableslot-getter-removed
 * @param {String} getterName
 */

// @if CK_DEBUG // function throwMissingGetterError( getterName ) {
// @if CK_DEBUG //		throw new CKEditorError( 'tableslot-getter-removed', this, {
// @if CK_DEBUG //			getterName
// @if CK_DEBUG //		} );
// @if CK_DEBUG // }
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Model table element to view table element conversion helper.
 *
 * @param {module:table/tableutils~TableUtils} tableUtils The `TableUtils` plugin instance.
 * @param {Object} [options]
 * @param {Boolean} [options.asWidget] If set to `true`, the downcast conversion will produce a widget.
 * @returns {Function} Element creator.
 */
function downcastTable( tableUtils, options = {} ) {
	return ( table, { writer } ) => {
		const headingRows = table.getAttribute( 'headingRows' ) || 0;
		const tableSections = [];

		// Table head slot.
		if ( headingRows > 0 ) {
			tableSections.push(
				writer.createContainerElement( 'thead', null,
					writer.createSlot( element => element.is( 'element', 'tableRow' ) && element.index < headingRows )
				)
			);
		}

		// Table body slot.
		if ( headingRows < tableUtils.getRows( table ) ) {
			tableSections.push(
				writer.createContainerElement( 'tbody', null,
					writer.createSlot( element => element.is( 'element', 'tableRow' ) && element.index >= headingRows )
				)
			);
		}

		const figureElement = writer.createContainerElement( 'figure', { class: 'table' }, [
			// Table with proper sections (thead, tbody).
			writer.createContainerElement( 'table', null, tableSections ),

			// Slot for the rest (for example caption).
			writer.createSlot( element => !element.is( 'element', 'tableRow' ) )
		] );

		return options.asWidget ? toTableWidget( figureElement, writer ) : figureElement;
	};
}

/**
 * Model table row element to view `<tr>` element conversion helper.
 *
 * @returns {Function} Element creator.
 */
function downcastRow() {
	return ( tableRow, { writer } ) => {
		return tableRow.isEmpty ?
			writer.createEmptyElement( 'tr' ) :
			writer.createContainerElement( 'tr' );
	};
}

/**
 * Model table cell element to view `<td>` or `<th>` element conversion helper.
 *
 * This conversion helper will create proper `<th>` elements for table cells that are in the heading section (heading row or column)
 * and `<td>` otherwise.
 *
 * @param {Object} [options]
 * @param {Boolean} [options.asWidget] If set to `true`, the downcast conversion will produce a widget.
 * @returns {Function} Element creator.
 */
function downcastCell( options = {} ) {
	return ( tableCell, { writer } ) => {
		const tableRow = tableCell.parent;
		const table = tableRow.parent;
		const rowIndex = table.getChildIndex( tableRow );

		const tableWalker = new TableWalker( table, { row: rowIndex } );
		const headingRows = table.getAttribute( 'headingRows' ) || 0;
		const headingColumns = table.getAttribute( 'headingColumns' ) || 0;

		// We need to iterate over a table in order to get proper row & column values from a walker.
		for ( const tableSlot of tableWalker ) {
			if ( tableSlot.cell == tableCell ) {
				const isHeading = tableSlot.row < headingRows || tableSlot.column < headingColumns;
				const cellElementName = isHeading ? 'th' : 'td';

				return options.asWidget ?
					toWidgetEditable( writer.createEditableElement( cellElementName ), writer ) :
					writer.createContainerElement( cellElementName );
			}
		}
	};
}

/**
 * Overrides paragraph inside table cell conversion.
 *
 * This converter:
 * * should be used to override default paragraph conversion.
 * * It will only convert `<paragraph>` placed directly inside `<tableCell>`.
 * * For a single paragraph without attributes it returns `<span>` to simulate data table.
 * * For all other cases it returns `<p>` element.
 *
 * @param {Object} [options]
 * @param {Boolean} [options.asWidget] If set to `true`, the downcast conversion will produce a widget.
 * @returns {Function} Element creator.
 */
function convertParagraphInTableCell( options = {} ) {
	return ( modelElement, { writer, consumable, mapper } ) => {
		if ( !modelElement.parent.is( 'element', 'tableCell' ) ) {
			return;
		}

		if ( !isSingleParagraphWithoutAttributes( modelElement ) ) {
			return;
		}

		if ( options.asWidget ) {
			return writer.createContainerElement( 'span', { class: 'ck-table-bogus-paragraph' } );
		} else {
			// Additional requirement for data pipeline to have backward compatible data tables.
			consumable.consume( modelElement, 'insert' );
			mapper.bindElements( modelElement, mapper.toViewElement( modelElement.parent ) );
		}
	};
}

/**
 * Checks if given model `<paragraph>` is an only child of a parent (`<tableCell>`) and if it has any attribute set.
 *
 * The paragraph should be converted in the editing view to:
 *
 * * If returned `true` - to a `<span class="ck-table-bogus-paragraph">`
 * * If returned `false` - to a `<p>`
 *
 * @param {module:engine/model/element~Element} modelElement
 * @returns {Boolean}
 */
function isSingleParagraphWithoutAttributes( modelElement ) {
	const tableCell = modelElement.parent;

	const isSingleParagraph = tableCell.childCount == 1;

	return isSingleParagraph && !hasAnyAttribute( modelElement );
}

// Converts a given {@link module:engine/view/element~Element} to a table widget:
// * Adds a {@link module:engine/view/element~Element#_setCustomProperty custom property} allowing to recognize the table widget element.
// * Calls the {@link module:widget/utils~toWidget} function with the proper element's label creator.
//
// @param {module:engine/view/element~Element} viewElement
// @param {module:engine/view/downcastwriter~DowncastWriter} writer An instance of the view writer.
// @param {String} label The element's label. It will be concatenated with the table `alt` attribute if one is present.
// @returns {module:engine/view/element~Element}
function toTableWidget( viewElement, writer ) {
	writer.setCustomProperty( 'table', true, viewElement );

	return toWidget( viewElement, writer, { hasSelectionHandle: true } );
}

// Checks if an element has any attributes set.
//
// @param {module:engine/model/element~Element element
// @returns {Boolean}
function hasAnyAttribute( element ) {
	return !![ ...element.getAttributeKeys() ].length;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The insert table command.
 *
 * The command is registered by {@link module:table/tableediting~TableEditing} as the `'insertTable'` editor command.
 *
 * To insert a table at the current selection, execute the command and specify the dimensions:
 *
 *		editor.execute( 'insertTable', { rows: 20, columns: 5 } );
 *
 * @extends module:core/command~Command
 */
class InsertTableCommand extends Command {
	/**
	 * @inheritDoc
	 */
	refresh() {
		const model = this.editor.model;
		const selection = model.document.selection;
		const schema = model.schema;

		this.isEnabled = isAllowedInParent( selection, schema );
	}

	/**
	 * Executes the command.
	 *
	 * Inserts a table with the given number of rows and columns into the editor.
	 *
	 * @param {Object} options
	 * @param {Number} [options.rows=2] The number of rows to create in the inserted table.
	 * @param {Number} [options.columns=2] The number of columns to create in the inserted table.
	 * @param {Number} [options.headingRows] The number of heading rows.
	 * If not provided it will default to {@link module:table/table~TableConfig#defaultHeadings `config.table.defaultHeadings.rows`}
	 * table config.
	 * @param {Number} [options.headingColumns] The number of heading columns.
	 * If not provided it will default to {@link module:table/table~TableConfig#defaultHeadings `config.table.defaultHeadings.columns`}
	 * table config.
	 * @fires execute
	 */
	execute( options = {} ) {
		const model = this.editor.model;
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const config = this.editor.config.get( 'table' );

		const defaultRows = config.defaultHeadings.rows;
		const defaultColumns = config.defaultHeadings.columns;

		if ( options.headingRows === undefined && defaultRows ) {
			options.headingRows = defaultRows;
		}

		if ( options.headingColumns === undefined && defaultColumns ) {
			options.headingColumns = defaultColumns;
		}

		model.change( writer => {
			const table = tableUtils.createTable( writer, options );

			model.insertObject( table, null, null, { findOptimalPosition: 'auto' } );

			writer.setSelection( writer.createPositionAt( table.getNodeByPath( [ 0, 0, 0 ] ), 0 ) );
		} );
	}
}

// Checks if the table is allowed in the parent.
//
// @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
// @param {module:engine/model/schema~Schema} schema
// @returns {Boolean}
function isAllowedInParent( selection, schema ) {
	const positionParent = selection.getFirstPosition().parent;
	const validParent = positionParent === positionParent.root ? positionParent : positionParent.parent;

	return schema.checkChild( validParent, 'table' );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The insert row command.
 *
 * The command is registered by {@link module:table/tableediting~TableEditing} as the `'insertTableRowBelow'` and
 * `'insertTableRowAbove'` editor commands.
 *
 * To insert a row below the selected cell, execute the following command:
 *
 *		editor.execute( 'insertTableRowBelow' );
 *
 * To insert a row above the selected cell, execute the following command:
 *
 *		editor.execute( 'insertTableRowAbove' );
 *
 * @extends module:core/command~Command
 */
class InsertRowCommand extends Command {
	/**
	 * Creates a new `InsertRowCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor The editor on which this command will be used.
	 * @param {Object} options
	 * @param {String} [options.order="below"] The order of insertion relative to the row in which the caret is located.
	 * Possible values: `"above"` and `"below"`.
	 */
	constructor( editor, options = {} ) {
		super( editor );

		/**
		 * The order of insertion relative to the row in which the caret is located.
		 *
		 * @readonly
		 * @member {String} module:table/commands/insertrowcommand~InsertRowCommand#order
		 */
		this.order = options.order || 'below';
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		const selection = this.editor.model.document.selection;
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const isAnyCellSelected = !!tableUtils.getSelectionAffectedTableCells( selection ).length;

		this.isEnabled = isAnyCellSelected;
	}

	/**
	 * Executes the command.
	 *
	 * Depending on the command's {@link #order} value, it inserts a row `'below'` or `'above'` the row in which selection is set.
	 *
	 * @fires execute
	 */
	execute() {
		const editor = this.editor;
		const selection = editor.model.document.selection;
		const tableUtils = editor.plugins.get( 'TableUtils' );
		const insertAbove = this.order === 'above';

		const affectedTableCells = tableUtils.getSelectionAffectedTableCells( selection );
		const rowIndexes = tableUtils.getRowIndexes( affectedTableCells );

		const row = insertAbove ? rowIndexes.first : rowIndexes.last;
		const table = affectedTableCells[ 0 ].findAncestor( 'table' );

		tableUtils.insertRows( table, { at: insertAbove ? row : row + 1, copyStructureFromAbove: !insertAbove } );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The insert column command.
 *
 * The command is registered by {@link module:table/tableediting~TableEditing} as the `'insertTableColumnLeft'` and
 * `'insertTableColumnRight'` editor commands.
 *
 * To insert a column to the left of the selected cell, execute the following command:
 *
 *		editor.execute( 'insertTableColumnLeft' );
 *
 * To insert a column to the right of the selected cell, execute the following command:
 *
 *		editor.execute( 'insertTableColumnRight' );
 *
 * @extends module:core/command~Command
 */
class InsertColumnCommand extends Command {
	/**
	 * Creates a new `InsertColumnCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor on which this command will be used.
	 * @param {Object} options
	 * @param {String} [options.order="right"] The order of insertion relative to the column in which the caret is located.
	 * Possible values: `"left"` and `"right"`.
	 */
	constructor( editor, options = {} ) {
		super( editor );

		/**
		 * The order of insertion relative to the column in which the caret is located.
		 *
		 * @readonly
		 * @member {String} module:table/commands/insertcolumncommand~InsertColumnCommand#order
		 */
		this.order = options.order || 'right';
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		const selection = this.editor.model.document.selection;
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const isAnyCellSelected = !!tableUtils.getSelectionAffectedTableCells( selection ).length;

		this.isEnabled = isAnyCellSelected;
	}

	/**
	 * Executes the command.
	 *
	 * Depending on the command's {@link #order} value, it inserts a column to the `'left'` or `'right'` of the column
	 * in which the selection is set.
	 *
	 * @fires execute
	 */
	execute() {
		const editor = this.editor;
		const selection = editor.model.document.selection;
		const tableUtils = editor.plugins.get( 'TableUtils' );
		const insertBefore = this.order === 'left';

		const affectedTableCells = tableUtils.getSelectionAffectedTableCells( selection );
		const columnIndexes = tableUtils.getColumnIndexes( affectedTableCells );

		const column = insertBefore ? columnIndexes.first : columnIndexes.last;
		const table = affectedTableCells[ 0 ].findAncestor( 'table' );

		tableUtils.insertColumns( table, { columns: 1, at: insertBefore ? column : column + 1 } );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The split cell command.
 *
 * The command is registered by {@link module:table/tableediting~TableEditing} as the `'splitTableCellVertically'`
 * and `'splitTableCellHorizontally'`  editor commands.
 *
 * You can split any cell vertically or horizontally by executing this command. For example, to split the selected table cell vertically:
 *
 *		editor.execute( 'splitTableCellVertically' );
 *
 * @extends module:core/command~Command
 */
class SplitCellCommand extends Command {
	/**
	 * Creates a new `SplitCellCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor The editor on which this command will be used.
	 * @param {Object} options
	 * @param {String} options.direction Indicates whether the command should split cells `'horizontally'` or `'vertically'`.
	 */
	constructor( editor, options = {} ) {
		super( editor );

		/**
		 * The direction that indicates which cell will be split.
		 *
		 * @readonly
		 * @member {String} #direction
		 */
		this.direction = options.direction || 'horizontally';
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const selectedCells = tableUtils.getSelectionAffectedTableCells( this.editor.model.document.selection );

		this.isEnabled = selectedCells.length === 1;
	}

	/**
	 * @inheritDoc
	 */
	execute() {
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const tableCell = tableUtils.getSelectionAffectedTableCells( this.editor.model.document.selection )[ 0 ];
		const isHorizontal = this.direction === 'horizontally';

		if ( isHorizontal ) {
			tableUtils.splitCellHorizontally( tableCell, 2 );
		} else {
			tableUtils.splitCellVertically( tableCell, 2 );
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Returns a cropped table according to given dimensions.

 * To return a cropped table that starts at first row and first column and end in third row and column:
 *
 *		const croppedTable = cropTableToDimensions( table, {
 *			startRow: 1,
 *			endRow: 3,
 *			startColumn: 1,
 *			endColumn: 3
 *		}, writer );
 *
 * Calling the code above for the table below:
 *
 *		      0   1   2   3   4                      0   1   2
 *		    ┌───┬───┬───┬───┬───┐
 *		 0  │ a │ b │ c │ d │ e │
 *		    ├───┴───┤   ├───┴───┤                  ┌───┬───┬───┐
 *		 1  │ f     │   │ g     │                  │   │   │ g │  0
 *		    ├───┬───┴───┼───┬───┤   will return:   ├───┴───┼───┤
 *		 2  │ h │ i     │ j │ k │                  │ i     │ j │  1
 *		    ├───┤       ├───┤   │                  │       ├───┤
 *		 3  │ l │       │ m │   │                  │       │ m │  2
 *		    ├───┼───┬───┤   ├───┤                  └───────┴───┘
 *		 4  │ n │ o │ p │   │ q │
 *		    └───┴───┴───┴───┴───┘
 *
 * @param {module:engine/model/element~Element} sourceTable
 * @param {Object} cropDimensions
 * @param {Number} cropDimensions.startRow
 * @param {Number} cropDimensions.startColumn
 * @param {Number} cropDimensions.endRow
 * @param {Number} cropDimensions.endColumn
 * @param {module:engine/model/writer~Writer} writer
 * @returns {module:engine/model/element~Element}
 */
function cropTableToDimensions( sourceTable, cropDimensions, writer ) {
	const { startRow, startColumn, endRow, endColumn } = cropDimensions;

	// Create empty table with empty rows equal to crop height.
	const croppedTable = writer.createElement( 'table' );
	const cropHeight = endRow - startRow + 1;

	for ( let i = 0; i < cropHeight; i++ ) {
		writer.insertElement( 'tableRow', croppedTable, 'end' );
	}

	const tableMap = [ ...new TableWalker( sourceTable, { startRow, endRow, startColumn, endColumn, includeAllSlots: true } ) ];

	// Iterate over source table slots (including empty - spanned - ones).
	for ( const { row: sourceRow, column: sourceColumn, cell: tableCell, isAnchor, cellAnchorRow, cellAnchorColumn } of tableMap ) {
		// Row index in cropped table.
		const rowInCroppedTable = sourceRow - startRow;
		const row = croppedTable.getChild( rowInCroppedTable );

		// For empty slots: fill the gap with empty table cell.
		if ( !isAnchor ) {
			// But fill the gap only if the spanning cell is anchored outside cropped area.
			// In the table from method jsdoc those cells are: "c" & "f".
			if ( cellAnchorRow < startRow || cellAnchorColumn < startColumn ) {
				createEmptyTableCell( writer, writer.createPositionAt( row, 'end' ) );
			}
		}
		// Otherwise clone the cell with all children and trim if it exceeds cropped area.
		else {
			const tableCellCopy = writer.cloneElement( tableCell );

			writer.append( tableCellCopy, row );

			// Trim table if it exceeds cropped area.
			// In the table from method jsdoc those cells are: "g" & "m".
			trimTableCellIfNeeded( tableCellCopy, sourceRow, sourceColumn, endRow, endColumn, writer );
		}
	}

	// Adjust heading rows & columns in cropped table if crop selection includes headings parts.
	addHeadingsToCroppedTable( croppedTable, sourceTable, startRow, startColumn, writer );

	return croppedTable;
}

/**
 * Returns slot info of cells that starts above and overlaps a given row.
 *
 * In a table below, passing `overlapRow = 3`
 *
 *		   ┌───┬───┬───┬───┬───┐
 *		0  │ a │ b │ c │ d │ e │
 *		   │   ├───┼───┼───┼───┤
 *		1  │   │ f │ g │ h │ i │
 *		   ├───┤   ├───┼───┤   │
 *		2  │ j │   │ k │ l │   │
 *		   │   │   │   ├───┼───┤
 *		3  │   │   │   │ m │ n │  <- overlap row to check
 *		   ├───┼───┤   │   ├───│
 *		4  │ o │ p │   │   │ q │
 *		   └───┴───┴───┴───┴───┘
 *
 * will return slot info for cells: "j", "f", "k".
 *
 * @param {module:engine/model/element~Element} table The table to check.
 * @param {Number} overlapRow The index of the row to check.
 * @param {Number} [startRow=0] A row to start analysis. Use it when it is known that the cells above that row will not overlap.
 * @returns {Array.<module:table/tablewalker~TableSlot>}
 */
function getVerticallyOverlappingCells( table, overlapRow, startRow = 0 ) {
	const cells = [];

	const tableWalker = new TableWalker( table, { startRow, endRow: overlapRow - 1 } );

	for ( const slotInfo of tableWalker ) {
		const { row, cellHeight } = slotInfo;
		const cellEndRow = row + cellHeight - 1;

		if ( row < overlapRow && overlapRow <= cellEndRow ) {
			cells.push( slotInfo );
		}
	}

	return cells;
}

/**
 * Splits the table cell horizontally.
 *
 * @param {module:engine/model/element~Element} tableCell
 * @param {Number} splitRow
 * @param {module:engine/model/writer~Writer} writer
 * @returns {module:engine/model/element~Element} Created table cell.
 */
function splitHorizontally( tableCell, splitRow, writer ) {
	const tableRow = tableCell.parent;
	const table = tableRow.parent;
	const rowIndex = tableRow.index;

	const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) );
	const newRowspan = splitRow - rowIndex;

	const newCellAttributes = {};
	const newCellRowSpan = rowspan - newRowspan;

	if ( newCellRowSpan > 1 ) {
		newCellAttributes.rowspan = newCellRowSpan;
	}

	const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );

	if ( colspan > 1 ) {
		newCellAttributes.colspan = colspan;
	}

	const startRow = rowIndex;
	const endRow = startRow + newRowspan;
	const tableMap = [ ...new TableWalker( table, { startRow, endRow, includeAllSlots: true } ) ];

	let newCell = null;
	let columnIndex;

	for ( const tableSlot of tableMap ) {
		const { row, column, cell } = tableSlot;

		if ( cell === tableCell && columnIndex === undefined ) {
			columnIndex = column;
		}

		if ( columnIndex !== undefined && columnIndex === column && row === endRow ) {
			newCell = createEmptyTableCell( writer, tableSlot.getPositionBefore(), newCellAttributes );
		}
	}

	// Update the rowspan attribute after updating table.
	updateNumericAttribute( 'rowspan', newRowspan, tableCell, writer );

	return newCell;
}

/**
 * Returns slot info of cells that starts before and overlaps a given column.
 *
 * In a table below, passing `overlapColumn = 3`
 *
 *		  0   1   2   3   4
 *		┌───────┬───────┬───┐
 *		│ a     │ b     │ c │
 *		│───┬───┴───────┼───┤
 *		│ d │ e         │ f │
 *		├───┼───┬───────┴───┤
 *		│ g │ h │ i         │
 *		├───┼───┼───┬───────┤
 *		│ j │ k │ l │ m     │
 *		├───┼───┴───┼───┬───┤
 *		│ n │ o     │ p │ q │
 *		└───┴───────┴───┴───┘
 *		              ^
 *		              Overlap column to check
 *
 * will return slot info for cells: "b", "e", "i".
 *
 * @param {module:engine/model/element~Element} table The table to check.
 * @param {Number} overlapColumn The index of the column to check.
 * @returns {Array.<module:table/tablewalker~TableSlot>}
 */
function getHorizontallyOverlappingCells( table, overlapColumn ) {
	const cellsToSplit = [];

	const tableWalker = new TableWalker( table );

	for ( const slotInfo of tableWalker ) {
		const { column, cellWidth } = slotInfo;
		const cellEndColumn = column + cellWidth - 1;

		if ( column < overlapColumn && overlapColumn <= cellEndColumn ) {
			cellsToSplit.push( slotInfo );
		}
	}

	return cellsToSplit;
}

/**
 * Splits the table cell vertically.
 *
 * @param {module:engine/model/element~Element} tableCell
 * @param {Number} columnIndex The table cell column index.
 * @param {Number} splitColumn The index of column to split cell on.
 * @param {module:engine/model/writer~Writer} writer
 * @returns {module:engine/model/element~Element} Created table cell.
 */
function splitVertically( tableCell, columnIndex, splitColumn, writer ) {
	const colspan = parseInt( tableCell.getAttribute( 'colspan' ) );
	const newColspan = splitColumn - columnIndex;

	const newCellAttributes = {};
	const newCellColSpan = colspan - newColspan;

	if ( newCellColSpan > 1 ) {
		newCellAttributes.colspan = newCellColSpan;
	}

	const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );

	if ( rowspan > 1 ) {
		newCellAttributes.rowspan = rowspan;
	}

	const newCell = createEmptyTableCell( writer, writer.createPositionAfter( tableCell ), newCellAttributes );

	// Update the colspan attribute after updating table.
	updateNumericAttribute( 'colspan', newColspan, tableCell, writer );

	return newCell;
}

/**
 * Adjusts table cell dimensions to not exceed limit row and column.
 *
 * If table cell width (or height) covers a column (or row) that is after a limit column (or row)
 * this method will trim "colspan" (or "rowspan") attribute so the table cell will fit in a defined limits.
 *
 * @param {module:engine/model/element~Element} tableCell
 * @param {Number} cellRow
 * @param {Number} cellColumn
 * @param {Number} limitRow
 * @param {Number} limitColumn
 * @param {module:engine/model/writer~Writer} writer
 */
function trimTableCellIfNeeded( tableCell, cellRow, cellColumn, limitRow, limitColumn, writer ) {
	const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
	const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );

	const endColumn = cellColumn + colspan - 1;

	if ( endColumn > limitColumn ) {
		const trimmedSpan = limitColumn - cellColumn + 1;

		updateNumericAttribute( 'colspan', trimmedSpan, tableCell, writer, 1 );
	}

	const endRow = cellRow + rowspan - 1;

	if ( endRow > limitRow ) {
		const trimmedSpan = limitRow - cellRow + 1;

		updateNumericAttribute( 'rowspan', trimmedSpan, tableCell, writer, 1 );
	}
}

// Sets proper heading attributes to a cropped table.
function addHeadingsToCroppedTable( croppedTable, sourceTable, startRow, startColumn, writer ) {
	const headingRows = parseInt( sourceTable.getAttribute( 'headingRows' ) || 0 );

	if ( headingRows > 0 ) {
		const headingRowsInCrop = headingRows - startRow;
		updateNumericAttribute( 'headingRows', headingRowsInCrop, croppedTable, writer, 0 );
	}

	const headingColumns = parseInt( sourceTable.getAttribute( 'headingColumns' ) || 0 );

	if ( headingColumns > 0 ) {
		const headingColumnsInCrop = headingColumns - startColumn;
		updateNumericAttribute( 'headingColumns', headingColumnsInCrop, croppedTable, writer, 0 );
	}
}

/**
 * Removes columns that have no cells anchored.
 *
 * In table below:
 *
 *     +----+----+----+----+----+----+----+
 *     | 00 | 01      | 03 | 04      | 06 |
 *     +----+----+----+----+         +----+
 *     | 10 | 11      | 13 |         | 16 |
 *     +----+----+----+----+----+----+----+
 *     | 20 | 21      | 23 | 24      | 26 |
 *     +----+----+----+----+----+----+----+
 *                  ^--- empty ---^
 *
 * Will remove columns 2 and 5.
 *
 * **Note:** This is a low-level helper method for clearing invalid model state when doing table modifications.
 * To remove a column from a table use {@link module:table/tableutils~TableUtils#removeColumns `TableUtils.removeColumns()`}.
 *
 * @protected
 * @param {module:engine/model/element~Element} table
 * @param {module:table/tableutils~TableUtils} tableUtils
 * @returns {Boolean} True if removed some columns.
 */
function removeEmptyColumns( table, tableUtils ) {
	const width = tableUtils.getColumns( table );
	const columnsMap = new Array( width ).fill( 0 );

	for ( const { column } of new TableWalker( table ) ) {
		columnsMap[ column ]++;
	}

	const emptyColumns = columnsMap.reduce( ( result, cellsCount, column ) => {
		return cellsCount ? result : [ ...result, column ];
	}, [] );

	if ( emptyColumns.length > 0 ) {
		// Remove only last empty column because it will recurrently trigger removing empty rows.
		const emptyColumn = emptyColumns[ emptyColumns.length - 1 ];

		// @if CK_DEBUG_TABLE // console.log( `Removing empty column: ${ emptyColumn }.` );
		tableUtils.removeColumns( table, { at: emptyColumn } );

		return true;
	}

	return false;
}

/**
 * Removes rows that have no cells anchored.
 *
 * In table below:
 *
 *     +----+----+----+
 *     | 00 | 01 | 02 |
 *     +----+----+----+
 *     | 10 | 11 | 12 |
 *     +    +    +    +
 *     |    |    |    | <-- empty
 *     +----+----+----+
 *     | 30 | 31 | 32 |
 *     +----+----+----+
 *     | 40      | 42 |
 *     +         +    +
 *     |         |    | <-- empty
 *     +----+----+----+
 *     | 60 | 61 | 62 |
 *     +----+----+----+
 *
 * Will remove rows 2 and 5.
 *
 * **Note:** This is a low-level helper method for clearing invalid model state when doing table modifications.
 * To remove a row from a table use {@link module:table/tableutils~TableUtils#removeRows `TableUtils.removeRows()`}.
 *
 * @protected
 * @param {module:engine/model/element~Element} table
 * @param {module:table/tableutils~TableUtils} tableUtils
 * @returns {Boolean} True if removed some rows.
 */
function removeEmptyRows( table, tableUtils ) {
	const emptyRows = [];
	const tableRowCount = tableUtils.getRows( table );

	for ( let rowIndex = 0; rowIndex < tableRowCount; rowIndex++ ) {
		const tableRow = table.getChild( rowIndex );

		if ( tableRow.isEmpty ) {
			emptyRows.push( rowIndex );
		}
	}

	if ( emptyRows.length > 0 ) {
		// Remove only last empty row because it will recurrently trigger removing empty columns.
		const emptyRow = emptyRows[ emptyRows.length - 1 ];

		// @if CK_DEBUG_TABLE // console.log( `Removing empty row: ${ emptyRow }.` );
		tableUtils.removeRows( table, { at: emptyRow } );

		return true;
	}

	return false;
}

/**
 * Removes rows and columns that have no cells anchored.
 *
 * In table below:
 *
 *     +----+----+----+----+
 *     | 00      | 02      |
 *     +----+----+         +
 *     | 10      |         |
 *     +----+----+----+----+
 *     | 20      | 22 | 23 |
 *     +         +    +    +
 *     |         |    |    | <-- empty row
 *     +----+----+----+----+
 *             ^--- empty column
 *
 * Will remove row 3 and column 1.
 *
 * **Note:** This is a low-level helper method for clearing invalid model state when doing table modifications.
 * To remove a rows from a table use {@link module:table/tableutils~TableUtils#removeRows `TableUtils.removeRows()`} and
 * {@link module:table/tableutils~TableUtils#removeColumns `TableUtils.removeColumns()`} to remove a column.
 *
 * @protected
 * @param {module:engine/model/element~Element} table
 * @param {module:table/tableutils~TableUtils} tableUtils
 */
function removeEmptyRowsColumns( table, tableUtils ) {
	const removedColumns = removeEmptyColumns( table, tableUtils );

	// If there was some columns removed then cleaning empty rows was already triggered.
	if ( !removedColumns ) {
		removeEmptyRows( table, tableUtils );
	}
}

/**
 * Returns adjusted last row index if selection covers part of a row with empty slots (spanned by other cells).
 * The `dimensions.lastRow` is equal to last row index but selection might be bigger.
 *
 * This happens *only* on rectangular selection so we analyze a case like this:
 *
 *        +---+---+---+---+
 *      0 | a | b | c | d |
 *        +   +   +---+---+
 *      1 |   | e | f | g |
 *        +   +---+   +---+
 *      2 |   | h |   | i | <- last row, each cell has rowspan = 2,
 *        +   +   +   +   +    so we need to return 3, not 2
 *      3 |   |   |   |   |
 *        +---+---+---+---+
 *
 * @param {module:engine/model/element~Element} table
 * @param {Object} dimensions
 * @param {Number} dimensions.firstRow
 * @param {Number} dimensions.firstColumn
 * @param {Number} dimensions.lastRow
 * @param {Number} dimensions.lastColumn
 * @returns {Number} Adjusted last row index.
 */
function adjustLastRowIndex( table, dimensions ) {
	const lastRowMap = Array.from( new TableWalker( table, {
		startColumn: dimensions.firstColumn,
		endColumn: dimensions.lastColumn,
		row: dimensions.lastRow
	} ) );

	const everyCellHasSingleRowspan = lastRowMap.every( ( { cellHeight } ) => cellHeight === 1 );

	// It is a "flat" row, so the last row index is OK.
	if ( everyCellHasSingleRowspan ) {
		return dimensions.lastRow;
	}

	// Otherwise get any cell's rowspan and adjust the last row index.
	const rowspanAdjustment = lastRowMap[ 0 ].cellHeight - 1;
	return dimensions.lastRow + rowspanAdjustment;
}

/**
 * Returns adjusted last column index if selection covers part of a column with empty slots (spanned by other cells).
 * The `dimensions.lastColumn` is equal to last column index but selection might be bigger.
 *
 * This happens *only* on rectangular selection so we analyze a case like this:
 *
 *       0   1   2   3
 *     +---+---+---+---+
 *     | a             |
 *     +---+---+---+---+
 *     | b | c | d     |
 *     +---+---+---+---+
 *     | e     | f     |
 *     +---+---+---+---+
 *     | g | h         |
 *     +---+---+---+---+
 *               ^
 *              last column, each cell has colspan = 2, so we need to return 3, not 2
 *
 * @param {module:engine/model/element~Element} table
 * @param {Object} dimensions
 * @param {Number} dimensions.firstRow
 * @param {Number} dimensions.firstColumn
 * @param {Number} dimensions.lastRow
 * @param {Number} dimensions.lastColumn
 * @returns {Number} Adjusted last column index.
 */
function adjustLastColumnIndex( table, dimensions ) {
	const lastColumnMap = Array.from( new TableWalker( table, {
		startRow: dimensions.firstRow,
		endRow: dimensions.lastRow,
		column: dimensions.lastColumn
	} ) );

	const everyCellHasSingleColspan = lastColumnMap.every( ( { cellWidth } ) => cellWidth === 1 );

	// It is a "flat" column, so the last column index is OK.
	if ( everyCellHasSingleColspan ) {
		return dimensions.lastColumn;
	}

	// Otherwise get any cell's colspan and adjust the last column index.
	const colspanAdjustment = lastColumnMap[ 0 ].cellWidth - 1;
	return dimensions.lastColumn + colspanAdjustment;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The merge cell command.
 *
 * The command is registered by {@link module:table/tableediting~TableEditing} as the `'mergeTableCellRight'`, `'mergeTableCellLeft'`,
 * `'mergeTableCellUp'` and `'mergeTableCellDown'` editor commands.
 *
 * To merge a table cell at the current selection with another cell, execute the command corresponding with the preferred direction.
 *
 * For example, to merge with a cell to the right:
 *
 *		editor.execute( 'mergeTableCellRight' );
 *
 * **Note**: If a table cell has a different [`rowspan`](https://www.w3.org/TR/html50/tabular-data.html#attr-tdth-rowspan)
 * (for `'mergeTableCellRight'` and `'mergeTableCellLeft'`) or [`colspan`](https://www.w3.org/TR/html50/tabular-data.html#attr-tdth-colspan)
 * (for `'mergeTableCellUp'` and `'mergeTableCellDown'`), the command will be disabled.
 *
 * @extends module:core/command~Command
 */
class MergeCellCommand extends Command {
	/**
	 * Creates a new `MergeCellCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor The editor on which this command will be used.
	 * @param {Object} options
	 * @param {String} options.direction Indicates which cell to merge with the currently selected one.
	 * Possible values are: `'left'`, `'right'`, `'up'` and `'down'`.
	 */
	constructor( editor, options ) {
		super( editor );

		/**
		 * The direction that indicates which cell will be merged with the currently selected one.
		 *
		 * @readonly
		 * @member {String} #direction
		 */
		this.direction = options.direction;

		/**
		 * Whether the merge is horizontal (left/right) or vertical (up/down).
		 *
		 * @readonly
		 * @member {Boolean} #isHorizontal
		 */
		this.isHorizontal = this.direction == 'right' || this.direction == 'left';
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		const cellToMerge = this._getMergeableCell();

		this.value = cellToMerge;
		this.isEnabled = !!cellToMerge;
	}

	/**
	 * Executes the command.
	 *
	 * Depending on the command's {@link #direction} value, it will merge the cell that is to the `'left'`, `'right'`, `'up'` or `'down'`.
	 *
	 * @fires execute
	 */
	execute() {
		const model = this.editor.model;
		const doc = model.document;
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const tableCell = tableUtils.getTableCellsContainingSelection( doc.selection )[ 0 ];

		const cellToMerge = this.value;
		const direction = this.direction;

		model.change( writer => {
			const isMergeNext = direction == 'right' || direction == 'down';

			// The merge mechanism is always the same so sort cells to be merged.
			const cellToExpand = isMergeNext ? tableCell : cellToMerge;
			const cellToRemove = isMergeNext ? cellToMerge : tableCell;

			// Cache the parent of cell to remove for later check.
			const removedTableCellRow = cellToRemove.parent;

			mergeTableCells$1( cellToRemove, cellToExpand, writer );

			const spanAttribute = this.isHorizontal ? 'colspan' : 'rowspan';
			const cellSpan = parseInt( tableCell.getAttribute( spanAttribute ) || 1 );
			const cellToMergeSpan = parseInt( cellToMerge.getAttribute( spanAttribute ) || 1 );

			// Update table cell span attribute and merge set selection on merged contents.
			writer.setAttribute( spanAttribute, cellSpan + cellToMergeSpan, cellToExpand );
			writer.setSelection( writer.createRangeIn( cellToExpand ) );

			const tableUtils = this.editor.plugins.get( 'TableUtils' );
			const table = removedTableCellRow.findAncestor( 'table' );

			// Remove empty rows and columns after merging.
			removeEmptyRowsColumns( table, tableUtils );
		} );
	}

	/**
	 * Returns a cell that can be merged with the current cell depending on the command's direction.
	 *
	 * @returns {module:engine/model/element~Element|undefined}
	 * @private
	 */
	_getMergeableCell() {
		const model = this.editor.model;
		const doc = model.document;
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const tableCell = tableUtils.getTableCellsContainingSelection( doc.selection )[ 0 ];

		if ( !tableCell ) {
			return;
		}

		// First get the cell on proper direction.
		const cellToMerge = this.isHorizontal ?
			getHorizontalCell( tableCell, this.direction, tableUtils ) :
			getVerticalCell( tableCell, this.direction, tableUtils );

		if ( !cellToMerge ) {
			return;
		}

		// If found check if the span perpendicular to merge direction is equal on both cells.
		const spanAttribute = this.isHorizontal ? 'rowspan' : 'colspan';
		const span = parseInt( tableCell.getAttribute( spanAttribute ) || 1 );

		const cellToMergeSpan = parseInt( cellToMerge.getAttribute( spanAttribute ) || 1 );

		if ( cellToMergeSpan === span ) {
			return cellToMerge;
		}
	}
}

// Returns the cell that can be merged horizontally.
//
// @param {module:engine/model/element~Element} tableCell
// @param {String} direction
// @param {module:table/tableutils~TableUtils} tableUtils
// @returns {module:engine/model/node~Node|null}
function getHorizontalCell( tableCell, direction, tableUtils ) {
	const tableRow = tableCell.parent;
	const table = tableRow.parent;
	const horizontalCell = direction == 'right' ? tableCell.nextSibling : tableCell.previousSibling;
	const hasHeadingColumns = ( table.getAttribute( 'headingColumns' ) || 0 ) > 0;

	if ( !horizontalCell ) {
		return;
	}

	// Sort cells:
	const cellOnLeft = direction == 'right' ? tableCell : horizontalCell;
	const cellOnRight = direction == 'right' ? horizontalCell : tableCell;

	// Get their column indexes:
	const { column: leftCellColumn } = tableUtils.getCellLocation( cellOnLeft );
	const { column: rightCellColumn } = tableUtils.getCellLocation( cellOnRight );

	const leftCellSpan = parseInt( cellOnLeft.getAttribute( 'colspan' ) || 1 );

	const isCellOnLeftInHeadingColumn = isHeadingColumnCell( tableUtils, cellOnLeft);
	const isCellOnRightInHeadingColumn = isHeadingColumnCell( tableUtils, cellOnRight);

	// We cannot merge heading columns cells with regular cells.
	if ( hasHeadingColumns && isCellOnLeftInHeadingColumn != isCellOnRightInHeadingColumn ) {
		return;
	}

	// The cell on the right must have index that is distant to the cell on the left by the left cell's width (colspan).
	const cellsAreTouching = leftCellColumn + leftCellSpan === rightCellColumn;

	// If the right cell's column index is different it means that there are rowspanned cells between them.
	return cellsAreTouching ? horizontalCell : undefined;
}

// Returns the cell that can be merged vertically.
//
// @param {module:engine/model/element~Element} tableCell
// @param {String} direction
// @param {module:table/tableutils~TableUtils} tableUtils
// @returns {module:engine/model/node~Node|null}
function getVerticalCell( tableCell, direction, tableUtils ) {
	const tableRow = tableCell.parent;
	const table = tableRow.parent;

	const rowIndex = table.getChildIndex( tableRow );

	// Don't search for mergeable cell if direction points out of the table.
	if ( ( direction == 'down' && rowIndex === tableUtils.getRows( table ) - 1 ) || ( direction == 'up' && rowIndex === 0 ) ) {
		return;
	}

	const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
	const headingRows = table.getAttribute( 'headingRows' ) || 0;

	const isMergeWithBodyCell = direction == 'down' && ( rowIndex + rowspan ) === headingRows;
	const isMergeWithHeadCell = direction == 'up' && rowIndex === headingRows;

	// Don't search for mergeable cell if direction points out of the current table section.
	if ( headingRows && ( isMergeWithBodyCell || isMergeWithHeadCell ) ) {
		return;
	}

	const currentCellRowSpan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
	const rowOfCellToMerge = direction == 'down' ? rowIndex + currentCellRowSpan : rowIndex;

	const tableMap = [ ...new TableWalker( table, { endRow: rowOfCellToMerge } ) ];

	const currentCellData = tableMap.find( value => value.cell === tableCell );
	const mergeColumn = currentCellData.column;

	const cellToMergeData = tableMap.find( ( { row, cellHeight, column } ) => {
		if ( column !== mergeColumn ) {
			return false;
		}

		if ( direction == 'down' ) {
			// If merging a cell below the mergeRow is already calculated.
			return row === rowOfCellToMerge;
		} else {
			// If merging a cell above calculate if it spans to mergeRow.
			return rowOfCellToMerge === row + cellHeight;
		}
	} );

	return cellToMergeData && cellToMergeData.cell;
}

// Merges two table cells. It will ensure that after merging cells with an empty paragraph, the resulting table cell will only have one
// paragraph. If one of the merged table cells is empty, the merged table cell will have the contents of the non-empty table cell.
// If both are empty, the merged table cell will have only one empty paragraph.
//
// @param {module:engine/model/element~Element} cellToRemove
// @param {module:engine/model/element~Element} cellToExpand
// @param {module:engine/model/writer~Writer} writer
function mergeTableCells$1( cellToRemove, cellToExpand, writer ) {
	if ( !isEmpty$2( cellToRemove ) ) {
		if ( isEmpty$2( cellToExpand ) ) {
			writer.remove( writer.createRangeIn( cellToExpand ) );
		}

		writer.move( writer.createRangeIn( cellToRemove ), writer.createPositionAt( cellToExpand, 'end' ) );
	}

	// Remove merged table cell.
	writer.remove( cellToRemove );
}

// Checks if the passed table cell contains an empty paragraph.
//
// @param {module:engine/model/element~Element} tableCell
// @returns {Boolean}
function isEmpty$2( tableCell ) {
	return tableCell.childCount == 1 && tableCell.getChild( 0 ).is( 'element', 'paragraph' ) && tableCell.getChild( 0 ).isEmpty;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The remove row command.
 *
 * The command is registered by {@link module:table/tableediting~TableEditing} as the `'removeTableRow'` editor command.
 *
 * To remove the row containing the selected cell, execute the command:
 *
 *		editor.execute( 'removeTableRow' );
 *
 * @extends module:core/command~Command
 */
class RemoveRowCommand extends Command {
	/**
	 * @inheritDoc
	 */
	refresh() {
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const selectedCells = tableUtils.getSelectionAffectedTableCells( this.editor.model.document.selection );
		const firstCell = selectedCells[ 0 ];

		if ( firstCell ) {
			const table = firstCell.findAncestor( 'table' );
			const tableRowCount = this.editor.plugins.get( 'TableUtils' ).getRows( table );
			const lastRowIndex = tableRowCount - 1;

			const selectedRowIndexes = tableUtils.getRowIndexes( selectedCells );

			const areAllRowsSelected = selectedRowIndexes.first === 0 && selectedRowIndexes.last === lastRowIndex;

			// Disallow selecting whole table -> delete whole table should be used instead.
			this.isEnabled = !areAllRowsSelected;
		} else {
			this.isEnabled = false;
		}
	}

	/**
	 * @inheritDoc
	 */
	execute() {
		const model = this.editor.model;
		const tableUtils = this.editor.plugins.get( 'TableUtils' );

		const referenceCells = tableUtils.getSelectionAffectedTableCells( model.document.selection );
		const removedRowIndexes = tableUtils.getRowIndexes( referenceCells );

		const firstCell = referenceCells[ 0 ];
		const table = firstCell.findAncestor( 'table' );

		const columnIndexToFocus = tableUtils.getCellLocation( firstCell ).column;

		model.change( writer => {
			const rowsToRemove = removedRowIndexes.last - removedRowIndexes.first + 1;

			tableUtils.removeRows( table, {
				at: removedRowIndexes.first,
				rows: rowsToRemove
			} );

			const cellToFocus = getCellToFocus$1( table, removedRowIndexes.first, columnIndexToFocus, tableUtils.getRows( table ) );

			writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) );
		} );
	}
}

// Returns a cell that should be focused before removing the row, belonging to the same column as the currently focused cell.
// * If the row was not the last one, the cell to focus will be in the row that followed it (before removal).
// * If the row was the last one, the cell to focus will be in the row that preceded it (before removal).
function getCellToFocus$1( table, removedRowIndex, columnToFocus, tableRowCount ) {
	// Don't go beyond last row's index.
	const row = table.getChild( Math.min( removedRowIndex, tableRowCount - 1 ) );

	// Default to first table cell.
	let cellToFocus = row.getChild( 0 );
	let column = 0;

	for ( const tableCell of row.getChildren() ) {
		if ( column > columnToFocus ) {
			return cellToFocus;
		}

		cellToFocus = tableCell;
		column += parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
	}

	return cellToFocus;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The remove column command.
 *
 * The command is registered by {@link module:table/tableediting~TableEditing} as the `'removeTableColumn'` editor command.
 *
 * To remove the column containing the selected cell, execute the command:
 *
 *		editor.execute( 'removeTableColumn' );
 *
 * @extends module:core/command~Command
 */
class RemoveColumnCommand extends Command {
	/**
	 * @inheritDoc
	 */
	refresh() {
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const selectedCells = tableUtils.getSelectionAffectedTableCells( this.editor.model.document.selection );
		const firstCell = selectedCells[ 0 ];

		if ( firstCell ) {
			const table = firstCell.findAncestor( 'table' );
			const tableColumnCount = tableUtils.getColumns( table );

			const { first, last } = tableUtils.getColumnIndexes( selectedCells );

			this.isEnabled = last - first < ( tableColumnCount - 1 );
		} else {
			this.isEnabled = false;
		}
	}

	/**
	 * @inheritDoc
	 */
	execute() {
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const [ firstCell, lastCell ] = getBoundaryCells( this.editor.model.document.selection, tableUtils );
		const table = firstCell.parent.parent;

		// Cache the table before removing or updating colspans.
		const tableMap = [ ...new TableWalker( table ) ];

		// Store column indexes of removed columns.
		const removedColumnIndexes = {
			first: tableMap.find( value => value.cell === firstCell ).column,
			last: tableMap.find( value => value.cell === lastCell ).column
		};

		const cellToFocus = getCellToFocus( tableMap, firstCell, lastCell, removedColumnIndexes );

		this.editor.model.change( writer => {
			const columnsToRemove = removedColumnIndexes.last - removedColumnIndexes.first + 1;

			this.editor.plugins.get( 'TableUtils' ).removeColumns( table, {
				at: removedColumnIndexes.first,
				columns: columnsToRemove
			} );

			writer.setSelection( writer.createPositionAt( cellToFocus, 0 ) );
		} );
	}
}

// Returns a proper table cell to focus after removing a column.
// - selection is on last table cell it will return previous cell.
function getCellToFocus( tableMap, firstCell, lastCell, removedColumnIndexes ) {
	const colspan = parseInt( lastCell.getAttribute( 'colspan' ) || 1 );

	// If the table cell is spanned over 2+ columns - it will be truncated so the selection should
	// stay in that cell.
	if ( colspan > 1 ) {
		return lastCell;
	}
	// Normally, look for the cell in the same row that precedes the first cell to put selection there ("column on the left").
	// If the deleted column is the first column of the table, there will be no predecessor: use the cell
	// from the column that follows then (also in the same row).
	else if ( firstCell.previousSibling || lastCell.nextSibling ) {
		return lastCell.nextSibling || firstCell.previousSibling;
	}
	// It can happen that table cells have no siblings in a row, for instance, when there are row spans
	// in the table (in the previous row). Then just look for the closest cell that is in a column
	// that will not be removed to put the selection there.
	else {
		// Look for any cell in a column that precedes the first removed column.
		if ( removedColumnIndexes.first ) {
			return tableMap.reverse().find( ( { column } ) => {
				return column < removedColumnIndexes.first;
			} ).cell;
		}
		// If the first removed column is the first column of the table, then
		// look for any cell that is in a column that follows the last removed column.
		else {
			return tableMap.reverse().find( ( { column } ) => {
				return column > removedColumnIndexes.last;
			} ).cell;
		}
	}
}

// Returns helper object returning the first and the last cell contained in given selection, based on DOM order.
function getBoundaryCells( selection, tableUtils ) {
	const referenceCells = tableUtils.getSelectionAffectedTableCells( selection );
	const firstCell = referenceCells[ 0 ];
	const lastCell = referenceCells.pop();

	const returnValue = [ firstCell, lastCell ];

	return firstCell.isBefore( lastCell ) ? returnValue : returnValue.reverse();
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The header row command.
 *
 * The command is registered by {@link module:table/tableediting~TableEditing} as the `'setTableColumnHeader'` editor command.
 *
 * You can make the row containing the selected cell a [header](https://www.w3.org/TR/html50/tabular-data.html#the-th-element) by executing:
 *
 *		editor.execute( 'setTableRowHeader' );
 *
 * **Note:** All preceding rows will also become headers. If the current row is already a header, executing this command
 * will make it a regular row back again (including the following rows).
 *
 * @extends module:core/command~Command
 */
class SetHeaderRowCommand extends Command {
	/**
	 * @inheritDoc
	 */
	refresh() {
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const model = this.editor.model;
		const selectedCells = tableUtils.getSelectionAffectedTableCells( model.document.selection );
		const isInTable = selectedCells.length > 0;

		this.isEnabled = isInTable;

		/**
		 * Flag indicating whether the command is active. The command is active when the
		 * {@link module:engine/model/selection~Selection} is in a header row.
		 *
		 * @observable
		 * @readonly
		 * @member {Boolean} #value
		 */
		this.value = isInTable && selectedCells.every( cell => this._isInHeading( cell, cell.parent.parent ) );
	}

	/**
	 * Executes the command.
	 *
	 * When the selection is in a non-header row, the command will set the `headingRows` table attribute to cover that row.
	 *
	 * When the selection is already in a header row, it will set `headingRows` so the heading section will end before that row.
	 *
	 * @fires execute
	 * @param {Object} options
	 * @param {Boolean} [options.forceValue] If set, the command will set (`true`) or unset (`false`) the header rows according to
	 * the `forceValue` parameter instead of the current model state.
	 */
	execute( options = {} ) {
		if ( options.forceValue === this.value ) {
			return;
		}

		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const model = this.editor.model;

		const selectedCells = tableUtils.getSelectionAffectedTableCells( model.document.selection );
		const table = selectedCells[ 0 ].findAncestor( 'table' );

		const { first, last } = tableUtils.getRowIndexes( selectedCells );
		const headingRowsToSet = this.value ? first : last + 1;
		const currentHeadingRows = table.getAttribute( 'headingRows' ) || 0;

		model.change( writer => {
			if ( headingRowsToSet ) {
				// Changing heading rows requires to check if any of a heading cell is overlapping vertically the table head.
				// Any table cell that has a rowspan attribute > 1 will not exceed the table head so we need to fix it in rows below.
				const startRow = headingRowsToSet > currentHeadingRows ? currentHeadingRows : 0;
				const overlappingCells = getVerticallyOverlappingCells( table, headingRowsToSet, startRow );

				for ( const { cell } of overlappingCells ) {
					splitHorizontally( cell, headingRowsToSet, writer );
				}
			}

			updateNumericAttribute( 'headingRows', headingRowsToSet, table, writer, 0 );
		} );
	}

	/**
	 * Checks if a table cell is in the heading section.
	 *
	 * @param {module:engine/model/element~Element} tableCell
	 * @param {module:engine/model/element~Element} table
	 * @returns {Boolean}
	 * @private
	 */
	_isInHeading( tableCell, table ) {
		const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 );

		return !!headingRows && tableCell.parent.index < headingRows;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The header column command.
 *
 * The command is registered by {@link module:table/tableediting~TableEditing} as the `'setTableColumnHeader'` editor command.
 *
 * You can make the column containing the selected cell a [header](https://www.w3.org/TR/html50/tabular-data.html#the-th-element)
 * by executing:
 *
 *		editor.execute( 'setTableColumnHeader' );
 *
 * **Note:** All preceding columns will also become headers. If the current column is already a header, executing this command
 * will make it a regular column back again (including the following columns).
 *
 * @extends module:core/command~Command
 */
class SetHeaderColumnCommand extends Command {
	/**
	 * @inheritDoc
	 */
	refresh() {
		const model = this.editor.model;
		const tableUtils = this.editor.plugins.get( 'TableUtils' );

		const selectedCells = tableUtils.getSelectionAffectedTableCells( model.document.selection );
		const isInTable = selectedCells.length > 0;

		this.isEnabled = isInTable;

		/**
		 * Flag indicating whether the command is active. The command is active when the
		 * {@link module:engine/model/selection~Selection} is in a header column.
		 *
		 * @observable
		 * @readonly
		 * @member {Boolean} #value
		 */
		this.value = isInTable && selectedCells.every( cell => isHeadingColumnCell( tableUtils, cell ) );
	}

	/**
	 * Executes the command.
	 *
	 * When the selection is in a non-header column, the command will set the `headingColumns` table attribute to cover that column.
	 *
	 * When the selection is already in a header column, it will set `headingColumns` so the heading section will end before that column.
	 *
	 * @fires execute
	 * @param {Object} [options]
	 * @param {Boolean} [options.forceValue] If set, the command will set (`true`) or unset (`false`) the header columns according to
	 * the `forceValue` parameter instead of the current model state.
	 */
	execute( options = {} ) {
		if ( options.forceValue === this.value ) {
			return;
		}

		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const model = this.editor.model;
		const selectedCells = tableUtils.getSelectionAffectedTableCells( model.document.selection );
		const table = selectedCells[ 0 ].findAncestor( 'table' );

		const { first, last } = tableUtils.getColumnIndexes( selectedCells );
		const headingColumnsToSet = this.value ? first : last + 1;

		model.change( writer => {
			if ( headingColumnsToSet ) {
				// Changing heading columns requires to check if any of a heading cell is overlapping horizontally the table head.
				// Any table cell that has a colspan attribute > 1 will not exceed the table head so we need to fix it in columns before.
				const overlappingCells = getHorizontallyOverlappingCells( table, headingColumnsToSet );

				for ( const { cell, column } of overlappingCells ) {
					splitVertically( cell, column, headingColumnsToSet, writer );
				}
			}

			updateNumericAttribute( 'headingColumns', headingColumnsToSet, table, writer, 0 );
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table utilities plugin.
 *
 * @extends module:core/plugin~Plugin
 */
class TableUtils extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TableUtils';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		this.decorate( 'insertColumns' );
		this.decorate( 'insertRows' );
	}

	/**
	 * Returns the table cell location as an object with table row and table column indexes.
	 *
	 * For instance, in the table below:
	 *
	 *		    0   1   2   3
	 *		  +---+---+---+---+
	 *		0 | a     | b | c |
	 *		  +       +   +---+
	 *		1 |       |   | d |
	 *		  +---+---+   +---+
	 *		2 | e     |   | f |
	 *		  +---+---+---+---+
	 *
	 * the method will return:
	 *
	 *		const cellA = table.getNodeByPath( [ 0, 0 ] );
	 *		editor.plugins.get( 'TableUtils' ).getCellLocation( cellA );
	 *		// will return { row: 0, column: 0 }
	 *
	 *		const cellD = table.getNodeByPath( [ 1, 0 ] );
	 *		editor.plugins.get( 'TableUtils' ).getCellLocation( cellD );
	 *		// will return { row: 1, column: 3 }
	 *
	 * @param {module:engine/model/element~Element} tableCell
	 * @returns {Object} Returns a `{row, column}` object.
	 */
	getCellLocation( tableCell ) {
		const tableRow = tableCell.parent;
		const table = tableRow.parent;

		const rowIndex = table.getChildIndex( tableRow );

		const tableWalker = new TableWalker( table, { row: rowIndex } );

		for ( const { cell, row, column } of tableWalker ) {
			if ( cell === tableCell ) {
				return { row, column };
			}
		}
	}

	/**
	 * Creates an empty table with a proper structure. The table needs to be inserted into the model,
	 * for example, by using the {@link module:engine/model/model~Model#insertContent} function.
	 *
	 *		model.change( ( writer ) => {
	 *			// Create a table of 2 rows and 7 columns:
	 *			const table = tableUtils.createTable( writer, { rows: 2, columns: 7 } );
	 *
	 *			// Insert a table to the model at the best position taking the current selection:
	 *			model.insertContent( table );
	 *		}
	 *
	 * @param {module:engine/model/writer~Writer} writer The model writer.
	 * @param {Object} options
	 * @param {Number} [options.rows=2] The number of rows to create.
	 * @param {Number} [options.columns=2] The number of columns to create.
	 * @param {Number} [options.headingRows=0] The number of heading rows.
	 * @param {Number} [options.headingColumns=0] The number of heading columns.
	 * @returns {module:engine/model/element~Element} The created table element.
	 */
	createTable( writer, options ) {
		const table = writer.createElement( 'table' );

		const rows = parseInt( options.rows ) || 2;
		const columns = parseInt( options.columns ) || 2;

		createEmptyRows( writer, table, 0, rows, columns );

		if ( options.headingRows ) {
			updateNumericAttribute( 'headingRows', Math.min( options.headingRows, rows ), table, writer, 0 );
		}

		if ( options.headingColumns ) {
			updateNumericAttribute( 'headingColumns', Math.min( options.headingColumns, columns ), table, writer, 0 );
		}

		return table;
	}

	/**
	 * Inserts rows into a table.
	 *
	 *		editor.plugins.get( 'TableUtils' ).insertRows( table, { at: 1, rows: 2 } );
	 *
	 * Assuming the table on the left, the above code will transform it to the table on the right:
	 *
	 *		row index
	 *		  0 +---+---+---+       `at` = 1,      +---+---+---+ 0
	 *		    | a | b | c |       `rows` = 2,    | a | b | c |
	 *		  1 +   +---+---+   <-- insert here    +   +---+---+ 1
	 *		    |   | d | e |                      |   |   |   |
	 *		  2 +   +---+---+       will give:     +   +---+---+ 2
	 *		    |   | f | g |                      |   |   |   |
	 *		  3 +---+---+---+                      +   +---+---+ 3
	 *		                                       |   | d | e |
	 *		                                       +   +---+---+ 4
	 *		                                       +   + f | g |
	 *		                                       +---+---+---+ 5
	 *
	 * @param {module:engine/model/element~Element} table The table model element where the rows will be inserted.
	 * @param {Object} options
	 * @param {Number} [options.at=0] The row index at which the rows will be inserted.
	 * @param {Number} [options.rows=1] The number of rows to insert.
	 * @param {Boolean|undefined} [options.copyStructureFromAbove] The flag for copying row structure. Note that
	 * the row structure will not be copied if this option is not provided.
	 */
	insertRows( table, options = {} ) {
		const model = this.editor.model;

		const insertAt = options.at || 0;
		const rowsToInsert = options.rows || 1;
		const isCopyStructure = options.copyStructureFromAbove !== undefined;
		const copyStructureFrom = options.copyStructureFromAbove ? insertAt - 1 : insertAt;

		const rows = this.getRows( table );
		const columns = this.getColumns( table );

		if ( insertAt > rows ) {
			/**
			 * The `options.at` points at a row position that does not exist.
			 *
			 * @error tableutils-insertrows-insert-out-of-range
			 */
			throw new CKEditorError(
				'tableutils-insertrows-insert-out-of-range',
				this,
				{ options }
			);
		}

		model.change( writer => {
			const headingRows = table.getAttribute( 'headingRows' ) || 0;

			// Inserting rows inside heading section requires to update `headingRows` attribute as the heading section will grow.
			if ( headingRows > insertAt ) {
				updateNumericAttribute( 'headingRows', headingRows + rowsToInsert, table, writer, 0 );
			}

			// Inserting at the end or at the beginning of a table doesn't require to calculate anything special.
			if ( !isCopyStructure && ( insertAt === 0 || insertAt === rows ) ) {
				createEmptyRows( writer, table, insertAt, rowsToInsert, columns );

				return;
			}

			// Iterate over all the rows above the inserted rows in order to check for the row-spanned cells.
			const walkerEndRow = isCopyStructure ? Math.max( insertAt, copyStructureFrom ) : insertAt;
			const tableIterator = new TableWalker( table, { endRow: walkerEndRow } );

			// Store spans of the reference row to reproduce it's structure. This array is column number indexed.
			const rowColSpansMap = new Array( columns ).fill( 1 );

			for ( const { row, column, cellHeight, cellWidth, cell } of tableIterator ) {
				const lastCellRow = row + cellHeight - 1;

				const isOverlappingInsertedRow = row < insertAt && insertAt <= lastCellRow;
				const isReferenceRow = row <= copyStructureFrom && copyStructureFrom <= lastCellRow;

				// If the cell is row-spanned and overlaps the inserted row, then reserve space for it in the row map.
				if ( isOverlappingInsertedRow ) {
					// This cell overlaps the inserted rows so we need to expand it further.
					writer.setAttribute( 'rowspan', cellHeight + rowsToInsert, cell );

					// Mark this cell with negative number to indicate how many cells should be skipped when adding the new cells.
					rowColSpansMap[ column ] = -cellWidth;
				}
				// Store the colspan from reference row.
				else if ( isCopyStructure && isReferenceRow ) {
					rowColSpansMap[ column ] = cellWidth;
				}
			}

			for ( let rowIndex = 0; rowIndex < rowsToInsert; rowIndex++ ) {
				const tableRow = writer.createElement( 'tableRow' );

				writer.insert( tableRow, table, insertAt );

				for ( let cellIndex = 0; cellIndex < rowColSpansMap.length; cellIndex++ ) {
					const colspan = rowColSpansMap[ cellIndex ];
					const insertPosition = writer.createPositionAt( tableRow, 'end' );

					// Insert the empty cell only if this slot is not row-spanned from any other cell.
					if ( colspan > 0 ) {
						createEmptyTableCell( writer, insertPosition, colspan > 1 ? { colspan } : null );
					}

					// Skip the col-spanned slots, there won't be any cells.
					cellIndex += Math.abs( colspan ) - 1;
				}
			}
		} );
	}

	/**
	 * Inserts columns into a table.
	 *
	 *		editor.plugins.get( 'TableUtils' ).insertColumns( table, { at: 1, columns: 2 } );
	 *
	 * Assuming the table on the left, the above code will transform it to the table on the right:
	 *
	 *		0   1   2   3                   0   1   2   3   4   5
	 *		+---+---+---+                   +---+---+---+---+---+
	 *		| a     | b |                   | a             | b |
	 *		+       +---+                   +               +---+
	 *		|       | c |                   |               | c |
	 *		+---+---+---+     will give:    +---+---+---+---+---+
	 *		| d | e | f |                   | d |   |   | e | f |
	 *		+---+   +---+                   +---+---+---+   +---+
	 *		| g |   | h |                   | g |   |   |   | h |
	 *		+---+---+---+                   +---+---+---+---+---+
	 *		| i         |                   | i                 |
	 *		+---+---+---+                   +---+---+---+---+---+
	 *		    ^---- insert here, `at` = 1, `columns` = 2
	 *
	 * @param {module:engine/model/element~Element} table The table model element where the columns will be inserted.
	 * @param {Object} options
	 * @param {Number} [options.at=0] The column index at which the columns will be inserted.
	 * @param {Number} [options.columns=1] The number of columns to insert.
	 */
	insertColumns( table, options = {} ) {
		const model = this.editor.model;

		const insertAt = options.at || 0;
		const columnsToInsert = options.columns || 1;

		model.change( writer => {
			const headingColumns = table.getAttribute( 'headingColumns' );

			// Inserting columns inside heading section requires to update `headingColumns` attribute as the heading section will grow.
			if ( insertAt < headingColumns ) {
				writer.setAttribute( 'headingColumns', headingColumns + columnsToInsert, table );
			}

			const tableColumns = this.getColumns( table );

			// Inserting at the end and at the beginning of a table doesn't require to calculate anything special.
			if ( insertAt === 0 || tableColumns === insertAt ) {
				for ( const tableRow of table.getChildren() ) {
					// Ignore non-row elements inside the table (e.g. caption).
					if ( !tableRow.is( 'element', 'tableRow' ) ) {
						continue;
					}

					createCells( columnsToInsert, writer, writer.createPositionAt( tableRow, insertAt ? 'end' : 0 ) );
				}

				return;
			}

			const tableWalker = new TableWalker( table, { column: insertAt, includeAllSlots: true } );

			for ( const tableSlot of tableWalker ) {
				const { row, cell, cellAnchorColumn, cellAnchorRow, cellWidth, cellHeight } = tableSlot;

				// When iterating over column the table walker outputs either:
				// - cells at given column index (cell "e" from method docs),
				// - spanned columns (spanned cell from row between cells "g" and "h" - spanned by "e", only if `includeAllSlots: true`),
				// - or a cell from the same row which spans over this column (cell "a").

				if ( cellAnchorColumn < insertAt ) {
					// If cell is anchored in previous column, it is a cell that spans over an inserted column (cell "a" & "i").
					// For such cells expand them by a number of columns inserted.
					writer.setAttribute( 'colspan', cellWidth + columnsToInsert, cell );

					// This cell will overlap cells in rows below so skip them (because of `includeAllSlots` option) - (cell "a")
					const lastCellRow = cellAnchorRow + cellHeight - 1;

					for ( let i = row; i <= lastCellRow; i++ ) {
						tableWalker.skipRow( i );
					}
				} else {
					// It's either cell at this column index or spanned cell by a row-spanned cell from row above.
					// In table above it's cell "e" and a spanned position from row below (empty cell between cells "g" and "h")
					createCells( columnsToInsert, writer, tableSlot.getPositionBefore() );
				}
			}
		} );
	}

	/**
	 * Removes rows from the given `table`.
	 *
	 * This method re-calculates the table geometry including `rowspan` attribute of table cells overlapping removed rows
	 * and table headings values.
	 *
	 *		editor.plugins.get( 'TableUtils' ).removeRows( table, { at: 1, rows: 2 } );
	 *
	 * Executing the above code in the context of the table on the left will transform its structure as presented on the right:
	 *
	 *		row index
	 *		    ┌───┬───┬───┐        `at` = 1        ┌───┬───┬───┐
	 *		  0 │ a │ b │ c │        `rows` = 2      │ a │ b │ c │ 0
	 *		    │   ├───┼───┤                        │   ├───┼───┤
	 *		  1 │   │ d │ e │  <-- remove from here  │   │ d │ g │ 1
	 *		    │   │   ├───┤        will give:      ├───┼───┼───┤
	 *		  2 │   │   │ f │                        │ h │ i │ j │ 2
	 *		    │   │   ├───┤                        └───┴───┴───┘
	 *		  3 │   │   │ g │
	 *		    ├───┼───┼───┤
	 *		  4 │ h │ i │ j │
	 *		    └───┴───┴───┘
	 *
	 * @param {module:engine/model/element~Element} table
	 * @param {Object} options
	 * @param {Number} options.at The row index at which the removing rows will start.
	 * @param {Number} [options.rows=1] The number of rows to remove.
	 */
	removeRows( table, options ) {
		const model = this.editor.model;

		const rowsToRemove = options.rows || 1;
		const rowCount = this.getRows( table );
		const first = options.at;
		const last = first + rowsToRemove - 1;

		if ( last > rowCount - 1 ) {
			/**
			 * The `options.at` param must point at existing row and `options.rows` must not exceed the rows in the table.
			 *
			 * @error tableutils-removerows-row-index-out-of-range
			 */
			throw new CKEditorError(
				'tableutils-removerows-row-index-out-of-range',
				this,
				{ table, options }
			);
		}

		model.change( writer => {
			// Removing rows from the table require that most calculations to be done prior to changing table structure.
			// Preparations must be done in the same enqueueChange callback to use the current table structure.

			// 1. Preparation - get row-spanned cells that have to be modified after removing rows.
			const { cellsToMove, cellsToTrim } = getCellsToMoveAndTrimOnRemoveRow( table, first, last );

			// 2. Execution

			// 2a. Move cells from removed rows that extends over a removed section - must be done before removing rows.
			// This will fill any gaps in a rows below that previously were empty because of row-spanned cells.
			if ( cellsToMove.size ) {
				const rowAfterRemovedSection = last + 1;
				moveCellsToRow( table, rowAfterRemovedSection, cellsToMove, writer );
			}

			// 2b. Remove all required rows.
			for ( let i = last; i >= first; i-- ) {
				writer.remove( table.getChild( i ) );
			}

			// 2c. Update cells from rows above that overlap removed section. Similar to step 2 but does not involve moving cells.
			for ( const { rowspan, cell } of cellsToTrim ) {
				updateNumericAttribute( 'rowspan', rowspan, cell, writer );
			}

			// 2d. Adjust heading rows if removed rows were in a heading section.
			updateHeadingRows( table, first, last, writer );

			// 2e. Remove empty columns (without anchored cells) if there are any.
			if ( !removeEmptyColumns( table, this ) ) {
				// If there wasn't any empty columns then we still need to check if this wasn't called
				// because of cleaning empty rows and we only removed one of them.
				removeEmptyRows( table, this );
			}
		} );
	}

	/**
	 * Removes columns from the given `table`.
	 *
	 * This method re-calculates the table geometry including the `colspan` attribute of table cells overlapping removed columns
	 * and table headings values.
	 *
	 *		editor.plugins.get( 'TableUtils' ).removeColumns( table, { at: 1, columns: 2 } );
	 *
	 * Executing the above code in the context of the table on the left will transform its structure as presented on the right:
	 *
	 *		  0   1   2   3   4                       0   1   2
	 *		┌───────────────┬───┐                   ┌───────┬───┐
	 *		│ a             │ b │                   │ a     │ b │
	 *		│               ├───┤                   │       ├───┤
	 *		│               │ c │                   │       │ c │
	 *		├───┬───┬───┬───┼───┤     will give:    ├───┬───┼───┤
	 *		│ d │ e │ f │ g │ h │                   │ d │ g │ h │
	 *		├───┼───┼───┤   ├───┤                   ├───┤   ├───┤
	 *		│ i │ j │ k │   │ l │                   │ i │   │ l │
	 *		├───┴───┴───┴───┴───┤                   ├───┴───┴───┤
	 *		│ m                 │                   │ m         │
	 *		└───────────────────┘                   └───────────┘
	 *		      ^---- remove from here, `at` = 1, `columns` = 2
	 *
	 * @param {module:engine/model/element~Element} table
	 * @param {Object} options
	 * @param {Number} options.at The row index at which the removing columns will start.
	 * @param {Number} [options.columns=1] The number of columns to remove.
	 */
	removeColumns( table, options ) {
		const model = this.editor.model;
		const first = options.at;
		const columnsToRemove = options.columns || 1;
		const last = options.at + columnsToRemove - 1;

		model.change( writer => {
			adjustHeadingColumns( table, { first, last }, writer );

			for ( let removedColumnIndex = last; removedColumnIndex >= first; removedColumnIndex-- ) {
				for ( const { cell, column, cellWidth } of [ ...new TableWalker( table ) ] ) {
					// If colspaned cell overlaps removed column decrease its span.
					if ( column <= removedColumnIndex && cellWidth > 1 && column + cellWidth > removedColumnIndex ) {
						updateNumericAttribute( 'colspan', cellWidth - 1, cell, writer );
					} else if ( column === removedColumnIndex ) {
						// The cell in removed column has colspan of 1.
						writer.remove( cell );
					}
				}
			}

			// Remove empty rows that could appear after removing columns.
			if ( !removeEmptyRows( table, this ) ) {
				// If there wasn't any empty rows then we still need to check if this wasn't called
				// because of cleaning empty columns and we only removed one of them.
				removeEmptyColumns( table, this );
			}
		} );
	}

	/**
	 * Divides a table cell vertically into several ones.
	 *
	 * The cell will be visually split into more cells by updating colspans of other cells in a column
	 * and inserting cells (columns) after that cell.
	 *
	 * In the table below, if cell "a" is split into 3 cells:
	 *
	 *		+---+---+---+
	 *		| a | b | c |
	 *		+---+---+---+
	 *		| d | e | f |
	 *		+---+---+---+
	 *
	 * it will result in the table below:
	 *
	 *		+---+---+---+---+---+
	 *		| a |   |   | b | c |
	 *		+---+---+---+---+---+
	 *		| d         | e | f |
	 *		+---+---+---+---+---+
	 *
	 * So cell "d" will get its `colspan` updated to `3` and 2 cells will be added (2 columns will be created).
	 *
	 * Splitting a cell that already has a `colspan` attribute set will distribute the cell `colspan` evenly and the remainder
	 * will be left to the original cell:
	 *
	 *		+---+---+---+
	 *		| a         |
	 *		+---+---+---+
	 *		| b | c | d |
	 *		+---+---+---+
	 *
	 * Splitting cell "a" with `colspan=3` into 2 cells will create 1 cell with a `colspan=a` and cell "a" that will have `colspan=2`:
	 *
	 *		+---+---+---+
	 *		| a     |   |
	 *		+---+---+---+
	 *		| b | c | d |
	 *		+---+---+---+
	 *
	 * @param {module:engine/model/element~Element} tableCell
	 * @param {Number} numberOfCells
	 */
	splitCellVertically( tableCell, numberOfCells = 2 ) {
		const model = this.editor.model;
		const tableRow = tableCell.parent;
		const table = tableRow.parent;

		const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
		const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );

		model.change( writer => {
			// First check - the cell spans over multiple rows so before doing anything else just split this cell.
			if ( colspan > 1 ) {
				// Get spans of new (inserted) cells and span to update of split cell.
				const { newCellsSpan, updatedSpan } = breakSpanEvenly( colspan, numberOfCells );

				updateNumericAttribute( 'colspan', updatedSpan, tableCell, writer );

				// Each inserted cell will have the same attributes:
				const newCellsAttributes = {};

				// Do not store default value in the model.
				if ( newCellsSpan > 1 ) {
					newCellsAttributes.colspan = newCellsSpan;
				}

				// Copy rowspan of split cell.
				if ( rowspan > 1 ) {
					newCellsAttributes.rowspan = rowspan;
				}

				const cellsToInsert = colspan > numberOfCells ? numberOfCells - 1 : colspan - 1;
				createCells( cellsToInsert, writer, writer.createPositionAfter( tableCell ), newCellsAttributes );
			}

			// Second check - the cell has colspan of 1 or we need to create more cells then the currently one spans over.
			if ( colspan < numberOfCells ) {
				const cellsToInsert = numberOfCells - colspan;

				// First step: expand cells on the same column as split cell.
				const tableMap = [ ...new TableWalker( table ) ];

				// Get the column index of split cell.
				const { column: splitCellColumn } = tableMap.find( ( { cell } ) => cell === tableCell );

				// Find cells which needs to be expanded vertically - those on the same column or those that spans over split cell's column.
				const cellsToUpdate = tableMap.filter( ( { cell, cellWidth, column } ) => {
					const isOnSameColumn = cell !== tableCell && column === splitCellColumn;
					const spansOverColumn = ( column < splitCellColumn && column + cellWidth > splitCellColumn );

					return isOnSameColumn || spansOverColumn;
				} );

				// Expand cells vertically.
				for ( const { cell, cellWidth } of cellsToUpdate ) {
					writer.setAttribute( 'colspan', cellWidth + cellsToInsert, cell );
				}

				// Second step: create columns after split cell.

				// Each inserted cell will have the same attributes:
				const newCellsAttributes = {};

				// Do not store default value in the model.

				// Copy rowspan of split cell.
				if ( rowspan > 1 ) {
					newCellsAttributes.rowspan = rowspan;
				}

				createCells( cellsToInsert, writer, writer.createPositionAfter( tableCell ), newCellsAttributes );

				const headingColumns = table.getAttribute( 'headingColumns' ) || 0;

				// Update heading section if split cell is in heading section.
				if ( headingColumns > splitCellColumn ) {
					updateNumericAttribute( 'headingColumns', headingColumns + cellsToInsert, table, writer );
				}
			}
		} );
	}

	/**
	 * Divides a table cell horizontally into several ones.
	 *
	 * The cell will be visually split into more cells by updating rowspans of other cells in the row and inserting rows with a single cell
	 * below.
	 *
	 * If in the table below cell "b" is split into 3 cells:
	 *
	 *		+---+---+---+
	 *		| a | b | c |
	 *		+---+---+---+
	 *		| d | e | f |
	 *		+---+---+---+
	 *
	 * It will result in the table below:
	 *
	 *		+---+---+---+
	 *		| a | b | c |
	 *		+   +---+   +
	 *		|   |   |   |
	 *		+   +---+   +
	 *		|   |   |   |
	 *		+---+---+---+
	 *		| d | e | f |
	 *		+---+---+---+
	 *
	 * So cells "a" and "b" will get their `rowspan` updated to `3` and 2 rows with a single cell will be added.
	 *
	 * Splitting a cell that already has a `rowspan` attribute set will distribute the cell `rowspan` evenly and the remainder
	 * will be left to the original cell:
	 *
	 *		+---+---+---+
	 *		| a | b | c |
	 *		+   +---+---+
	 *		|   | d | e |
	 *		+   +---+---+
	 *		|   | f | g |
	 *		+   +---+---+
	 *		|   | h | i |
	 *		+---+---+---+
	 *
	 * Splitting cell "a" with `rowspan=4` into 3 cells will create 2 cells with a `rowspan=1` and cell "a" will have `rowspan=2`:
	 *
	 *		+---+---+---+
	 *		| a | b | c |
	 *		+   +---+---+
	 *		|   | d | e |
	 *		+---+---+---+
	 *		|   | f | g |
	 *		+---+---+---+
	 *		|   | h | i |
	 *		+---+---+---+
	 *
	 * @param {module:engine/model/element~Element} tableCell
	 * @param {Number} numberOfCells
	 */
	splitCellHorizontally( tableCell, numberOfCells = 2 ) {
		const model = this.editor.model;

		const tableRow = tableCell.parent;
		const table = tableRow.parent;
		const splitCellRow = table.getChildIndex( tableRow );

		const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
		const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );

		model.change( writer => {
			// First check - the cell spans over multiple rows so before doing anything else just split this cell.
			if ( rowspan > 1 ) {
				// Cache table map before updating table.
				const tableMap = [ ...new TableWalker( table, {
					startRow: splitCellRow,
					endRow: splitCellRow + rowspan - 1,
					includeAllSlots: true
				} ) ];

				// Get spans of new (inserted) cells and span to update of split cell.
				const { newCellsSpan, updatedSpan } = breakSpanEvenly( rowspan, numberOfCells );

				updateNumericAttribute( 'rowspan', updatedSpan, tableCell, writer );

				const { column: cellColumn } = tableMap.find( ( { cell } ) => cell === tableCell );

				// Each inserted cell will have the same attributes:
				const newCellsAttributes = {};

				// Do not store default value in the model.
				if ( newCellsSpan > 1 ) {
					newCellsAttributes.rowspan = newCellsSpan;
				}

				// Copy colspan of split cell.
				if ( colspan > 1 ) {
					newCellsAttributes.colspan = colspan;
				}

				for ( const tableSlot of tableMap ) {
					const { column, row } = tableSlot;

					// As both newly created cells and the split cell might have rowspan,
					// the insertion of new cells must go to appropriate rows:
					//
					// 1. It's a row after split cell + it's height.
					const isAfterSplitCell = row >= splitCellRow + updatedSpan;
					// 2. Is on the same column.
					const isOnSameColumn = column === cellColumn;
					// 3. And it's row index is after previous cell height.
					const isInEvenlySplitRow = ( row + splitCellRow + updatedSpan ) % newCellsSpan === 0;

					if ( isAfterSplitCell && isOnSameColumn && isInEvenlySplitRow ) {
						createCells( 1, writer, tableSlot.getPositionBefore(), newCellsAttributes );
					}
				}
			}

			// Second check - the cell has rowspan of 1 or we need to create more cells than the current cell spans over.
			if ( rowspan < numberOfCells ) {
				// We already split the cell in check one so here we split to the remaining number of cells only.
				const cellsToInsert = numberOfCells - rowspan;

				// This check is needed since we need to check if there are any cells from previous rows than spans over this cell's row.
				const tableMap = [ ...new TableWalker( table, { startRow: 0, endRow: splitCellRow } ) ];

				// First step: expand cells.
				for ( const { cell, cellHeight, row } of tableMap ) {
					// Expand rowspan of cells that are either:
					// - on the same row as current cell,
					// - or are below split cell row and overlaps that row.
					if ( cell !== tableCell && row + cellHeight > splitCellRow ) {
						const rowspanToSet = cellHeight + cellsToInsert;

						writer.setAttribute( 'rowspan', rowspanToSet, cell );
					}
				}

				// Second step: create rows with single cell below split cell.
				const newCellsAttributes = {};

				// Copy colspan of split cell.
				if ( colspan > 1 ) {
					newCellsAttributes.colspan = colspan;
				}

				createEmptyRows( writer, table, splitCellRow + 1, cellsToInsert, 1, newCellsAttributes );

				// Update heading section if split cell is in heading section.
				const headingRows = table.getAttribute( 'headingRows' ) || 0;

				if ( headingRows > splitCellRow ) {
					updateNumericAttribute( 'headingRows', headingRows + cellsToInsert, table, writer );
				}
			}
		} );
	}

	/**
	 * Returns the number of columns for a given table.
	 *
	 *		editor.plugins.get( 'TableUtils' ).getColumns( table );
	 *
	 * @param {module:engine/model/element~Element} table The table to analyze.
	 * @returns {Number}
	 */
	getColumns( table ) {
		// Analyze first row only as all the rows should have the same width.
		// Using the first row without checking if it's a tableRow because we expect
		// that table will have only tableRow model elements at the beginning.
		const row = table.getChild( 0 );

		return [ ...row.getChildren() ].reduce( ( columns, row ) => {
			const columnWidth = parseInt( row.getAttribute( 'colspan' ) || 1 );

			return columns + columnWidth;
		}, 0 );
	}

	/**
	 * Returns the number of rows for a given table. Any other element present in the table model is omitted.
	 *
	 *		editor.plugins.get( 'TableUtils' ).getRows( table );
	 *
	 * @param {module:engine/model/element~Element} table The table to analyze.
	 * @returns {Number}
	 */
	getRows( table ) {
		// Rowspan not included due to #6427.
		return Array.from( table.getChildren() )
			.reduce( ( rowCount, child ) => child.is( 'element', 'tableRow' ) ? rowCount + 1 : rowCount, 0 );
	}

	/**
	 * Creates an instance of the table walker.
	 *
	 * The table walker iterates internally by traversing the table from row index = 0 and column index = 0.
	 * It walks row by row and column by column in order to output values defined in the options.
	 * By default it will output only the locations that are occupied by a cell. To include also spanned rows and columns,
	 * pass the `includeAllSlots` option.
	 *
	 * @protected
	 * @param {module:engine/model/element~Element} table A table over which the walker iterates.
	 * @param {Object} [options={}] An object with configuration.
	 * @param {Number} [options.row] A row index for which this iterator will output cells.
	 * Can't be used together with `startRow` and `endRow`.
	 * @param {Number} [options.startRow=0] A row index from which this iterator should start. Can't be used together with `row`.
	 * @param {Number} [options.endRow] A row index at which this iterator should end. Can't be used together with `row`.
	 * @param {Number} [options.column] A column index for which this iterator will output cells.
	 * Can't be used together with `startColumn` and `endColumn`.
	 * @param {Number} [options.startColumn=0] A column index from which this iterator should start. Can't be used together with `column`.
	 * @param {Number} [options.endColumn] A column index at which this iterator should end. Can't be used together with `column`.
	 * @param {Boolean} [options.includeAllSlots=false] Also return values for spanned cells.
	 */
	createTableWalker( table, options = {} ) {
		return new TableWalker( table, options );
	}

	/**
	 * Returns all model table cells that are fully selected (from the outside)
	 * within the provided model selection's ranges.
	 *
	 * To obtain the cells selected from the inside, use
	 * {@link #getTableCellsContainingSelection}.
	 *
	 * @param {module:engine/model/selection~Selection} selection
	 * @returns {Array.<module:engine/model/element~Element>}
	 */
	getSelectedTableCells( selection ) {
		const cells = [];

		for ( const range of this.sortRanges( selection.getRanges() ) ) {
			const element = range.getContainedElement();

			if ( element && element.is( 'element', 'tableCell' ) ) {
				cells.push( element );
			}
		}

		return cells;
	}

	/**
	 * Returns all model table cells that the provided model selection's ranges
	 * {@link module:engine/model/range~Range#start} inside.
	 *
	 * To obtain the cells selected from the outside, use
	 * {@link #getSelectedTableCells}.
	 *
	 * @param {module:engine/model/selection~Selection} selection
	 * @returns {Array.<module:engine/model/element~Element>}
	 */
	getTableCellsContainingSelection( selection ) {
		const cells = [];

		for ( const range of selection.getRanges() ) {
			const cellWithSelection = range.start.findAncestor( 'tableCell' );

			if ( cellWithSelection ) {
				cells.push( cellWithSelection );
			}
		}

		return cells;
	}

	/**
	 * Returns all model table cells that are either completely selected
	 * by selection ranges or host selection range
	 * {@link module:engine/model/range~Range#start start positions} inside them.
	 *
	 * Combines {@link #getTableCellsContainingSelection} and
	 * {@link #getSelectedTableCells}.
	 *
	 * @param {module:engine/model/selection~Selection} selection
	 * @returns {Array.<module:engine/model/element~Element>}
	 */
	getSelectionAffectedTableCells( selection ) {
		const selectedCells = this.getSelectedTableCells( selection );

		if ( selectedCells.length ) {
			return selectedCells;
		}

		return this.getTableCellsContainingSelection( selection );
	}

	/**
	 * Returns an object with the `first` and `last` row index contained in the given `tableCells`.
	 *
	 *		const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
	*
	*		const { first, last } = getRowIndexes( selectedTableCells );
	*
	*		console.log( `Selected rows: ${ first } to ${ last }` );
	*
	* @param {Array.<module:engine/model/element~Element>} tableCells
	* @returns {Object} Returns an object with the `first` and `last` table row indexes.
	*/
	getRowIndexes( tableCells ) {
		const indexes = tableCells.map( cell => cell.parent.index );

		return this._getFirstLastIndexesObject( indexes );
	}

	/**
	 * Returns an object with the `first` and `last` column index contained in the given `tableCells`.
	 *
	 *		const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
	*
	*		const { first, last } = getColumnIndexes( selectedTableCells );
	*
	*		console.log( `Selected columns: ${ first } to ${ last }` );
	*
	* @param {Array.<module:engine/model/element~Element>} tableCells
	* @returns {Object} Returns an object with the `first` and `last` table column indexes.
	*/
	getColumnIndexes( tableCells ) {
		const table = tableCells[ 0 ].findAncestor( 'table' );
		const tableMap = [ ...new TableWalker( table ) ];

		const indexes = tableMap
			.filter( entry => tableCells.includes( entry.cell ) )
			.map( entry => entry.column );

		return this._getFirstLastIndexesObject( indexes );
	}

	/**
	 * Checks if the selection contains cells that do not exceed rectangular selection.
	 *
	 * In a table below:
	 *
	 *		┌───┬───┬───┬───┐
	*		│ a │ b │ c │ d │
	*		├───┴───┼───┤   │
	*		│ e     │ f │   │
	*		│       ├───┼───┤
	*		│       │ g │ h │
	*		└───────┴───┴───┘
	*
	* Valid selections are these which create a solid rectangle (without gaps), such as:
	*   - a, b (two horizontal cells)
	*   - c, f (two vertical cells)
	*   - a, b, e (cell "e" spans over four cells)
	*   - c, d, f (cell d spans over a cell in the row below)
	*
	* While an invalid selection would be:
	*   - a, c (the unselected cell "b" creates a gap)
	*   - f, g, h (cell "d" spans over a cell from the row of "f" cell - thus creates a gap)
	*
	* @param {Array.<module:engine/model/element~Element>} selectedTableCells
	* @returns {Boolean}
	*/
	isSelectionRectangular( selectedTableCells ) {
		if ( selectedTableCells.length < 2 || !this._areCellInTheSameTableSection( selectedTableCells ) ) {
			return false;
		}

		// A valid selection is a fully occupied rectangle composed of table cells.
		// Below we will calculate the area of a selected table cells and the area of valid selection.
		// The area of a valid selection is defined by top-left and bottom-right cells.
		const rows = new Set();
		const columns = new Set();

		let areaOfSelectedCells = 0;

		for ( const tableCell of selectedTableCells ) {
			const { row, column } = this.getCellLocation( tableCell );
			const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
			const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );

			// Record row & column indexes of current cell.
			rows.add( row );
			columns.add( column );

			// For cells that spans over multiple rows add also the last row that this cell spans over.
			if ( rowspan > 1 ) {
				rows.add( row + rowspan - 1 );
			}

			// For cells that spans over multiple columns add also the last column that this cell spans over.
			if ( colspan > 1 ) {
				columns.add( column + colspan - 1 );
			}

			areaOfSelectedCells += ( rowspan * colspan );
		}

		// We can only merge table cells that are in adjacent rows...
		const areaOfValidSelection = getBiggestRectangleArea( rows, columns );

		return areaOfValidSelection == areaOfSelectedCells;
	}

	/**
	 * Returns array of sorted ranges.
	 *
	 * @param {Iterable.<module:engine/model/range~Range>} ranges
	 * @return {Array.<module:engine/model/range~Range>}
	 */
	sortRanges( ranges ) {
		return Array.from( ranges ).sort( compareRangeOrder );
	}

	/**
	 * Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes.
	 *
	 * @private
	 * @param {Number[]} indexes
	 * @returns {Object}
	 */
	_getFirstLastIndexesObject( indexes ) {
		const allIndexesSorted = indexes.sort( ( indexA, indexB ) => indexA - indexB );

		const first = allIndexesSorted[ 0 ];
		const last = allIndexesSorted[ allIndexesSorted.length - 1 ];

		return { first, last };
	}

	/**
	 * Checks if the selection does not mix a header (column or row) with other cells.
	 *
	 * For instance, in the table below valid selections consist of cells with the same letter only.
	 * So, a-a (same heading row and column) or d-d (body cells) are valid while c-d or a-b are not.
	 *
	 * header columns
	 *		  ↓   ↓
	 *		┌───┬───┬───┬───┐
	 *		│ a │ a │ b │ b │  ← header row
	 *		├───┼───┼───┼───┤
	 *		│ c │ c │ d │ d │
	 *		├───┼───┼───┼───┤
	 *		│ c │ c │ d │ d │
	 *		└───┴───┴───┴───┘
	 *
	 * @private
	 * @param {Array.<module:engine/model/element~Element>} tableCells
	 * @returns {Boolean}
	 */
	_areCellInTheSameTableSection( tableCells ) {
		const table = tableCells[ 0 ].findAncestor( 'table' );

		const rowIndexes = this.getRowIndexes( tableCells );
		const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 );

		// Calculating row indexes is a bit cheaper so if this check fails we can't merge.
		if ( !this._areIndexesInSameSection( rowIndexes, headingRows ) ) {
			return false;
		}

		const headingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
		const columnIndexes = this.getColumnIndexes( tableCells );

		// Similarly cells must be in same column section.
		return this._areIndexesInSameSection( columnIndexes, headingColumns );
	}

	/**
	 * Unified check if table rows/columns indexes are in the same heading/body section.
	 *
	 * @private
	 * @param {Object} params
	 * @param {Number} params.first
	 * @param {Number} params.last
	 * @param {Number} headingSectionSize
	 */
	_areIndexesInSameSection( { first, last }, headingSectionSize ) {
		const firstCellIsInHeading = first < headingSectionSize;
		const lastCellIsInHeading = last < headingSectionSize;

		return firstCellIsInHeading === lastCellIsInHeading;
	}
}

// Creates empty rows at the given index in an existing table.
//
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/element~Element} table
// @param {Number} insertAt The row index of row insertion.
// @param {Number} rows The number of rows to create.
// @param {Number} tableCellToInsert The number of cells to insert in each row.
function createEmptyRows( writer, table, insertAt, rows, tableCellToInsert, attributes = {} ) {
	for ( let i = 0; i < rows; i++ ) {
		const tableRow = writer.createElement( 'tableRow' );

		writer.insert( tableRow, table, insertAt );

		createCells( tableCellToInsert, writer, writer.createPositionAt( tableRow, 'end' ), attributes );
	}
}

// Creates cells at a given position.
//
// @param {Number} columns The number of columns to create
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/position~Position} insertPosition
function createCells( cells, writer, insertPosition, attributes = {} ) {
	for ( let i = 0; i < cells; i++ ) {
		createEmptyTableCell( writer, insertPosition, attributes );
	}
}

// Evenly distributes the span of a cell to a number of provided cells.
// The resulting spans will always be integer values.
//
// For instance breaking a span of 7 into 3 cells will return:
//
//		{ newCellsSpan: 2, updatedSpan: 3 }
//
// as two cells will have a span of 2 and the remainder will go the first cell so its span will change to 3.
//
// @param {Number} span The span value do break.
// @param {Number} numberOfCells The number of resulting spans.
// @returns {{newCellsSpan: Number, updatedSpan: Number}}
function breakSpanEvenly( span, numberOfCells ) {
	if ( span < numberOfCells ) {
		return { newCellsSpan: 1, updatedSpan: 1 };
	}

	const newCellsSpan = Math.floor( span / numberOfCells );
	const updatedSpan = ( span - newCellsSpan * numberOfCells ) + newCellsSpan;

	return { newCellsSpan, updatedSpan };
}

// Updates heading columns attribute if removing a row from head section.
function adjustHeadingColumns( table, removedColumnIndexes, writer ) {
	const headingColumns = table.getAttribute( 'headingColumns' ) || 0;

	if ( headingColumns && removedColumnIndexes.first < headingColumns ) {
		const headingsRemoved = Math.min( headingColumns - 1 /* Other numbers are 0-based */, removedColumnIndexes.last ) -
			removedColumnIndexes.first + 1;

		writer.setAttribute( 'headingColumns', headingColumns - headingsRemoved, table );
	}
}

// Calculates a new heading rows value for removing rows from heading section.
function updateHeadingRows( table, first, last, writer ) {
	const headingRows = table.getAttribute( 'headingRows' ) || 0;

	if ( first < headingRows ) {
		const newRows = last < headingRows ? headingRows - ( last - first + 1 ) : first;

		updateNumericAttribute( 'headingRows', newRows, table, writer, 0 );
	}
}

// Finds cells that will be:
// - trimmed - Cells that are "above" removed rows sections and overlap the removed section - their rowspan must be trimmed.
// - moved - Cells from removed rows section might stick out of. These cells are moved to the next row after a removed section.
//
// Sample table with overlapping & sticking out cells:
//
//      +----+----+----+----+----+
//      | 00 | 01 | 02 | 03 | 04 |
//      +----+    +    +    +    +
//      | 10 |    |    |    |    |
//      +----+----+    +    +    +
//      | 20 | 21 |    |    |    | <-- removed row
//      +    +    +----+    +    +
//      |    |    | 32 |    |    | <-- removed row
//      +----+    +    +----+    +
//      | 40 |    |    | 43 |    |
//      +----+----+----+----+----+
//
// In a table above:
// - cells to trim: '02', '03' & '04'.
// - cells to move: '21' & '32'.
function getCellsToMoveAndTrimOnRemoveRow( table, first, last ) {
	const cellsToMove = new Map();
	const cellsToTrim = [];

	for ( const { row, column, cellHeight, cell } of new TableWalker( table, { endRow: last } ) ) {
		const lastRowOfCell = row + cellHeight - 1;

		const isCellStickingOutFromRemovedRows = row >= first && row <= last && lastRowOfCell > last;

		if ( isCellStickingOutFromRemovedRows ) {
			const rowspanInRemovedSection = last - row + 1;
			const rowSpanToSet = cellHeight - rowspanInRemovedSection;

			cellsToMove.set( column, {
				cell,
				rowspan: rowSpanToSet
			} );
		}

		const isCellOverlappingRemovedRows = row < first && lastRowOfCell >= first;

		if ( isCellOverlappingRemovedRows ) {
			let rowspanAdjustment;

			// Cell fully covers removed section - trim it by removed rows count.
			if ( lastRowOfCell >= last ) {
				rowspanAdjustment = last - first + 1;
			}
			// Cell partially overlaps removed section - calculate cell's span that is in removed section.
			else {
				rowspanAdjustment = lastRowOfCell - first + 1;
			}

			cellsToTrim.push( {
				cell,
				rowspan: cellHeight - rowspanAdjustment
			} );
		}
	}
	return { cellsToMove, cellsToTrim };
}

function moveCellsToRow( table, targetRowIndex, cellsToMove, writer ) {
	const tableWalker = new TableWalker( table, {
		includeAllSlots: true,
		row: targetRowIndex
	} );

	const tableRowMap = [ ...tableWalker ];
	const row = table.getChild( targetRowIndex );

	let previousCell;

	for ( const { column, cell, isAnchor } of tableRowMap ) {
		if ( cellsToMove.has( column ) ) {
			const { cell: cellToMove, rowspan } = cellsToMove.get( column );

			const targetPosition = previousCell ?
				writer.createPositionAfter( previousCell ) :
				writer.createPositionAt( row, 0 );

			writer.move( writer.createRangeOn( cellToMove ), targetPosition );
			updateNumericAttribute( 'rowspan', rowspan, cellToMove, writer );

			previousCell = cellToMove;
		} else if ( isAnchor ) {
			// If cell is spanned then `cell` holds reference to overlapping cell. See ckeditor/ckeditor5#6502.
			previousCell = cell;
		}
	}
}

function compareRangeOrder( rangeA, rangeB ) {
	// Since table cell ranges are disjoint, it's enough to check their start positions.
	const posA = rangeA.start;
	const posB = rangeB.start;

	// Checking for equal position (returning 0) is not needed because this would be either:
	// a. Intersecting range (not allowed by model)
	// b. Collapsed range on the same position (allowed by model but should not happen).
	return posA.isBefore( posB ) ? -1 : 1;
}

// Calculates the area of a maximum rectangle that can span over the provided row & column indexes.
//
// @param {Array.<Number>} rows
// @param {Array.<Number>} columns
// @returns {Number}
function getBiggestRectangleArea( rows, columns ) {
	const rowsIndexes = Array.from( rows.values() );
	const columnIndexes = Array.from( columns.values() );

	const lastRow = Math.max( ...rowsIndexes );
	const firstRow = Math.min( ...rowsIndexes );
	const lastColumn = Math.max( ...columnIndexes );
	const firstColumn = Math.min( ...columnIndexes );

	return ( lastRow - firstRow + 1 ) * ( lastColumn - firstColumn + 1 );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The merge cells command.
 *
 * The command is registered by {@link module:table/tableediting~TableEditing} as the `'mergeTableCells'` editor command.
 *
 * For example, to merge selected table cells:
 *
 *		editor.execute( 'mergeTableCells' );
 *
 * @extends module:core/command~Command
 */
class MergeCellsCommand extends Command {
	/**
	 * @inheritDoc
	 */
	refresh() {
		const tableUtils = this.editor.plugins.get( TableUtils );

		const selectedTableCells = tableUtils.getSelectedTableCells( this.editor.model.document.selection );
		this.isEnabled = tableUtils.isSelectionRectangular( selectedTableCells, this.editor.plugins.get( TableUtils ) );
	}

	/**
	 * Executes the command.
	 *
	 * @fires execute
	 */
	execute() {
		const model = this.editor.model;
		const tableUtils = this.editor.plugins.get( TableUtils );

		model.change( writer => {
			const selectedTableCells = tableUtils.getSelectedTableCells( model.document.selection );

			// All cells will be merged into the first one.
			const firstTableCell = selectedTableCells.shift();

			// Update target cell dimensions.
			const { mergeWidth, mergeHeight } = getMergeDimensions( firstTableCell, selectedTableCells, tableUtils );
			updateNumericAttribute( 'colspan', mergeWidth, firstTableCell, writer );
			updateNumericAttribute( 'rowspan', mergeHeight, firstTableCell, writer );

			for ( const tableCell of selectedTableCells ) {
				mergeTableCells( tableCell, firstTableCell, writer );
			}

			const table = firstTableCell.findAncestor( 'table' );

			// Remove rows and columns that become empty (have no anchored cells).
			removeEmptyRowsColumns( table, tableUtils );

			writer.setSelection( firstTableCell, 'in' );
		} );
	}
}

// Merges two table cells. It will ensure that after merging cells with empty paragraphs the resulting table cell will only have one
// paragraph. If one of the merged table cells is empty, the merged table cell will have contents of the non-empty table cell.
// If both are empty, the merged table cell will have only one empty paragraph.
//
// @param {module:engine/model/element~Element} cellBeingMerged
// @param {module:engine/model/element~Element} targetCell
// @param {module:engine/model/writer~Writer} writer
function mergeTableCells( cellBeingMerged, targetCell, writer ) {
	if ( !isEmpty$1( cellBeingMerged ) ) {
		if ( isEmpty$1( targetCell ) ) {
			writer.remove( writer.createRangeIn( targetCell ) );
		}

		writer.move( writer.createRangeIn( cellBeingMerged ), writer.createPositionAt( targetCell, 'end' ) );
	}

	// Remove merged table cell.
	writer.remove( cellBeingMerged );
}

// Checks if the passed table cell contains an empty paragraph.
//
// @param {module:engine/model/element~Element} tableCell
// @returns {Boolean}
function isEmpty$1( tableCell ) {
	return tableCell.childCount == 1 && tableCell.getChild( 0 ).is( 'element', 'paragraph' ) && tableCell.getChild( 0 ).isEmpty;
}

function getMergeDimensions( firstTableCell, selectedTableCells, tableUtils ) {
	let maxWidthOffset = 0;
	let maxHeightOffset = 0;

	for ( const tableCell of selectedTableCells ) {
		const { row, column } = tableUtils.getCellLocation( tableCell );

		maxWidthOffset = getMaxOffset( tableCell, column, maxWidthOffset, 'colspan' );
		maxHeightOffset = getMaxOffset( tableCell, row, maxHeightOffset, 'rowspan' );
	}

	// Update table cell span attribute and merge set selection on a merged contents.
	const { row: firstCellRow, column: firstCellColumn } = tableUtils.getCellLocation( firstTableCell );

	const mergeWidth = maxWidthOffset - firstCellColumn;
	const mergeHeight = maxHeightOffset - firstCellRow;

	return { mergeWidth, mergeHeight };
}

function getMaxOffset( tableCell, start, currentMaxOffset, which ) {
	const dimensionValue = parseInt( tableCell.getAttribute( which ) || 1 );

	return Math.max( currentMaxOffset, start + dimensionValue );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The select row command.
 *
 * The command is registered by {@link module:table/tableediting~TableEditing} as the `'selectTableRow'` editor command.
 *
 * To select the rows containing the selected cells, execute the command:
 *
 *		editor.execute( 'selectTableRow' );
 *
 * @extends module:core/command~Command
 */
class SelectRowCommand extends Command {
	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		// It does not affect data so should be enabled in read-only mode.
		this.affectsData = false;
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const selectedCells = tableUtils.getSelectionAffectedTableCells( this.editor.model.document.selection );

		this.isEnabled = selectedCells.length > 0;
	}

	/**
	 * @inheritDoc
	 */
	execute() {
		const model = this.editor.model;
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const referenceCells = tableUtils.getSelectionAffectedTableCells( model.document.selection );
		const rowIndexes = tableUtils.getRowIndexes( referenceCells );

		const table = referenceCells[ 0 ].findAncestor( 'table' );
		const rangesToSelect = [];

		for ( let rowIndex = rowIndexes.first; rowIndex <= rowIndexes.last; rowIndex++ ) {
			for ( const cell of table.getChild( rowIndex ).getChildren() ) {
				rangesToSelect.push( model.createRangeOn( cell ) );
			}
		}

		model.change( writer => {
			writer.setSelection( rangesToSelect );
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The select column command.
 *
 * The command is registered by {@link module:table/tableediting~TableEditing} as the `'selectTableColumn'` editor command.
 *
 * To select the columns containing the selected cells, execute the command:
 *
 *		editor.execute( 'selectTableColumn' );
 *
 * @extends module:core/command~Command
 */
class SelectColumnCommand extends Command {
	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		// It does not affect data so should be enabled in read-only mode.
		this.affectsData = false;
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const selectedCells = tableUtils.getSelectionAffectedTableCells( this.editor.model.document.selection );

		this.isEnabled = selectedCells.length > 0;
	}

	/**
	 * @inheritDoc
	 */
	execute() {
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const model = this.editor.model;
		const referenceCells = tableUtils.getSelectionAffectedTableCells( model.document.selection );
		const firstCell = referenceCells[ 0 ];
		const lastCell = referenceCells.pop();
		const table = firstCell.findAncestor( 'table' );

		const startLocation = tableUtils.getCellLocation( firstCell );
		const endLocation = tableUtils.getCellLocation( lastCell );

		const startColumn = Math.min( startLocation.column, endLocation.column );
		const endColumn = Math.max( startLocation.column, endLocation.column );

		const rangesToSelect = [];

		for ( const cellInfo of new TableWalker( table, { startColumn, endColumn } ) ) {
			rangesToSelect.push( model.createRangeOn( cellInfo.cell ) );
		}

		model.change( writer => {
			writer.setSelection( rangesToSelect );
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Injects a table layout post-fixer into the model.
 *
 * The role of the table layout post-fixer is to ensure that the table rows have the correct structure
 * after a {@link module:engine/model/model~Model#change `change()`} block was executed.
 *
 * The correct structure means that:
 *
 * * All table rows have the same size.
 * * None of the table cells extend vertically beyond their section (either header or body).
 * * A table cell has always at least one element as a child.
 *
 * If the table structure is not correct, the post-fixer will automatically correct it in two steps:
 *
 * 1. It will clip table cells that extend beyond their section.
 * 2. It will add empty table cells to the rows that are narrower than the widest table row.
 *
 * ## Clipping overlapping table cells
 *
 * Such situation may occur when pasting a table (or a part of a table) to the editor from external sources.
 *
 * For example, see the following table which has a cell (FOO) with the rowspan attribute (2):
 *
 *		<table headingRows="1">
 *			<tableRow>
 *				<tableCell rowspan="2"><paragraph>FOO</paragraph></tableCell>
 *				<tableCell colspan="2"><paragraph>BAR</paragraph></tableCell>
 *			</tableRow>
 *			<tableRow>
 *				<tableCell><paragraph>BAZ</paragraph></tableCell>
 *				<tableCell><paragraph>XYZ</paragraph></tableCell>
 *			</tableRow>
 *		</table>
 *
 * It will be rendered in the view as:
 *
 *		<table>
 *			<thead>
 *				<tr>
 *					<td rowspan="2">FOO</td>
 *					<td colspan="2">BAR</td>
 *				</tr>
 *			</thead>
 *			<tbody>
 *				<tr>
 *					<td>BAZ</td>
 *					<td>XYZ</td>
 *				</tr>
 *			</tbody>
 *		</table>
 *
 * In the above example the table will be rendered as a table with two rows: one in the header and second one in the body.
 * The table cell (FOO) cannot span over multiple rows as it would extend from the header to the body section.
 * The `rowspan` attribute must be changed to (1). The value (1) is the default value of the `rowspan` attribute
 * so the `rowspan` attribute will be removed from the model.
 *
 * The table cell with BAZ in the content will be in the first column of the table.
 *
 * ## Adding missing table cells
 *
 * The table post-fixer will insert empty table cells to equalize table row sizes (the number of columns).
 * The size of a table row is calculated by counting column spans of table cells, both horizontal (from the same row) and
 * vertical (from the rows above).
 *
 * In the above example, the table row in the body section of the table is narrower then the row from the header: it has two cells
 * with the default colspan (1). The header row has one cell with colspan (1) and the second with colspan (2).
 * The table cell (FOO) does not extend beyond the head section (and as such will be fixed in the first step of this post-fixer).
 * The post-fixer will add a missing table cell to the row in the body section of the table.
 *
 * The table from the above example will be fixed and rendered to the view as below:
 *
 *		<table>
 *			<thead>
 *				<tr>
 *					<td rowspan="2">FOO</td>
 *					<td colspan="2">BAR</td>
 *				</tr>
 *			</thead>
 *			<tbody>
 *				<tr>
 *					<td>BAZ</td>
 *					<td>XYZ</td>
 *				</tr>
 *			</tbody>
 *		</table>
 *
 * ## Collaboration and undo - Expectations vs post-fixer results
 *
 * The table post-fixer only ensures proper structure without a deeper analysis of the nature of the change. As such, it might lead
 * to a structure which was not intended by the user. In particular, it will also fix undo steps (in conjunction with collaboration)
 * in which the editor content might not return to the original state.
 *
 * This will usually happen when one or more users change the size of the table.
 *
 * As an example see the table below:
 *
 *		<table>
 *			<tbody>
 *				<tr>
 *					<td>11</td>
 *					<td>12</td>
 *				</tr>
 *				<tr>
 *					<td>21</td>
 *					<td>22</td>
 *				</tr>
 *			</tbody>
 *		</table>
 *
 * and the user actions:
 *
 * 1. Both users have a table with two rows and two columns.
 * 2. User A adds a column at the end of the table. This will insert empty table cells to two rows.
 * 3. User B adds a row at the end of the table. This will insert a row with two empty table cells.
 * 4. Both users will have a table as below:
 *
 *
 *		<table>
 *			<tbody>
 *				<tr>
 *					<td>11</td>
 *					<td>12</td>
 *					<td>(empty, inserted by A)</td>
 *				</tr>
 *				<tr>
 *					<td>21</td>
 *					<td>22</td>
 *					<td>(empty, inserted by A)</td>
 *				</tr>
 *				<tr>
 *					<td>(empty, inserted by B)</td>
 *					<td>(empty, inserted by B)</td>
 *				</tr>
 *			</tbody>
 *		</table>
 *
 * The last row is shorter then others so the table post-fixer will add an empty row to the last row:
 *
 *		<table>
 *			<tbody>
 *				<tr>
 *					<td>11</td>
 *					<td>12</td>
 *					<td>(empty, inserted by A)</td>
 *				</tr>
 *				<tr>
 *					<td>21</td>
 *					<td>22</td>
 *					<td>(empty, inserted by A)</td>
 *				</tr>
 *				<tr>
 *					<td>(empty, inserted by B)</td>
 *					<td>(empty, inserted by B)</td>
 *					<td>(empty, inserted by the post-fixer)</td>
 *				</tr>
 *			</tbody>
 *		</table>
 *
 * Unfortunately undo does not know the nature of the changes and depending on which user applies the post-fixer changes, undoing them
 * might lead to a broken table. If User B undoes inserting the column to the table, the undo engine will undo only the operations of
 * inserting empty cells to rows from the initial table state (row 1 and 2) but the cell in the post-fixed row will remain:
 *
 *		<table>
 *			<tbody>
 *				<tr>
 *					<td>11</td>
 *					<td>12</td>
 *				</tr>
 *				<tr>
 *					<td>21</td>
 *					<td>22</td>
 *				</tr>
 *				<tr>
 *					<td>(empty, inserted by B)</td>
 *					<td>(empty, inserted by B)</td>
 *					<td>(empty, inserted by a post-fixer)</td>
 *				</tr>
 *			</tbody>
 *		</table>
 *
 * After undo, the table post-fixer will detect that two rows are shorter than others and will fix the table to:
 *
 *		<table>
 *			<tbody>
 *				<tr>
 *					<td>11</td>
 *					<td>12</td>
 *					<td>(empty, inserted by a post-fixer after undo)</td>
 *				</tr>
 *				<tr>
 *					<td>21</td>
 *					<td>22</td>
 *					<td>(empty, inserted by a post-fixer after undo)</td>
 *				</tr>
 *				<tr>
 *					<td>(empty, inserted by B)</td>
 *					<td>(empty, inserted by B)</td>
 *					<td>(empty, inserted by a post-fixer)</td>
 *				</tr>
 *			</tbody>
 *		</table>
 * @param {module:engine/model/model~Model} model
 */
function injectTableLayoutPostFixer( model ) {
	model.document.registerPostFixer( writer => tableLayoutPostFixer( writer, model ) );
}

// The table layout post-fixer.
//
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/model~Model} model
function tableLayoutPostFixer( writer, model ) {
	const changes = model.document.differ.getChanges();

	let wasFixed = false;

	// Do not analyze the same table more then once - may happen for multiple changes in the same table.
	const analyzedTables = new Set();

	for ( const entry of changes ) {
		let table;

		if ( entry.name == 'table' && entry.type == 'insert' ) {
			table = entry.position.nodeAfter;
		}

		// Fix table on adding/removing table cells and rows.
		if ( entry.name == 'tableRow' || entry.name == 'tableCell' ) {
			table = entry.position.findAncestor( 'table' );
		}

		// Fix table on any table's attribute change - including attributes of table cells.
		if ( isTableAttributeEntry( entry ) ) {
			table = entry.range.start.findAncestor( 'table' );
		}

		if ( table && !analyzedTables.has( table ) ) {
			// Step 1: correct rowspans of table cells if necessary.
			// The wasFixed flag should be true if any of tables in batch was fixed - might be more then one.
			wasFixed = fixTableCellsRowspan( table, writer ) || wasFixed;
			// Step 2: fix table rows sizes.
			wasFixed = fixTableRowsSizes( table, writer ) || wasFixed;

			analyzedTables.add( table );
		}
	}

	return wasFixed;
}

// Fixes the invalid value of the `rowspan` attribute because a table cell cannot vertically extend beyond the table section it belongs to.
//
// @param {module:engine/model/element~Element} table
// @param {module:engine/model/writer~Writer} writer
// @returns {Boolean} Returns `true` if the table was fixed.
function fixTableCellsRowspan( table, writer ) {
	let wasFixed = false;

	const cellsToTrim = findCellsToTrim( table );

	if ( cellsToTrim.length ) {
		// @if CK_DEBUG_TABLE // console.log( `Post-fixing table: trimming cells row-spans (${ cellsToTrim.length }).` );

		wasFixed = true;

		for ( const data of cellsToTrim ) {
			updateNumericAttribute( 'rowspan', data.rowspan, data.cell, writer, 1 );
		}
	}

	return wasFixed;
}

// Makes all table rows in a table the same size.
//
// @param {module:engine/model/element~Element} table
// @param {module:engine/model/writer~Writer} writer
// @returns {Boolean} Returns `true` if the table was fixed.
function fixTableRowsSizes( table, writer ) {
	let wasFixed = false;

	const childrenLengths = getChildrenLengths( table );
	const rowsToRemove = [];

	// Find empty rows.
	for ( const [ rowIndex, size ] of childrenLengths.entries() ) {
		// Ignore all non-row models.
		if ( !size && table.getChild( rowIndex ).is( 'element', 'tableRow' ) ) {
			rowsToRemove.push( rowIndex );
		}
	}

	// Remove empty rows.
	if ( rowsToRemove.length ) {
		// @if CK_DEBUG_TABLE // console.log( `Post-fixing table: remove empty rows (${ rowsToRemove.length }).` );

		wasFixed = true;

		for ( const rowIndex of rowsToRemove.reverse() ) {
			writer.remove( table.getChild( rowIndex ) );
			childrenLengths.splice( rowIndex, 1 );
		}
	}

	// Filter out everything that's not a table row.
	const rowsLengths = childrenLengths.filter( ( row, rowIndex ) => table.getChild( rowIndex ).is( 'element', 'tableRow' ) );

	// Verify if all the rows have the same number of columns.
	const tableSize = rowsLengths[ 0 ];
	const isValid = rowsLengths.every( length => length === tableSize );

	if ( !isValid ) {
		// @if CK_DEBUG_TABLE // console.log( 'Post-fixing table: adding missing cells.' );

		// Find the maximum number of columns.
		const maxColumns = rowsLengths.reduce( ( prev, current ) => current > prev ? current : prev, 0 );

		for ( const [ rowIndex, size ] of rowsLengths.entries() ) {
			const columnsToInsert = maxColumns - size;

			if ( columnsToInsert ) {
				for ( let i = 0; i < columnsToInsert; i++ ) {
					createEmptyTableCell( writer, writer.createPositionAt( table.getChild( rowIndex ), 'end' ) );
				}

				wasFixed = true;
			}
		}
	}

	return wasFixed;
}

// Searches for table cells that extend beyond the table section to which they belong to. It will return an array of objects
// that stores table cells to be trimmed and the correct value of the `rowspan` attribute to set.
//
// @param {module:engine/model/element~Element} table
// @returns {Array.<{{cell, rowspan}}>}
function findCellsToTrim( table ) {
	const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 );
	const maxRows = Array.from( table.getChildren() )
		.reduce( ( count, row ) => row.is( 'element', 'tableRow' ) ? count + 1 : count, 0 );

	const cellsToTrim = [];

	for ( const { row, cell, cellHeight } of new TableWalker( table ) ) {
		// Skip cells that do not expand over its row.
		if ( cellHeight < 2 ) {
			continue;
		}

		const isInHeader = row < headingRows;

		// Row limit is either end of header section or whole table as table body is after the header.
		const rowLimit = isInHeader ? headingRows : maxRows;

		// If table cell expands over its limit reduce it height to proper value.
		if ( row + cellHeight > rowLimit ) {
			const newRowspan = rowLimit - row;

			cellsToTrim.push( { cell, rowspan: newRowspan } );
		}
	}

	return cellsToTrim;
}

// Returns an array with lengths of rows assigned to the corresponding row index.
//
// @param {module:engine/model/element~Element} table
// @returns {Array.<Number>}
function getChildrenLengths( table ) {
	// TableWalker will not provide items for the empty rows, we need to pre-fill this array.
	const lengths = new Array( table.childCount ).fill( 0 );

	for ( const { rowIndex } of new TableWalker( table, { includeAllSlots: true } ) ) {
		lengths[ rowIndex ]++;
	}

	return lengths;
}

// Checks if the differ entry for an attribute change is one of the table's attributes.
//
// @param entry
// @returns {Boolean}
function isTableAttributeEntry( entry ) {
	const isAttributeType = entry.type === 'attribute';
	const key = entry.attributeKey;

	return isAttributeType && ( key === 'headingRows' || key === 'colspan' || key === 'rowspan' );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module table/converters/table-cell-paragraph-post-fixer
 */

/**
 * Injects a table cell post-fixer into the model which inserts a `paragraph` element into empty table cells.
 *
 * A table cell must contain at least one block element as a child. An empty table cell will have an empty `paragraph` as a child.
 *
 *		<table>
 *			<tableRow>
 *				<tableCell></tableCell>
 *			</tableRow>
 *		</table>
 *
 * Will be fixed to:
 *
 *		<table>
 *			<tableRow>
 *				<tableCell><paragraph></paragraph></tableCell>
 *			</tableRow>
 *		</table>
 *
 * @param {module:engine/model/model~Model} model
 */
function injectTableCellParagraphPostFixer( model ) {
	model.document.registerPostFixer( writer => tableCellContentsPostFixer( writer, model ) );
}

// The table cell contents post-fixer.
//
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/model~Model} model
function tableCellContentsPostFixer( writer, model ) {
	const changes = model.document.differ.getChanges();

	let wasFixed = false;

	for ( const entry of changes ) {
		if ( entry.type == 'insert' && entry.name == 'table' ) {
			wasFixed = fixTable( entry.position.nodeAfter, writer ) || wasFixed;
		}

		if ( entry.type == 'insert' && entry.name == 'tableRow' ) {
			wasFixed = fixTableRow( entry.position.nodeAfter, writer ) || wasFixed;
		}

		if ( entry.type == 'insert' && entry.name == 'tableCell' ) {
			wasFixed = fixTableCellContent( entry.position.nodeAfter, writer ) || wasFixed;
		}

		if ( checkTableCellChange( entry ) ) {
			wasFixed = fixTableCellContent( entry.position.parent, writer ) || wasFixed;
		}
	}

	return wasFixed;
}

// Fixes all table cells in a table.
//
// @param {module:engine/model/element~Element} table
// @param {module:engine/model/writer~Writer} writer
function fixTable( table, writer ) {
	let wasFixed = false;

	for ( const row of table.getChildren() ) {
		if ( row.is( 'element', 'tableRow' ) ) {
			wasFixed = fixTableRow( row, writer ) || wasFixed;
		}
	}

	return wasFixed;
}

// Fixes all table cells in a table row.
//
// @param {module:engine/model/element~Element} tableRow
// @param {module:engine/model/writer~Writer} writer
function fixTableRow( tableRow, writer ) {
	let wasFixed = false;

	for ( const tableCell of tableRow.getChildren() ) {
		wasFixed = fixTableCellContent( tableCell, writer ) || wasFixed;
	}

	return wasFixed;
}

// Fixes all table cell content by:
// - Adding a paragraph to a table cell without any child.
// - Wrapping direct $text in a `<paragraph>`.
//
// @param {module:engine/model/element~Element} table
// @param {module:engine/model/writer~Writer} writer
// @returns {Boolean}
function fixTableCellContent( tableCell, writer ) {
	// Insert paragraph to an empty table cell.
	if ( tableCell.childCount == 0 ) {
		// @if CK_DEBUG_TABLE // console.log( 'Post-fixing table: insert paragraph in empty cell.' );

		writer.insertElement( 'paragraph', tableCell );

		return true;
	}

	// Check table cell children for directly placed text nodes.
	// Temporary solution. See https://github.com/ckeditor/ckeditor5/issues/1464.
	const textNodes = Array.from( tableCell.getChildren() ).filter( child => child.is( '$text' ) );

	// @if CK_DEBUG_TABLE // textNodes.length && console.log( 'Post-fixing table: wrap cell content with paragraph.' );

	for ( const child of textNodes ) {
		writer.wrap( writer.createRangeOn( child ), 'paragraph' );
	}

	// Return true when there were text nodes to fix.
	return !!textNodes.length;
}

// Checks if a differ change should fix the table cell. This happens on:
// - Removing content from the table cell (i.e. `tableCell` can be left empty).
// - Adding a text node directly into a table cell.
//
// @param {Object} differ change entry
// @returns {Boolean}
function checkTableCellChange( entry ) {
	if ( !entry.position || !entry.position.parent.is( 'element', 'tableCell' ) ) {
		return false;
	}

	return entry.type == 'insert' && entry.name == '$text' || entry.type == 'remove';
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A table headings refresh handler which marks the table cells or rows in the differ to have it re-rendered
 * if the headings attribute changed.
 *
 * Table heading rows and heading columns are represented in the model by a `headingRows` and `headingColumns` attributes.
 *
 * When table headings attribute changes, all the cells/rows are marked to re-render to change between `<td>` and `<th>`.
 *
 * @param {module:engine/model/model~Model} model
 * @param {module:engine/controller/editingcontroller~EditingController} editing
 */
function tableHeadingsRefreshHandler( model, editing ) {
	const differ = model.document.differ;

	for ( const change of differ.getChanges() ) {
		let table;
		let isRowChange = false;

		if ( change.type == 'attribute' ) {
			const element = change.range.start.nodeAfter;

			if ( !element || !element.is( 'element', 'table' ) ) {
				continue;
			}

			if ( change.attributeKey != 'headingRows' && change.attributeKey != 'headingColumns' ) {
				continue;
			}

			table = element;
			isRowChange = change.attributeKey == 'headingRows';
		} else if ( change.name == 'tableRow' || change.name == 'tableCell' ) {
			table = change.position.findAncestor( 'table' );
			isRowChange = change.name == 'tableRow';
		}

		if ( !table ) {
			continue;
		}

		const headingRows = table.getAttribute( 'headingRows' ) || 0;
		const headingColumns = table.getAttribute( 'headingColumns' ) || 0;

		const tableWalker = new TableWalker( table );

		for ( const tableSlot of tableWalker ) {
			const isHeading = tableSlot.row < headingRows || tableSlot.column < headingColumns;
			const expectedElementName = isHeading ? 'th' : 'td';

			const viewElement = editing.mapper.toViewElement( tableSlot.cell );

			if ( viewElement && viewElement.is( 'element' ) && viewElement.name != expectedElementName ) {
				editing.reconvertItem( isRowChange ? tableSlot.cell.parent : tableSlot.cell );
			}
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A table cell refresh handler which marks the table cell in the differ to have it re-rendered.
 *
 * Model `paragraph` inside a table cell can be rendered as `<span>` or `<p>`. It is rendered as `<span>` if this is the only block
 * element in that table cell and it does not have any attributes. It is rendered as `<p>` otherwise.
 *
 * When table cell content changes, for example a second `paragraph` element is added, we need to ensure that the first `paragraph` is
 * re-rendered so it changes from `<span>` to `<p>`. The easiest way to do it is to re-render the entire table cell.
 *
 * @param {module:engine/model/model~Model} model
 * @param {module:engine/controller/editingcontroller~EditingController} editing
 */
function tableCellRefreshHandler( model, editing ) {
	const differ = model.document.differ;

	// Stores cells to be refreshed, so the table cell will be refreshed once for multiple changes.
	const cellsToCheck = new Set();

	for ( const change of differ.getChanges() ) {
		const parent = change.type == 'attribute' ? change.range.start.parent : change.position.parent;

		if ( parent.is( 'element', 'tableCell' ) ) {
			cellsToCheck.add( parent );
		}
	}

	for ( const tableCell of cellsToCheck.values() ) {
		const paragraphsToRefresh = Array.from( tableCell.getChildren() ).filter( child => shouldRefresh( child, editing.mapper ) );

		for ( const paragraph of paragraphsToRefresh ) {
			editing.reconvertItem( paragraph );
		}
	}
}

// Check if given model element needs refreshing.
//
// @param {module:engine/model/element~Element} modelElement
// @param {module:engine/conversion/mapper~Mapper} mapper
// @returns {Boolean}
function shouldRefresh( child, mapper ) {
	if ( !child.is( 'element', 'paragraph' ) ) {
		return false;
	}

	const viewElement = mapper.toViewElement( child );

	if ( !viewElement ) {
		return false;
	}

	return isSingleParagraphWithoutAttributes( child ) !== viewElement.is( 'element', 'span' );
}var css_248z$l = ":root{--ck-color-table-focused-cell-background:rgba(158,201,250,.3)}.ck-widget.table td.ck-editor__nested-editable.ck-editor__nested-editable_focused,.ck-widget.table td.ck-editor__nested-editable:focus,.ck-widget.table th.ck-editor__nested-editable.ck-editor__nested-editable_focused,.ck-widget.table th.ck-editor__nested-editable:focus{background:var(--ck-color-table-focused-cell-background);border-style:none;outline:1px solid var(--ck-color-focus-border);outline-offset:-1px}";
styleInject(css_248z$l);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table editing feature.
 *
 * @extends module:core/plugin~Plugin
 */
class TableEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TableEditing';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ TableUtils ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const model = editor.model;
		const schema = model.schema;
		const conversion = editor.conversion;
		const tableUtils = editor.plugins.get( TableUtils );

		schema.register( 'table', {
			inheritAllFrom: '$blockObject',
			allowAttributes: [ 'headingRows', 'headingColumns' ]
		} );

		schema.register( 'tableRow', {
			allowIn: 'table',
			isLimit: true
		} );

		schema.register( 'tableCell', {
			allowContentOf: '$container',
			allowIn: 'tableRow',
			allowAttributes: [ 'colspan', 'rowspan' ],
			isLimit: true,
			isSelectable: true
		} );

		// Figure conversion.
		conversion.for( 'upcast' ).add( upcastTableFigure() );

		// Table conversion.
		conversion.for( 'upcast' ).add( upcastTable() );

		conversion.for( 'editingDowncast' ).elementToStructure( {
			model: {
				name: 'table',
				attributes: [ 'headingRows' ]
			},
			view: downcastTable( tableUtils, { asWidget: true } )
		} );
		conversion.for( 'dataDowncast' ).elementToStructure( {
			model: {
				name: 'table',
				attributes: [ 'headingRows' ]
			},
			view: downcastTable( tableUtils )
		} );

		// Table row conversion.
		conversion.for( 'upcast' ).elementToElement( { model: 'tableRow', view: 'tr' } );
		conversion.for( 'upcast' ).add( skipEmptyTableRow() );

		conversion.for( 'downcast' ).elementToElement( {
			model: 'tableRow',
			view: downcastRow()
		} );

		// Table cell conversion.
		conversion.for( 'upcast' ).elementToElement( { model: 'tableCell', view: 'td' } );
		conversion.for( 'upcast' ).elementToElement( { model: 'tableCell', view: 'th' } );
		conversion.for( 'upcast' ).add( ensureParagraphInTableCell( 'td' ) );
		conversion.for( 'upcast' ).add( ensureParagraphInTableCell( 'th' ) );

		conversion.for( 'editingDowncast' ).elementToElement( {
			model: 'tableCell',
			view: downcastCell( { asWidget: true } )
		} );
		conversion.for( 'dataDowncast' ).elementToElement( {
			model: 'tableCell',
			view: downcastCell()
		} );

		// Duplicates code - needed to properly refresh paragraph inside a table cell.
		conversion.for( 'editingDowncast' ).elementToElement( {
			model: 'paragraph',
			view: convertParagraphInTableCell( { asWidget: true } ),
			converterPriority: 'high'
		} );
		conversion.for( 'dataDowncast' ).elementToElement( {
			model: 'paragraph',
			view: convertParagraphInTableCell(),
			converterPriority: 'high'
		} );

		// Table attributes conversion.
		conversion.for( 'downcast' ).attributeToAttribute( { model: 'colspan', view: 'colspan' } );
		conversion.for( 'upcast' ).attributeToAttribute( {
			model: { key: 'colspan', value: upcastCellSpan( 'colspan' ) },
			view: 'colspan'
		} );

		conversion.for( 'downcast' ).attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
		conversion.for( 'upcast' ).attributeToAttribute( {
			model: { key: 'rowspan', value: upcastCellSpan( 'rowspan' ) },
			view: 'rowspan'
		} );

		// Manually adjust model position mappings in a special case, when a table cell contains a paragraph, which is bound
		// to its parent (to the table cell). This custom model-to-view position mapping is necessary in data pipeline only,
		// because only during this conversion a paragraph can be bound to its parent.
		editor.data.mapper.on( 'modelToViewPosition', mapTableCellModelPositionToView() );

		// Define the config.
		editor.config.define( 'table.defaultHeadings.rows', 0 );
		editor.config.define( 'table.defaultHeadings.columns', 0 );

		// Define all the commands.
		editor.commands.add( 'insertTable', new InsertTableCommand( editor ) );
		editor.commands.add( 'insertTableRowAbove', new InsertRowCommand( editor, { order: 'above' } ) );
		editor.commands.add( 'insertTableRowBelow', new InsertRowCommand( editor, { order: 'below' } ) );
		editor.commands.add( 'insertTableColumnLeft', new InsertColumnCommand( editor, { order: 'left' } ) );
		editor.commands.add( 'insertTableColumnRight', new InsertColumnCommand( editor, { order: 'right' } ) );

		editor.commands.add( 'removeTableRow', new RemoveRowCommand( editor ) );
		editor.commands.add( 'removeTableColumn', new RemoveColumnCommand( editor ) );

		editor.commands.add( 'splitTableCellVertically', new SplitCellCommand( editor, { direction: 'vertically' } ) );
		editor.commands.add( 'splitTableCellHorizontally', new SplitCellCommand( editor, { direction: 'horizontally' } ) );

		editor.commands.add( 'mergeTableCells', new MergeCellsCommand( editor ) );

		editor.commands.add( 'mergeTableCellRight', new MergeCellCommand( editor, { direction: 'right' } ) );
		editor.commands.add( 'mergeTableCellLeft', new MergeCellCommand( editor, { direction: 'left' } ) );
		editor.commands.add( 'mergeTableCellDown', new MergeCellCommand( editor, { direction: 'down' } ) );
		editor.commands.add( 'mergeTableCellUp', new MergeCellCommand( editor, { direction: 'up' } ) );

		editor.commands.add( 'setTableColumnHeader', new SetHeaderColumnCommand( editor ) );
		editor.commands.add( 'setTableRowHeader', new SetHeaderRowCommand( editor ) );

		editor.commands.add( 'selectTableRow', new SelectRowCommand( editor ) );
		editor.commands.add( 'selectTableColumn', new SelectColumnCommand( editor ) );

		injectTableLayoutPostFixer( model );
		injectTableCellParagraphPostFixer( model );

		this.listenTo( model.document, 'change:data', () => {
			tableHeadingsRefreshHandler( model, editor.editing );
			tableCellRefreshHandler( model, editor.editing );
		} );
	}
}

// Creates a mapper callback to adjust model position mappings in a table cell containing a paragraph, which is bound to its parent
// (to the table cell). Only positions after this paragraph have to be adjusted, because after binding this paragraph to the table cell,
// elements located after this paragraph would point either to a non-existent offset inside `tableCell` (if paragraph is empty), or after
// the first character of the paragraph's text. See https://github.com/ckeditor/ckeditor5/issues/10116.
//
// <tableCell><paragraph></paragraph>^</tableCell> -> <td>^&nbsp;</td>
//
// <tableCell><paragraph>foobar</paragraph>^</tableCell> -> <td>foobar^</td>
//
// @returns {Function}
function mapTableCellModelPositionToView() {
	return ( evt, data ) => {
		const modelParent = data.modelPosition.parent;
		const modelNodeBefore = data.modelPosition.nodeBefore;

		if ( !modelParent.is( 'element', 'tableCell' ) ) {
			return;
		}

		if ( !modelNodeBefore || !modelNodeBefore.is( 'element', 'paragraph' ) ) {
			return;
		}

		const viewNodeBefore = data.mapper.toViewElement( modelNodeBefore );
		const viewParent = data.mapper.toViewElement( modelParent );

		if ( viewNodeBefore === viewParent ) {
			// Since the paragraph has already been bound to its parent, update the current position in the model with paragraph's
			// max offset, so it points to the place which should normally (in all other cases) be the end position of this paragraph.
			data.viewPosition = data.mapper.findPositionIn( viewParent, modelNodeBefore.maxOffset );
		}
	};
}

// Returns fixed colspan and rowspan attrbutes values.
//
// @private
// @param {String} type colspan or rowspan.
// @returns {Function} conversion value function.
function upcastCellSpan( type ) {
	return cell => {
		const span = parseInt( cell.getAttribute( type ) );

		if ( Number.isNaN( span ) || span <= 0 ) {
			return null;
		}

		return span;
	};
}var css_248z$k = ".ck .ck-insert-table-dropdown__grid{display:flex;flex-direction:row;flex-wrap:wrap}:root{--ck-insert-table-dropdown-padding:10px;--ck-insert-table-dropdown-box-height:11px;--ck-insert-table-dropdown-box-width:12px;--ck-insert-table-dropdown-box-margin:1px}.ck .ck-insert-table-dropdown__grid{padding:var(--ck-insert-table-dropdown-padding) var(--ck-insert-table-dropdown-padding) 0;width:calc(var(--ck-insert-table-dropdown-box-width)*10 + var(--ck-insert-table-dropdown-box-margin)*20 + var(--ck-insert-table-dropdown-padding)*2)}.ck .ck-insert-table-dropdown__label{text-align:center}.ck .ck-insert-table-dropdown-grid-box{border:1px solid var(--ck-color-base-border);border-radius:1px;height:var(--ck-insert-table-dropdown-box-height);margin:var(--ck-insert-table-dropdown-box-margin);outline:none;width:var(--ck-insert-table-dropdown-box-width)}.ck .ck-insert-table-dropdown-grid-box.ck-on{background:var(--ck-color-focus-outer-shadow);border-color:var(--ck-color-focus-border)}";
styleInject(css_248z$k);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table size view.
 *
 * It renders a 10x10 grid to choose the inserted table size.
 *
 * @extends module:ui/view~View
 * @implements module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable
 */
class InsertTableView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * A unique id of a label element displaying the current geometry of the table.
		 * Used by every {@link #items item} of the view as a pointer to an accessible label.
		 *
		 * @private
		 * @readonly
		 * @member {String}
		 */
		this._geometryLabelId = `ck-editor__label_${ uid() }`;

		/**
		 * A collection of table size box items.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.items = this._createGridCollection();

		this.keystrokes = new KeystrokeHandler();

		/**
		 * Tracks information about the DOM focus in the grid.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * The currently selected number of rows of the new table.
		 *
		 * @observable
		 * @member {Number} #rows
		 */
		this.set( 'rows', 0 );

		/**
		 * The currently selected number of columns of the new table.
		 *
		 * @observable
		 * @member {Number} #columns
		 */
		this.set( 'columns', 0 );

		/**
		 * The label text displayed under the boxes.
		 *
		 * @observable
		 * @member {String} #label
		 */
		this.bind( 'label' )
			.to( this, 'columns', this, 'rows', ( columns, rows ) => `${ rows } × ${ columns }` );

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [ 'ck' ]
			},

			children: [
				{
					tag: 'div',
					attributes: {
						class: [ 'ck-insert-table-dropdown__grid' ]
					},
					on: {
						'mouseover@.ck-insert-table-dropdown-grid-box': bind.to( 'boxover' )
					},
					children: this.items
				},
				{
					tag: 'div',
					attributes: {
						id: this._geometryLabelId,
						class: [
							'ck',
							'ck-insert-table-dropdown__label'
						]
					},
					children: [
						{
							text: bind.to( 'label' )
						}
					]
				}
			],

			on: {
				mousedown: bind.to( evt => {
					evt.preventDefault();
				} ),

				click: bind.to( () => {
					this.fire( 'execute' );
				} ),

				keydown: bind.to( evt => {
					if ( evt.key === 'Enter' ) {
						this.fire( 'execute' );
						evt.preventDefault();
					}
				} )
			}
		} );

		// #rows and #columns are set via changes to #focusTracker on mouse over.
		this.on( 'boxover', ( evt, domEvt ) => {
			const { row, column } = domEvt.target.dataset;

			this.items.get( ( row - 1 ) * 10 + ( column - 1 ) ).focus();
		} );

		// This allows the #rows and #columns to be updated when:
		// * the user navigates the grid using the keyboard,
		// * the user moves the mouse over grid items.
		this.focusTracker.on( 'change:focusedElement', ( evt, name, focusedElement ) => {
			if ( !focusedElement ) {
				return;
			}

			const { row, column } = focusedElement.dataset;

			// As row & column indexes are zero-based transform it to number of selected rows & columns.
			this.set( {
				rows: parseInt( row ),
				columns: parseInt( column )
			} );
		} );

		this.on( 'change:columns', () => this._highlightGridBoxes() );
		this.on( 'change:rows', () => this._highlightGridBoxes() );
	}

	render() {
		super.render();

		addKeyboardHandlingForGrid( {
			keystrokeHandler: this.keystrokes,
			focusTracker: this.focusTracker,
			gridItems: this.items,
			numberOfColumns: 10
		} );

		for ( const item of this.items ) {
			this.focusTracker.add( item.element );
		}

		this.keystrokes.listenTo( this.element );
	}

	/**
	 * @inheritDoc
	 */
	focus() {
		this.items.get( 0 ).focus();
	}

	/**
	 * @inheritDoc
	 */
	focusLast() {
		this.items.get( 0 ).focus();
	}

	/**
	 * Highlights grid boxes depending on rows and columns selected.
	 *
	 * @private
	 */
	_highlightGridBoxes() {
		const rows = this.rows;
		const columns = this.columns;

		this.items.map( ( boxView, index ) => {
			// Translate box index to the row & column index.
			const itemRow = Math.floor( index / 10 );
			const itemColumn = index % 10;

			// Grid box is highlighted when its row & column index belongs to selected number of rows & columns.
			const isOn = itemRow < rows && itemColumn < columns;

			boxView.set( 'isOn', isOn );
		} );
	}

	/**
	 * @private
	 * @returns {module:ui/viewcollection~ViewCollection} A view collection containing boxes to be placed in a table grid.
	 */
	_createGridCollection() {
		const boxes = [];

		// Add grid boxes to table selection view.
		for ( let index = 0; index < 100; index++ ) {
			const row = Math.floor( index / 10 );
			const column = index % 10;

			boxes.push( new TableSizeGridBoxView( this.locale, row + 1, column + 1, this._geometryLabelId ) );
		}

		return this.createCollection( boxes );
	}

	/**
	 * Fired when the mouse hover over one of the {@link #items child grid boxes}.
	 *
	 * @event boxover
	 */
}

/**
 * A single grid box view element.
 *
 * This class is used to render the table size selection grid in {@link module:table/ui/inserttableview~InsertTableView}.
 *
 * @private
 */
class TableSizeGridBoxView extends View {
	/**
	 * @inheritDoc
	 */
	constructor( locale, row, column, ariaLabelledById ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * Controls whether the grid box view is "on".
		 *
		 * @observable
		 * @member {Boolean} #isOn
		 */
		this.set( 'isOn', false );

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-insert-table-dropdown-grid-box',
					bind.if( 'isOn', 'ck-on' )
				],
				'data-row': row,
				'data-column': column,
				'tabindex': -1,
				'aria-labelledby': ariaLabelledById
			}
		} );
	}

	/**
	 * @inheritDoc
	 */
	focus() {
		this.element.focus();
	}
}var tableIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M3 6v3h4V6H3zm0 4v3h4v-3H3zm0 4v3h4v-3H3zm5 3h4v-3H8v3zm5 0h4v-3h-4v3zm4-4v-3h-4v3h4zm0-4V6h-4v3h4zm1.5 8a1.5 1.5 0 0 1-1.5 1.5H3A1.5 1.5 0 0 1 1.5 17V4c.222-.863 1.068-1.5 2-1.5h13c.932 0 1.778.637 2 1.5v13zM12 13v-3H8v3h4zm0-4V6H8v3h4z\"/></svg>";
var tableColumnIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2.5 1h15A1.5 1.5 0 0 1 19 2.5v15a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1zM2 2v16h16V2H2z\" opacity=\".6\"/><path d=\"M18 7v1H2V7h16zm0 5v1H2v-1h16z\" opacity=\".6\"/><path d=\"M14 1v18a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1zm-2 1H8v4h4V2zm0 6H8v4h4V8zm0 6H8v4h4v-4z\"/></svg>";
var tableRowIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2.5 1h15A1.5 1.5 0 0 1 19 2.5v15a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1zM2 2v16h16V2H2z\" opacity=\".6\"/><path d=\"M7 2h1v16H7V2zm5 0h1v16h-1V2z\" opacity=\".6\"/><path d=\"M1 6h18a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm1 2v4h4V8H2zm6 0v4h4V8H8zm6 0v4h4V8h-4z\"/></svg>";
var tableMergeCellIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2.5 1h15A1.5 1.5 0 0 1 19 2.5v15a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1zM2 2v16h16V2H2z\" opacity=\".6\"/><path d=\"M7 2h1v16H7V2zm5 0h1v7h-1V2zm6 5v1H2V7h16zM8 12v1H2v-1h6z\" opacity=\".6\"/><path d=\"M7 7h12a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1zm1 2v9h10V9H8z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table UI plugin. It introduces:
 *
 * * The `'insertTable'` dropdown,
 * * The `'tableColumn'` dropdown,
 * * The `'tableRow'` dropdown,
 * * The `'mergeTableCells'` split button.
 *
 * The `'tableColumn'`, `'tableRow'` and `'mergeTableCells'` dropdowns work best with {@link module:table/tabletoolbar~TableToolbar}.
 *
 * @extends module:core/plugin~Plugin
 */
class TableUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TableUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = this.editor.t;
		const contentLanguageDirection = editor.locale.contentLanguageDirection;
		const isContentLtr = contentLanguageDirection === 'ltr';

		editor.ui.componentFactory.add( 'insertTable', locale => {
			const command = editor.commands.get( 'insertTable' );
			const dropdownView = createDropdown( locale );

			dropdownView.bind( 'isEnabled' ).to( command );

			// Decorate dropdown's button.
			dropdownView.buttonView.set( {
				icon: tableIcon,
				label: t( 'Insert table' ),
				tooltip: true
			} );

			let insertTableView;

			dropdownView.on( 'change:isOpen', () => {
				if ( insertTableView ) {
					return;
				}

				// Prepare custom view for dropdown's panel.
				insertTableView = new InsertTableView( locale );
				dropdownView.panelView.children.add( insertTableView );

				insertTableView.delegate( 'execute' ).to( dropdownView );

				dropdownView.on( 'execute', () => {
					editor.execute( 'insertTable', { rows: insertTableView.rows, columns: insertTableView.columns } );
					editor.editing.view.focus();
				} );
			} );

			return dropdownView;
		} );

		editor.ui.componentFactory.add( 'tableColumn', locale => {
			const options = [
				{
					type: 'switchbutton',
					model: {
						commandName: 'setTableColumnHeader',
						label: t( 'Header column' ),
						bindIsOn: true
					}
				},
				{ type: 'separator' },
				{
					type: 'button',
					model: {
						commandName: isContentLtr ? 'insertTableColumnLeft' : 'insertTableColumnRight',
						label: t( 'Insert column left' )
					}
				},
				{
					type: 'button',
					model: {
						commandName: isContentLtr ? 'insertTableColumnRight' : 'insertTableColumnLeft',
						label: t( 'Insert column right' )
					}
				},
				{
					type: 'button',
					model: {
						commandName: 'removeTableColumn',
						label: t( 'Delete column' )
					}
				},
				{
					type: 'button',
					model: {
						commandName: 'selectTableColumn',
						label: t( 'Select column' )
					}
				}
			];

			return this._prepareDropdown( t( 'Column' ), tableColumnIcon, options, locale );
		} );

		editor.ui.componentFactory.add( 'tableRow', locale => {
			const options = [
				{
					type: 'switchbutton',
					model: {
						commandName: 'setTableRowHeader',
						label: t( 'Header row' ),
						bindIsOn: true
					}
				},
				{ type: 'separator' },
				{
					type: 'button',
					model: {
						commandName: 'insertTableRowAbove',
						label: t( 'Insert row above' )
					}
				},
				{
					type: 'button',
					model: {
						commandName: 'insertTableRowBelow',
						label: t( 'Insert row below' )
					}
				},
				{
					type: 'button',
					model: {
						commandName: 'removeTableRow',
						label: t( 'Delete row' )
					}
				},
				{
					type: 'button',
					model: {
						commandName: 'selectTableRow',
						label: t( 'Select row' )
					}
				}
			];

			return this._prepareDropdown( t( 'Row' ), tableRowIcon, options, locale );
		} );

		editor.ui.componentFactory.add( 'mergeTableCells', locale => {
			const options = [
				{
					type: 'button',
					model: {
						commandName: 'mergeTableCellUp',
						label: t( 'Merge cell up' )
					}
				},
				{
					type: 'button',
					model: {
						commandName: isContentLtr ? 'mergeTableCellRight' : 'mergeTableCellLeft',
						label: t( 'Merge cell right' )
					}
				},
				{
					type: 'button',
					model: {
						commandName: 'mergeTableCellDown',
						label: t( 'Merge cell down' )
					}
				},
				{
					type: 'button',
					model: {
						commandName: isContentLtr ? 'mergeTableCellLeft' : 'mergeTableCellRight',
						label: t( 'Merge cell left' )
					}
				},
				{ type: 'separator' },
				{
					type: 'button',
					model: {
						commandName: 'splitTableCellVertically',
						label: t( 'Split cell vertically' )
					}
				},
				{
					type: 'button',
					model: {
						commandName: 'splitTableCellHorizontally',
						label: t( 'Split cell horizontally' )
					}
				}
			];

			return this._prepareMergeSplitButtonDropdown( t( 'Merge cells' ), tableMergeCellIcon, options, locale );
		} );
	}

	/**
	 * Creates a dropdown view from a set of options.
	 *
	 * @private
	 * @param {String} label The dropdown button label.
	 * @param {String} icon An icon for the dropdown button.
	 * @param {Array.<module:ui/dropdown/utils~ListDropdownItemDefinition>} options The list of options for the dropdown.
	 * @param {module:utils/locale~Locale} locale
	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
	 */
	_prepareDropdown( label, icon, options, locale ) {
		const editor = this.editor;
		const dropdownView = createDropdown( locale );
		const commands = this._fillDropdownWithListOptions( dropdownView, options );

		// Decorate dropdown's button.
		dropdownView.buttonView.set( {
			label,
			icon,
			tooltip: true
		} );

		// Make dropdown button disabled when all options are disabled.
		dropdownView.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( ...areEnabled ) => {
			return areEnabled.some( isEnabled => isEnabled );
		} );

		this.listenTo( dropdownView, 'execute', evt => {
			editor.execute( evt.source.commandName );

			// Toggling a switch button view should not move the focus to the editable.
			if ( !( evt.source instanceof SwitchButtonView ) ) {
				editor.editing.view.focus();
			}
		} );

		return dropdownView;
	}

	/**
	 * Creates a dropdown view with a {@link module:ui/dropdown/button/splitbuttonview~SplitButtonView} for
	 * merge (and split)–related commands.
	 *
	 * @private
	 * @param {String} label The dropdown button label.
	 * @param {String} icon An icon for the dropdown button.
	 * @param {Array.<module:ui/dropdown/utils~ListDropdownItemDefinition>} options The list of options for the dropdown.
	 * @param {module:utils/locale~Locale} locale
	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
	 */
	_prepareMergeSplitButtonDropdown( label, icon, options, locale ) {
		const editor = this.editor;
		const dropdownView = createDropdown( locale, SplitButtonView );
		const mergeCommandName = 'mergeTableCells';

		// Main command.
		const mergeCommand = editor.commands.get( mergeCommandName );

		// Subcommands in the dropdown.
		const commands = this._fillDropdownWithListOptions( dropdownView, options );

		dropdownView.buttonView.set( {
			label,
			icon,
			tooltip: true,
			isEnabled: true
		} );

		// Make dropdown button disabled when all options are disabled together with the main command.
		dropdownView.bind( 'isEnabled' ).toMany( [ mergeCommand, ...commands ], 'isEnabled', ( ...areEnabled ) => {
			return areEnabled.some( isEnabled => isEnabled );
		} );

		// Merge selected table cells when the main part of the split button is clicked.
		this.listenTo( dropdownView.buttonView, 'execute', () => {
			editor.execute( mergeCommandName );
			editor.editing.view.focus();
		} );

		// Execute commands for events coming from the list in the dropdown panel.
		this.listenTo( dropdownView, 'execute', evt => {
			editor.execute( evt.source.commandName );
			editor.editing.view.focus();
		} );

		return dropdownView;
	}

	/**
	 * Injects a {@link module:ui/list/listview~ListView} into the passed dropdown with buttons
	 * which execute editor commands as configured in passed options.
	 *
	 * @private
	 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
	 * @param {Array.<module:ui/dropdown/utils~ListDropdownItemDefinition>} options The list of options for the dropdown.
	 * @returns {Array.<module:core/command~Command>} Commands the list options are interacting with.
	 */
	_fillDropdownWithListOptions( dropdownView, options ) {
		const editor = this.editor;
		const commands = [];
		const itemDefinitions = new Collection();

		for ( const option of options ) {
			addListOption( option, editor, commands, itemDefinitions );
		}

		addListToDropdown( dropdownView, itemDefinitions, editor.ui.componentFactory );

		return commands;
	}
}

// Adds an option to a list view.
//
// @param {module:table/tableui~DropdownOption} option A configuration option.
// @param {module:core/editor/editor~Editor} editor
// @param {Array.<module:core/command~Command>} commands The list of commands to update.
// @param {Iterable.<module:ui/dropdown/utils~ListDropdownItemDefinition>} itemDefinitions
// A collection of dropdown items to update with the given option.
function addListOption( option, editor, commands, itemDefinitions ) {
	const model = option.model = new Model( option.model );
	const { commandName, bindIsOn } = option.model;

	if ( option.type === 'button' || option.type === 'switchbutton' ) {
		const command = editor.commands.get( commandName );

		commands.push( command );

		model.set( { commandName } );

		model.bind( 'isEnabled' ).to( command );

		if ( bindIsOn ) {
			model.bind( 'isOn' ).to( command, 'value' );
		}
	}

	model.set( {
		withText: true
	} );

	itemDefinitions.add( option );
}var css_248z$j = ":root{--ck-table-selected-cell-background:rgba(158,207,250,.3)}.ck.ck-editor__editable .table table td.ck-editor__editable_selected,.ck.ck-editor__editable .table table th.ck-editor__editable_selected{box-shadow:unset;caret-color:transparent;outline:unset;position:relative}.ck.ck-editor__editable .table table td.ck-editor__editable_selected:after,.ck.ck-editor__editable .table table th.ck-editor__editable_selected:after{background-color:var(--ck-table-selected-cell-background);bottom:0;content:\"\";left:0;pointer-events:none;position:absolute;right:0;top:0}.ck.ck-editor__editable .table table td.ck-editor__editable_selected ::selection,.ck.ck-editor__editable .table table td.ck-editor__editable_selected:focus,.ck.ck-editor__editable .table table th.ck-editor__editable_selected ::selection,.ck.ck-editor__editable .table table th.ck-editor__editable_selected:focus{background-color:transparent}.ck.ck-editor__editable .table table td.ck-editor__editable_selected .ck-widget,.ck.ck-editor__editable .table table th.ck-editor__editable_selected .ck-widget{outline:unset}.ck.ck-editor__editable .table table td.ck-editor__editable_selected .ck-widget>.ck-widget__selection-handle,.ck.ck-editor__editable .table table th.ck-editor__editable_selected .ck-widget>.ck-widget__selection-handle{display:none}";
styleInject(css_248z$j);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * This plugin enables the advanced table cells, rows and columns selection.
 * It is loaded automatically by the {@link module:table/table~Table} plugin.
 *
 * @extends module:core/plugin~Plugin
 */
class TableSelection extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TableSelection';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ TableUtils, TableUtils ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const model = editor.model;

		this.listenTo( model, 'deleteContent', ( evt, args ) => this._handleDeleteContent( evt, args ), { priority: 'high' } );

		this._defineSelectionConverter();
		this._enablePluginDisabling(); // sic!
	}

	/**
	 * Returns the currently selected table cells or `null` if it is not a table cells selection.
	 *
	 * @returns {Array.<module:engine/model/element~Element>|null}
	 */
	getSelectedTableCells() {
		const tableUtils = this.editor.plugins.get( TableUtils );
		const selection = this.editor.model.document.selection;

		const selectedCells = tableUtils.getSelectedTableCells( selection );

		if ( selectedCells.length == 0 ) {
			return null;
		}

		// This should never happen, but let's know if it ever happens.
		// @if CK_DEBUG //	/* istanbul ignore next */
		// @if CK_DEBUG //	if ( selectedCells.length != selection.rangeCount ) {
		// @if CK_DEBUG //		console.warn( 'Mixed selection warning. The selection contains table cells and some other ranges.' );
		// @if CK_DEBUG //	}

		return selectedCells;
	}

	/**
	 * Returns the selected table fragment as a document fragment.
	 *
	 * @returns {module:engine/model/documentfragment~DocumentFragment|null}
	 */
	getSelectionAsFragment() {
		const tableUtils = this.editor.plugins.get( TableUtils );
		const selectedCells = this.getSelectedTableCells();

		if ( !selectedCells ) {
			return null;
		}

		return this.editor.model.change( writer => {
			const documentFragment = writer.createDocumentFragment();

			const { first: firstColumn, last: lastColumn } = tableUtils.getColumnIndexes( selectedCells );
			const { first: firstRow, last: lastRow } = tableUtils.getRowIndexes( selectedCells );

			const sourceTable = selectedCells[ 0 ].findAncestor( 'table' );

			let adjustedLastRow = lastRow;
			let adjustedLastColumn = lastColumn;

			// If the selection is rectangular there could be a case of all cells in the last row/column spanned over
			// next row/column so the real lastRow/lastColumn should be updated.
			if ( tableUtils.isSelectionRectangular( selectedCells ) ) {
				const dimensions = {
					firstColumn,
					lastColumn,
					firstRow,
					lastRow
				};

				adjustedLastRow = adjustLastRowIndex( sourceTable, dimensions );
				adjustedLastColumn = adjustLastColumnIndex( sourceTable, dimensions );
			}

			const cropDimensions = {
				startRow: firstRow,
				startColumn: firstColumn,
				endRow: adjustedLastRow,
				endColumn: adjustedLastColumn
			};

			const table = cropTableToDimensions( sourceTable, cropDimensions, writer );

			writer.insert( table, documentFragment, 0 );

			return documentFragment;
		} );
	}

	/**
	 * Sets the model selection based on given anchor and target cells (can be the same cell).
	 * Takes care of setting the backward flag.
	 *
	 *		const modelRoot = editor.model.document.getRoot();
	 *		const firstCell = modelRoot.getNodeByPath( [ 0, 0, 0 ] );
	 *		const lastCell = modelRoot.getNodeByPath( [ 0, 0, 1 ] );
	 *
	 *		const tableSelection = editor.plugins.get( 'TableSelection' );
	 *		tableSelection.setCellSelection( firstCell, lastCell );
	 *
	 * @param {module:engine/model/element~Element} anchorCell
	 * @param {module:engine/model/element~Element} targetCell
	 */
	setCellSelection( anchorCell, targetCell ) {
		const cellsToSelect = this._getCellsToSelect( anchorCell, targetCell );

		this.editor.model.change( writer => {
			writer.setSelection(
				cellsToSelect.cells.map( cell => writer.createRangeOn( cell ) ),
				{ backward: cellsToSelect.backward }
			);
		} );
	}

	/**
	 * Returns the focus cell from the current selection.
	 *
	 * @returns {module:engine/model/element~Element}
	 */
	getFocusCell() {
		const selection = this.editor.model.document.selection;
		const focusCellRange = [ ...selection.getRanges() ].pop();
		const element = focusCellRange.getContainedElement();

		if ( element && element.is( 'element', 'tableCell' ) ) {
			return element;
		}

		return null;
	}

	/**
	 * Returns the anchor cell from the current selection.
	 *
	 * @returns {module:engine/model/element~Element} anchorCell
	 */
	getAnchorCell() {
		const selection = this.editor.model.document.selection;
		const anchorCellRange = first( selection.getRanges() );
		const element = anchorCellRange.getContainedElement();

		if ( element && element.is( 'element', 'tableCell' ) ) {
			return element;
		}

		return null;
	}

	/**
	 * Defines a selection converter which marks the selected cells with a specific class.
	 *
	 * The real DOM selection is put in the last cell. Since the order of ranges is dependent on whether the
	 * selection is backward or not, the last cell will usually be close to the "focus" end of the selection
	 * (a selection has anchor and focus).
	 *
	 * The real DOM selection is then hidden with CSS.
	 *
	 * @private
	 */
	_defineSelectionConverter() {
		const editor = this.editor;
		const highlighted = new Set();

		editor.conversion.for( 'editingDowncast' ).add( dispatcher => dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
			const viewWriter = conversionApi.writer;

			clearHighlightedTableCells( viewWriter );

			const selectedCells = this.getSelectedTableCells();

			if ( !selectedCells ) {
				return;
			}

			for ( const tableCell of selectedCells ) {
				const viewElement = conversionApi.mapper.toViewElement( tableCell );

				viewWriter.addClass( 'ck-editor__editable_selected', viewElement );
				highlighted.add( viewElement );
			}

			const lastViewCell = conversionApi.mapper.toViewElement( selectedCells[ selectedCells.length - 1 ] );
			viewWriter.setSelection( lastViewCell, 0 );
		}, { priority: 'lowest' } ) );

		function clearHighlightedTableCells( writer ) {
			for ( const previouslyHighlighted of highlighted ) {
				writer.removeClass( 'ck-editor__editable_selected', previouslyHighlighted );
			}

			highlighted.clear();
		}
	}

	/**
	 * Creates a listener that reacts to changes in {@link #isEnabled} and, if the plugin was disabled,
	 * it collapses the multi-cell selection to a regular selection placed inside a table cell.
	 *
	 * This listener helps features that disable the table selection plugin bring the selection
	 * to a clear state they can work with (for instance, because they don't support multiple cell selection).
	 */
	_enablePluginDisabling() {
		const editor = this.editor;

		this.on( 'change:isEnabled', () => {
			if ( !this.isEnabled ) {
				const selectedCells = this.getSelectedTableCells();

				if ( !selectedCells ) {
					return;
				}

				editor.model.change( writer => {
					const position = writer.createPositionAt( selectedCells[ 0 ], 0 );
					const range = editor.model.schema.getNearestSelectionRange( position );

					writer.setSelection( range );
				} );
			}
		} );
	}

	/**
	 * Overrides the default `model.deleteContent()` behavior over a selected table fragment.
	 *
	 * @private
	 * @param {module:utils/eventinfo~EventInfo} event
	 * @param {Array.<*>} args Delete content method arguments.
	 */
	_handleDeleteContent( event, args ) {
		const tableUtils = this.editor.plugins.get( TableUtils );
		const [ selection, options ] = args;
		const model = this.editor.model;
		const isBackward = !options || options.direction == 'backward';
		const selectedTableCells = tableUtils.getSelectedTableCells( selection );

		if ( !selectedTableCells.length ) {
			return;
		}

		event.stop();

		model.change( writer => {
			const tableCellToSelect = selectedTableCells[ isBackward ? selectedTableCells.length - 1 : 0 ];

			model.change( writer => {
				for ( const tableCell of selectedTableCells ) {
					model.deleteContent( writer.createSelection( tableCell, 'in' ) );
				}
			} );

			const rangeToSelect = model.schema.getNearestSelectionRange( writer.createPositionAt( tableCellToSelect, 0 ) );

			// Note: we ignore the case where rangeToSelect may be null because deleteContent() will always (unless someone broke it)
			// create an empty paragraph to accommodate the selection.

			if ( selection.is( 'documentSelection' ) ) {
				writer.setSelection( rangeToSelect );
			} else {
				selection.setTo( rangeToSelect );
			}
		} );
	}

	/**
	 * Returns an array of table cells that should be selected based on the
	 * given anchor cell and target (focus) cell.
	 *
	 * The cells are returned in a reverse direction if the selection is backward.
	 *
	 * @private
	 * @param {module:engine/model/element~Element} anchorCell
	 * @param {module:engine/model/element~Element} targetCell
	 * @returns {Array.<module:engine/model/element~Element>}
	 */
	_getCellsToSelect( anchorCell, targetCell ) {
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const startLocation = tableUtils.getCellLocation( anchorCell );
		const endLocation = tableUtils.getCellLocation( targetCell );

		const startRow = Math.min( startLocation.row, endLocation.row );
		const endRow = Math.max( startLocation.row, endLocation.row );

		const startColumn = Math.min( startLocation.column, endLocation.column );
		const endColumn = Math.max( startLocation.column, endLocation.column );

		// 2-dimensional array of the selected cells to ease flipping the order of cells for backward selections.
		const selectionMap = new Array( endRow - startRow + 1 ).fill( null ).map( () => [] );

		const walkerOptions = {
			startRow,
			endRow,
			startColumn,
			endColumn
		};

		for ( const { row, cell } of new TableWalker( anchorCell.findAncestor( 'table' ), walkerOptions ) ) {
			selectionMap[ row - startRow ].push( cell );
		}

		const flipVertically = endLocation.row < startLocation.row;
		const flipHorizontally = endLocation.column < startLocation.column;

		if ( flipVertically ) {
			selectionMap.reverse();
		}

		if ( flipHorizontally ) {
			selectionMap.forEach( row => row.reverse() );
		}

		return {
			cells: selectionMap.flat(),
			backward: flipVertically || flipHorizontally
		};
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * This plugin adds support for copying/cutting/pasting fragments of tables.
 * It is loaded automatically by the {@link module:table/table~Table} plugin.
 *
 * @extends module:core/plugin~Plugin
 */
class TableClipboard extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TableClipboard';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ TableSelection, TableUtils ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const viewDocument = editor.editing.view.document;

		this.listenTo( viewDocument, 'copy', ( evt, data ) => this._onCopyCut( evt, data ) );
		this.listenTo( viewDocument, 'cut', ( evt, data ) => this._onCopyCut( evt, data ) );
		this.listenTo( editor.model, 'insertContent', ( evt, args ) => this._onInsertContent( evt, ...args ), { priority: 'high' } );

		this.decorate( '_replaceTableSlotCell' );
	}

	/**
	 * Copies table content to a clipboard on "copy" & "cut" events.
	 *
	 * @private
	 * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the handled event.
	 * @param {Object} data Clipboard event data.
	 */
	_onCopyCut( evt, data ) {
		const tableSelection = this.editor.plugins.get( TableSelection );

		if ( !tableSelection.getSelectedTableCells() ) {
			return;
		}

		if ( evt.name == 'cut' && this.editor.isReadOnly ) {
			return;
		}

		data.preventDefault();
		evt.stop();

		const dataController = this.editor.data;
		const viewDocument = this.editor.editing.view.document;

		const content = dataController.toView( tableSelection.getSelectionAsFragment() );

		viewDocument.fire( 'clipboardOutput', {
			dataTransfer: data.dataTransfer,
			content,
			method: evt.name
		} );
	}

	/**
	 * Overrides default {@link module:engine/model/model~Model#insertContent `model.insertContent()`} method to handle pasting table inside
	 * selected table fragment.
	 *
	 * Depending on selected table fragment:
	 * - If a selected table fragment is smaller than paste table it will crop pasted table to match dimensions.
	 * - If dimensions are equal it will replace selected table fragment with a pasted table contents.
	 *
	 * @private
	 * @param evt
	 * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
	 * @param {module:engine/model/selection~Selectable} [selectable=model.document.selection]
	 * The selection into which the content should be inserted. If not provided the current model document selection will be used.
	 */
	_onInsertContent( evt, content, selectable ) {
		if ( selectable && !selectable.is( 'documentSelection' ) ) {
			return;
		}

		const model = this.editor.model;
		const tableUtils = this.editor.plugins.get( TableUtils );

		// We might need to crop table before inserting so reference might change.
		let pastedTable = getTableIfOnlyTableInContent( content, model );

		if ( !pastedTable ) {
			return;
		}

		const selectedTableCells = tableUtils.getSelectionAffectedTableCells( model.document.selection );

		if ( !selectedTableCells.length ) {
			removeEmptyRowsColumns( pastedTable, tableUtils );

			return;
		}

		// Override default model.insertContent() handling at this point.
		evt.stop();

		model.change( writer => {
			const pastedDimensions = {
				width: tableUtils.getColumns( pastedTable ),
				height: tableUtils.getRows( pastedTable )
			};

			// Prepare the table for pasting.
			const selection = prepareTableForPasting( selectedTableCells, pastedDimensions, writer, tableUtils );

			// Beyond this point we operate on a fixed content table with rectangular selection and proper last row/column values.

			const selectionHeight = selection.lastRow - selection.firstRow + 1;
			const selectionWidth = selection.lastColumn - selection.firstColumn + 1;

			// Crop pasted table if:
			// - Pasted table dimensions exceeds selection area.
			// - Pasted table has broken layout (ie some cells sticks out by the table dimensions established by the first and last row).
			//
			// Note: The table dimensions are established by the width of the first row and the total number of rows.
			// It is possible to programmatically create a table that has rows which would have cells anchored beyond first row width but
			// such table will not be created by other editing solutions.
			const cropDimensions = {
				startRow: 0,
				startColumn: 0,
				endRow: Math.min( selectionHeight, pastedDimensions.height ) - 1,
				endColumn: Math.min( selectionWidth, pastedDimensions.width ) - 1
			};

			pastedTable = cropTableToDimensions( pastedTable, cropDimensions, writer );

			// Content table to which we insert a pasted table.
			const selectedTable = selectedTableCells[ 0 ].findAncestor( 'table' );

			const cellsToSelect = this._replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer );

			if ( this.editor.plugins.get( 'TableSelection' ).isEnabled ) {
				// Selection ranges must be sorted because the first and last selection ranges are considered
				// as anchor/focus cell ranges for multi-cell selection.
				const selectionRanges = tableUtils.sortRanges( cellsToSelect.map( cell => writer.createRangeOn( cell ) ) );

				writer.setSelection( selectionRanges );
			} else {
				// Set selection inside first cell if multi-cell selection is disabled.
				writer.setSelection( cellsToSelect[ 0 ], 0 );
			}
		} );
	}

	/**
	 * Replaces the part of selectedTable with pastedTable.
	 *
	 * @private
	 * @param {module:engine/model/element~Element} pastedTable
	 * @param {Object} pastedDimensions
	 * @param {Number} pastedDimensions.height
	 * @param {Number} pastedDimensions.width
	 * @param {module:engine/model/element~Element} selectedTable
	 * @param {Object} selection
	 * @param {Number} selection.firstColumn
	 * @param {Number} selection.firstRow
	 * @param {Number} selection.lastColumn
	 * @param {Number} selection.lastRow
	 * @param {module:engine/model/writer~Writer} writer
	 * @returns {Array.<module:engine/model/element~Element>}
	 */
	_replaceSelectedCellsWithPasted( pastedTable, pastedDimensions, selectedTable, selection, writer ) {
		const { width: pastedWidth, height: pastedHeight } = pastedDimensions;

		// Holds two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
		const pastedTableLocationMap = createLocationMap( pastedTable, pastedWidth, pastedHeight );

		const selectedTableMap = [ ...new TableWalker( selectedTable, {
			startRow: selection.firstRow,
			endRow: selection.lastRow,
			startColumn: selection.firstColumn,
			endColumn: selection.lastColumn,
			includeAllSlots: true
		} ) ];

		// Selection must be set to pasted cells (some might be removed or new created).
		const cellsToSelect = [];

		// Store next cell insert position.
		let insertPosition;

		// Content table replace cells algorithm iterates over a selected table fragment and:
		//
		// - Removes existing table cells at current slot (location).
		// - Inserts cell from a pasted table for a matched slots.
		//
		// This ensures proper table geometry after the paste
		for ( const tableSlot of selectedTableMap ) {
			const { row, column } = tableSlot;

			// Save the insert position for current row start.
			if ( column === selection.firstColumn ) {
				insertPosition = tableSlot.getPositionBefore();
			}

			// Map current table slot location to an pasted table slot location.
			const pastedRow = row - selection.firstRow;
			const pastedColumn = column - selection.firstColumn;
			const pastedCell = pastedTableLocationMap[ pastedRow % pastedHeight ][ pastedColumn % pastedWidth ];

			// Clone cell to insert (to duplicate its attributes and children).
			// Cloning is required to support repeating pasted table content when inserting to a bigger selection.
			const cellToInsert = pastedCell ? writer.cloneElement( pastedCell ) : null;

			// Replace the cell from the current slot with new table cell.
			const newTableCell = this._replaceTableSlotCell( tableSlot, cellToInsert, insertPosition, writer );

			// The cell was only removed.
			if ( !newTableCell ) {
				continue;
			}

			// Trim the cell if it's row/col-spans would exceed selection area.
			trimTableCellIfNeeded( newTableCell, row, column, selection.lastRow, selection.lastColumn, writer );

			cellsToSelect.push( newTableCell );

			insertPosition = writer.createPositionAfter( newTableCell );
		}

		// If there are any headings, all the cells that overlap from heading must be splitted.
		const headingRows = parseInt( selectedTable.getAttribute( 'headingRows' ) || 0 );
		const headingColumns = parseInt( selectedTable.getAttribute( 'headingColumns' ) || 0 );

		const areHeadingRowsIntersectingSelection = selection.firstRow < headingRows && headingRows <= selection.lastRow;
		const areHeadingColumnsIntersectingSelection = selection.firstColumn < headingColumns && headingColumns <= selection.lastColumn;

		if ( areHeadingRowsIntersectingSelection ) {
			const columnsLimit = { first: selection.firstColumn, last: selection.lastColumn };
			const newCells = doHorizontalSplit( selectedTable, headingRows, columnsLimit, writer, selection.firstRow );

			cellsToSelect.push( ...newCells );
		}

		if ( areHeadingColumnsIntersectingSelection ) {
			const rowsLimit = { first: selection.firstRow, last: selection.lastRow };
			const newCells = doVerticalSplit( selectedTable, headingColumns, rowsLimit, writer );

			cellsToSelect.push( ...newCells );
		}

		return cellsToSelect;
	}

	/**
	 * Replaces a single table slot.
	 *
	 * @private
	 * @param {module:table/tablewalker~TableSlot} tableSlot
	 * @param {module:engine/model/element~Element} cellToInsert
	 * @param {module:engine/model/position~Position} insertPosition
	 * @param {module:engine/model/writer~Writer} writer
	 * @returns {module:engine/model/element~Element|null} Inserted table cell or null if slot should remain empty.
	 */
	_replaceTableSlotCell( tableSlot, cellToInsert, insertPosition, writer ) {
		const { cell, isAnchor } = tableSlot;

		// If the slot is occupied by a cell in a selected table - remove it.
		// The slot of this cell will be either:
		// - Replaced by a pasted table cell.
		// - Spanned by a previously pasted table cell.
		if ( isAnchor ) {
			writer.remove( cell );
		}

		// There is no cell to insert (might be spanned by other cell in a pasted table) - advance to the next content table slot.
		if ( !cellToInsert ) {
			return null;
		}

		writer.insert( cellToInsert, insertPosition );

		return cellToInsert;
	}

	/**
	 * Extracts the table for pasting into a table.
	 *
	 * @protected
	 * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
	 * @param {module:engine/model/model~Model} model The editor model.
	 * @returns {module:engine/model/element~Element|null}
	 */
	getTableIfOnlyTableInContent( content, model ) {
		return getTableIfOnlyTableInContent( content, model );
	}
}

function getTableIfOnlyTableInContent( content, model ) {
	if ( !content.is( 'documentFragment' ) && !content.is( 'element' ) ) {
		return null;
	}

	// Table passed directly.
	if ( content.is( 'element', 'table' ) ) {
		return content;
	}

	// We do not support mixed content when pasting table into table.
	// See: https://github.com/ckeditor/ckeditor5/issues/6817.
	if ( content.childCount == 1 && content.getChild( 0 ).is( 'element', 'table' ) ) {
		return content.getChild( 0 );
	}

	// If there are only whitespaces around a table then use that table for pasting.

	const contentRange = model.createRangeIn( content );

	for ( const element of contentRange.getItems() ) {
		if ( element.is( 'element', 'table' ) ) {
			// Stop checking if there is some content before table.
			const rangeBefore = model.createRange( contentRange.start, model.createPositionBefore( element ) );

			if ( model.hasContent( rangeBefore, { ignoreWhitespaces: true } ) ) {
				return null;
			}

			// Stop checking if there is some content after table.
			const rangeAfter = model.createRange( model.createPositionAfter( element ), contentRange.end );

			if ( model.hasContent( rangeAfter, { ignoreWhitespaces: true } ) ) {
				return null;
			}

			// There wasn't any content neither before nor after.
			return element;
		}
	}

	return null;
}

// Prepares a table for pasting and returns adjusted selection dimensions.
//
// @param {Array.<module:engine/model/element~Element>} selectedTableCells
// @param {Object} pastedDimensions
// @param {Number} pastedDimensions.height
// @param {Number} pastedDimensions.width
// @param {module:engine/model/writer~Writer} writer
// @param {module:table/tableutils~TableUtils} tableUtils
// @returns {Object} selection
// @returns {Number} selection.firstColumn
// @returns {Number} selection.firstRow
// @returns {Number} selection.lastColumn
// @returns {Number} selection.lastRow
function prepareTableForPasting( selectedTableCells, pastedDimensions, writer, tableUtils ) {
	const selectedTable = selectedTableCells[ 0 ].findAncestor( 'table' );

	const columnIndexes = tableUtils.getColumnIndexes( selectedTableCells );
	const rowIndexes = tableUtils.getRowIndexes( selectedTableCells );

	const selection = {
		firstColumn: columnIndexes.first,
		lastColumn: columnIndexes.last,
		firstRow: rowIndexes.first,
		lastRow: rowIndexes.last
	};

	// Single cell selected - expand selection to pasted table dimensions.
	const shouldExpandSelection = selectedTableCells.length === 1;

	if ( shouldExpandSelection ) {
		selection.lastRow += pastedDimensions.height - 1;
		selection.lastColumn += pastedDimensions.width - 1;

		expandTableSize( selectedTable, selection.lastRow + 1, selection.lastColumn + 1, tableUtils );
	}

	// In case of expanding selection we do not reset the selection so in this case we will always try to fix selection
	// like in the case of a non-rectangular area. This might be fixed by re-setting selected cells array but this shortcut is safe.
	if ( shouldExpandSelection || !tableUtils.isSelectionRectangular( selectedTableCells ) ) {
		// For a non-rectangular selection (ie in which some cells sticks out from a virtual selection rectangle) we need to create
		// a table layout that has a rectangular selection. This will split cells so the selection become rectangular.
		// Beyond this point we will operate on fixed content table.
		splitCellsToRectangularSelection( selectedTable, selection, writer );
	}
	// However a selected table fragment might be invalid if examined alone. Ie such table fragment:
	//
	//    +---+---+---+---+
	//  0 | a | b | c | d |
	//    +   +   +---+---+
	//  1 |   | e | f | g |
	//    +   +---+   +---+
	//  2 |   | h |   | i | <- last row, each cell has rowspan = 2,
	//    +   +   +   +   +    so we need to return 3, not 2
	//  3 |   |   |   |   |
	//    +---+---+---+---+
	//
	// is invalid as the cells "h" and "i" have rowspans.
	// This case needs only adjusting the selection dimension as the rest of the algorithm operates on empty slots also.
	else {
		selection.lastRow = adjustLastRowIndex( selectedTable, selection );
		selection.lastColumn = adjustLastColumnIndex( selectedTable, selection );
	}

	return selection;
}

// Expand table (in place) to expected size.
function expandTableSize( table, expectedHeight, expectedWidth, tableUtils ) {
	const tableWidth = tableUtils.getColumns( table );
	const tableHeight = tableUtils.getRows( table );

	if ( expectedWidth > tableWidth ) {
		tableUtils.insertColumns( table, {
			at: tableWidth,
			columns: expectedWidth - tableWidth
		} );
	}

	if ( expectedHeight > tableHeight ) {
		tableUtils.insertRows( table, {
			at: tableHeight,
			rows: expectedHeight - tableHeight
		} );
	}
}

// Returns two-dimensional array that is addressed by [ row ][ column ] that stores cells anchored at given location.
//
// At given row & column location it might be one of:
//
// * cell - cell from pasted table anchored at this location.
// * null - if no cell is anchored at this location.
//
// For instance, from a table below:
//
//		+----+----+----+----+
//		| 00 | 01 | 02 | 03 |
//		+    +----+----+----+
//		|    | 11      | 13 |
//		+----+         +----+
//		| 20 |         | 23 |
//		+----+----+----+----+
//
// The method will return an array (numbers represents cell element):
//
//	const map = [
//		[ '00', '01', '02', '03' ],
//		[ null, '11', null, '13' ],
//		[ '20', null, null, '23' ]
//	]
//
// This allows for a quick access to table at give row & column. For instance to access table cell "13" from pasted table call:
//
//		const cell = map[ 1 ][ 3 ]
//
function createLocationMap( table, width, height ) {
	// Create height x width (row x column) two-dimensional table to store cells.
	const map = new Array( height ).fill( null )
		.map( () => new Array( width ).fill( null ) );

	for ( const { column, row, cell } of new TableWalker( table ) ) {
		map[ row ][ column ] = cell;
	}

	return map;
}

// Make selected cells rectangular by splitting the cells that stand out from a rectangular selection.
//
// In the table below a selection is shown with "::" and slots with anchor cells are named.
//
// +----+----+----+----+----+                    +----+----+----+----+----+
// | 00 | 01 | 02 | 03      |                    | 00 | 01 | 02 | 03      |
// +    +----+    +----+----+                    |    ::::::::::::::::----+
// |    | 11 |    | 13 | 14 |                    |    ::11 |    | 13:: 14 |    <- first row
// +----+----+    +    +----+                    +----::---|    |   ::----+
// | 20 | 21 |    |    | 24 |   select cells:    | 20 ::21 |    |   :: 24 |
// +----+----+    +----+----+     11 -> 33       +----::---|    |---::----+
// | 30      |    | 33 | 34 |                    | 30 ::   |    | 33:: 34 |    <- last row
// +         +    +----+    +                    |    ::::::::::::::::    +
// |         |    | 43 |    |                    |         |    | 43 |    |
// +----+----+----+----+----+                    +----+----+----+----+----+
//                                                      ^          ^
//                                                     first & last columns
//
// Will update table to:
//
//                       +----+----+----+----+----+
//                       | 00 | 01 | 02 | 03      |
//                       +    +----+----+----+----+
//                       |    | 11 |    | 13 | 14 |
//                       +----+----+    +    +----+
//                       | 20 | 21 |    |    | 24 |
//                       +----+----+    +----+----+
//                       | 30 |    |    | 33 | 34 |
//                       +    +----+----+----+    +
//                       |    |    |    | 43 |    |
//                       +----+----+----+----+----+
//
// In th example above:
// - Cell "02" which have `rowspan = 4` must be trimmed at first and at after last row.
// - Cell "03" which have `rowspan = 2` and `colspan = 2` must be trimmed at first column and after last row.
// - Cells "00", "03" & "30" which cannot be cut by this algorithm as they are outside the trimmed area.
// - Cell "13" cannot be cut as it is inside the trimmed area.
function splitCellsToRectangularSelection( table, dimensions, writer ) {
	const { firstRow, lastRow, firstColumn, lastColumn } = dimensions;

	const rowIndexes = { first: firstRow, last: lastRow };
	const columnIndexes = { first: firstColumn, last: lastColumn };

	// 1. Split cells vertically in two steps as first step might create cells that needs to split again.
	doVerticalSplit( table, firstColumn, rowIndexes, writer );
	doVerticalSplit( table, lastColumn + 1, rowIndexes, writer );

	// 2. Split cells horizontally in two steps as first step might create cells that needs to split again.
	doHorizontalSplit( table, firstRow, columnIndexes, writer );
	doHorizontalSplit( table, lastRow + 1, columnIndexes, writer, firstRow );
}

function doHorizontalSplit( table, splitRow, limitColumns, writer, startRow = 0 ) {
	// If selection starts at first row then no split is needed.
	if ( splitRow < 1 ) {
		return;
	}

	const overlappingCells = getVerticallyOverlappingCells( table, splitRow, startRow );

	// Filter out cells that are not touching insides of the rectangular selection.
	const cellsToSplit = overlappingCells.filter( ( { column, cellWidth } ) => isAffectedBySelection( column, cellWidth, limitColumns ) );

	return cellsToSplit.map( ( { cell } ) => splitHorizontally( cell, splitRow, writer ) );
}

function doVerticalSplit( table, splitColumn, limitRows, writer ) {
	// If selection starts at first column then no split is needed.
	if ( splitColumn < 1 ) {
		return;
	}

	const overlappingCells = getHorizontallyOverlappingCells( table, splitColumn );

	// Filter out cells that are not touching insides of the rectangular selection.
	const cellsToSplit = overlappingCells.filter( ( { row, cellHeight } ) => isAffectedBySelection( row, cellHeight, limitRows ) );

	return cellsToSplit.map( ( { cell, column } ) => splitVertically( cell, column, splitColumn, writer ) );
}

// Checks if cell at given row (column) is affected by a rectangular selection defined by first/last column (row).
//
// The same check is used for row as for column.
function isAffectedBySelection( index, span, limit ) {
	const endIndex = index + span - 1;
	const { first, last } = limit;

	const isInsideSelection = index >= first && index <= last;
	const overlapsSelectionFromOutside = index < first && endIndex >= first;

	return isInsideSelection || overlapsSelectionFromOutside;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * This plugin enables keyboard navigation for tables.
 * It is loaded automatically by the {@link module:table/table~Table} plugin.
 *
 * @extends module:core/plugin~Plugin
 */
class TableKeyboard extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TableKeyboard';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ TableSelection, TableUtils ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const view = this.editor.editing.view;
		const viewDocument = view.document;

		this.listenTo( viewDocument, 'arrowKey', ( ...args ) => this._onArrowKey( ...args ), { context: 'table' } );
		this.listenTo( viewDocument, 'tab', ( ...args ) => this._handleTabOnSelectedTable( ...args ), { context: 'figure' } );
		this.listenTo( viewDocument, 'tab', ( ...args ) => this._handleTab( ...args ), { context: [ 'th', 'td' ] } );
	}

	/**
	 * Handles {@link module:engine/view/document~Document#event:tab tab} events for the <kbd>Tab</kbd> key executed
	 * when the table widget is selected.
	 *
	 * @private
	 * @param {module:engine/view/observer/bubblingeventinfo~BubblingEventInfo} bubblingEventInfo
	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
	 */
	_handleTabOnSelectedTable( bubblingEventInfo, domEventData ) {
		const editor = this.editor;
		const selection = editor.model.document.selection;
		const selectedElement = selection.getSelectedElement();

		if ( !selectedElement || !selectedElement.is( 'element', 'table' ) ) {
			return;
		}

		domEventData.preventDefault();
		domEventData.stopPropagation();
		bubblingEventInfo.stop();

		editor.model.change( writer => {
			writer.setSelection( writer.createRangeIn( selectedElement.getChild( 0 ).getChild( 0 ) ) );
		} );
	}

	/**
	 * Handles {@link module:engine/view/document~Document#event:tab tab} events for the <kbd>Tab</kbd> key executed
	 * inside table cells.
	 *
	 * @private
	 * @param {module:engine/view/observer/bubblingeventinfo~BubblingEventInfo} bubblingEventInfo
	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
	 */
	_handleTab( bubblingEventInfo, domEventData ) {
		const editor = this.editor;
		const tableUtils = this.editor.plugins.get( TableUtils );

		const selection = editor.model.document.selection;
		const isForward = !domEventData.shiftKey;

		let tableCell = tableUtils.getTableCellsContainingSelection( selection )[ 0 ];

		if ( !tableCell ) {
			tableCell = this.editor.plugins.get( 'TableSelection' ).getFocusCell();
		}

		if ( !tableCell ) {
			return;
		}

		domEventData.preventDefault();
		domEventData.stopPropagation();
		bubblingEventInfo.stop();

		const tableRow = tableCell.parent;
		const table = tableRow.parent;

		const currentRowIndex = table.getChildIndex( tableRow );
		const currentCellIndex = tableRow.getChildIndex( tableCell );

		const isFirstCellInRow = currentCellIndex === 0;

		if ( !isForward && isFirstCellInRow && currentRowIndex === 0 ) {
			// Set the selection over the whole table if the selection was in the first table cell.
			editor.model.change( writer => {
				writer.setSelection( writer.createRangeOn( table ) );
			} );

			return;
		}

		const isLastCellInRow = currentCellIndex === tableRow.childCount - 1;
		const isLastRow = currentRowIndex === tableUtils.getRows( table ) - 1;

		if ( isForward && isLastRow && isLastCellInRow ) {
			editor.execute( 'insertTableRowBelow' );

			// Check if the command actually added a row. If `insertTableRowBelow` execution didn't add a row (because it was disabled
			// or it got overwritten) set the selection over the whole table to mirror the first cell case.
			if ( currentRowIndex === tableUtils.getRows( table ) - 1 ) {
				editor.model.change( writer => {
					writer.setSelection( writer.createRangeOn( table ) );
				} );

				return;
			}
		}

		let cellToFocus;

		// Move to the first cell in the next row.
		if ( isForward && isLastCellInRow ) {
			const nextRow = table.getChild( currentRowIndex + 1 );

			cellToFocus = nextRow.getChild( 0 );
		}
		// Move to the last cell in the previous row.
		else if ( !isForward && isFirstCellInRow ) {
			const previousRow = table.getChild( currentRowIndex - 1 );

			cellToFocus = previousRow.getChild( previousRow.childCount - 1 );
		}
		// Move to the next/previous cell.
		else {
			cellToFocus = tableRow.getChild( currentCellIndex + ( isForward ? 1 : -1 ) );
		}

		editor.model.change( writer => {
			writer.setSelection( writer.createRangeIn( cellToFocus ) );
		} );
	}

	/**
	 * Handles {@link module:engine/view/document~Document#event:keydown keydown} events.
	 *
	 * @private
	 * @param {module:utils/eventinfo~EventInfo} eventInfo
	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
	 */
	_onArrowKey( eventInfo, domEventData ) {
		const editor = this.editor;
		const keyCode = domEventData.keyCode;

		const direction = getLocalizedArrowKeyCodeDirection( keyCode, editor.locale.contentLanguageDirection );
		const wasHandled = this._handleArrowKeys( direction, domEventData.shiftKey );

		if ( wasHandled ) {
			domEventData.preventDefault();
			domEventData.stopPropagation();
			eventInfo.stop();
		}
	}

	/**
	 * Handles arrow keys to move the selection around the table.
	 *
	 * @private
	 * @param {'left'|'up'|'right'|'down'} direction The direction of the arrow key.
	 * @param {Boolean} expandSelection If the current selection should be expanded.
	 * @returns {Boolean} Returns `true` if key was handled.
	 */
	_handleArrowKeys( direction, expandSelection ) {
		const tableUtils = this.editor.plugins.get( TableUtils );
		const model = this.editor.model;
		const selection = model.document.selection;
		const isForward = [ 'right', 'down' ].includes( direction );

		// In case one or more table cells are selected (from outside),
		// move the selection to a cell adjacent to the selected table fragment.
		const selectedCells = tableUtils.getSelectedTableCells( selection );

		if ( selectedCells.length ) {
			let focusCell;

			if ( expandSelection ) {
				focusCell = this.editor.plugins.get( 'TableSelection' ).getFocusCell();
			} else {
				focusCell = isForward ? selectedCells[ selectedCells.length - 1 ] : selectedCells[ 0 ];
			}

			this._navigateFromCellInDirection( focusCell, direction, expandSelection );

			return true;
		}

		// Abort if we're not in a table cell.
		const tableCell = selection.focus.findAncestor( 'tableCell' );

		/* istanbul ignore if: paranoid check */
		if ( !tableCell ) {
			return false;
		}

		// When the selection is not collapsed.
		if ( !selection.isCollapsed ) {
			if ( expandSelection ) {
				// Navigation is in the opposite direction than the selection direction so this is shrinking of the selection.
				// Selection for sure will not approach cell edge.
				//
				// With a special case when all cell content is selected - then selection should expand to the other cell.
				// Note: When the entire cell gets selected using CTRL+A, the selection is always forward.
				if ( selection.isBackward == isForward && !selection.containsEntireContent( tableCell ) ) {
					return false;
				}
			} else {
				const selectedElement = selection.getSelectedElement();

				// It will collapse for non-object selected so it's not going to move to other cell.
				if ( !selectedElement || !model.schema.isObject( selectedElement ) ) {
					return false;
				}
			}
		}

		// Let's check if the selection is at the beginning/end of the cell.
		if ( this._isSelectionAtCellEdge( selection, tableCell, isForward ) ) {
			this._navigateFromCellInDirection( tableCell, direction, expandSelection );

			return true;
		}

		return false;
	}

	/**
	 * Returns `true` if the selection is at the boundary of a table cell according to the navigation direction.
	 *
	 * @private
	 * @param {module:engine/model/selection~Selection} selection The current selection.
	 * @param {module:engine/model/element~Element} tableCell The current table cell element.
	 * @param {Boolean} isForward The expected navigation direction.
	 * @returns {Boolean}
	 */
	_isSelectionAtCellEdge( selection, tableCell, isForward ) {
		const model = this.editor.model;
		const schema = this.editor.model.schema;

		const focus = isForward ? selection.getLastPosition() : selection.getFirstPosition();

		// If the current limit element is not table cell we are for sure not at the cell edge.
		// Also `modifySelection` will not let us out of it.
		if ( !schema.getLimitElement( focus ).is( 'element', 'tableCell' ) ) {
			const boundaryPosition = model.createPositionAt( tableCell, isForward ? 'end' : 0 );

			return boundaryPosition.isTouching( focus );
		}

		const probe = model.createSelection( focus );

		model.modifySelection( probe, { direction: isForward ? 'forward' : 'backward' } );

		// If there was no change in the focus position, then it's not possible to move the selection there.
		return focus.isEqual( probe.focus );
	}

	/**
	 * Moves the selection from the given table cell in the specified direction.
	 *
	 * @protected
	 * @param {module:engine/model/element~Element} focusCell The table cell that is current multi-cell selection focus.
	 * @param {'left'|'up'|'right'|'down'} direction Direction in which selection should move.
	 * @param {Boolean} [expandSelection=false] If the current selection should be expanded.
	 */
	_navigateFromCellInDirection( focusCell, direction, expandSelection = false ) {
		const model = this.editor.model;

		const table = focusCell.findAncestor( 'table' );
		const tableMap = [ ...new TableWalker( table, { includeAllSlots: true } ) ];
		const { row: lastRow, column: lastColumn } = tableMap[ tableMap.length - 1 ];

		const currentCellInfo = tableMap.find( ( { cell } ) => cell == focusCell );
		let { row, column } = currentCellInfo;

		switch ( direction ) {
			case 'left':
				column--;
				break;

			case 'up':
				row--;
				break;

			case 'right':
				column += currentCellInfo.cellWidth;
				break;

			case 'down':
				row += currentCellInfo.cellHeight;
				break;
		}

		const isOutsideVertically = row < 0 || row > lastRow;
		const isBeforeFirstCell = column < 0 && row <= 0;
		const isAfterLastCell = column > lastColumn && row >= lastRow;

		// Note that if the table cell at the end of a row is row-spanned then isAfterLastCell will never be true.
		// However, we don't know if user was navigating on the last row or not, so let's stay in the table.

		if ( isOutsideVertically || isBeforeFirstCell || isAfterLastCell ) {
			model.change( writer => {
				writer.setSelection( writer.createRangeOn( table ) );
			} );

			return;
		}

		if ( column < 0 ) {
			column = expandSelection ? 0 : lastColumn;
			row--;
		} else if ( column > lastColumn ) {
			column = expandSelection ? lastColumn : 0;
			row++;
		}

		const cellToSelect = tableMap.find( cellInfo => cellInfo.row == row && cellInfo.column == column ).cell;
		const isForward = [ 'right', 'down' ].includes( direction );
		const tableSelection = this.editor.plugins.get( 'TableSelection' );

		if ( expandSelection && tableSelection.isEnabled ) {
			const anchorCell = tableSelection.getAnchorCell() || focusCell;

			tableSelection.setCellSelection( anchorCell, cellToSelect );
		} else {
			const positionToSelect = model.createPositionAt( cellToSelect, isForward ? 0 : 'end' );

			model.change( writer => {
				writer.setSelection( positionToSelect );
			} );
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The mouse selection event observer.
 *
 * It registers listeners for the following DOM events:
 *
 * - `'mousemove'`
 * - `'mouseup'`
 * - `'mouseleave'`
 *
 * Note that this observer is disabled by default. To enable this observer, it needs to be added to
 * {@link module:engine/view/view~View} using the {@link module:engine/view/view~View#addObserver} method.
 *
 * The observer is registered by the {@link module:table/tableselection~TableSelection} plugin.
 *
 * @extends module:engine/view/observer/domeventobserver~DomEventObserver
 */
class MouseEventsObserver extends DomEventObserver {
	/**
	 * @inheritDoc
	 */
	constructor( view ) {
		super( view );

		this.domEventType = [ 'mousemove', 'mouseleave' ];
	}

	/**
	 * @inheritDoc
	 */
	onDomEvent( domEvent ) {
		this.fire( domEvent.type, domEvent );
	}
}

/**
 * Fired when the mouse is moved over one of the editables.
 *
 * Introduced by {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver}.
 *
 * Note that this event is not available by default. To make it available,
 * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added
 * to {@link module:engine/view/view~View} using the {@link module:engine/view/view~View#addObserver} method.
 *
 * @see module:table/tableselection/mouseeventsobserver~MouseEventsObserver
 * @event module:engine/view/document~Document#event:mousemove
 * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
 */

/**
 * Fired when the mouse is moved out of one of the editables.
 *
 * Introduced by {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver}.
 *
 * Note that this event is not available by default. To make it available,
 * {@link module:table/tableselection/mouseeventsobserver~MouseEventsObserver} needs to be added
 * to {@link module:engine/view/view~View} using the {@link module:engine/view/view~View#addObserver} method.
 *
 * @see module:table/tableselection/mouseeventsobserver~MouseEventsObserver
 * @event module:engine/view/document~Document#event:mouseleave
 * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * This plugin enables a table cells' selection with the mouse.
 * It is loaded automatically by the {@link module:table/table~Table} plugin.
 *
 * @extends module:core/plugin~Plugin
 */
class TableMouse extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TableMouse';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ TableSelection, TableUtils ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		// Currently the MouseObserver only handles `mousedown` and `mouseup` events.
		// TODO move to the engine?
		editor.editing.view.addObserver( MouseEventsObserver );

		this._enableShiftClickSelection();
		this._enableMouseDragSelection();
	}

	/**
	 * Enables making cells selection by <kbd>Shift</kbd>+click. Creates a selection from the cell which previously held
	 * the selection to the cell which was clicked. It can be the same cell, in which case it selects a single cell.
	 *
	 * @private
	 */
	_enableShiftClickSelection() {
		const editor = this.editor;
		const tableUtils = editor.plugins.get( TableUtils );
		let blockSelectionChange = false;

		const tableSelection = editor.plugins.get( TableSelection );

		this.listenTo( editor.editing.view.document, 'mousedown', ( evt, domEventData ) => {
			const selection = editor.model.document.selection;

			if ( !this.isEnabled || !tableSelection.isEnabled ) {
				return;
			}

			if ( !domEventData.domEvent.shiftKey ) {
				return;
			}

			const anchorCell = tableSelection.getAnchorCell() || tableUtils.getTableCellsContainingSelection( selection )[ 0 ];

			if ( !anchorCell ) {
				return;
			}

			const targetCell = this._getModelTableCellFromDomEvent( domEventData );

			if ( targetCell && haveSameTableParent( anchorCell, targetCell ) ) {
				blockSelectionChange = true;
				tableSelection.setCellSelection( anchorCell, targetCell );

				domEventData.preventDefault();
			}
		} );

		this.listenTo( editor.editing.view.document, 'mouseup', () => {
			blockSelectionChange = false;
		} );

		// We need to ignore a `selectionChange` event that is fired after we render our new table cells selection.
		// When downcasting table cells selection to the view, we put the view selection in the last selected cell
		// in a place that may not be natively a "correct" location. This is – we put it directly in the `<td>` element.
		// All browsers fire the native `selectionchange` event.
		// However, all browsers except Safari return the selection in the exact place where we put it
		// (even though it's visually normalized). Safari returns `<td><p>^foo` that makes our selection observer
		// fire our `selectionChange` event (because the view selection that we set in the first step differs from the DOM selection).
		// Since `selectionChange` is fired, we automatically update the model selection that moves it that paragraph.
		// This breaks our dear cells selection.
		//
		// Theoretically this issue concerns only Safari that is the only browser that do normalize the selection.
		// However, to avoid code branching and to have a good coverage for this event blocker, I enabled it for all browsers.
		//
		// Note: I'm keeping the `blockSelectionChange` state separately for shift+click and mouse drag (exact same logic)
		// so I don't have to try to analyze whether they don't overlap in some weird cases. Probably they don't.
		// But I have other things to do, like writing this comment.
		this.listenTo( editor.editing.view.document, 'selectionChange', evt => {
			if ( blockSelectionChange ) {
				// @if CK_DEBUG // console.log( 'Blocked selectionChange to avoid breaking table cells selection.' );

				evt.stop();
			}
		}, { priority: 'highest' } );
	}

	/**
	 * Enables making cells selection by dragging.
	 *
	 * The selection is made only on mousemove. Mouse tracking is started on mousedown.
	 * However, the cells selection is enabled only after the mouse cursor left the anchor cell.
	 * Thanks to that normal text selection within one cell works just fine. However, you can still select
	 * just one cell by leaving the anchor cell and moving back to it.
	 *
	 * @private
	 */
	_enableMouseDragSelection() {
		const editor = this.editor;
		let anchorCell, targetCell;
		let beganCellSelection = false;
		let blockSelectionChange = false;

		const tableSelection = editor.plugins.get( TableSelection );

		this.listenTo( editor.editing.view.document, 'mousedown', ( evt, domEventData ) => {
			if ( !this.isEnabled || !tableSelection.isEnabled ) {
				return;
			}

			// Make sure to not conflict with the shift+click listener and any other possible handler.
			if ( domEventData.domEvent.shiftKey || domEventData.domEvent.ctrlKey || domEventData.domEvent.altKey ) {
				return;
			}

			anchorCell = this._getModelTableCellFromDomEvent( domEventData );
		} );

		this.listenTo( editor.editing.view.document, 'mousemove', ( evt, domEventData ) => {
			if ( !domEventData.domEvent.buttons ) {
				return;
			}

			if ( !anchorCell ) {
				return;
			}

			const newTargetCell = this._getModelTableCellFromDomEvent( domEventData );

			if ( newTargetCell && haveSameTableParent( anchorCell, newTargetCell ) ) {
				targetCell = newTargetCell;

				// Switch to the cell selection mode after the mouse cursor left the anchor cell.
				// Switch off only on mouseup (makes selecting a single cell possible).
				if ( !beganCellSelection && targetCell != anchorCell ) {
					beganCellSelection = true;
				}
			}

			// Yep, not making a cell selection yet. See method docs.
			if ( !beganCellSelection ) {
				return;
			}

			blockSelectionChange = true;
			tableSelection.setCellSelection( anchorCell, targetCell );

			domEventData.preventDefault();
		} );

		this.listenTo( editor.editing.view.document, 'mouseup', () => {
			beganCellSelection = false;
			blockSelectionChange = false;
			anchorCell = null;
			targetCell = null;
		} );

		// See the explanation in `_enableShiftClickSelection()`.
		this.listenTo( editor.editing.view.document, 'selectionChange', evt => {
			if ( blockSelectionChange ) {
				// @if CK_DEBUG // console.log( 'Blocked selectionChange to avoid breaking table cells selection.' );

				evt.stop();
			}
		}, { priority: 'highest' } );
	}

	/**
	 * Returns the model table cell element based on the target element of the passed DOM event.
	 *
	 * @private
	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
	 * @returns {module:engine/model/element~Element|undefined} Returns the table cell or `undefined`.
	 */
	_getModelTableCellFromDomEvent( domEventData ) {
		// Note: Work with positions (not element mapping) because the target element can be an attribute or other non-mapped element.
		const viewTargetElement = domEventData.target;
		const viewPosition = this.editor.editing.view.createPositionAt( viewTargetElement, 0 );
		const modelPosition = this.editor.editing.mapper.toModelPosition( viewPosition );
		const modelElement = modelPosition.parent;

		return modelElement.findAncestor( 'tableCell', { includeSelf: true } );
	}
}

function haveSameTableParent( cellA, cellB ) {
	return cellA.parent.parent == cellB.parent.parent;
}var css_248z$i = ".ck-content .table{display:table;margin:.9em auto}.ck-content .table table{border:1px double #b3b3b3;border-collapse:collapse;border-spacing:0;height:100%;width:100%}.ck-content .table table td,.ck-content .table table th{border:1px solid #bfbfbf;min-width:2em;padding:.4em}.ck-content .table table th{background:rgba(0,0,0,.05);font-weight:700}.ck-content[dir=rtl] .table th{text-align:right}.ck-content[dir=ltr] .table th{text-align:left}.ck-editor__editable .ck-table-bogus-paragraph{display:inline-block;width:100%}";
styleInject(css_248z$i);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table plugin.
 *
 * For a detailed overview, check the {@glink features/table Table feature documentation}.
 *
 * This is a "glue" plugin that loads the following table features:
 *
 * * {@link module:table/tableediting~TableEditing editing feature},
 * * {@link module:table/tableselection~TableSelection selection feature},
 * * {@link module:table/tablekeyboard~TableKeyboard keyboard navigation feature},
 * * {@link module:table/tablemouse~TableMouse mouse selection feature},
 * * {@link module:table/tableclipboard~TableClipboard clipboard feature},
 * * {@link module:table/tableui~TableUI UI feature}.
 *
 * @extends module:core/plugin~Plugin
 */
class Table extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ TableEditing, TableUI, TableSelection, TableMouse, TableKeyboard, TableClipboard, Widget ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Table';
	}
}

/**
 * The configuration of the table feature. Used by the table feature in the `@ckeditor/ckeditor5-table` package.
 *
 *		ClassicEditor
 *			.create( editorElement, {
 * 				table: ... // Table feature options.
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
 *
 * @interface TableConfig
 */

/**
 * The configuration of the {@link module:table/table~Table} feature.
 *
 * Read more in {@link module:table/table~TableConfig}.
 *
 * @member {module:table/table~TableConfig} module:core/editor/editorconfig~EditorConfig#table
 */

/**
 * Number of rows and columns to render by default as table heading when inserting new tables.
 *
 * You can configure it like this:
 *
 *		const tableConfig = {
 *			defaultHeadings: {
 *				rows: 1,
 *				columns: 1
 *			}
 *		};
 *
 * Both rows and columns properties are optional defaulting to 0 (no heading).
 *
 * @member {Object} module:table/table~TableConfig#defaultHeadings
 */

/**
 * An array of color definitions (either strings or objects).
 *
 *		const colors = [
 *			{
 *				color: 'hsl(0, 0%, 60%)',
 *				label: 'Grey'
 *			},
 *			'hsl(0, 0%, 80%)',
 *			{
 *				color: 'hsl(0, 0%, 90%)',
 *				label: 'Light grey'
 *			},
 *			{
 *				color: 'hsl(0, 0%, 100%)',
 *				label: 'White',
 *				hasBorder: true
 *			},
 *			'#FF0000'
 *		]
 *
 * Usually used as a configuration parameter, for instance in
 * {@link module:table/table~TableConfig#tableProperties `config.table.tableProperties`}
 * or {@link module:table/table~TableConfig#tableCellProperties `config.table.tableCellProperties`}.
 *
 * @typedef {Array.<String|Object>} module:table/table~TableColorConfig
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Returns a table widget editing view element if one is selected.
 *
 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} selection
 * @returns {module:engine/view/element~Element|null}
 */
function getSelectedTableWidget( selection ) {
	const viewElement = selection.getSelectedElement();

	if ( viewElement && isTableWidget( viewElement ) ) {
		return viewElement;
	}

	return null;
}

/**
 * Returns a table widget editing view element if one is among the selection's ancestors.
 *
 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} selection
 * @returns {module:engine/view/element~Element|null}
 */
function getTableWidgetAncestor( selection ) {
	const selectionPosition = selection.getFirstPosition();

	if ( !selectionPosition ) {
		return null;
	}

	let parent = selectionPosition.parent;

	while ( parent ) {
		if ( parent.is( 'element' ) && isTableWidget( parent ) ) {
			return parent;
		}

		parent = parent.parent;
	}

	return null;
}

// Checks if a given view element is a table widget.
//
// @param {module:engine/view/element~Element} viewElement
// @returns {Boolean}
function isTableWidget( viewElement ) {
	return !!viewElement.getCustomProperty( 'table' ) && isWidget( viewElement );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table toolbar class. It creates toolbars for the table feature and its content (for now only for the table cell content).
 *
 * The table toolbar shows up when a table widget is selected. Its components (e.g. buttons) are created based on the
 * {@link module:table/table~TableConfig#tableToolbar `table.tableToolbar` configuration option}.
 *
 * Table content toolbar shows up when the selection is inside the content of a table. It creates its component based on the
 * {@link module:table/table~TableConfig#contentToolbar `table.contentToolbar` configuration option}.
 *
 * @extends module:core/plugin~Plugin
 */
class TableToolbar extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ WidgetToolbarRepository ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TableToolbar';
	}

	/**
	 * @inheritDoc
	 */
	afterInit() {
		const editor = this.editor;
		const t = editor.t;
		const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );

		const tableContentToolbarItems = editor.config.get( 'table.contentToolbar' );

		const tableToolbarItems = editor.config.get( 'table.tableToolbar' );

		if ( tableContentToolbarItems ) {
			widgetToolbarRepository.register( 'tableContent', {
				ariaLabel: t( 'Table toolbar' ),
				items: tableContentToolbarItems,
				getRelatedElement: getTableWidgetAncestor
			} );
		}

		if ( tableToolbarItems ) {
			widgetToolbarRepository.register( 'table', {
				ariaLabel: t( 'Table toolbar' ),
				items: tableToolbarItems,
				getRelatedElement: getSelectedTableWidget
			} );
		}
	}
}

/**
 * Items to be placed in the table content toolbar.
 * The {@link module:table/tabletoolbar~TableToolbar} plugin is required to make this toolbar work.
 *
 * Assuming that you use the {@link module:table/tableui~TableUI} feature, the following toolbar items will be available
 * in {@link module:ui/componentfactory~ComponentFactory}:
 *
 * * `'tableRow'`,
 * * `'tableColumn'`,
 * * `'mergeTableCells'`.
 *
 * You can thus configure the toolbar like this:
 *
 *		const tableConfig = {
 *			contentToolbar: [ 'tableRow', 'tableColumn', 'mergeTableCells' ]
 *		};
 *
 * Of course, the same buttons can also be used in the
 * {@link module:core/editor/editorconfig~EditorConfig#toolbar main editor toolbar}.
 *
 * Read more about configuring the toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
 *
 * @member {Array.<String>} module:table/table~TableConfig#contentToolbar
 */

/**
 * Items to be placed in the table toolbar.
 * The {@link module:table/tabletoolbar~TableToolbar} plugin is required to make this toolbar work.
 *
 * You can thus configure the toolbar like this:
 *
 *		const tableConfig = {
 *			tableToolbar: [ 'blockQuote' ]
 *		};
 *
 * Of course, the same buttons can also be used in the
 * {@link module:core/editor/editorconfig~EditorConfig#toolbar main editor toolbar}.
 *
 * Read more about configuring the toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
 *
 * @member {Array.<String>} module:table/table~TableConfig#tableToolbar
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module table/converters/tableproperites
 */

/**
 * Conversion helper for upcasting attributes using normalized styles.
 *
 * @param {module:engine/conversion/conversion~Conversion} conversion
 * @param {Object} options
 * @param {String} options.modelAttribute The attribute to set.
 * @param {String} options.styleName The style name to convert.
 * @param {String} options.viewElement The view element name that should be converted.
 * @param {String} options.defaultValue The default value for the specified `modelAttribute`.
 * @param {Boolean} [options.reduceBoxSides=false]
 */
function upcastStyleToAttribute( conversion, options ) {
	const { viewElement, defaultValue, modelAttribute, styleName, reduceBoxSides = false } = options;

	conversion.for( 'upcast' ).attributeToAttribute( {
		view: {
			name: viewElement,
			styles: {
				[ styleName ]: /[\s\S]+/
			}
		},
		model: {
			key: modelAttribute,
			value: viewElement => {
				const normalized = viewElement.getNormalizedStyle( styleName );
				const value = reduceBoxSides ? reduceBoxSidesValue( normalized ) : normalized;

				if ( defaultValue !== value ) {
					return value;
				}
			}
		}
	} );
}

/**
 * Conversion helper for upcasting border styles for view elements.
 *
 * @param {module:engine/conversion/conversion~Conversion} conversion
 * @param {String} viewElementName
 * @param {Object} modelAttributes
 * @param {Object} defaultBorder The default border values.
 * @param {String} defaultBorder.color The default `borderColor` value.
 * @param {String} defaultBorder.style The default `borderStyle` value.
 * @param {String} defaultBorder.width The default `borderWidth` value.
 */
function upcastBorderStyles( conversion, viewElementName, modelAttributes, defaultBorder ) {
	conversion.for( 'upcast' ).add( dispatcher => dispatcher.on( 'element:' + viewElementName, ( evt, data, conversionApi ) => {
		// If the element was not converted by element-to-element converter,
		// we should not try to convert the style. See #8393.
		if ( !data.modelRange ) {
			return;
		}

		// Check the most detailed properties. These will be always set directly or
		// when using the "group" properties like: `border-(top|right|bottom|left)` or `border`.
		const stylesToConsume = [
			'border-top-width',
			'border-top-color',
			'border-top-style',
			'border-bottom-width',
			'border-bottom-color',
			'border-bottom-style',
			'border-right-width',
			'border-right-color',
			'border-right-style',
			'border-left-width',
			'border-left-color',
			'border-left-style'
		].filter( styleName => data.viewItem.hasStyle( styleName ) );

		if ( !stylesToConsume.length ) {
			return;
		}

		const matcherPattern = {
			styles: stylesToConsume
		};

		// Try to consume appropriate values from consumable values list.
		if ( !conversionApi.consumable.test( data.viewItem, matcherPattern ) ) {
			return;
		}

		const modelElement = [ ...data.modelRange.getItems( { shallow: true } ) ].pop();

		conversionApi.consumable.consume( data.viewItem, matcherPattern );

		const normalizedBorder = {
			style: data.viewItem.getNormalizedStyle( 'border-style' ),
			color: data.viewItem.getNormalizedStyle( 'border-color' ),
			width: data.viewItem.getNormalizedStyle( 'border-width' )
		};

		const reducedBorder = {
			style: reduceBoxSidesValue( normalizedBorder.style ),
			color: reduceBoxSidesValue( normalizedBorder.color ),
			width: reduceBoxSidesValue( normalizedBorder.width )
		};

		if ( reducedBorder.style !== defaultBorder.style ) {
			conversionApi.writer.setAttribute( modelAttributes.style, reducedBorder.style, modelElement );
		}

		if ( reducedBorder.color !== defaultBorder.color ) {
			conversionApi.writer.setAttribute( modelAttributes.color, reducedBorder.color, modelElement );
		}

		if ( reducedBorder.width !== defaultBorder.width ) {
			conversionApi.writer.setAttribute( modelAttributes.width, reducedBorder.width, modelElement );
		}
	} ) );
}

/**
 * Conversion helper for downcasting an attribute to a style.
 *
 * @param {module:engine/conversion/conversion~Conversion} conversion
 * @param {Object} options
 * @param {String} options.modelElement
 * @param {String} options.modelAttribute
 * @param {String} options.styleName
 */
function downcastAttributeToStyle( conversion, { modelElement, modelAttribute, styleName } ) {
	conversion.for( 'downcast' ).attributeToAttribute( {
		model: {
			name: modelElement,
			key: modelAttribute
		},
		view: modelAttributeValue => ( {
			key: 'style',
			value: {
				[ styleName ]: modelAttributeValue
			}
		} )
	} );
}

/**
 * Conversion helper for downcasting attributes from the model table to a view table (not to `<figure>`).
 *
 * @param {module:engine/conversion/conversion~Conversion} conversion
 * @param {Object} options
 * @param {String} options.modelAttribute
 * @param {String} options.styleName
 */
function downcastTableAttribute( conversion, { modelAttribute, styleName } ) {
	conversion.for( 'downcast' ).add( dispatcher => dispatcher.on( `attribute:${ modelAttribute }:table`, ( evt, data, conversionApi ) => {
		const { item, attributeNewValue } = data;
		const { mapper, writer } = conversionApi;

		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
			return;
		}

		const table = [ ...mapper.toViewElement( item ).getChildren() ].find( child => child.is( 'element', 'table' ) );

		if ( attributeNewValue ) {
			writer.setStyle( styleName, attributeNewValue, table );
		} else {
			writer.removeStyle( styleName, table );
		}
	} ) );
}

// Reduces the full top, right, bottom, left object to a single string if all sides are equal.
function reduceBoxSidesValue( style ) {
	if ( !style ) {
		return;
	}

	const commonValue = [ 'top', 'right', 'bottom', 'left' ]
		.map( side => style[ side ] )
		.reduce( ( result, side ) => result == side ? result : null );

	return commonValue || style;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table cell attribute command.
 *
 * This command is a base command for other table property commands.
 *
 * @extends module:core/command~Command
 */
class TablePropertyCommand extends Command {
	/**
	 * Creates a new `TablePropertyCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} attributeName Table cell attribute name.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, attributeName, defaultValue ) {
		super( editor );

		/**
		 * The attribute that will be set by the command.
		 *
		 * @readonly
		 * @member {String}
		 */
		this.attributeName = attributeName;

		/**
		 * The default value for the attribute.
		 *
		 * @readonly
		 * @protected
		 * @member {String}
		 */
		this._defaultValue = defaultValue;
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		const editor = this.editor;
		const selection = editor.model.document.selection;

		const table = selection.getFirstPosition().findAncestor( 'table' );

		this.isEnabled = !!table;
		this.value = this._getValue( table );
	}

	/**
	 * Executes the command.
	 *
	 * @fires execute
	 * @param {Object} [options]
	 * @param {*} [options.value] If set, the command will set the attribute on the selected table.
	 * If not set, the command will remove the attribute from the selected table.
	 * @param {module:engine/model/batch~Batch} [options.batch] Pass the model batch instance to the command to aggregate changes,
	 * for example, to allow a single undo step for multiple executions.
	 */
	execute( options = {} ) {
		const model = this.editor.model;
		const selection = model.document.selection;

		const { value, batch } = options;

		const table = selection.getFirstPosition().findAncestor( 'table' );
		const valueToSet = this._getValueToSet( value );

		model.enqueueChange( batch, writer => {
			if ( valueToSet ) {
				writer.setAttribute( this.attributeName, valueToSet, table );
			} else {
				writer.removeAttribute( this.attributeName, table );
			}
		} );
	}

	/**
	 * Returns the attribute value for a table.
	 *
	 * @param {module:engine/model/element~Element} table
	 * @returns {String|undefined}
	 * @private
	 */
	_getValue( table ) {
		if ( !table ) {
			return;
		}

		const value = table.getAttribute( this.attributeName );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}

	/**
	 * Returns the proper model value. It can be used to add a default unit to numeric values.
	 *
	 * @private
	 * @param {*} value
	 * @returns {*}
	 */
	_getValueToSet( value ) {
		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table background color command.
 *
 * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
 * the `'tableBackgroundColor'` editor command.
 *
 * To change the background color of the selected table, execute the command:
 *
 *		editor.execute( 'tableBackgroundColor', {
 *			value: '#f00'
 *		} );
 *
 * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
 */
class TableBackgroundColorCommand extends TablePropertyCommand {
	/**
	 * Creates a new `TableBackgroundColorCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableBackgroundColor', defaultValue );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Returns a string if all four values of box sides are equal.
 *
 * If a string is passed, it is treated as a single value (pass-through).
 *
 *		// Returns 'foo':
 *		getSingleValue( { top: 'foo', right: 'foo', bottom: 'foo', left: 'foo' } );
 *		getSingleValue( 'foo' );
 *
 *		// Returns undefined:
 *		getSingleValue( { top: 'foo', right: 'foo', bottom: 'bar', left: 'foo' } );
 *		getSingleValue( { top: 'foo', right: 'foo' } );
 *
 * @param objectOrString
 * @returns {module:engine/view/stylesmap~BoxSides|String}
 */
function getSingleValue( objectOrString ) {
	if ( !objectOrString || !isObject( objectOrString ) ) {
		return objectOrString;
	}

	const { top, right, bottom, left } = objectOrString;

	if ( top == right && right == bottom && bottom == left ) {
		return top;
	}
}

/**
 * Adds a unit to a value if the value is a number or a string representing a number.
 *
 * **Note**: It does nothing to non-numeric values.
 *
 *		getSingleValue( 25, 'px' );		// '25px'
 *		getSingleValue( 25, 'em' );		// '25em'
 *		getSingleValue( '25em', 'px' );	// '25em'
 *		getSingleValue( 'foo', 'px' );	// 'foo'
 *
 * @param {*} value
 * @param {String} defaultUnit A default unit added to a numeric value.
 * @returns {String|*}
 */
function addDefaultUnitToNumericValue( value, defaultUnit ) {
	const numericValue = parseFloat( value );

	if ( Number.isNaN( numericValue ) ) {
		return value;
	}

	if ( String( numericValue ) !== String( value ) ) {
		return value;
	}

	return `${ numericValue }${ defaultUnit }`;
}

/**
 * Returns the normalized configuration.
 *
 * @param {Object} config
 * @param {Object} [options={}]
 * @param {Boolean} [options.includeAlignmentProperty=false] Whether the "alignment" property should be added.
 * @param {Boolean} [options.includePaddingProperty=false] Whether the "padding" property should be added.
 * @param {Boolean} [options.includeVerticalAlignmentProperty=false] Whether the "verticalAlignment" property should be added.
 * @param {Boolean} [options.includeHorizontalAlignmentProperty=false] Whether the "horizontalAlignment" property should be added.
 * @param {Boolean} [options.isRightToLeftContent=false] Whether the content is right-to-left.
 * @returns {Object}
 */
function getNormalizedDefaultProperties( config, options = {} ) {
	const normalizedConfig = Object.assign( {
		borderStyle: 'none',
		borderWidth: '',
		borderColor: '',
		backgroundColor: '',
		width: '',
		height: ''
	}, config );

	if ( options.includeAlignmentProperty && !normalizedConfig.alignment ) {
		normalizedConfig.alignment = 'center';
	}

	if ( options.includePaddingProperty && !normalizedConfig.padding ) {
		normalizedConfig.padding = '';
	}

	if ( options.includeVerticalAlignmentProperty && !normalizedConfig.verticalAlignment ) {
		normalizedConfig.verticalAlignment = 'middle';
	}

	if ( options.includeHorizontalAlignmentProperty && !normalizedConfig.horizontalAlignment ) {
		normalizedConfig.horizontalAlignment = options.isRightToLeftContent ? 'right' : 'left';
	}

	return normalizedConfig;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table border color command.
 *
 * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
 * the `'tableBorderColor'` editor command.
 *
 * To change the border color of the selected table, execute the command:
 *
 *		editor.execute( 'tableBorderColor', {
 *			value: '#f00'
 *		} );
 *
 * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
 */
class TableBorderColorCommand extends TablePropertyCommand {
	/**
	 * Creates a new `TableBorderColorCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableBorderColor', defaultValue );
	}

	/**
	 * @inheritDoc
	 */
	_getValue( table ) {
		if ( !table ) {
			return;
		}

		const value = getSingleValue( table.getAttribute( this.attributeName ) );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table style border command.
 *
 * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
 * the `'tableBorderStyle'` editor command.
 *
 * To change the border style of the selected table, execute the command:
 *
 *		editor.execute( 'tableBorderStyle', {
 *			value: 'dashed'
 *		} );
 *
 * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
 */
class TableBorderStyleCommand extends TablePropertyCommand {
	/**
	 * Creates a new `TableBorderStyleCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableBorderStyle', defaultValue );
	}

	/**
	 * @inheritDoc
	 */
	_getValue( table ) {
		if ( !table ) {
			return;
		}

		const value = getSingleValue( table.getAttribute( this.attributeName ) );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table width border command.
 *
 * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
 * the `'tableBorderWidth'` editor command.
 *
 * To change the border width of the selected table, execute the command:
 *
 *		editor.execute( 'tableBorderWidth', {
 *			value: '5px'
 *		} );
 *
 * **Note**: This command adds the default `'px'` unit to numeric values. Executing:
 *
 *		editor.execute( 'tableBorderWidth', {
 *			value: '5'
 *		} );
 *
 * will set the `borderWidth` attribute to `'5px'` in the model.
 *
 * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
 */
class TableBorderWidthCommand extends TablePropertyCommand {
	/**
	 * Creates a new `TableBorderWidthCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableBorderWidth', defaultValue );
	}

	/**
	 * @inheritDoc
	 */
	_getValue( table ) {
		if ( !table ) {
			return;
		}

		const value = getSingleValue( table.getAttribute( this.attributeName ) );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}

	/**
	 * @inheritDoc
	 */
	_getValueToSet( value ) {
		value = addDefaultUnitToNumericValue( value, 'px' );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table width command.
 *
 * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
 * the `'tableWidth'` editor command.
 *
 * To change the width of the selected table, execute the command:
 *
 *		editor.execute( 'tableWidth', {
 *			value: '400px'
 *		} );
 *
 * **Note**: This command adds the default `'px'` unit to numeric values. Executing:
 *
 *		editor.execute( 'tableWidth', {
 *			value: '50'
 *		} );
 *
 * will set the `width` attribute to `'50px'` in the model.
 *
 * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
 */
class TableWidthCommand extends TablePropertyCommand {
	/**
	 * Creates a new `TableWidthCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableWidth', defaultValue );
	}

	/**
	 * @inheritDoc
	 */
	_getValueToSet( value ) {
		value = addDefaultUnitToNumericValue( value, 'px' );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table height command.
 *
 * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
 * the `'tableHeight'` editor command.
 *
 * To change the height of the selected table, execute the command:
 *
 *		editor.execute( 'tableHeight', {
 *			value: '500px'
 *		} );
 *
 * **Note**: This command adds the default `'px'` unit to numeric values. Executing:
 *
 *		editor.execute( 'tableHeight', {
 *			value: '50'
 *		} );
 *
 * will set the `height` attribute to `'50px'` in the model.
 *
 * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
 */
class TableHeightCommand extends TablePropertyCommand {
	/**
	 * Creates a new `TableHeightCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableHeight', defaultValue );
	}

	/**
	 * @inheritDoc
	 */
	_getValueToSet( value ) {
		value = addDefaultUnitToNumericValue( value, 'px' );

		if ( value === this._defaultValue ) {
			return null;
		}

		return value;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table alignment command.
 *
 * The command is registered by the {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing} as
 * the `'tableAlignment'` editor command.
 *
 * To change the alignment of the selected table, execute the command:
 *
 *		editor.execute( 'tableAlignment', {
 *			value: 'right'
 *		} );
 *
 * @extends module:table/tableproperties/commands/tablepropertycommand~TablePropertyCommand
 */
class TableAlignmentCommand extends TablePropertyCommand {
	/**
	 * Creates a new `TableAlignmentCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value for the "alignment" attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableAlignment', defaultValue );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const ALIGN_VALUES_REG_EXP$1 = /^(left|center|right)$/;
const FLOAT_VALUES_REG_EXP = /^(left|none|right)$/;

/**
 * The table properties editing feature.
 *
 * Introduces table's model attributes and their conversion:
 *
 * - border: `tableBorderStyle`, `tableBorderColor` and `tableBorderWidth`
 * - background color: `tableBackgroundColor`
 * - horizontal alignment: `tableAlignment`
 * - width & height: `tableWidth` & `tableHeight`
 *
 * It also registers commands used to manipulate the above attributes:
 *
 * - border: `'tableBorderStyle'`, `'tableBorderColor'` and `'tableBorderWidth'` commands
 * - background color: `'tableBackgroundColor'`
 * - horizontal alignment: `'tableAlignment'`
 * - width & height: `'tableWidth'` & `'tableHeight'`
 *
 * @extends module:core/plugin~Plugin
 */
class TablePropertiesEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TablePropertiesEditing';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ TableEditing ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const schema = editor.model.schema;
		const conversion = editor.conversion;

		editor.config.define( 'table.tableProperties.defaultProperties', {} );

		const defaultTableProperties = getNormalizedDefaultProperties( editor.config.get( 'table.tableProperties.defaultProperties' ), {
			includeAlignmentProperty: true
		} );

		editor.data.addStyleProcessorRules( addBorderRules );
		enableBorderProperties$1( schema, conversion, {
			color: defaultTableProperties.borderColor,
			style: defaultTableProperties.borderStyle,
			width: defaultTableProperties.borderWidth
		} );
		editor.commands.add( 'tableBorderColor', new TableBorderColorCommand( editor, defaultTableProperties.borderColor ) );
		editor.commands.add( 'tableBorderStyle', new TableBorderStyleCommand( editor, defaultTableProperties.borderStyle ) );
		editor.commands.add( 'tableBorderWidth', new TableBorderWidthCommand( editor, defaultTableProperties.borderWidth ) );

		enableAlignmentProperty( schema, conversion, defaultTableProperties.alignment );
		editor.commands.add( 'tableAlignment', new TableAlignmentCommand( editor, defaultTableProperties.alignment ) );

		enableTableToFigureProperty( schema, conversion, {
			modelAttribute: 'tableWidth',
			styleName: 'width',
			defaultValue: defaultTableProperties.width
		} );
		editor.commands.add( 'tableWidth', new TableWidthCommand( editor, defaultTableProperties.width ) );

		enableTableToFigureProperty( schema, conversion, {
			modelAttribute: 'tableHeight',
			styleName: 'height',
			defaultValue: defaultTableProperties.height
		} );
		editor.commands.add( 'tableHeight', new TableHeightCommand( editor, defaultTableProperties.height ) );

		editor.data.addStyleProcessorRules( addBackgroundRules );
		enableProperty$1( schema, conversion, {
			modelAttribute: 'tableBackgroundColor',
			styleName: 'background-color',
			defaultValue: defaultTableProperties.backgroundColor
		} );
		editor.commands.add(
			'tableBackgroundColor',
			new TableBackgroundColorCommand( editor, defaultTableProperties.backgroundColor )
		);
	}
}

// Enables `tableBorderStyle'`, `tableBorderColor'` and `tableBorderWidth'` attributes for table.
//
// @param {module:engine/model/schema~Schema} schema
// @param {module:engine/conversion/conversion~Conversion} conversion
// @param {Object} defaultBorder The default border values.
// @param {String} defaultBorder.color The default `tableBorderColor` value.
// @param {String} defaultBorder.style The default `tableBorderStyle` value.
// @param {String} defaultBorder.width The default `tableBorderWidth` value.
function enableBorderProperties$1( schema, conversion, defaultBorder ) {
	const modelAttributes = {
		width: 'tableBorderWidth',
		color: 'tableBorderColor',
		style: 'tableBorderStyle'
	};

	schema.extend( 'table', {
		allowAttributes: Object.values( modelAttributes )
	} );

	upcastBorderStyles( conversion, 'table', modelAttributes, defaultBorder );

	downcastTableAttribute( conversion, { modelAttribute: modelAttributes.color, styleName: 'border-color' } );
	downcastTableAttribute( conversion, { modelAttribute: modelAttributes.style, styleName: 'border-style' } );
	downcastTableAttribute( conversion, { modelAttribute: modelAttributes.width, styleName: 'border-width' } );
}

// Enables the `'alignment'` attribute for table.
//
// @param {module:engine/model/schema~Schema} schema
// @param {module:engine/conversion/conversion~Conversion} conversion
// @param {String} defaultValue The default alignment value.
function enableAlignmentProperty( schema, conversion, defaultValue ) {
	schema.extend( 'table', {
		allowAttributes: [ 'tableAlignment' ]
	} );

	conversion.for( 'downcast' )
		.attributeToAttribute( {
			model: {
				name: 'table',
				key: 'tableAlignment'
			},
			view: alignment => ( {
				key: 'style',
				value: {
					// Model: `alignment:center` => CSS: `float:none`.
					float: alignment === 'center' ? 'none' : alignment
				}
			} ),
			converterPriority: 'high'
		} );

	conversion.for( 'upcast' )
		// Support for the `float:*;` CSS definition for the table alignment.
		.attributeToAttribute( {
			view: {
				name: /^(table|figure)$/,
				styles: {
					float: FLOAT_VALUES_REG_EXP
				}
			},
			model: {
				key: 'tableAlignment',
				value: viewElement => {
					let align = viewElement.getStyle( 'float' );

					// CSS: `float:none` => Model: `alignment:center`.
					if ( align === 'none' ) {
						align = 'center';
					}

					return align === defaultValue ? null : align;
				}
			}
		} )
		// Support for the `align` attribute as the backward compatibility while pasting from other sources.
		.attributeToAttribute( {
			view: {
				attributes: {
					align: ALIGN_VALUES_REG_EXP$1
				}
			},
			model: {
				name: 'table',
				key: 'tableAlignment',
				value: viewElement => {
					const align = viewElement.getAttribute( 'align' );

					return align === defaultValue ? null : align;
				}
			}
		} );
}

// Enables conversion for an attribute for simple view-model mappings.
//
// @param {module:engine/model/schema~Schema} schema
// @param {module:engine/conversion/conversion~Conversion} conversion
// @param {Object} options
// @param {String} options.modelAttribute
// @param {String} options.styleName
// @param {String} options.defaultValue The default value for the specified `modelAttribute`.
function enableProperty$1( schema, conversion, options ) {
	const { modelAttribute } = options;

	schema.extend( 'table', {
		allowAttributes: [ modelAttribute ]
	} );
	upcastStyleToAttribute( conversion, { viewElement: 'table', ...options } );
	downcastTableAttribute( conversion, options );
}

// Enables conversion for an attribute for simple view (figure) to model (table) mappings.
//
// @param {module:engine/model/schema~Schema} schema
// @param {module:engine/conversion/conversion~Conversion} conversion
// @param {Object} options
// @param {String} options.modelAttribute
// @param {String} options.styleName
function enableTableToFigureProperty( schema, conversion, options ) {
	const { modelAttribute } = options;

	schema.extend( 'table', {
		allowAttributes: [ modelAttribute ]
	} );
	upcastStyleToAttribute( conversion, { viewElement: /^(table|figure)$/, ...options } );
	downcastAttributeToStyle( conversion, { modelElement: 'table', ...options } );
}var css_248z$h = ".ck.ck-input-color{display:flex;flex-direction:row-reverse;width:100%}.ck.ck-input-color>input.ck.ck-input-text{flex-grow:1;min-width:auto}.ck.ck-input-color>div.ck.ck-dropdown{min-width:auto}.ck.ck-input-color>div.ck.ck-dropdown>.ck-input-color__button .ck-dropdown__arrow{display:none}.ck.ck-input-color .ck.ck-input-color__button{display:flex}.ck.ck-input-color .ck.ck-input-color__button .ck.ck-input-color__button__preview{overflow:hidden;position:relative}.ck.ck-input-color .ck.ck-input-color__button .ck.ck-input-color__button__preview>.ck.ck-input-color__button__preview__no-color-indicator{display:block;position:absolute}[dir=ltr] .ck.ck-input-color>.ck.ck-input-text{border-bottom-right-radius:0;border-top-right-radius:0}[dir=rtl] .ck.ck-input-color>.ck.ck-input-text{border-bottom-left-radius:0;border-top-left-radius:0}.ck.ck-input-color>.ck.ck-dropdown>.ck.ck-button.ck-input-color__button{padding:0}[dir=ltr] .ck.ck-input-color>.ck.ck-dropdown>.ck.ck-button.ck-input-color__button{border-bottom-left-radius:0;border-left-width:0;border-top-left-radius:0}[dir=rtl] .ck.ck-input-color>.ck.ck-dropdown>.ck.ck-button.ck-input-color__button{border-bottom-right-radius:0;border-right-width:0;border-top-right-radius:0}.ck.ck-input-color>.ck.ck-dropdown>.ck.ck-button.ck-input-color__button.ck-disabled{background:var(--ck-color-input-disabled-background)}.ck.ck-input-color>.ck.ck-dropdown>.ck.ck-button.ck-input-color__button>.ck.ck-input-color__button__preview{border-radius:0}.ck-rounded-corners .ck.ck-input-color>.ck.ck-dropdown>.ck.ck-button.ck-input-color__button>.ck.ck-input-color__button__preview,.ck.ck-input-color>.ck.ck-dropdown>.ck.ck-button.ck-input-color__button>.ck.ck-input-color__button__preview.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-input-color>.ck.ck-dropdown>.ck.ck-button.ck-input-color__button>.ck.ck-input-color__button__preview{border:1px solid var(--ck-color-input-border);height:20px;width:20px}.ck.ck-input-color>.ck.ck-dropdown>.ck.ck-button.ck-input-color__button>.ck.ck-input-color__button__preview>.ck.ck-input-color__button__preview__no-color-indicator{background:red;border-radius:2px;height:150%;left:50%;top:-30%;transform:rotate(45deg);transform-origin:50%;width:8%}.ck.ck-input-color .ck.ck-input-color__remove-color{border-bottom:1px solid var(--ck-color-input-border);border-bottom-left-radius:0;border-bottom-right-radius:0;padding:calc(var(--ck-spacing-standard)/2) var(--ck-spacing-standard);width:100%}[dir=ltr] .ck.ck-input-color .ck.ck-input-color__remove-color{border-top-right-radius:0}[dir=rtl] .ck.ck-input-color .ck.ck-input-color__remove-color{border-top-left-radius:0}.ck.ck-input-color .ck.ck-input-color__remove-color .ck.ck-icon{margin-right:var(--ck-spacing-standard)}[dir=rtl] .ck.ck-input-color .ck.ck-input-color__remove-color .ck.ck-icon{margin-left:var(--ck-spacing-standard);margin-right:0}";
styleInject(css_248z$h);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The color input view class. It allows the user to type in a color (hex, rgb, etc.)
 * or choose it from the configurable color palette with a preview.
 *
 * @private
 * @extends module:ui/view~View
 */
class ColorInputView extends View {
	/**
	 * Creates an instance of the color input view.
	 *
	 * @param {module:utils/locale~Locale} locale The locale instance.
	 * @param {Object} options The input options.
	 * @param {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>} options.colorDefinitions The colors to be displayed
	 * in the palette inside the input's dropdown.
	 * @param {Number} options.columns The number of columns in which the colors will be displayed.
	 * @param {String} [options.defaultColorValue] If specified, the color input view will replace the "Remove color" button with
	 * the "Restore default" button. Instead of clearing the input field, the default color value will be set.
	 */
	constructor( locale, options ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * The value of the input.
		 *
		 * @observable
		 * @member {String} #value
		 * @default ''
		 */
		this.set( 'value', '' );

		/**
		 * The `id` attribute of the input (i.e. to pair with the `<label>` element).
		 *
		 * @observable
		 * @member {String} #id
		 */
		this.set( 'id' );

		/**
		 * Controls whether the input view is in read-only mode.
		 *
		 * @observable
		 * @member {Boolean} #isReadOnly
		 * @default false
		 */
		this.set( 'isReadOnly', false );

		/**
		 * Set to `true` when the field has some error. Usually controlled via
		 * {@link module:ui/labeledinput/labeledinputview~LabeledInputView#errorText}.
		 *
		 * @observable
		 * @member {Boolean} #hasError
		 * @default false
		 */
		this.set( 'hasError', false );

		/**
		 * An observable flag set to `true` when the input is focused by the user.
		 * `false` otherwise.
		 *
		 * @readonly
		 * @observable
		 * @member {Boolean} #isFocused
		 * @default false
		 */
		this.set( 'isFocused', false );

		/**
		 * An observable flag set to `true` when the input contains no text.
		 *
		 * @readonly
		 * @observable
		 * @member {Boolean} #isEmpty
		 * @default true
		 */
		this.set( 'isEmpty', true );

		/**
		 * The `id` of the element describing this field. When the field has
		 * some error, it helps screen readers read the error text.
		 *
		 * @observable
		 * @member {String} #ariaDescribedById
		 */
		this.set( 'ariaDescribedById' );

		/**
		 * A cached reference to the options passed to the constructor.
		 *
		 * @member {Object}
		 */
		this.options = options;

		/**
		 * An instance of the dropdown allowing to select a color from a grid.
		 *
		 * @protected
		 * @member {module:ui/dropdown/dropdown~DropdownView}
		 */
		this._dropdownView = this._createDropdownView();

		/**
		 * An instance of the input allowing the user to type a color value.
		 *
		 * @protected
		 * @member {module:ui/inputtext/inputtextview~InputTextView}
		 */
		this._inputView = this._createInputTextView();

		/**
		 * The flag that indicates whether the user is still typing.
		 * If set to true, it means that the text input field ({@link #_inputView}) still has the focus.
		 * So, we should interrupt the user by replacing the input's value.
		 *
		 * @protected
		 * @member {Boolean}
		 */
		this._stillTyping = false;

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-input-color',
					bind.if( 'hasError', 'ck-error' )
				],
				id: bind.to( 'id' ),
				'aria-invalid': bind.if( 'hasError', true ),
				'aria-describedby': bind.to( 'ariaDescribedById' )
			},
			children: [
				this._dropdownView,
				this._inputView
			]
		} );

		this.on( 'change:value', ( evt, name, inputValue ) => this._setInputValue( inputValue ) );
	}

	/**
	 * Focuses the input.
	 */
	focus() {
		this._inputView.focus();
	}

	/**
	 * Creates and configures the {@link #_dropdownView}.
	 *
	 * @private
	 */
	_createDropdownView() {
		const locale = this.locale;
		const t = locale.t;
		const bind = this.bindTemplate;
		const colorGrid = this._createColorGrid( locale );
		const dropdown = createDropdown( locale );
		const colorPreview = new View();
		const removeColorButton = this._createRemoveColorButton();

		colorPreview.setTemplate( {
			tag: 'span',
			attributes: {
				class: [
					'ck',
					'ck-input-color__button__preview'
				],
				style: {
					backgroundColor: bind.to( 'value' )
				}
			},
			children: [ {
				tag: 'span',
				attributes: {
					class: [
						'ck',
						'ck-input-color__button__preview__no-color-indicator',
						bind.if( 'value', 'ck-hidden', value => value != '' )
					]
				}
			} ]
		} );

		dropdown.buttonView.extendTemplate( {
			attributes: {
				class: 'ck-input-color__button'
			}
		} );

		dropdown.buttonView.children.add( colorPreview );
		dropdown.buttonView.label = t( 'Color picker' );
		dropdown.buttonView.tooltip = true;

		dropdown.panelPosition = locale.uiLanguageDirection === 'rtl' ? 'se' : 'sw';
		dropdown.panelView.children.add( removeColorButton );
		dropdown.panelView.children.add( colorGrid );
		dropdown.bind( 'isEnabled' ).to( this, 'isReadOnly', value => !value );

		return dropdown;
	}

	/**
	 * Creates and configures an instance of {@link module:ui/inputtext/inputtextview~InputTextView}.
	 *
	 * @private
	 * @returns {module:ui/inputtext/inputtextview~InputTextView} A configured instance to be set as {@link #_inputView}.
	 */
	_createInputTextView() {
		const locale = this.locale;
		const inputView = new InputTextView( locale );

		inputView.extendTemplate( {
			on: {
				blur: inputView.bindTemplate.to( 'blur' )
			}
		} );

		inputView.value = this.value;
		inputView.bind( 'isReadOnly', 'hasError' ).to( this );
		this.bind( 'isFocused', 'isEmpty' ).to( inputView );

		inputView.on( 'input', () => {
			const inputValue = inputView.element.value;
			// Check if the value matches one of our defined colors' label.
			const mappedColor = this.options.colorDefinitions.find( def => inputValue === def.label );

			this._stillTyping = true;
			this.value = mappedColor && mappedColor.color || inputValue;
		} );

		inputView.on( 'blur', () => {
			this._stillTyping = false;
			this._setInputValue( inputView.element.value );
		} );

		inputView.delegate( 'input' ).to( this );

		return inputView;
	}

	/**
	 * Creates and configures the button that clears the color.
	 *
	 * @private
	 */
	_createRemoveColorButton() {
		const locale = this.locale;
		const t = locale.t;
		const removeColorButton = new ButtonView( locale );
		const defaultColor = this.options.defaultColorValue || '';
		const removeColorButtonLabel = defaultColor ? t( 'Restore default' ) : t( 'Remove color' );

		removeColorButton.class = 'ck-input-color__remove-color';
		removeColorButton.withText = true;
		removeColorButton.icon = icons.eraser;
		removeColorButton.label = removeColorButtonLabel;
		removeColorButton.on( 'execute', () => {
			this.value = defaultColor;
			this._dropdownView.isOpen = false;
			this.fire( 'input' );
		} );

		return removeColorButton;
	}

	/**
	 * Creates and configures the color grid inside the {@link #_dropdownView}.
	 *
	 * @private
	 */
	_createColorGrid( locale ) {
		const colorGrid = new ColorGridView( locale, {
			colorDefinitions: this.options.colorDefinitions,
			columns: this.options.columns
		} );

		colorGrid.on( 'execute', ( evtData, data ) => {
			this.value = data.value;
			this._dropdownView.isOpen = false;
			this.fire( 'input' );
		} );
		colorGrid.bind( 'selectedColor' ).to( this, 'value' );

		return colorGrid;
	}

	/**
	 * Sets {@link #_inputView}'s value property to the color value or color label,
	 * if there is one and the user is not typing.
	 *
	 * Handles cases like:
	 *
	 * * Someone picks the color in the grid.
	 * * The color is set from the plugin level.
	 *
	 * @private
	 * @param {String} inputValue Color value to be set.
	 */
	_setInputValue( inputValue ) {
		if ( !this._stillTyping ) {
			const normalizedInputValue = normalizeColor( inputValue );
			// Check if the value matches one of our defined colors.
			const mappedColor = this.options.colorDefinitions.find( def => normalizedInputValue === normalizeColor( def.color ) );

			if ( mappedColor ) {
				this._inputView.value = mappedColor.label;
			} else {
				this._inputView.value = inputValue || '';
			}
		}
	}
}

// Normalizes color value, by stripping extensive whitespace.
// For example., transforms:
// * `   rgb(  25 50    0 )` to `rgb(25 50 0)`,
// * "\t  rgb(  25 ,  50,0 )		" to `rgb(25 50 0)`.
//
// @param {String} colorString The value to be normalized.
// @returns {String}
function normalizeColor( colorString ) {
	return colorString
		// Remove any whitespace right after `(` or `,`.
		.replace( /([(,])\s+/g, '$1' )
		// Remove any whitespace at the beginning or right before the end, `)`, `,`, or another whitespace.
		.replace( /^\s+|\s+(?=[),\s]|$)/g, '' )
		// Then, replace `,` or whitespace with a single space.
		.replace( /,|\s/g, ' ' );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const isEmpty = val => val === '';

/**
 * Returns an object containing pairs of CSS border style values and their localized UI
 * labels. Used by {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView}
 * and {@link module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView}.
 *
 * @param {module:utils/locale~Locale#t} t The "t" function provided by the editor
 * that is used to localize strings.
 * @returns {Object.<String,String>}
 */
function getBorderStyleLabels( t ) {
	return {
		none: t( 'None' ),
		solid: t( 'Solid' ),
		dotted: t( 'Dotted' ),
		dashed: t( 'Dashed' ),
		double: t( 'Double' ),
		groove: t( 'Groove' ),
		ridge: t( 'Ridge' ),
		inset: t( 'Inset' ),
		outset: t( 'Outset' )
	};
}

/**
 * Returns a localized error string that can be displayed next to color (background, border)
 * fields that have an invalid value.
 *
 * @param {module:utils/locale~Locale#t} t The "t" function provided by the editor
 * that is used to localize strings.
 * @returns {String}
 */
function getLocalizedColorErrorText( t ) {
	return t( 'The color is invalid. Try "#FF0000" or "rgb(255,0,0)" or "red".' );
}

/**
 * Returns a localized error string that can be displayed next to length (padding, border width)
 * fields that have an invalid value.
 *
 * @param {module:utils/locale~Locale#t} t The "t" function provided by the editor
 * that is used to localize strings.
 * @returns {String}
 */
function getLocalizedLengthErrorText( t ) {
	return t( 'The value is invalid. Try "10px" or "2em" or simply "2".' );
}

/**
 * Returns `true` when the passed value is an empty string or a valid CSS color expression.
 * Otherwise, `false` is returned.
 *
 * See {@link module:engine/view/styles/utils~isColor}.
 *
 * @param {String} value
 * @returns {Boolean}
 */
function colorFieldValidator( value ) {
	value = value.trim();

	return isEmpty( value ) || isColor( value );
}

/**
 * Returns `true` when the passed value is an empty string, a number without a unit or a valid CSS length expression.
 * Otherwise, `false` is returned.
 *
 * See {@link module:engine/view/styles/utils~isLength}.
 * See {@link module:engine/view/styles/utils~isPercentage}.
 *
 * @param {String} value
 * @returns {Boolean}
 */
function lengthFieldValidator( value ) {
	value = value.trim();

	return isEmpty( value ) || isNumberString( value ) || isLength( value ) || isPercentage( value );
}

/**
 * Returns `true` when the passed value is an empty string, a number without a unit or a valid CSS length expression.
 * Otherwise, `false` is returned.
 *
 * See {@link module:engine/view/styles/utils~isLength}.
 *
 * @param {String} value
 * @returns {Boolean}
 */
function lineWidthFieldValidator( value ) {
	value = value.trim();

	return isEmpty( value ) || isNumberString( value ) || isLength( value );
}

/**
 * Generates item definitions for a UI dropdown that allows changing the border style of a table or a table cell.
 *
 * @param {module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView|
 * module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView} view
 * @param {String} defaultStyle The default border.
 * @returns {Iterable.<module:ui/dropdown/utils~ListDropdownItemDefinition>}
 */
function getBorderStyleDefinitions( view, defaultStyle ) {
	const itemDefinitions = new Collection();
	const styleLabels = getBorderStyleLabels( view.t );

	for ( const style in styleLabels ) {
		const definition = {
			type: 'button',
			model: new Model( {
				_borderStyleValue: style,
				label: styleLabels[ style ],
				withText: true
			} )
		};

		if ( style === 'none' ) {
			definition.model.bind( 'isOn' ).to( view, 'borderStyle', value => {
				if ( defaultStyle === 'none' ) {
					return !value;
				}

				return value === style;
			} );
		} else {
			definition.model.bind( 'isOn' ).to( view, 'borderStyle', value => {
				return value === style;
			} );
		}

		itemDefinitions.add( definition );
	}

	return itemDefinitions;
}

/**
 * A helper that fills a toolbar with buttons that:
 *
 * * have some labels,
 * * have some icons,
 * * set a certain UI view property value upon execution.
 *
 * @param {Object} options
 * @param {module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView|
 * module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView} options.view
 * @param {Array.<String>} options.icons
 * @param {module:ui/toolbar/toolbarview~ToolbarView} options.toolbar
 * @param {Object.<String,String>} labels
 * @param {String} propertyName
 * @param {Function} nameToValue A function that maps a button name to a value. By default names are the same as values.
 */
function fillToolbar( options ) {
	const { view, icons, toolbar, labels, propertyName, nameToValue, defaultValue } = options;
	for ( const name in labels ) {
		const button = new ButtonView( view.locale );

		button.set( {
			label: labels[ name ],
			icon: icons[ name ],
			tooltip: labels[ name ]
		} );

		// If specified the `nameToValue()` callback, map the value based on the option's name.
		const buttonValue = nameToValue ? nameToValue( name ) : name;

		button.bind( 'isOn' ).to( view, propertyName, value => {
			// `value` comes from `view[ propertyName ]`.
			let valueToCompare = value;

			// If it's empty, and the `defaultValue` is specified, use it instead.
			if ( value === '' && defaultValue ) {
				valueToCompare = defaultValue;
			}

			return buttonValue === valueToCompare;
		} );

		button.on( 'execute', () => {
			view[ propertyName ] = buttonValue;
		} );

		toolbar.items.add( button );
	}
}

/**
 * A default color palette used by various user interfaces related to tables, for instance,
 * by {@link module:table/tablecellproperties/tablecellpropertiesui~TableCellPropertiesUI} or
 * {@link module:table/tableproperties/tablepropertiesui~TablePropertiesUI}.
 *
 * The color palette follows the {@link module:table/table~TableColorConfig table color configuration format}
 * and contains the following color definitions:
 *
 *		const defaultColors = [
 *			{
 *				color: 'hsl(0, 0%, 0%)',
 *				label: 'Black'
 *			},
 *			{
 *				color: 'hsl(0, 0%, 30%)',
 *				label: 'Dim grey'
 *			},
 *			{
 *				color: 'hsl(0, 0%, 60%)',
 *				label: 'Grey'
 *			},
 *			{
 *				color: 'hsl(0, 0%, 90%)',
 *				label: 'Light grey'
 *			},
 *			{
 *				color: 'hsl(0, 0%, 100%)',
 *				label: 'White',
 *				hasBorder: true
 *			},
 *			{
 *				color: 'hsl(0, 75%, 60%)',
 *				label: 'Red'
 *			},
 *			{
 *				color: 'hsl(30, 75%, 60%)',
 *				label: 'Orange'
 *			},
 *			{
 *				color: 'hsl(60, 75%, 60%)',
 *				label: 'Yellow'
 *			},
 *			{
 *				color: 'hsl(90, 75%, 60%)',
 *				label: 'Light green'
 *			},
 *			{
 *				color: 'hsl(120, 75%, 60%)',
 *				label: 'Green'
 *			},
 *			{
 *				color: 'hsl(150, 75%, 60%)',
 *				label: 'Aquamarine'
 *			},
 *			{
 *				color: 'hsl(180, 75%, 60%)',
 *				label: 'Turquoise'
 *			},
 *			{
 *				color: 'hsl(210, 75%, 60%)',
 *				label: 'Light blue'
 *			},
 *			{
 *				color: 'hsl(240, 75%, 60%)',
 *				label: 'Blue'
 *			},
 *			{
 *				color: 'hsl(270, 75%, 60%)',
 *				label: 'Purple'
 *			}
 *		];
 */
const defaultColors = [
	{
		color: 'hsl(0, 0%, 0%)',
		label: 'Black'
	},
	{
		color: 'hsl(0, 0%, 30%)',
		label: 'Dim grey'
	},
	{
		color: 'hsl(0, 0%, 60%)',
		label: 'Grey'
	},
	{
		color: 'hsl(0, 0%, 90%)',
		label: 'Light grey'
	},
	{
		color: 'hsl(0, 0%, 100%)',
		label: 'White',
		hasBorder: true
	},
	{
		color: 'hsl(0, 75%, 60%)',
		label: 'Red'
	},
	{
		color: 'hsl(30, 75%, 60%)',
		label: 'Orange'
	},
	{
		color: 'hsl(60, 75%, 60%)',
		label: 'Yellow'
	},
	{
		color: 'hsl(90, 75%, 60%)',
		label: 'Light green'
	},
	{
		color: 'hsl(120, 75%, 60%)',
		label: 'Green'
	},
	{
		color: 'hsl(150, 75%, 60%)',
		label: 'Aquamarine'
	},
	{
		color: 'hsl(180, 75%, 60%)',
		label: 'Turquoise'
	},
	{
		color: 'hsl(210, 75%, 60%)',
		label: 'Light blue'
	},
	{
		color: 'hsl(240, 75%, 60%)',
		label: 'Blue'
	},
	{
		color: 'hsl(270, 75%, 60%)',
		label: 'Purple'
	}
];

/**
 * Returns a creator for a color input with a label.
 *
 * For given options, it returns a function that creates an instance of a
 * {@link module:table/ui/colorinputview~ColorInputView color input} logically related to
 * a {@link module:ui/labeledfield/labeledfieldview~LabeledFieldView labeled view} in the DOM.
 *
 * The helper does the following:
 *
 * * It sets the color input `id` and `ariaDescribedById` attributes.
 * * It binds the color input `isReadOnly` to the labeled view.
 * * It binds the color input `hasError` to the labeled view.
 * * It enables a logic that cleans up the error when the user starts typing in the color input.
 *
 * Usage:
 *
 *		const colorInputCreator = getLabeledColorInputCreator( {
 *			colorConfig: [ ... ],
 *			columns: 3,
 *		} );
 *
 *		const labeledInputView = new LabeledFieldView( locale, colorInputCreator );
 *		console.log( labeledInputView.view ); // A color input instance.
 *
 * @private
 * @param options Color input options.
 * @param {module:table/table~TableColorConfig} options.colorConfig The configuration of the color palette
 * displayed in the input's dropdown.
 * @param {Number} options.columns The configuration of the number of columns the color palette consists of
 * in the input's dropdown.
 * @param {String} [options.defaultColorValue] If specified, the color input view will replace the "Remove color" button with
 * the "Restore default" button. Instead of clearing the input field, the default color value will be set.
 * @returns {Function}
 */
function getLabeledColorInputCreator( options ) {
	return ( labeledFieldView, viewUid, statusUid ) => {
		const inputView = new ColorInputView( labeledFieldView.locale, {
			colorDefinitions: colorConfigToColorGridDefinitions( options.colorConfig ),
			columns: options.columns,
			defaultColorValue: options.defaultColorValue
		} );

		inputView.set( {
			id: viewUid,
			ariaDescribedById: statusUid
		} );

		inputView.bind( 'isReadOnly' ).to( labeledFieldView, 'isEnabled', value => !value );
		inputView.bind( 'hasError' ).to( labeledFieldView, 'errorText', value => !!value );

		inputView.on( 'input', () => {
			// UX: Make the error text disappear and disable the error indicator as the user
			// starts fixing the errors.
			labeledFieldView.errorText = null;
		} );

		labeledFieldView.bind( 'isEmpty', 'isFocused' ).to( inputView );

		return inputView;
	};
}

// A simple helper method to detect number strings.
// I allows full number notation, so omitting 0 is not allowed:
function isNumberString( value ) {
	const parsedValue = parseFloat( value );

	return !Number.isNaN( parsedValue ) && value === String( parsedValue );
}

// @param {Array.<Object>} colorConfig
// @returns {Array.<module:ui/colorgrid/colorgrid~ColorDefinition>}
function colorConfigToColorGridDefinitions( colorConfig ) {
	return colorConfig.map( item => ( {
		color: item.model,
		label: item.label,
		options: {
			hasBorder: item.hasBorder
		}
	} ) );
}var css_248z$g = ".ck.ck-form__row{display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-between}.ck.ck-form__row>:not(.ck-label){flex-grow:1}.ck.ck-form__row.ck-table-form__action-row .ck-button-cancel,.ck.ck-form__row.ck-table-form__action-row .ck-button-save{justify-content:center}.ck.ck-form__row{padding:var(--ck-spacing-standard) var(--ck-spacing-large) 0}[dir=ltr] .ck.ck-form__row>:not(.ck-label)+*{margin-left:var(--ck-spacing-large)}[dir=rtl] .ck.ck-form__row>:not(.ck-label)+*{margin-right:var(--ck-spacing-large)}.ck.ck-form__row>.ck-label{min-width:100%;width:100%}.ck.ck-form__row.ck-table-form__action-row{margin-top:var(--ck-spacing-large)}.ck.ck-form__row.ck-table-form__action-row .ck-button .ck-button__label{color:var(--ck-color-text)}";
styleInject(css_248z$g);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The class representing a single row in a complex form,
 * used by {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView}.
 *
 * **Note**: For now this class is private. When more use cases arrive (beyond ckeditor5-table),
 * it will become a component in ckeditor5-ui.
 *
 * @private
 * @extends module:ui/view~View
 */
class FormRowView extends View {
	/**
	 * Creates an instance of the form row class.
	 *
	 * @param {module:utils/locale~Locale} locale The locale instance.
	 * @param {Object} options
	 * @param {Array.<module:ui/view~View>} options.children
	 * @param {String} [options.class]
	 * @param {module:ui/view~View} [options.labelView] When passed, the row gets the `group` and `aria-labelledby`
	 * DOM attributes and gets described by the label.
	 */
	constructor( locale, options = {} ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * An additional CSS class added to the {@link #element}.
		 *
		 * @observable
		 * @member {String} #class
		 */
		this.set( 'class', options.class || null );

		/**
		 * A collection of row items (buttons, dropdowns, etc.).
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.children = this.createCollection();

		if ( options.children ) {
			options.children.forEach( child => this.children.add( child ) );
		}

		/**
		 * The role property reflected by the `role` DOM attribute of the {@link #element}.
		 *
		 * **Note**: Used only when a `labelView` is passed to constructor `options`.
		 *
		 * @private
		 * @observable
		 * @member {String} #role
		 */
		this.set( '_role', null );

		/**
		 * The ARIA property reflected by the `aria-labelledby` DOM attribute of the {@link #element}.
		 *
		 * **Note**: Used only when a `labelView` is passed to constructor `options`.
		 *
		 * @private
		 * @observable
		 * @member {String} #ariaLabelledBy
		 */
		this.set( '_ariaLabelledBy', null );

		if ( options.labelView ) {
			this.set( {
				_role: 'group',
				_ariaLabelledBy: options.labelView.id
			} );
		}

		this.setTemplate( {
			tag: 'div',
			attributes: {
				class: [
					'ck',
					'ck-form__row',
					bind.to( 'class' )
				],
				role: bind.to( '_role' ),
				'aria-labelledby': bind.to( '_ariaLabelledBy' )
			},
			children: this.children
		} );
	}
}var css_248z$f = ".ck.ck-form{padding:0 0 var(--ck-spacing-large)}.ck.ck-form:focus{outline:none}.ck.ck-form .ck.ck-input-text{min-width:100%;width:0}.ck.ck-form .ck.ck-dropdown{min-width:100%}.ck.ck-form .ck.ck-dropdown .ck-dropdown__button:not(:focus){border:1px solid var(--ck-color-base-border)}.ck.ck-form .ck.ck-dropdown .ck-dropdown__button .ck-button__label{width:100%}";
styleInject(css_248z$f);var css_248z$e = ".ck.ck-table-form .ck-form__row.ck-table-form__background-row,.ck.ck-table-form .ck-form__row.ck-table-form__border-row{flex-wrap:wrap}.ck.ck-table-form .ck-form__row.ck-table-form__dimensions-row{align-items:center;flex-wrap:wrap}.ck.ck-table-form .ck-form__row.ck-table-form__dimensions-row .ck-labeled-field-view{align-items:center;display:flex;flex-direction:column-reverse}.ck.ck-table-form .ck-form__row.ck-table-form__dimensions-row .ck-labeled-field-view .ck.ck-dropdown,.ck.ck-table-form .ck-form__row.ck-table-form__dimensions-row .ck-table-form__dimension-operator{flex-grow:0}.ck.ck-table-form .ck.ck-labeled-field-view{position:relative}.ck.ck-table-form .ck.ck-labeled-field-view .ck.ck-labeled-field-view__status{bottom:calc(var(--ck-table-properties-error-arrow-size)*-1);left:50%;position:absolute;transform:translate(-50%,100%);z-index:1}.ck.ck-table-form .ck.ck-labeled-field-view .ck.ck-labeled-field-view__status:after{content:\"\";left:50%;position:absolute;top:calc(var(--ck-table-properties-error-arrow-size)*-1);transform:translateX(-50%)}:root{--ck-table-properties-error-arrow-size:6px;--ck-table-properties-min-error-width:150px}.ck.ck-table-form .ck-form__row.ck-table-form__border-row .ck-labeled-field-view>.ck-label{font-size:var(--ck-font-size-tiny);text-align:center}.ck.ck-table-form .ck-form__row.ck-table-form__border-row .ck-table-form__border-style,.ck.ck-table-form .ck-form__row.ck-table-form__border-row .ck-table-form__border-width{max-width:80px;min-width:80px;width:80px}.ck.ck-table-form .ck-form__row.ck-table-form__dimensions-row{padding:0}.ck.ck-table-form .ck-form__row.ck-table-form__dimensions-row .ck-table-form__dimensions-row__height,.ck.ck-table-form .ck-form__row.ck-table-form__dimensions-row .ck-table-form__dimensions-row__width{margin:0}.ck.ck-table-form .ck-form__row.ck-table-form__dimensions-row .ck-table-form__dimension-operator{align-self:flex-end;display:inline-block;height:var(--ck-ui-component-min-height);line-height:var(--ck-ui-component-min-height);margin:0 var(--ck-spacing-small)}.ck.ck-table-form .ck.ck-labeled-field-view{padding-top:var(--ck-spacing-standard)}.ck.ck-table-form .ck.ck-labeled-field-view .ck.ck-labeled-field-view__status{border-radius:0}.ck-rounded-corners .ck.ck-table-form .ck.ck-labeled-field-view .ck.ck-labeled-field-view__status,.ck.ck-table-form .ck.ck-labeled-field-view .ck.ck-labeled-field-view__status.ck-rounded-corners{border-radius:var(--ck-border-radius)}.ck.ck-table-form .ck.ck-labeled-field-view .ck.ck-labeled-field-view__status{background:var(--ck-color-base-error);color:var(--ck-color-base-background);min-width:var(--ck-table-properties-min-error-width);padding:var(--ck-spacing-small) var(--ck-spacing-medium);text-align:center}.ck.ck-table-form .ck.ck-labeled-field-view .ck.ck-labeled-field-view__status:after{border-color:transparent transparent var(--ck-color-base-error) transparent;border-style:solid;border-width:0 var(--ck-table-properties-error-arrow-size) var(--ck-table-properties-error-arrow-size) var(--ck-table-properties-error-arrow-size)}.ck.ck-table-form .ck.ck-labeled-field-view .ck.ck-labeled-field-view__status{animation:ck-table-form-labeled-view-status-appear .15s ease both}.ck.ck-table-form .ck.ck-labeled-field-view .ck-input.ck-error:not(:focus)+.ck.ck-labeled-field-view__status{display:none}@keyframes ck-table-form-labeled-view-status-appear{0%{opacity:0}to{opacity:1}}";
styleInject(css_248z$e);var css_248z$d = ".ck.ck-table-properties-form .ck-form__row.ck-table-properties-form__alignment-row{align-content:baseline;flex-basis:0;flex-wrap:wrap}.ck.ck-table-properties-form .ck-form__row.ck-table-properties-form__alignment-row .ck.ck-toolbar .ck-toolbar__items{flex-wrap:nowrap}.ck.ck-table-properties-form{width:320px}.ck.ck-table-properties-form .ck-form__row.ck-table-properties-form__alignment-row{align-self:flex-end;padding:0}.ck.ck-table-properties-form .ck-form__row.ck-table-properties-form__alignment-row .ck.ck-toolbar{background:none;margin-top:var(--ck-spacing-standard)}.ck.ck-table-properties-form .ck-form__row.ck-table-properties-form__alignment-row .ck.ck-toolbar .ck-toolbar__items>*{width:40px}";
styleInject(css_248z$d);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const ALIGNMENT_ICONS$1 = {
	left: icons.objectLeft,
	center: icons.objectCenter,
	right: icons.objectRight
};

/**
 * The class representing a table properties form, allowing users to customize
 * certain style aspects of a table, for instance, border, background color, alignment, etc..
 *
 * @extends module:ui/view~View
 */
class TablePropertiesView extends View {
	/**
	 * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
	 * @param {Object} options Additional configuration of the view.
	 * @param {module:table/table~TableColorConfig} options.borderColors A configuration of the border
	 * color palette used by the
	 * {@link module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView#borderColorInput}.
	 * @param {module:table/table~TableColorConfig} options.backgroundColors A configuration of the background
	 * color palette used by the
	 * {@link module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView#backgroundInput}.
	 * @param {module:table/tableproperties~TablePropertiesOptions} options.defaultTableProperties The default table properties.
	 */
	constructor( locale, options ) {
		super( locale );

		this.set( {
			/**
			 * The value of the border style.
			 *
			 * @observable
			 * @default ''
			 * @member #borderStyle
			 */
			borderStyle: '',

			/**
			 * The value of the border width style.
			 *
			 * @observable
			 * @default ''
			 * @member #borderWidth
			 */
			borderWidth: '',

			/**
			 * The value of the border color style.
			 *
			 * @observable
			 * @default ''
			 * @member #borderColor
			 */
			borderColor: '',

			/**
			 * The value of the background color style.
			 *
			 * @observable
			 * @default ''
			 * @member #backgroundColor
			 */
			backgroundColor: '',

			/**
			 * The value of the table width style.
			 *
			 * @observable
			 * @default ''
			 * @member #width
			 */
			width: '',

			/**
			 * The value of the table height style.
			 *
			 * @observable
			 * @default ''
			 * @member #height
			 */
			height: '',

			/**
			 * The value of the table alignment style.
			 *
			 * @observable
			 * @default ''
			 * @member #alignment
			 */
			alignment: ''
		} );

		/**
		 * Options passed to the view. See {@link #constructor} to learn more.
		 *
		 * @protected
		 * @member {Object}
		 */
		this.options = options;

		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
		const { backgroundRowLabel, backgroundInput } = this._createBackgroundFields();
		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
		const { alignmentToolbar, alignmentLabel } = this._createAlignmentFields();

		/**
		 * Tracks information about the DOM focus in the form.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
		 *
		 * @readonly
		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
		 */
		this.keystrokes = new KeystrokeHandler();

		/**
		 * A collection of child views in the form.
		 *
		 * @readonly
		 * @type {module:ui/viewcollection~ViewCollection}
		 */
		this.children = this.createCollection();

		/**
		 * A dropdown that allows selecting the style of the table border.
		 *
		 * @readonly
		 * @member {module:ui/dropdown/dropdownview~DropdownView}
		 */
		this.borderStyleDropdown = borderStyleDropdown;

		/**
		 * An input that allows specifying the width of the table border.
		 *
		 * @readonly
		 * @member {module:ui/inputtext/inputtextview~InputTextView}
		 */
		this.borderWidthInput = borderWidthInput;

		/**
		 * An input that allows specifying the color of the table border.
		 *
		 * @readonly
		 * @member {module:table/ui/colorinputview~ColorInputView}
		 */
		this.borderColorInput = borderColorInput;

		/**
		 * An input that allows specifying the table background color.
		 *
		 * @readonly
		 * @member {module:table/ui/colorinputview~ColorInputView}
		 */
		this.backgroundInput = backgroundInput;

		/**
		 * An input that allows specifying the table width.
		 *
		 * @readonly
		 * @member {module:ui/inputtext/inputtextview~InputTextView}
		 */
		this.widthInput = widthInput;

		/**
		 * An input that allows specifying the table height.
		 *
		 * @readonly
		 * @member {module:ui/inputtext/inputtextview~InputTextView}
		 */
		this.heightInput = heightInput;

		/**
		 * A toolbar with buttons that allow changing the alignment of an entire table.
		 * @readonly
		 * @member {module:ui/toolbar/toolbarview~ToolbarView}
		 */
		this.alignmentToolbar = alignmentToolbar;

		// Defer creating to make sure other fields are present and the Save button can
		// bind its #isEnabled to their error messages so there's no way to save unless all
		// fields are valid.
		const { saveButtonView, cancelButtonView } = this._createActionButtons();

		/**
		 * The "Save" button view.
		 *
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this.saveButtonView = saveButtonView;

		/**
		 * The "Cancel" button view.
		 *
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this.cancelButtonView = cancelButtonView;

		/**
		 * A collection of views that can be focused in the form.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this._focusables = new ViewCollection();

		/**
		 * Helps cycling over {@link #_focusables} in the form.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/focuscycler~FocusCycler}
		 */
		this._focusCycler = new FocusCycler( {
			focusables: this._focusables,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				// Navigate form fields backwards using the Shift + Tab keystroke.
				focusPrevious: 'shift + tab',

				// Navigate form fields forwards using the Tab key.
				focusNext: 'tab'
			}
		} );

		// Form header.
		this.children.add( new FormHeaderView( locale, {
			label: this.t( 'Table properties' )
		} ) );

		// Border row.
		this.children.add( new FormRowView( locale, {
			labelView: borderRowLabel,
			children: [
				borderRowLabel,
				borderStyleDropdown,
				borderColorInput,
				borderWidthInput
			],
			class: 'ck-table-form__border-row'
		} ) );

		// Background row.
		this.children.add( new FormRowView( locale, {
			labelView: backgroundRowLabel,
			children: [
				backgroundRowLabel,
				backgroundInput
			],
			class: 'ck-table-form__background-row'
		} ) );

		this.children.add( new FormRowView( locale, {
			children: [
				// Dimensions row.
				new FormRowView( locale, {
					labelView: dimensionsLabel,
					children: [
						dimensionsLabel,
						widthInput,
						operatorLabel,
						heightInput
					],
					class: 'ck-table-form__dimensions-row'
				} ),
				// Alignment row.
				new FormRowView( locale, {
					labelView: alignmentLabel,
					children: [
						alignmentLabel,
						alignmentToolbar
					],
					class: 'ck-table-properties-form__alignment-row'
				} )
			]
		} ) );

		// Action row.
		this.children.add( new FormRowView( locale, {
			children: [
				this.saveButtonView,
				this.cancelButtonView
			],
			class: 'ck-table-form__action-row'
		} ) );

		this.setTemplate( {
			tag: 'form',
			attributes: {
				class: [
					'ck',
					'ck-form',
					'ck-table-form',
					'ck-table-properties-form'
				],
				// https://github.com/ckeditor/ckeditor5-link/issues/90
				tabindex: '-1'
			},
			children: this.children
		} );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		// Enable the "submit" event for this view. It can be triggered by the #saveButtonView
		// which is of the "submit" DOM "type".
		submitHandler( {
			view: this
		} );

		[
			this.borderStyleDropdown,
			this.borderColorInput,
			this.borderWidthInput,
			this.backgroundInput,
			this.widthInput,
			this.heightInput,
			this.alignmentToolbar,
			this.saveButtonView,
			this.cancelButtonView
		].forEach( view => {
			// Register the view as focusable.
			this._focusables.add( view );

			// Register the view in the focus tracker.
			this.focusTracker.add( view.element );
		} );

		// Mainly for closing using "Esc" and navigation using "Tab".
		this.keystrokes.listenTo( this.element );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}

	/**
	 * Focuses the fist focusable field in the form.
	 */
	focus() {
		this._focusCycler.focusFirst();
	}

	/**
	 * Creates the following form fields:
	 *
	 * * {@link #borderStyleDropdown},
	 * * {@link #borderWidthInput},
	 * * {@link #borderColorInput}.
	 *
	 * @private
	 * @returns {Object.<String,module:ui/view~View>}
	 */
	_createBorderFields() {
		const defaultTableProperties = this.options.defaultTableProperties;
		const defaultBorder = {
			style: defaultTableProperties.borderStyle,
			width: defaultTableProperties.borderWidth,
			color: defaultTableProperties.borderColor
		};

		const colorInputCreator = getLabeledColorInputCreator( {
			colorConfig: this.options.borderColors,
			columns: 5,
			defaultColorValue: defaultBorder.color
		} );
		const locale = this.locale;
		const t = this.t;

		// -- Group label ---------------------------------------------

		const borderRowLabel = new LabelView( locale );
		borderRowLabel.text = t( 'Border' );

		// -- Style ---------------------------------------------------

		const styleLabels = getBorderStyleLabels( this.t );
		const borderStyleDropdown = new LabeledFieldView( locale, createLabeledDropdown );
		borderStyleDropdown.set( {
			label: t( 'Style' ),
			class: 'ck-table-form__border-style'
		} );

		borderStyleDropdown.fieldView.buttonView.set( {
			isOn: false,
			withText: true,
			tooltip: t( 'Style' )
		} );

		borderStyleDropdown.fieldView.buttonView.bind( 'label' ).to( this, 'borderStyle', value => {
			return styleLabels[ value ? value : 'none' ];
		} );

		borderStyleDropdown.fieldView.on( 'execute', evt => {
			this.borderStyle = evt.source._borderStyleValue;
		} );

		borderStyleDropdown.bind( 'isEmpty' ).to( this, 'borderStyle', value => !value );

		addListToDropdown( borderStyleDropdown.fieldView, getBorderStyleDefinitions( this, defaultBorder.style ) );

		// -- Width ---------------------------------------------------

		const borderWidthInput = new LabeledFieldView( locale, createLabeledInputText );

		borderWidthInput.set( {
			label: t( 'Width' ),
			class: 'ck-table-form__border-width'
		} );

		borderWidthInput.fieldView.bind( 'value' ).to( this, 'borderWidth' );
		borderWidthInput.bind( 'isEnabled' ).to( this, 'borderStyle', isBorderStyleSet$1 );
		borderWidthInput.fieldView.on( 'input', () => {
			this.borderWidth = borderWidthInput.fieldView.element.value;
		} );

		// -- Color ---------------------------------------------------

		const borderColorInput = new LabeledFieldView( locale, colorInputCreator );

		borderColorInput.set( {
			label: t( 'Color' ),
			class: 'ck-table-form__border-color'
		} );

		borderColorInput.fieldView.bind( 'value' ).to( this, 'borderColor' );
		borderColorInput.bind( 'isEnabled' ).to( this, 'borderStyle', isBorderStyleSet$1 );

		borderColorInput.fieldView.on( 'input', () => {
			this.borderColor = borderColorInput.fieldView.value;
		} );

		// Reset the border color and width fields depending on the `border-style` value.
		this.on( 'change:borderStyle', ( evt, name, newValue, oldValue ) => {
			// When removing the border (`border-style:none`), clear the remaining `border-*` properties.
			// See: https://github.com/ckeditor/ckeditor5/issues/6227.
			if ( !isBorderStyleSet$1( newValue ) ) {
				this.borderColor = '';
				this.borderWidth = '';
			}

			// When setting the `border-style` from `none`, set the default `border-color` and `border-width` properties.
			if ( !isBorderStyleSet$1( oldValue ) ) {
				this.borderColor = defaultBorder.color;
				this.borderWidth = defaultBorder.width;
			}
		} );

		return {
			borderRowLabel,
			borderStyleDropdown,
			borderColorInput,
			borderWidthInput
		};
	}

	/**
	 * Creates the following form fields:
	 *
	 * * {@link #backgroundInput}.
	 *
	 * @private
	 * @returns {Object.<String,module:ui/view~View>}
	 */
	_createBackgroundFields() {
		const locale = this.locale;
		const t = this.t;

		// -- Group label ---------------------------------------------

		const backgroundRowLabel = new LabelView( locale );
		backgroundRowLabel.text = t( 'Background' );

		// -- Background color input -----------------------------------

		const backgroundInputCreator = getLabeledColorInputCreator( {
			colorConfig: this.options.backgroundColors,
			columns: 5,
			defaultColorValue: this.options.defaultTableProperties.backgroundColor
		} );

		const backgroundInput = new LabeledFieldView( locale, backgroundInputCreator );

		backgroundInput.set( {
			label: t( 'Color' ),
			class: 'ck-table-properties-form__background'
		} );

		backgroundInput.fieldView.bind( 'value' ).to( this, 'backgroundColor' );
		backgroundInput.fieldView.on( 'input', () => {
			this.backgroundColor = backgroundInput.fieldView.value;
		} );

		return {
			backgroundRowLabel,
			backgroundInput
		};
	}

	/**
	 * Creates the following form fields:
	 *
	 * * {@link #widthInput}.
	 * * {@link #heightInput}.
	 *
	 * @private
	 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
	 */
	_createDimensionFields() {
		const locale = this.locale;
		const t = this.t;

		// -- Label ---------------------------------------------------

		const dimensionsLabel = new LabelView( locale );
		dimensionsLabel.text = t( 'Dimensions' );

		// -- Width ---------------------------------------------------

		const widthInput = new LabeledFieldView( locale, createLabeledInputText );

		widthInput.set( {
			label: t( 'Width' ),
			class: 'ck-table-form__dimensions-row__width'
		} );

		widthInput.fieldView.bind( 'value' ).to( this, 'width' );
		widthInput.fieldView.on( 'input', () => {
			this.width = widthInput.fieldView.element.value;
		} );

		// -- Operator ---------------------------------------------------

		const operatorLabel = new View( locale );
		operatorLabel.setTemplate( {
			tag: 'span',
			attributes: {
				class: [
					'ck-table-form__dimension-operator'
				]
			},
			children: [
				{ text: '×' }
			]
		} );

		// -- Height ---------------------------------------------------

		const heightInput = new LabeledFieldView( locale, createLabeledInputText );

		heightInput.set( {
			label: t( 'Height' ),
			class: 'ck-table-form__dimensions-row__height'
		} );

		heightInput.fieldView.bind( 'value' ).to( this, 'height' );
		heightInput.fieldView.on( 'input', () => {
			this.height = heightInput.fieldView.element.value;
		} );

		return {
			dimensionsLabel,
			widthInput,
			operatorLabel,
			heightInput
		};
	}

	/**
	 * Creates the following form fields:
	 *
	 * * {@link #alignmentToolbar},
	 *
	 * @private
	 * @returns {Object.<String,module:ui/view~View>}
	 */
	_createAlignmentFields() {
		const locale = this.locale;
		const t = this.t;

		// -- Label ---------------------------------------------------

		const alignmentLabel = new LabelView( locale );
		alignmentLabel.text = t( 'Alignment' );

		// -- Toolbar ---------------------------------------------------

		const alignmentToolbar = new ToolbarView( locale );
		alignmentToolbar.set( {
			isCompact: true,
			ariaLabel: t( 'Table alignment toolbar' )
		} );

		fillToolbar( {
			view: this,
			icons: ALIGNMENT_ICONS$1,
			toolbar: alignmentToolbar,
			labels: this._alignmentLabels,
			propertyName: 'alignment',
			defaultValue: this.options.defaultTableProperties.alignment
		} );

		return {
			alignmentLabel,
			alignmentToolbar
		};
	}

	/**
	 * Creates the following form controls:
	 *
	 * * {@link #saveButtonView},
	 * * {@link #cancelButtonView}.
	 *
	 * @private
	 * @returns {Object.<String,module:ui/view~View>}
	 */
	_createActionButtons() {
		const locale = this.locale;
		const t = this.t;

		const saveButtonView = new ButtonView( locale );
		const cancelButtonView = new ButtonView( locale );
		const fieldsThatShouldValidateToSave = [
			this.borderWidthInput,
			this.borderColorInput,
			this.backgroundInput,
			this.widthInput,
			this.heightInput
		];

		saveButtonView.set( {
			label: t( 'Save' ),
			icon: icons.check,
			class: 'ck-button-save',
			type: 'submit',
			withText: true
		} );

		saveButtonView.bind( 'isEnabled' ).toMany( fieldsThatShouldValidateToSave, 'errorText', ( ...errorTexts ) => {
			return errorTexts.every( errorText => !errorText );
		} );

		cancelButtonView.set( {
			label: t( 'Cancel' ),
			icon: icons.cancel,
			class: 'ck-button-cancel',
			withText: true
		} );

		cancelButtonView.delegate( 'execute' ).to( this, 'cancel' );

		return {
			saveButtonView, cancelButtonView
		};
	}

	/**
	 * Provides localized labels for {@link #alignmentToolbar} buttons.
	 *
	 * @private
	 * @type {Object.<String,String>}
	 */
	get _alignmentLabels() {
		const locale = this.locale;
		const t = this.t;

		const left = t( 'Align table to the left' );
		const center = t( 'Center table' );
		const right = t( 'Align table to the right' );

		// Returns object with a proper order of labels.
		if ( locale.uiLanguageDirection === 'rtl' ) {
			return { right, center, left };
		} else {
			return { left, center, right };
		}
	}
}

function isBorderStyleSet$1( value ) {
	return value !== 'none';
}var tableProperties = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M8 2v5h4V2h1v5h5v1h-5v4h.021l-.172.351-1.916.28-.151.027c-.287.063-.54.182-.755.341L8 13v5H7v-5H2v-1h5V8H2V7h5V2h1zm4 6H8v4h4V8z\" opacity=\".6\"/><path d=\"m15.5 11.5 1.323 2.68 2.957.43-2.14 2.085.505 2.946L15.5 18.25l-2.645 1.39.505-2.945-2.14-2.086 2.957-.43L15.5 11.5zM17 1a2 2 0 0 1 2 2v9.475l-.85-.124-.857-1.736a2.048 2.048 0 0 0-.292-.44L17 3H3v14h7.808l.402.392L10.935 19H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h14z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const DEFAULT_BALLOON_POSITIONS = BalloonPanelView.defaultPositions;

const BALLOON_POSITIONS = [
	DEFAULT_BALLOON_POSITIONS.northArrowSouth,
	DEFAULT_BALLOON_POSITIONS.northArrowSouthWest,
	DEFAULT_BALLOON_POSITIONS.northArrowSouthEast,
	DEFAULT_BALLOON_POSITIONS.southArrowNorth,
	DEFAULT_BALLOON_POSITIONS.southArrowNorthWest,
	DEFAULT_BALLOON_POSITIONS.southArrowNorthEast,
	DEFAULT_BALLOON_POSITIONS.viewportStickyNorth
];

/**
 * A helper utility that positions the
 * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} instance
 * with respect to the table in the editor content, if one is selected.
 *
 * @param {module:core/editor/editor~Editor} editor The editor instance.
 * @param {String} target Either "cell" or "table". Determines the target the balloon will
 * be attached to.
 */
function repositionContextualBalloon( editor, target ) {
	const balloon = editor.plugins.get( 'ContextualBalloon' );

	if ( getTableWidgetAncestor( editor.editing.view.document.selection ) ) {
		let position;

		if ( target === 'cell' ) {
			position = getBalloonCellPositionData( editor );
		} else {
			position = getBalloonTablePositionData( editor );
		}

		balloon.updatePosition( position );
	}
}

/**
 * Returns the positioning options that control the geometry of the
 * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} with respect
 * to the selected table in the editor content.
 *
 * @param {module:core/editor/editor~Editor} editor The editor instance.
 * @returns {module:utils/dom/position~Options}
 */
function getBalloonTablePositionData( editor ) {
	const firstPosition = editor.model.document.selection.getFirstPosition();
	const modelTable = firstPosition.findAncestor( 'table' );
	const viewTable = editor.editing.mapper.toViewElement( modelTable );

	return {
		target: editor.editing.view.domConverter.mapViewToDom( viewTable ),
		positions: BALLOON_POSITIONS
	};
}

/**
 * Returns the positioning options that control the geometry of the
 * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} with respect
 * to the selected table cell in the editor content.
 *
 * @param {module:core/editor/editor~Editor} editor The editor instance.
 * @returns {module:utils/dom/position~Options}
 */
function getBalloonCellPositionData( editor ) {
	const mapper = editor.editing.mapper;
	const domConverter = editor.editing.view.domConverter;
	const selection = editor.model.document.selection;

	if ( selection.rangeCount > 1 ) {
		return {
			target: () => createBoundingRect( selection.getRanges(), editor ),
			positions: BALLOON_POSITIONS
		};
	}

	const modelTableCell = getTableCellAtPosition( selection.getFirstPosition() );
	const viewTableCell = mapper.toViewElement( modelTableCell );

	return {
		target: domConverter.mapViewToDom( viewTableCell ),
		positions: BALLOON_POSITIONS
	};
}

// Returns the first selected table cell from a multi-cell or in-cell selection.
//
// @param {module:engine/model/position~Position} position Document position.
// @returns {module:engine/model/element~Element}
function getTableCellAtPosition( position ) {
	const isTableCellSelected = position.nodeAfter && position.nodeAfter.is( 'element', 'tableCell' );

	return isTableCellSelected ? position.nodeAfter : position.findAncestor( 'tableCell' );
}

// Returns bounding rectangle for given model ranges.
//
// @param {Iterable.<module:engine/model/range~Range>} ranges Model ranges that the bounding rect should be returned for.
// @param {module:core/editor/editor~Editor} editor The editor instance.
// @returns {module:utils/dom/rect~Rect}
function createBoundingRect( ranges, editor ) {
	const mapper = editor.editing.mapper;
	const domConverter = editor.editing.view.domConverter;
	const rects = Array.from( ranges ).map( range => {
		const modelTableCell = getTableCellAtPosition( range.start );
		const viewTableCell = mapper.toViewElement( modelTableCell );
		return new Rect( domConverter.mapViewToDom( viewTableCell ) );
	} );

	return Rect.getBoundingRect( rects );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const ERROR_TEXT_TIMEOUT$1 = 500;

// Map of view properties and related commands.
const propertyToCommandMap$1 = {
	borderStyle: 'tableBorderStyle',
	borderColor: 'tableBorderColor',
	borderWidth: 'tableBorderWidth',
	backgroundColor: 'tableBackgroundColor',
	width: 'tableWidth',
	height: 'tableHeight',
	alignment: 'tableAlignment'
};

/**
 * The table properties UI plugin. It introduces the `'tableProperties'` button
 * that opens a form allowing to specify visual styling of an entire table.
 *
 * It uses the
 * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
 *
 * @extends module:core/plugin~Plugin
 */
class TablePropertiesUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ ContextualBalloon ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TablePropertiesUI';
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		editor.config.define( 'table.tableProperties', {
			borderColors: defaultColors,
			backgroundColors: defaultColors
		} );
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		/**
		 * The default table properties.
		 *
		 * @protected
		 * @member {module:table/tableproperties~TablePropertiesOptions}
		 */
		this._defaultTableProperties = getNormalizedDefaultProperties( editor.config.get( 'table.tableProperties.defaultProperties' ), {
			includeAlignmentProperty: true
		} );

		/**
		 * The contextual balloon plugin instance.
		 *
		 * @private
		 * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
		 */
		this._balloon = editor.plugins.get( ContextualBalloon );

		/**
		 * The properties form view displayed inside the balloon.
		 *
		 * @member {module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView}
		 */
		this.view = this._createPropertiesView();

		/**
		 * The batch used to undo all changes made by the form (which are live, as the user types)
		 * when "Cancel" was pressed. Each time the view is shown, a new batch is created.
		 *
		 * @protected
		 * @member {module:engine/model/batch~Batch}
		 */
		this._undoStepBatch = null;

		editor.ui.componentFactory.add( 'tableProperties', locale => {
			const view = new ButtonView( locale );

			view.set( {
				label: t( 'Table properties' ),
				icon: tableProperties,
				tooltip: true
			} );

			this.listenTo( view, 'execute', () => this._showView() );

			const commands = Object.values( propertyToCommandMap$1 )
				.map( commandName => editor.commands.get( commandName ) );

			view.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( ...areEnabled ) => (
				areEnabled.some( isCommandEnabled => isCommandEnabled )
			) );

			return view;
		} );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		// Destroy created UI components as they are not automatically destroyed.
		// See https://github.com/ckeditor/ckeditor5/issues/1341.
		this.view.destroy();
	}

	/**
	 * Creates the {@link module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView} instance.
	 *
	 * @private
	 * @returns {module:table/tableproperties/ui/tablepropertiesview~TablePropertiesView} The table
	 * properties form view instance.
	 */
	_createPropertiesView() {
		const editor = this.editor;
		const config = editor.config.get( 'table.tableProperties' );
		const borderColorsConfig = normalizeColorOptions( config.borderColors );
		const localizedBorderColors = getLocalizedColorOptions( editor.locale, borderColorsConfig );
		const backgroundColorsConfig = normalizeColorOptions( config.backgroundColors );
		const localizedBackgroundColors = getLocalizedColorOptions( editor.locale, backgroundColorsConfig );

		const view = new TablePropertiesView( editor.locale, {
			borderColors: localizedBorderColors,
			backgroundColors: localizedBackgroundColors,
			defaultTableProperties: this._defaultTableProperties
		} );
		const t = editor.t;

		// Render the view so its #element is available for the clickOutsideHandler.
		view.render();

		this.listenTo( view, 'submit', () => {
			this._hideView();
		} );

		this.listenTo( view, 'cancel', () => {
			// https://github.com/ckeditor/ckeditor5/issues/6180
			if ( this._undoStepBatch.operations.length ) {
				editor.execute( 'undo', this._undoStepBatch );
			}

			this._hideView();
		} );

		// Close the balloon on Esc key press.
		view.keystrokes.set( 'Esc', ( data, cancel ) => {
			this._hideView();
			cancel();
		} );

		// Close on click outside of balloon panel element.
		clickOutsideHandler( {
			emitter: view,
			activator: () => this._isViewInBalloon,
			contextElements: [ this._balloon.view.element ],
			callback: () => this._hideView()
		} );

		const colorErrorText = getLocalizedColorErrorText( t );
		const lengthErrorText = getLocalizedLengthErrorText( t );

		// Create the "UI -> editor data" binding.
		// These listeners update the editor data (via table commands) when any observable
		// property of the view has changed. They also validate the value and display errors in the UI
		// when necessary. This makes the view live, which means the changes are
		// visible in the editing as soon as the user types or changes fields' values.
		view.on(
			'change:borderStyle',
			this._getPropertyChangeCallback( 'tableBorderStyle', this._defaultTableProperties.borderStyle )
		);

		view.on( 'change:borderColor', this._getValidatedPropertyChangeCallback( {
			viewField: view.borderColorInput,
			commandName: 'tableBorderColor',
			errorText: colorErrorText,
			validator: colorFieldValidator,
			defaultValue: this._defaultTableProperties.borderColor
		} ) );

		view.on( 'change:borderWidth', this._getValidatedPropertyChangeCallback( {
			viewField: view.borderWidthInput,
			commandName: 'tableBorderWidth',
			errorText: lengthErrorText,
			validator: lineWidthFieldValidator,
			defaultValue: this._defaultTableProperties.borderWidth
		} ) );

		view.on( 'change:backgroundColor', this._getValidatedPropertyChangeCallback( {
			viewField: view.backgroundInput,
			commandName: 'tableBackgroundColor',
			errorText: colorErrorText,
			validator: colorFieldValidator,
			defaultValue: this._defaultTableProperties.backgroundColor
		} ) );

		view.on( 'change:width', this._getValidatedPropertyChangeCallback( {
			viewField: view.widthInput,
			commandName: 'tableWidth',
			errorText: lengthErrorText,
			validator: lengthFieldValidator,
			defaultValue: this._defaultTableProperties.width
		} ) );

		view.on( 'change:height', this._getValidatedPropertyChangeCallback( {
			viewField: view.heightInput,
			commandName: 'tableHeight',
			errorText: lengthErrorText,
			validator: lengthFieldValidator,
			defaultValue: this._defaultTableProperties.height
		} ) );

		view.on(
			'change:alignment',
			this._getPropertyChangeCallback( 'tableAlignment', this._defaultTableProperties.alignment )
		);

		return view;
	}

	/**
	 * In this method the "editor data -> UI" binding is happening.
	 *
	 * When executed, this method obtains selected table property values from various table commands
	 * and passes them to the {@link #view}.
	 *
	 * This way, the UI stays up–to–date with the editor data.
	 *
	 * @private
	 */
	_fillViewFormFromCommandValues() {
		const commands = this.editor.commands;
		const borderStyleCommand = commands.get( 'tableBorderStyle' );

		Object.entries( propertyToCommandMap$1 )
			.map( ( [ property, commandName ] ) => {
				const defaultValue = this._defaultTableProperties[ property ] || '';

				return [ property, commands.get( commandName ).value || defaultValue ];
			} )
			.forEach( ( [ property, value ] ) => {
				// Do not set the `border-color` and `border-width` fields if `border-style:none`.
				if ( ( property === 'borderColor' || property === 'borderWidth' ) && borderStyleCommand.value === 'none' ) {
					return;
				}

				this.view.set( property, value );
			} );
	}

	/**
	 * Shows the {@link #view} in the {@link #_balloon}.
	 *
	 * **Note**: Each time a view is shown, the new {@link #_undoStepBatch} is created that contains
	 * all changes made to the document when the view is visible, allowing a single undo step
	 * for all of them.
	 *
	 * @protected
	 */
	_showView() {
		const editor = this.editor;

		this.listenTo( editor.ui, 'update', () => {
			this._updateView();
		} );

		// Update the view with the model values.
		this._fillViewFormFromCommandValues();

		this._balloon.add( {
			view: this.view,
			position: getBalloonTablePositionData( editor )
		} );

		// Create a new batch. Clicking "Cancel" will undo this batch.
		this._undoStepBatch = editor.model.createBatch();

		// Basic a11y.
		this.view.focus();
	}

	/**
	 * Removes the {@link #view} from the {@link #_balloon}.
	 *
	 * @protected
	 */
	_hideView() {
		const editor = this.editor;

		this.stopListening( editor.ui, 'update' );

		// Blur any input element before removing it from DOM to prevent issues in some browsers.
		// See https://github.com/ckeditor/ckeditor5/issues/1501.
		this.view.saveButtonView.focus();

		this._balloon.remove( this.view );

		// Make sure the focus is not lost in the process by putting it directly
		// into the editing view.
		this.editor.editing.view.focus();
	}

	/**
	 * Repositions the {@link #_balloon} or hides the {@link #view} if a table is no longer selected.
	 *
	 * @protected
	 */
	_updateView() {
		const editor = this.editor;
		const viewDocument = editor.editing.view.document;

		if ( !getTableWidgetAncestor( viewDocument.selection ) ) {
			this._hideView();
		} else if ( this._isViewVisible ) {
			repositionContextualBalloon( editor, 'table' );
		}
	}

	/**
	 * Returns `true` when the {@link #view} is the visible in the {@link #_balloon}.
	 *
	 * @private
	 * @type {Boolean}
	 */
	get _isViewVisible() {
		return this._balloon.visibleView === this.view;
	}

	/**
	 * Returns `true` when the {@link #view} is in the {@link #_balloon}.
	 *
	 * @private
	 * @type {Boolean}
	 */
	get _isViewInBalloon() {
		return this._balloon.hasView( this.view );
	}

	/**
	 * Creates a callback that when executed upon {@link #view view's} property change
	 * executes a related editor command with the new property value.
	 *
	 * If new value will be set to the default value, the command will not be executed.
	 *
	 * @private
	 * @param {String} commandName The command that will be executed.
	 * @param {String} defaultValue The default value of the command.
	 * @returns {Function}
	 */
	_getPropertyChangeCallback( commandName, defaultValue ) {
		return ( evt, propertyName, newValue, oldValue ) => {
			// If the "oldValue" is missing and "newValue" is set to the default value, do not execute the command.
			// It is an initial call (when opening the table properties view).
			if ( !oldValue && defaultValue === newValue ) {
				return;
			}

			this.editor.execute( commandName, {
				value: newValue,
				batch: this._undoStepBatch
			} );
		};
	}

	/**
	 * Creates a callback that when executed upon {@link #view view's} property change:
	 * * executes a related editor command with the new property value if the value is valid,
	 * * or sets the error text next to the invalid field, if the value did not pass the validation.
	 *
	 * @private
	 * @param {Object} options
	 * @param {String} options.commandName
	 * @param {module:ui/view~View} options.viewField
	 * @param {Function} options.validator
	 * @param {String} options.errorText
	 * @param {String} options.defaultValue
	 * @returns {Function}
	 */
	_getValidatedPropertyChangeCallback( options ) {
		const { commandName, viewField, validator, errorText, defaultValue } = options;
		const setErrorTextDebounced = debounce( () => {
			viewField.errorText = errorText;
		}, ERROR_TEXT_TIMEOUT$1 );

		return ( evt, propertyName, newValue, oldValue ) => {
			setErrorTextDebounced.cancel();

			// If the "oldValue" is missing and "newValue" is set to the default value, do not execute the command.
			// It is an initial call (when opening the table properties view).
			if ( !oldValue && defaultValue === newValue ) {
				return;
			}

			if ( validator( newValue ) ) {
				this.editor.execute( commandName, {
					value: newValue,
					batch: this._undoStepBatch
				} );

				viewField.errorText = null;
			} else {
				setErrorTextDebounced();
			}
		};
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table properties feature. Enables support for setting properties of tables (size, border, background, etc.).
 *
 * Read more in the {@glink features/table#table-and-cell-styling-tools Table and cell styling tools} section.
 * See also the {@link module:table/tablecellproperties~TableCellProperties} plugin.
 *
 * This is a "glue" plugin that loads the
 * {@link module:table/tableproperties/tablepropertiesediting~TablePropertiesEditing table properties editing feature} and
 * the {@link module:table/tableproperties/tablepropertiesui~TablePropertiesUI table properties UI feature}.
 *
 * @extends module:core/plugin~Plugin
 */
class TableProperties extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TableProperties';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ TablePropertiesEditing, TablePropertiesUI ];
	}
}

/**
 * The configuration of the table properties user interface (balloon). It allows to define:
 *
 * * The color palette for the table border color style field (`tableProperties.borderColors`),
 * * The color palette for the table background style field (`tableProperties.backgroundColors`).
 *
 *		const tableConfig = {
 *			tableProperties: {
 *				borderColors: [
 *					{
 *						color: 'hsl(0, 0%, 90%)',
 *						label: 'Light grey'
 *					},
 *					// ...
 *				],
 *				backgroundColors: [
 *					{
 *						color: 'hsl(120, 75%, 60%)',
 *						label: 'Green'
 *					},
 *					// ...
 *				]
 *			}
 *		};
 *
 * * The default styles for tables (`tableProperties.defaultProperties`):
 *
 *		const tableConfig = {
 *			tableProperties: {
 *				defaultProperties: {
 *					borderStyle: 'dashed',
 *					borderColor: 'hsl(0, 0%, 90%)',
 *					borderWidth: '3px',
 *					alignment: 'left'
 *				}
 *			}
 *		}
 *
 * 	 {@link module:table/tableproperties~TablePropertiesOptions Read more about the supported properties.}
 *
 * **Note**: The `borderColors` and `backgroundColors` options do not impact the data loaded into the editor,
 * i.e. they do not limit or filter the colors in the data. They are used only in the user interface
 * allowing users to pick colors in a more convenient way. The `defaultProperties` option does impact the data.
 * Default values will not be kept in the editor model.
 *
 * The default color palettes for the table background and the table border are the same
 * ({@link module:table/utils/ui/table-properties~defaultColors check out their content}).
 *
 * Both color palette configurations must follow the
 * {@link module:table/table~TableColorConfig table color configuration format}.
 *
 * Read more about configuring the table feature in {@link module:table/table~TableConfig}.
 *
 * @member {Object} module:table/table~TableConfig#tableProperties
 */

/**
 * The configuration of the table default properties feature.
 *
 * @typedef {Object} module:table/tableproperties~TablePropertiesOptions
 *
 * @property {String} width The default `width` of the table.
 *
 * @property {String} height The default `height` of the table.
 *
 * @property {String} backgroundColor The default `background-color` of the table.
 *
 * @property {String} borderColor The default `border-color` of the table.
 *
 * @property {String} borderWidth The default `border-width` of the table.
 *
 * @property {String} [borderStyle='none'] The default `border-style` of the table.
 *
 * @property {String} [alignment='center'] The default `alignment` of the table.
 */var css_248z$c = ".ck.ck-table-cell-properties-form .ck-form__row.ck-table-cell-properties-form__alignment-row{flex-wrap:wrap}.ck.ck-table-cell-properties-form .ck-form__row.ck-table-cell-properties-form__alignment-row .ck.ck-toolbar:first-of-type{flex-grow:0.57}.ck.ck-table-cell-properties-form .ck-form__row.ck-table-cell-properties-form__alignment-row .ck.ck-toolbar:last-of-type{flex-grow:0.43}.ck.ck-table-cell-properties-form .ck-form__row.ck-table-cell-properties-form__alignment-row .ck.ck-toolbar .ck-button{flex-grow:1}.ck.ck-table-cell-properties-form{width:320px}.ck.ck-table-cell-properties-form .ck-form__row.ck-table-cell-properties-form__padding-row{align-self:flex-end;padding:0;width:25%}.ck.ck-table-cell-properties-form .ck-form__row.ck-table-cell-properties-form__alignment-row .ck.ck-toolbar{background:none;margin-top:var(--ck-spacing-standard)}";
styleInject(css_248z$c);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const ALIGNMENT_ICONS = {
	left: icons.alignLeft,
	center: icons.alignCenter,
	right: icons.alignRight,
	justify: icons.alignJustify,
	top: icons.alignTop,
	middle: icons.alignMiddle,
	bottom: icons.alignBottom
};

/**
 * The class representing a table cell properties form, allowing users to customize
 * certain style aspects of a table cell, for instance, border, padding, text alignment, etc..
 *
 * @extends module:ui/view~View
 */
class TableCellPropertiesView extends View {
	/**
	 * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
	 * @param {Object} options Additional configuration of the view.
	 * @param {module:table/table~TableColorConfig} options.borderColors A configuration of the border
	 * color palette used by the
	 * {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView#borderColorInput}.
	 * @param {module:table/table~TableColorConfig} options.backgroundColors A configuration of the background
	 * color palette used by the
	 * {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView#backgroundInput}.
	 * @param {module:table/tablecellproperties~TableCellPropertiesOptions} options.defaultTableCellProperties The default
	 * table cell properties.
	 */
	constructor( locale, options ) {
		super( locale );

		this.set( {
			/**
			 * The value of the cell border style.
			 *
			 * @observable
			 * @default ''
			 * @member #borderStyle
			 */
			borderStyle: '',

			/**
			 * The value of the cell border width style.
			 *
			 * @observable
			 * @default ''
			 * @member #borderWidth
			 */
			borderWidth: '',

			/**
			 * The value of the cell border color style.
			 *
			 * @observable
			 * @default ''
			 * @member #borderColor
			 */
			borderColor: '',

			/**
			 * The value of the cell padding style.
			 *
			 * @observable
			 * @default ''
			 * @member #padding
			 */
			padding: '',

			/**
			 * The value of the cell background color style.
			 *
			 * @observable
			 * @default ''
			 * @member #backgroundColor
			 */
			backgroundColor: '',

			/**
			 * The value of the table cell width style.
			 *
			 * @observable
			 * @default ''
			 * @member #width
			 */
			width: '',

			/**
			 * The value of the table cell height style.
			 *
			 * @observable
			 * @default ''
			 * @member #height
			 */
			height: '',

			/**
			 * The value of the horizontal text alignment style.
			 *
			 * @observable
			 * @default ''
			 * @member #horizontalAlignment
			 */
			horizontalAlignment: '',

			/**
			 * The value of the vertical text alignment style.
			 *
			 * @observable
			 * @default ''
			 * @member #verticalAlignment
			 */
			verticalAlignment: ''
		} );

		/**
		 * Options passed to the view. See {@link #constructor} to learn more.
		 *
		 * @member {Object}
		 */
		this.options = options;

		const { borderStyleDropdown, borderWidthInput, borderColorInput, borderRowLabel } = this._createBorderFields();
		const { backgroundRowLabel, backgroundInput } = this._createBackgroundFields();
		const { widthInput, operatorLabel, heightInput, dimensionsLabel } = this._createDimensionFields();
		const { horizontalAlignmentToolbar, verticalAlignmentToolbar, alignmentLabel } = this._createAlignmentFields();

		/**
		 * Tracks information about the DOM focus in the form.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
		 *
		 * @readonly
		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
		 */
		this.keystrokes = new KeystrokeHandler();

		/**
		 * A collection of child views in the form.
		 *
		 * @readonly
		 * @type {module:ui/viewcollection~ViewCollection}
		 */
		this.children = this.createCollection();

		/**
		 * A dropdown that allows selecting the style of the table cell border.
		 *
		 * @readonly
		 * @member {module:ui/dropdown/dropdownview~DropdownView}
		 */
		this.borderStyleDropdown = borderStyleDropdown;

		/**
		 * An input that allows specifying the width of the table cell border.
		 *
		 * @readonly
		 * @member {module:ui/inputtext/inputtextview~InputTextView}
		 */
		this.borderWidthInput = borderWidthInput;

		/**
		 * An input that allows specifying the color of the table cell border.
		 *
		 * @readonly
		 * @member {module:table/ui/colorinputview~ColorInputView}
		 */
		this.borderColorInput = borderColorInput;

		/**
		 * An input that allows specifying the table cell background color.
		 *
		 * @readonly
		 * @member {module:table/ui/colorinputview~ColorInputView}
		 */
		this.backgroundInput = backgroundInput;

		/**
		 * An input that allows specifying the table cell padding.
		 *
		 * @readonly
		 * @member {module:ui/inputtext/inputtextview~InputTextView}
		 */
		this.paddingInput = this._createPaddingField();

		/**
		 * An input that allows specifying the table cell width.
		 *
		 * @readonly
		 * @member {module:ui/inputtext/inputtextview~InputTextView}
		 */
		this.widthInput = widthInput;

		/**
		 * An input that allows specifying the table cell height.
		 *
		 * @readonly
		 * @member {module:ui/inputtext/inputtextview~InputTextView}
		 */
		this.heightInput = heightInput;

		/**
		 * A toolbar with buttons that allow changing the horizontal text alignment in a table cell.
		 *
		 * @readonly
		 * @member {module:ui/toolbar/toolbarview~ToolbarView}
		 */
		this.horizontalAlignmentToolbar = horizontalAlignmentToolbar;

		/**
		 * A toolbar with buttons that allow changing the vertical text alignment in a table cell.
		 *
		 * @readonly
		 * @member {module:ui/toolbar/toolbarview~ToolbarView}
		 */
		this.verticalAlignmentToolbar = verticalAlignmentToolbar;

		// Defer creating to make sure other fields are present and the Save button can
		// bind its #isEnabled to their error messages so there's no way to save unless all
		// fields are valid.
		const { saveButtonView, cancelButtonView } = this._createActionButtons();

		/**
		 * The "Save" button view.
		 *
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this.saveButtonView = saveButtonView;

		/**
		 * The "Cancel" button view.
		 *
		 * @member {module:ui/button/buttonview~ButtonView}
		 */
		this.cancelButtonView = cancelButtonView;

		/**
		 * A collection of views that can be focused in the form.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this._focusables = new ViewCollection();

		/**
		 * Helps cycling over {@link #_focusables} in the form.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/focuscycler~FocusCycler}
		 */
		this._focusCycler = new FocusCycler( {
			focusables: this._focusables,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				// Navigate form fields backwards using the Shift + Tab keystroke.
				focusPrevious: 'shift + tab',

				// Navigate form fields forwards using the Tab key.
				focusNext: 'tab'
			}
		} );

		// Form header.
		this.children.add( new FormHeaderView( locale, {
			label: this.t( 'Cell properties' )
		} ) );

		// Border row.
		this.children.add( new FormRowView( locale, {
			labelView: borderRowLabel,
			children: [
				borderRowLabel,
				borderStyleDropdown,
				borderColorInput,
				borderWidthInput
			],
			class: 'ck-table-form__border-row'
		} ) );

		// Background.
		this.children.add( new FormRowView( locale, {
			labelView: backgroundRowLabel,
			children: [
				backgroundRowLabel,
				backgroundInput
			],
			class: 'ck-table-form__background-row'
		} ) );

		// Dimensions row and padding.
		this.children.add( new FormRowView( locale, {
			children: [
				// Dimensions row.
				new FormRowView( locale, {
					labelView: dimensionsLabel,
					children: [
						dimensionsLabel,
						widthInput,
						operatorLabel,
						heightInput
					],
					class: 'ck-table-form__dimensions-row'
				} ),
				// Padding row.
				new FormRowView( locale, {
					children: [
						this.paddingInput
					],
					class: 'ck-table-cell-properties-form__padding-row'
				} )
			]
		} ) );

		// Text alignment row.
		this.children.add( new FormRowView( locale, {
			labelView: alignmentLabel,
			children: [
				alignmentLabel,
				horizontalAlignmentToolbar,
				verticalAlignmentToolbar
			],
			class: 'ck-table-cell-properties-form__alignment-row'
		} ) );

		// Action row.
		this.children.add( new FormRowView( locale, {
			children: [
				this.saveButtonView,
				this.cancelButtonView
			],
			class: 'ck-table-form__action-row'
		} ) );

		this.setTemplate( {
			tag: 'form',
			attributes: {
				class: [
					'ck',
					'ck-form',
					'ck-table-form',
					'ck-table-cell-properties-form'
				],
				// https://github.com/ckeditor/ckeditor5-link/issues/90
				tabindex: '-1'
			},
			children: this.children
		} );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		// Enable the "submit" event for this view. It can be triggered by the #saveButtonView
		// which is of the "submit" DOM "type".
		submitHandler( {
			view: this
		} );

		[
			this.borderStyleDropdown,
			this.borderColorInput,
			this.borderWidthInput,
			this.backgroundInput,
			this.widthInput,
			this.heightInput,
			this.paddingInput,
			this.horizontalAlignmentToolbar,
			this.verticalAlignmentToolbar,
			this.saveButtonView,
			this.cancelButtonView
		].forEach( view => {
			// Register the view as focusable.
			this._focusables.add( view );

			// Register the view in the focus tracker.
			this.focusTracker.add( view.element );
		} );

		// Mainly for closing using "Esc" and navigation using "Tab".
		this.keystrokes.listenTo( this.element );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}

	/**
	 * Focuses the fist focusable field in the form.
	 */
	focus() {
		this._focusCycler.focusFirst();
	}

	/**
	 * Creates the following form fields:
	 *
	 * * {@link #borderStyleDropdown},
	 * * {@link #borderWidthInput},
	 * * {@link #borderColorInput}.
	 *
	 * @private
	 * @returns {Object.<String,module:ui/view~View>}
	 */
	_createBorderFields() {
		const defaultTableCellProperties = this.options.defaultTableCellProperties;
		const defaultBorder = {
			style: defaultTableCellProperties.borderStyle,
			width: defaultTableCellProperties.borderWidth,
			color: defaultTableCellProperties.borderColor
		};

		const colorInputCreator = getLabeledColorInputCreator( {
			colorConfig: this.options.borderColors,
			columns: 5,
			defaultColorValue: defaultBorder.color
		} );
		const locale = this.locale;
		const t = this.t;

		// -- Group label ---------------------------------------------

		const borderRowLabel = new LabelView( locale );
		borderRowLabel.text = t( 'Border' );

		// -- Style ---------------------------------------------------

		const styleLabels = getBorderStyleLabels( t );
		const borderStyleDropdown = new LabeledFieldView( locale, createLabeledDropdown );
		borderStyleDropdown.set( {
			label: t( 'Style' ),
			class: 'ck-table-form__border-style'
		} );

		borderStyleDropdown.fieldView.buttonView.set( {
			isOn: false,
			withText: true,
			tooltip: t( 'Style' )
		} );

		borderStyleDropdown.fieldView.buttonView.bind( 'label' ).to( this, 'borderStyle', value => {
			return styleLabels[ value ? value : 'none' ];
		} );

		borderStyleDropdown.fieldView.on( 'execute', evt => {
			this.borderStyle = evt.source._borderStyleValue;
		} );

		borderStyleDropdown.bind( 'isEmpty' ).to( this, 'borderStyle', value => !value );

		addListToDropdown( borderStyleDropdown.fieldView, getBorderStyleDefinitions( this, defaultBorder.style ) );

		// -- Width ---------------------------------------------------

		const borderWidthInput = new LabeledFieldView( locale, createLabeledInputText );

		borderWidthInput.set( {
			label: t( 'Width' ),
			class: 'ck-table-form__border-width'
		} );

		borderWidthInput.fieldView.bind( 'value' ).to( this, 'borderWidth' );
		borderWidthInput.bind( 'isEnabled' ).to( this, 'borderStyle', isBorderStyleSet );
		borderWidthInput.fieldView.on( 'input', () => {
			this.borderWidth = borderWidthInput.fieldView.element.value;
		} );

		// -- Color ---------------------------------------------------

		const borderColorInput = new LabeledFieldView( locale, colorInputCreator );

		borderColorInput.set( {
			label: t( 'Color' ),
			class: 'ck-table-form__border-color'
		} );

		borderColorInput.fieldView.bind( 'value' ).to( this, 'borderColor' );
		borderColorInput.bind( 'isEnabled' ).to( this, 'borderStyle', isBorderStyleSet );

		borderColorInput.fieldView.on( 'input', () => {
			this.borderColor = borderColorInput.fieldView.value;
		} );

		// Reset the border color and width fields depending on the `border-style` value.
		this.on( 'change:borderStyle', ( evt, name, newValue, oldValue ) => {
			// When removing the border (`border-style:none`), clear the remaining `border-*` properties.
			// See: https://github.com/ckeditor/ckeditor5/issues/6227.
			if ( !isBorderStyleSet( newValue ) ) {
				this.borderColor = '';
				this.borderWidth = '';
			}

			// When setting the `border-style` from `none`, set the default `border-color` and `border-width` properties.
			if ( !isBorderStyleSet( oldValue ) ) {
				this.borderColor = defaultBorder.color;
				this.borderWidth = defaultBorder.width;
			}
		} );

		return {
			borderRowLabel,
			borderStyleDropdown,
			borderColorInput,
			borderWidthInput
		};
	}

	/**
	 * Creates the following form fields:
	 *
	 * * {@link #backgroundInput}.
	 *
	 * @private
	 * @returns {Object.<String,module:ui/view~View>}
	 */
	_createBackgroundFields() {
		const locale = this.locale;
		const t = this.t;

		// -- Group label ---------------------------------------------

		const backgroundRowLabel = new LabelView( locale );
		backgroundRowLabel.text = t( 'Background' );

		// -- Background color input -----------------------------------

		const colorInputCreator = getLabeledColorInputCreator( {
			colorConfig: this.options.backgroundColors,
			columns: 5,
			defaultColorValue: this.options.defaultTableCellProperties.backgroundColor
		} );

		const backgroundInput = new LabeledFieldView( locale, colorInputCreator );

		backgroundInput.set( {
			label: t( 'Color' ),
			class: 'ck-table-cell-properties-form__background'
		} );

		backgroundInput.fieldView.bind( 'value' ).to( this, 'backgroundColor' );
		backgroundInput.fieldView.on( 'input', () => {
			this.backgroundColor = backgroundInput.fieldView.value;
		} );

		return {
			backgroundRowLabel,
			backgroundInput
		};
	}

	/**
	 * Creates the following form fields:
	 *
	 * * {@link #widthInput}.
	 * * {@link #heightInput}.
	 *
	 * @private
	 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
	 */
	_createDimensionFields() {
		const locale = this.locale;
		const t = this.t;

		// -- Label ---------------------------------------------------

		const dimensionsLabel = new LabelView( locale );
		dimensionsLabel.text = t( 'Dimensions' );

		// -- Width ---------------------------------------------------

		const widthInput = new LabeledFieldView( locale, createLabeledInputText );

		widthInput.set( {
			label: t( 'Width' ),
			class: 'ck-table-form__dimensions-row__width'
		} );

		widthInput.fieldView.bind( 'value' ).to( this, 'width' );
		widthInput.fieldView.on( 'input', () => {
			this.width = widthInput.fieldView.element.value;
		} );

		// -- Operator ---------------------------------------------------

		const operatorLabel = new View( locale );
		operatorLabel.setTemplate( {
			tag: 'span',
			attributes: {
				class: [
					'ck-table-form__dimension-operator'
				]
			},
			children: [
				{ text: '×' }
			]
		} );

		// -- Height ---------------------------------------------------

		const heightInput = new LabeledFieldView( locale, createLabeledInputText );

		heightInput.set( {
			label: t( 'Height' ),
			class: 'ck-table-form__dimensions-row__height'
		} );

		heightInput.fieldView.bind( 'value' ).to( this, 'height' );
		heightInput.fieldView.on( 'input', () => {
			this.height = heightInput.fieldView.element.value;
		} );

		return {
			dimensionsLabel,
			widthInput,
			operatorLabel,
			heightInput
		};
	}

	/**
	 * Creates the following form fields:
	 *
	 * * {@link #paddingInput}.
	 *
	 * @private
	 * @returns {module:ui/labeledfield/labeledfieldview~LabeledFieldView}
	 */
	_createPaddingField() {
		const locale = this.locale;
		const t = this.t;

		const paddingInput = new LabeledFieldView( locale, createLabeledInputText );

		paddingInput.set( {
			label: t( 'Padding' ),
			class: 'ck-table-cell-properties-form__padding'
		} );

		paddingInput.fieldView.bind( 'value' ).to( this, 'padding' );
		paddingInput.fieldView.on( 'input', () => {
			this.padding = paddingInput.fieldView.element.value;
		} );

		return paddingInput;
	}

	/**
	 * Creates the following form fields:
	 *
	 * * {@link #horizontalAlignmentToolbar},
	 * * {@link #verticalAlignmentToolbar}.
	 *
	 * @private
	 * @returns {Object.<String,module:ui/view~View>}
	 */
	_createAlignmentFields() {
		const locale = this.locale;
		const t = this.t;

		const alignmentLabel = new LabelView( locale );

		alignmentLabel.text = t( 'Table cell text alignment' );

		// -- Horizontal ---------------------------------------------------

		const horizontalAlignmentToolbar = new ToolbarView( locale );
		const isContentRTL = this.locale.contentLanguageDirection === 'rtl';

		horizontalAlignmentToolbar.set( {
			isCompact: true,
			ariaLabel: t( 'Horizontal text alignment toolbar' )
		} );

		fillToolbar( {
			view: this,
			icons: ALIGNMENT_ICONS,
			toolbar: horizontalAlignmentToolbar,
			labels: this._horizontalAlignmentLabels,
			propertyName: 'horizontalAlignment',
			nameToValue: name => {
				// For the RTL content, we want to swap the buttons "align to the left" and "align to the right".
				if ( isContentRTL ) {
					if ( name === 'left' ) {
						return 'right';
					} else if ( name === 'right' ) {
						return 'left';
					}
				}

				return name;
			},
			defaultValue: this.options.defaultTableCellProperties.horizontalAlignment
		} );

		// -- Vertical -----------------------------------------------------

		const verticalAlignmentToolbar = new ToolbarView( locale );

		verticalAlignmentToolbar.set( {
			isCompact: true,
			ariaLabel: t( 'Vertical text alignment toolbar' )
		} );

		fillToolbar( {
			view: this,
			icons: ALIGNMENT_ICONS,
			toolbar: verticalAlignmentToolbar,
			labels: this._verticalAlignmentLabels,
			propertyName: 'verticalAlignment',
			defaultValue: this.options.defaultTableCellProperties.verticalAlignment
		} );

		return {
			horizontalAlignmentToolbar,
			verticalAlignmentToolbar,
			alignmentLabel
		};
	}

	/**
	 * Creates the following form controls:
	 *
	 * * {@link #saveButtonView},
	 * * {@link #cancelButtonView}.
	 *
	 * @private
	 * @returns {Object.<String,module:ui/view~View>}
	 */
	_createActionButtons() {
		const locale = this.locale;
		const t = this.t;
		const saveButtonView = new ButtonView( locale );
		const cancelButtonView = new ButtonView( locale );
		const fieldsThatShouldValidateToSave = [
			this.borderWidthInput,
			this.borderColorInput,
			this.backgroundInput,
			this.paddingInput
		];

		saveButtonView.set( {
			label: t( 'Save' ),
			icon: icons.check,
			class: 'ck-button-save',
			type: 'submit',
			withText: true
		} );

		saveButtonView.bind( 'isEnabled' ).toMany( fieldsThatShouldValidateToSave, 'errorText', ( ...errorTexts ) => {
			return errorTexts.every( errorText => !errorText );
		} );

		cancelButtonView.set( {
			label: t( 'Cancel' ),
			icon: icons.cancel,
			class: 'ck-button-cancel',
			withText: true
		} );

		cancelButtonView.delegate( 'execute' ).to( this, 'cancel' );

		return {
			saveButtonView, cancelButtonView
		};
	}

	/**
	 * Provides localized labels for {@link #horizontalAlignmentToolbar} buttons.
	 *
	 * @private
	 * @type {Object.<String,String>}
	 */
	get _horizontalAlignmentLabels() {
		const locale = this.locale;
		const t = this.t;

		const left = t( 'Align cell text to the left' );
		const center = t( 'Align cell text to the center' );
		const right = t( 'Align cell text to the right' );
		const justify = t( 'Justify cell text' );

		// Returns object with a proper order of labels.
		if ( locale.uiLanguageDirection === 'rtl' ) {
			return { right, center, left, justify };
		} else {
			return { left, center, right, justify };
		}
	}

	/**
	 * Provides localized labels for {@link #verticalAlignmentToolbar} buttons.
	 *
	 * @private
	 * @type {Object.<String,String>}
	 */
	get _verticalAlignmentLabels() {
		const t = this.t;

		return {
			top: t( 'Align cell text to the top' ),
			middle: t( 'Align cell text to the middle' ),
			bottom: t( 'Align cell text to the bottom' )
		};
	}
}

function isBorderStyleSet( value ) {
	return value !== 'none';
}var tableCellProperties = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m11.105 18-.17 1H2.5A1.5 1.5 0 0 1 1 17.5v-15A1.5 1.5 0 0 1 2.5 1h15A1.5 1.5 0 0 1 19 2.5v9.975l-.85-.124-.15-.302V8h-5v4h.021l-.172.351-1.916.28-.151.027c-.287.063-.54.182-.755.341L8 13v5h3.105zM2 12h5V8H2v4zm10-4H8v4h4V8zM2 2v5h5V2H2zm0 16h5v-5H2v5zM13 7h5V2h-5v5zM8 2v5h4V2H8z\" opacity=\".6\"/><path d=\"m15.5 11.5 1.323 2.68 2.957.43-2.14 2.085.505 2.946L15.5 18.25l-2.645 1.39.505-2.945-2.14-2.086 2.957-.43L15.5 11.5zM13 6a1 1 0 0 1 1 1v3.172a2.047 2.047 0 0 0-.293.443l-.858 1.736-1.916.28-.151.027A1.976 1.976 0 0 0 9.315 14H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6zm-1 2H8v4h4V8z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const ERROR_TEXT_TIMEOUT = 500;

// Map of view properties and related commands.
const propertyToCommandMap = {
	borderStyle: 'tableCellBorderStyle',
	borderColor: 'tableCellBorderColor',
	borderWidth: 'tableCellBorderWidth',
	width: 'tableCellWidth',
	height: 'tableCellHeight',
	padding: 'tableCellPadding',
	backgroundColor: 'tableCellBackgroundColor',
	horizontalAlignment: 'tableCellHorizontalAlignment',
	verticalAlignment: 'tableCellVerticalAlignment'
};

/**
 * The table cell properties UI plugin. It introduces the `'tableCellProperties'` button
 * that opens a form allowing to specify the visual styling of a table cell.
 *
 * It uses the
 * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
 *
 * @extends module:core/plugin~Plugin
 */
class TableCellPropertiesUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ ContextualBalloon ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TableCellPropertiesUI';
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		editor.config.define( 'table.tableCellProperties', {
			borderColors: defaultColors,
			backgroundColors: defaultColors
		} );
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		/**
		 * The default table cell properties.
		 *
		 * @protected
		 * @member {module:table/tablecellproperties~TableCellPropertiesOptions}
		 */
		this._defaultTableCellProperties = getNormalizedDefaultProperties(
			editor.config.get( 'table.tableCellProperties.defaultProperties' ),
			{
				includeVerticalAlignmentProperty: true,
				includeHorizontalAlignmentProperty: true,
				includePaddingProperty: true,
				isRightToLeftContent: editor.locale.contentLanguageDirection === 'rtl'
			}
		);

		/**
		 * The contextual balloon plugin instance.
		 *
		 * @private
		 * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
		 */
		this._balloon = editor.plugins.get( ContextualBalloon );

		/**
		 * The cell properties form view displayed inside the balloon.
		 *
		 * @member {module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView}
		 */
		this.view = this._createPropertiesView();

		/**
		 * The batch used to undo all changes made by the form (which are live, as the user types)
		 * when "Cancel" was pressed. Each time the view is shown, a new batch is created.
		 *
		 * @protected
		 * @member {module:engine/model/batch~Batch}
		 */
		this._undoStepBatch = null;

		editor.ui.componentFactory.add( 'tableCellProperties', locale => {
			const view = new ButtonView( locale );

			view.set( {
				label: t( 'Cell properties' ),
				icon: tableCellProperties,
				tooltip: true
			} );

			this.listenTo( view, 'execute', () => this._showView() );

			const commands = Object.values( propertyToCommandMap )
				.map( commandName => editor.commands.get( commandName ) );

			view.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( ...areEnabled ) => (
				areEnabled.some( isCommandEnabled => isCommandEnabled )
			) );

			return view;
		} );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		// Destroy created UI components as they are not automatically destroyed.
		// See https://github.com/ckeditor/ckeditor5/issues/1341.
		this.view.destroy();
	}

	/**
	 * Creates the {@link module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView} instance.
	 *
	 * @private
	 * @returns {module:table/tablecellproperties/ui/tablecellpropertiesview~TableCellPropertiesView} The cell
	 * properties form view instance.
	 */
	_createPropertiesView() {
		const editor = this.editor;
		const viewDocument = editor.editing.view.document;
		const config = editor.config.get( 'table.tableCellProperties' );
		const borderColorsConfig = normalizeColorOptions( config.borderColors );
		const localizedBorderColors = getLocalizedColorOptions( editor.locale, borderColorsConfig );
		const backgroundColorsConfig = normalizeColorOptions( config.backgroundColors );
		const localizedBackgroundColors = getLocalizedColorOptions( editor.locale, backgroundColorsConfig );
		const view = new TableCellPropertiesView( editor.locale, {
			borderColors: localizedBorderColors,
			backgroundColors: localizedBackgroundColors,
			defaultTableCellProperties: this._defaultTableCellProperties
		} );
		const t = editor.t;

		// Render the view so its #element is available for the clickOutsideHandler.
		view.render();

		this.listenTo( view, 'submit', () => {
			this._hideView();
		} );

		this.listenTo( view, 'cancel', () => {
			// https://github.com/ckeditor/ckeditor5/issues/6180
			if ( this._undoStepBatch.operations.length ) {
				editor.execute( 'undo', this._undoStepBatch );
			}

			this._hideView();
		} );

		// Close the balloon on Esc key press.
		view.keystrokes.set( 'Esc', ( data, cancel ) => {
			this._hideView();
			cancel();
		} );

		// Reposition the balloon or hide the form if a table cell is no longer selected.
		this.listenTo( editor.ui, 'update', () => {
			if ( !getTableWidgetAncestor( viewDocument.selection ) ) {
				this._hideView();
			} else if ( this._isViewVisible ) {
				repositionContextualBalloon( editor, 'cell' );
			}
		} );

		// Close on click outside of balloon panel element.
		clickOutsideHandler( {
			emitter: view,
			activator: () => this._isViewInBalloon,
			contextElements: [ this._balloon.view.element ],
			callback: () => this._hideView()
		} );

		const colorErrorText = getLocalizedColorErrorText( t );
		const lengthErrorText = getLocalizedLengthErrorText( t );

		// Create the "UI -> editor data" binding.
		// These listeners update the editor data (via table commands) when any observable
		// property of the view has changed. They also validate the value and display errors in the UI
		// when necessary. This makes the view live, which means the changes are
		// visible in the editing as soon as the user types or changes fields' values.
		view.on(
			'change:borderStyle',
			this._getPropertyChangeCallback( 'tableCellBorderStyle', this._defaultTableCellProperties.borderStyle )
		);

		view.on( 'change:borderColor', this._getValidatedPropertyChangeCallback( {
			viewField: view.borderColorInput,
			commandName: 'tableCellBorderColor',
			errorText: colorErrorText,
			validator: colorFieldValidator,
			defaultValue: this._defaultTableCellProperties.borderColor
		} ) );

		view.on( 'change:borderWidth', this._getValidatedPropertyChangeCallback( {
			viewField: view.borderWidthInput,
			commandName: 'tableCellBorderWidth',
			errorText: lengthErrorText,
			validator: lineWidthFieldValidator,
			defaultValue: this._defaultTableCellProperties.borderWidth
		} ) );

		view.on( 'change:padding', this._getValidatedPropertyChangeCallback( {
			viewField: view.paddingInput,
			commandName: 'tableCellPadding',
			errorText: lengthErrorText,
			validator: lengthFieldValidator,
			defaultValue: this._defaultTableCellProperties.padding
		} ) );

		view.on( 'change:width', this._getValidatedPropertyChangeCallback( {
			viewField: view.widthInput,
			commandName: 'tableCellWidth',
			errorText: lengthErrorText,
			validator: lengthFieldValidator,
			defaultValue: this._defaultTableCellProperties.width
		} ) );

		view.on( 'change:height', this._getValidatedPropertyChangeCallback( {
			viewField: view.heightInput,
			commandName: 'tableCellHeight',
			errorText: lengthErrorText,
			validator: lengthFieldValidator,
			defaultValue: this._defaultTableCellProperties.height
		} ) );

		view.on( 'change:backgroundColor', this._getValidatedPropertyChangeCallback( {
			viewField: view.backgroundInput,
			commandName: 'tableCellBackgroundColor',
			errorText: colorErrorText,
			validator: colorFieldValidator,
			defaultValue: this._defaultTableCellProperties.backgroundColor
		} ) );

		view.on(
			'change:horizontalAlignment',
			this._getPropertyChangeCallback( 'tableCellHorizontalAlignment', this._defaultTableCellProperties.horizontalAlignment )
		);
		view.on(
			'change:verticalAlignment',
			this._getPropertyChangeCallback( 'tableCellVerticalAlignment', this._defaultTableCellProperties.verticalAlignment )
		);

		return view;
	}

	/**
	 * In this method the "editor data -> UI" binding is happening.
	 *
	 * When executed, this method obtains selected cell property values from various table commands
	 * and passes them to the {@link #view}.
	 *
	 * This way, the UI stays up–to–date with the editor data.
	 *
	 * @private
	 */
	_fillViewFormFromCommandValues() {
		const commands = this.editor.commands;
		const borderStyleCommand = commands.get( 'tableCellBorderStyle' );

		Object.entries( propertyToCommandMap )
			.map( ( [ property, commandName ] ) => {
				const defaultValue = this._defaultTableCellProperties[ property ] || '';

				return [ property, commands.get( commandName ).value || defaultValue ];
			} )
			.forEach( ( [ property, value ] ) => {
				// Do not set the `border-color` and `border-width` fields if `border-style:none`.
				if ( ( property === 'borderColor' || property === 'borderWidth' ) && borderStyleCommand.value === 'none' ) {
					return;
				}

				this.view.set( property, value );
			} );
	}

	/**
	 * Shows the {@link #view} in the {@link #_balloon}.
	 *
	 * **Note**: Each time a view is shown, a new {@link #_undoStepBatch} is created. It contains
	 * all changes made to the document when the view is visible, allowing a single undo step
	 * for all of them.
	 *
	 * @protected
	 */
	_showView() {
		const editor = this.editor;

		// Update the view with the model values.
		this._fillViewFormFromCommandValues();

		this._balloon.add( {
			view: this.view,
			position: getBalloonCellPositionData( editor )
		} );

		// Create a new batch. Clicking "Cancel" will undo this batch.
		this._undoStepBatch = editor.model.createBatch();

		// Basic a11y.
		this.view.focus();
	}

	/**
	 * Removes the {@link #view} from the {@link #_balloon}.
	 *
	 * @protected
	 */
	_hideView() {
		if ( !this._isViewInBalloon ) {
			return;
		}

		const editor = this.editor;

		this.stopListening( editor.ui, 'update' );

		// Blur any input element before removing it from DOM to prevent issues in some browsers.
		// See https://github.com/ckeditor/ckeditor5/issues/1501.
		this.view.saveButtonView.focus();

		this._balloon.remove( this.view );

		// Make sure the focus is not lost in the process by putting it directly
		// into the editing view.
		this.editor.editing.view.focus();
	}

	/**
	 * Returns `true` when the {@link #view} is visible in the {@link #_balloon}.
	 *
	 * @private
	 * @type {Boolean}
	 */
	get _isViewVisible() {
		return this._balloon.visibleView === this.view;
	}

	/**
	 * Returns `true` when the {@link #view} is in the {@link #_balloon}.
	 *
	 * @private
	 * @type {Boolean}
	 */
	get _isViewInBalloon() {
		return this._balloon.hasView( this.view );
	}

	/**
	 * Creates a callback that when executed upon the {@link #view view's} property change
	 * executes a related editor command with the new property value.
	 *
	 * @private
	 * @param {String} commandName
	 * @param {String} defaultValue The default value of the command.
	 * @returns {Function}
	 */
	_getPropertyChangeCallback( commandName, defaultValue ) {
		return ( evt, propertyName, newValue, oldValue ) => {
			// If the "oldValue" is missing and "newValue" is set to the default value, do not execute the command.
			// It is an initial call (when opening the table properties view).
			if ( !oldValue && defaultValue === newValue ) {
				return;
			}

			this.editor.execute( commandName, {
				value: newValue,
				batch: this._undoStepBatch
			} );
		};
	}

	/**
	 * Creates a callback that when executed upon the {@link #view view's} property change:
	 * * Executes a related editor command with the new property value if the value is valid,
	 * * Or sets the error text next to the invalid field, if the value did not pass the validation.
	 *
	 * @private
	 * @param {Object} options
	 * @param {String} options.commandName
	 * @param {module:ui/view~View} options.viewField
	 * @param {Function} options.validator
	 * @param {String} options.errorText
	 * @param {String} options.defaultValue
	 * @returns {Function}
	 */
	_getValidatedPropertyChangeCallback( options ) {
		const { commandName, viewField, validator, errorText, defaultValue } = options;
		const setErrorTextDebounced = debounce( () => {
			viewField.errorText = errorText;
		}, ERROR_TEXT_TIMEOUT );

		return ( evt, propertyName, newValue, oldValue ) => {
			setErrorTextDebounced.cancel();

			// If the "oldValue" is missing and "newValue" is set to the default value, do not execute the command.
			// It is an initial call (when opening the table properties view).
			if ( !oldValue && defaultValue === newValue ) {
				return;
			}

			if ( validator( newValue ) ) {
				this.editor.execute( commandName, {
					value: newValue,
					batch: this._undoStepBatch
				} );

				viewField.errorText = null;
			} else {
				setErrorTextDebounced();
			}
		};
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table cell attribute command.
 *
 * The command is a base command for other table cell property commands.
 *
 * @extends module:core/command~Command
 */
class TableCellPropertyCommand extends Command {
	/**
	 * Creates a new `TableCellPropertyCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} attributeName Table cell attribute name.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, attributeName, defaultValue ) {
		super( editor );

		/**
		 * The attribute that will be set by the command.
		 *
		 * @readonly
		 * @member {String}
		 */
		this.attributeName = attributeName;

		/**
		 * The default value for the attribute.
		 *
		 * @readonly
		 * @protected
		 * @member {String}
		 */
		this._defaultValue = defaultValue;
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		const editor = this.editor;
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const selectedTableCells = tableUtils.getSelectionAffectedTableCells( editor.model.document.selection );

		this.isEnabled = !!selectedTableCells.length;
		this.value = this._getSingleValue( selectedTableCells );
	}

	/**
	 * Executes the command.
	 *
	 * @fires execute
	 * @param {Object} [options]
	 * @param {*} [options.value] If set, the command will set the attribute on selected table cells.
	 * If it is not set, the command will remove the attribute from the selected table cells.
	 * @param {module:engine/model/batch~Batch} [options.batch] Pass the model batch instance to the command to aggregate changes,
	 * for example to allow a single undo step for multiple executions.
	 */
	execute( options = {} ) {
		const { value, batch } = options;
		const model = this.editor.model;
		const tableUtils = this.editor.plugins.get( 'TableUtils' );
		const tableCells = tableUtils.getSelectionAffectedTableCells( model.document.selection );
		const valueToSet = this._getValueToSet( value );

		model.enqueueChange( batch, writer => {
			if ( valueToSet ) {
				tableCells.forEach( tableCell => writer.setAttribute( this.attributeName, valueToSet, tableCell ) );
			} else {
				tableCells.forEach( tableCell => writer.removeAttribute( this.attributeName, tableCell ) );
			}
		} );
	}

	/**
	 * Returns the attribute value for a table cell.
	 *
	 * @param {module:engine/model/element~Element} tableCell
	 * @returns {String|undefined}
	 * @private
	 */
	_getAttribute( tableCell ) {
		if ( !tableCell ) {
			return;
		}

		const value = tableCell.getAttribute( this.attributeName );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}

	/**
	 * Returns the proper model value. It can be used to add a default unit to numeric values.
	 *
	 * @private
	 * @param {*} value
	 * @returns {*}
	 */
	_getValueToSet( value ) {
		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}

	/**
	 * Returns a single value for all selected table cells. If the value is the same for all cells,
	 * it will be returned (`undefined` otherwise).
	 *
	 * @param {Array.<module:engine/model/element~Element>} tableCell
	 * @returns {*}
	 * @private
	 */
	_getSingleValue( tableCell ) {
		const firstCellValue = this._getAttribute( tableCell[ 0 ] );

		const everyCellHasAttribute = tableCell.every( tableCell => this._getAttribute( tableCell ) === firstCellValue );

		return everyCellHasAttribute ? firstCellValue : undefined;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table cell padding command.
 *
 * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
 * the `'tableCellPadding'` editor command.
 *
 * To change the padding of selected cells, execute the command:
 *
 *		editor.execute( 'tableCellPadding', {
 *			value: '5px'
 *		} );
 *
 * **Note**: This command adds the default `'px'` unit to numeric values. Executing:
 *
 *		editor.execute( 'tableCellPadding', {
 *			value: '5'
 *		} );
 *
 * will set the `padding` attribute to `'5px'` in the model.
 *
 * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
 */
class TableCellPaddingCommand extends TableCellPropertyCommand {
	/**
	 * Creates a new `TableCellPaddingCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableCellPadding', defaultValue );
	}

	/**
	 * @inheritDoc
	 */
	_getAttribute( tableCell ) {
		if ( !tableCell ) {
			return;
		}

		const value = getSingleValue( tableCell.getAttribute( this.attributeName ) );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}

	/**
	 * @inheritDoc
	 */
	_getValueToSet( value ) {
		value = addDefaultUnitToNumericValue( value, 'px' );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table cell width command.
 *
 * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
 * the `'tableCellWidth'` editor command.
 *
 * To change the width of selected cells, execute the command:
 *
 *		editor.execute( 'tableCellWidth', {
 *			value: '50px'
 *		} );
 *
 * **Note**: This command adds a default `'px'` unit to numeric values. Executing:
 *
 *		editor.execute( 'tableCellWidth', {
 *			value: '50'
 *		} );
 *
 * will set the `width` attribute to `'50px'` in the model.
 *
 * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
 */
class TableCellWidthCommand extends TableCellPropertyCommand {
	/**
	 * Creates a new `TableCellWidthCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableCellWidth', defaultValue );
	}

	/**
	 * @inheritDoc
	 */
	_getValueToSet( value ) {
		value = addDefaultUnitToNumericValue( value, 'px' );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table cell height command.
 *
 * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
 * the `'tableCellHeight'` editor command.
 *
 * To change the height of selected cells, execute the command:
 *
 *		editor.execute( 'tableCellHeight', {
 *			value: '50px'
 *		} );
 *
 * **Note**: This command adds the default `'px'` unit to numeric values. Executing:
 *
 *		editor.execute( 'tableCellHeight', {
 *			value: '50'
 *		} );
 *
 * will set the `height` attribute to `'50px'` in the model.
 *
 * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
 */
class TableCellHeightCommand extends TableCellPropertyCommand {
	/**
	 * Creates a new `TableCellHeightCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableCellHeight', defaultValue );
	}

	/**
	 * @inheritDoc
	 */
	_getValueToSet( value ) {
		value = addDefaultUnitToNumericValue( value, 'px' );

		if ( value === this._defaultValue ) {
			return null;
		}

		return value;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table cell background color command.
 *
 * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
 * the `'tableCellBackgroundColor'` editor command.
 *
 * To change the background color of selected cells, execute the command:
 *
 *		editor.execute( 'tableCellBackgroundColor', {
 *			value: '#f00'
 *		} );
 *
 * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
 */
class TableCellBackgroundColorCommand extends TableCellPropertyCommand {
	/**
	 * Creates a new `TableCellBackgroundColorCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableCellBackgroundColor', defaultValue );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table cell vertical alignment command.
 *
 * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
 * the `'tableCellVerticalAlignment'` editor command.
 *
 * To change the vertical text alignment of selected cells, execute the command:
 *
 *		editor.execute( 'tableCellVerticalAlignment', {
 *			value: 'top'
 *		} );
 *
 * The following values, corresponding to the
 * [`vertical-align` CSS attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align), are allowed:
 *
 * * `'top'`
 * * `'bottom'`
 *
 * The `'middle'` value is the default one so there is no need to set it.
 *
 * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
 */
class TableCellVerticalAlignmentCommand extends TableCellPropertyCommand {
	/**
	 * Creates a new `TableCellVerticalAlignmentCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value for the "alignment" attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableCellVerticalAlignment', defaultValue );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table cell horizontal alignment command.
 *
 * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
 * the `'tableCellHorizontalAlignment'` editor command.
 *
 * To change the horizontal text alignment of selected cells, execute the command:
 *
 *		editor.execute( 'tableCellHorizontalAlignment', {
 *			value: 'right'
 *		} );
 *
 * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
 */
class TableCellHorizontalAlignmentCommand extends TableCellPropertyCommand {
	/**
	 * Creates a new `TableCellHorizontalAlignmentCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value for the "alignment" attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableCellHorizontalAlignment', defaultValue );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table cell border style command.
 *
 * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
 * the `'tableCellBorderStyle'` editor command.
 *
 * To change the border style of selected cells, execute the command:
 *
 *		editor.execute( 'tableCellBorderStyle', {
 *			value: 'dashed'
 *		} );
 *
 * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
 */
class TableCellBorderStyleCommand extends TableCellPropertyCommand {
	/**
	 * Creates a new `TableCellBorderStyleCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableCellBorderStyle', defaultValue );
	}

	/**
	 * @inheritDoc
	 */
	_getAttribute( tableCell ) {
		if ( !tableCell ) {
			return;
		}

		const value = getSingleValue( tableCell.getAttribute( this.attributeName ) );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table cell border color command.
 *
 * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
 * the `'tableCellBorderColor'` editor command.
 *
 * To change the border color of selected cells, execute the command:
 *
 *		editor.execute( 'tableCellBorderColor', {
 *			value: '#f00'
 *		} );
 *
 * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
 */
class TableCellBorderColorCommand extends TableCellPropertyCommand {
	/**
	 * Creates a new `TableCellBorderColorCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableCellBorderColor', defaultValue );
	}

	/**
	 * @inheritDoc
	 */
	_getAttribute( tableCell ) {
		if ( !tableCell ) {
			return;
		}

		const value = getSingleValue( tableCell.getAttribute( this.attributeName ) );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table cell border width command.
 *
 * The command is registered by the {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing} as
 * the `'tableCellBorderWidth'` editor command.
 *
 * To change the border width of selected cells, execute the command:
 *
 *		editor.execute( 'tableCellBorderWidth', {
 *			value: '5px'
 *		} );
 *
 * **Note**: This command adds the default `'px'` unit to numeric values. Executing:
 *
 *		editor.execute( 'tableCellBorderWidth', {
 *			value: '5'
 *		} );
 *
 * will set the `borderWidth` attribute to `'5px'` in the model.
 *
 * @extends module:table/tablecellproperties/commands/tablecellpropertycommand~TableCellPropertyCommand
 */
class TableCellBorderWidthCommand extends TableCellPropertyCommand {
	/**
	 * Creates a new `TableCellBorderWidthCommand` instance.
	 *
	 * @param {module:core/editor/editor~Editor} editor An editor in which this command will be used.
	 * @param {String} defaultValue The default value of the attribute.
	 */
	constructor( editor, defaultValue ) {
		super( editor, 'tableCellBorderWidth', defaultValue );
	}

	/**
	 * @inheritDoc
	 */
	_getAttribute( tableCell ) {
		if ( !tableCell ) {
			return;
		}

		const value = getSingleValue( tableCell.getAttribute( this.attributeName ) );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}

	/**
	 * @inheritDoc
	 */
	_getValueToSet( value ) {
		value = addDefaultUnitToNumericValue( value, 'px' );

		if ( value === this._defaultValue ) {
			return;
		}

		return value;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const VALIGN_VALUES_REG_EXP = /^(top|middle|bottom)$/;
const ALIGN_VALUES_REG_EXP = /^(left|center|right|justify)$/;

/**
 * The table cell properties editing feature.
 *
 * Introduces table cell model attributes and their conversion:
 *
 * - border: `tableCellBorderStyle`, `tableCellBorderColor` and `tableCellBorderWidth`
 * - background color: `tableCellBackgroundColor`
 * - cell padding: `tableCellPadding`
 * - horizontal and vertical alignment: `tableCellHorizontalAlignment`, `tableCellVerticalAlignment`
 * - cell width and height: `tableCellWidth`, `tableCellHeight`
 *
 * It also registers commands used to manipulate the above attributes:
 *
 * - border: the `'tableCellBorderStyle'`, `'tableCellBorderColor'` and `'tableCellBorderWidth'` commands
 * - background color: the `'tableCellBackgroundColor'` command
 * - cell padding: the `'tableCellPadding'` command
 * - horizontal and vertical alignment: the `'tableCellHorizontalAlignment'` and `'tableCellVerticalAlignment'` commands
 * - width and height: the `'tableCellWidth'` and `'tableCellHeight'` commands
 *
 * @extends module:core/plugin~Plugin
 */
class TableCellPropertiesEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TableCellPropertiesEditing';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ TableEditing ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const schema = editor.model.schema;
		const conversion = editor.conversion;

		editor.config.define( 'table.tableCellProperties.defaultProperties', {} );

		const defaultTableCellProperties = getNormalizedDefaultProperties(
			editor.config.get( 'table.tableCellProperties.defaultProperties' ),
			{
				includeVerticalAlignmentProperty: true,
				includeHorizontalAlignmentProperty: true,
				includePaddingProperty: true,
				isRightToLeftContent: editor.locale.contentLanguageDirection === 'rtl'
			}
		);

		editor.data.addStyleProcessorRules( addBorderRules );
		enableBorderProperties( schema, conversion, {
			color: defaultTableCellProperties.borderColor,
			style: defaultTableCellProperties.borderStyle,
			width: defaultTableCellProperties.borderWidth
		} );
		editor.commands.add( 'tableCellBorderStyle', new TableCellBorderStyleCommand( editor, defaultTableCellProperties.borderStyle ) );
		editor.commands.add( 'tableCellBorderColor', new TableCellBorderColorCommand( editor, defaultTableCellProperties.borderColor ) );
		editor.commands.add( 'tableCellBorderWidth', new TableCellBorderWidthCommand( editor, defaultTableCellProperties.borderWidth ) );

		enableProperty( schema, conversion, {
			modelAttribute: 'tableCellWidth',
			styleName: 'width',
			defaultValue: defaultTableCellProperties.width
		} );
		editor.commands.add( 'tableCellWidth', new TableCellWidthCommand( editor, defaultTableCellProperties.width ) );

		enableProperty( schema, conversion, {
			modelAttribute: 'tableCellHeight',
			styleName: 'height',
			defaultValue: defaultTableCellProperties.height
		} );
		editor.commands.add( 'tableCellHeight', new TableCellHeightCommand( editor, defaultTableCellProperties.height ) );

		editor.data.addStyleProcessorRules( addPaddingRules );
		enableProperty( schema, conversion, {
			modelAttribute: 'tableCellPadding',
			styleName: 'padding',
			reduceBoxSides: true,
			defaultValue: defaultTableCellProperties.padding
		} );
		editor.commands.add( 'tableCellPadding', new TableCellPaddingCommand( editor, defaultTableCellProperties.padding ) );

		editor.data.addStyleProcessorRules( addBackgroundRules );
		enableProperty( schema, conversion, {
			modelAttribute: 'tableCellBackgroundColor',
			styleName: 'background-color',
			defaultValue: defaultTableCellProperties.backgroundColor
		} );
		editor.commands.add(
			'tableCellBackgroundColor',
			new TableCellBackgroundColorCommand( editor, defaultTableCellProperties.backgroundColor )
		);

		enableHorizontalAlignmentProperty( schema, conversion, defaultTableCellProperties.horizontalAlignment );
		editor.commands.add(
			'tableCellHorizontalAlignment',
			new TableCellHorizontalAlignmentCommand( editor, defaultTableCellProperties.horizontalAlignment )
		);

		enableVerticalAlignmentProperty( schema, conversion, defaultTableCellProperties.verticalAlignment );
		editor.commands.add(
			'tableCellVerticalAlignment',
			new TableCellVerticalAlignmentCommand( editor, defaultTableCellProperties.verticalAlignment )
		);
	}
}

// Enables the `'tableCellBorderStyle'`, `'tableCellBorderColor'` and `'tableCellBorderWidth'` attributes for table cells.
//
// @param {module:engine/model/schema~Schema} schema
// @param {module:engine/conversion/conversion~Conversion} conversion
// @param {Object} defaultBorder The default border values.
// @param {String} defaultBorder.color The default `tableCellBorderColor` value.
// @param {String} defaultBorder.style The default `tableCellBorderStyle` value.
// @param {String} defaultBorder.width The default `tableCellBorderWidth` value.
function enableBorderProperties( schema, conversion, defaultBorder ) {
	const modelAttributes = {
		width: 'tableCellBorderWidth',
		color: 'tableCellBorderColor',
		style: 'tableCellBorderStyle'
	};

	schema.extend( 'tableCell', {
		allowAttributes: Object.values( modelAttributes )
	} );

	upcastBorderStyles( conversion, 'td', modelAttributes, defaultBorder );
	upcastBorderStyles( conversion, 'th', modelAttributes, defaultBorder );
	downcastAttributeToStyle( conversion, { modelElement: 'tableCell', modelAttribute: modelAttributes.style, styleName: 'border-style' } );
	downcastAttributeToStyle( conversion, { modelElement: 'tableCell', modelAttribute: modelAttributes.color, styleName: 'border-color' } );
	downcastAttributeToStyle( conversion, { modelElement: 'tableCell', modelAttribute: modelAttributes.width, styleName: 'border-width' } );
}

// Enables the `'tableCellHorizontalAlignment'` attribute for table cells.
//
// @param {module:engine/model/schema~Schema} schema
// @param {module:engine/conversion/conversion~Conversion} conversion
// @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
// @param {String} defaultValue The default horizontal alignment value.
function enableHorizontalAlignmentProperty( schema, conversion, defaultValue ) {
	schema.extend( 'tableCell', {
		allowAttributes: [ 'tableCellHorizontalAlignment' ]
	} );

	conversion.for( 'downcast' )
		.attributeToAttribute( {
			model: {
				name: 'tableCell',
				key: 'tableCellHorizontalAlignment'
			},
			view: alignment => ( {
				key: 'style',
				value: {
					'text-align': alignment
				}
			} )
		} );

	conversion.for( 'upcast' )
		// Support for the `text-align:*;` CSS definition for the table cell alignment.
		.attributeToAttribute( {
			view: {
				name: /^(td|th)$/,
				styles: {
					'text-align': ALIGN_VALUES_REG_EXP
				}
			},
			model: {
				key: 'tableCellHorizontalAlignment',
				value: viewElement => {
					const align = viewElement.getStyle( 'text-align' );

					return align === defaultValue ? null : align;
				}
			}
		} )
		// Support for the `align` attribute as the backward compatibility while pasting from other sources.
		.attributeToAttribute( {
			view: {
				name: /^(td|th)$/,
				attributes: {
					align: ALIGN_VALUES_REG_EXP
				}
			},
			model: {
				key: 'tableCellHorizontalAlignment',
				value: viewElement => {
					const align = viewElement.getAttribute( 'align' );

					return align === defaultValue ? null : align;
				}
			}
		} );
}

// Enables the `'verticalAlignment'` attribute for table cells.
//
// @param {module:engine/model/schema~Schema} schema
// @param {module:engine/conversion/conversion~Conversion} conversion
// @param {String} defaultValue The default vertical alignment value.
function enableVerticalAlignmentProperty( schema, conversion, defaultValue ) {
	schema.extend( 'tableCell', {
		allowAttributes: [ 'tableCellVerticalAlignment' ]
	} );

	conversion.for( 'downcast' )
		.attributeToAttribute( {
			model: {
				name: 'tableCell',
				key: 'tableCellVerticalAlignment'
			},
			view: alignment => ( {
				key: 'style',
				value: {
					'vertical-align': alignment
				}
			} )
		} );

	conversion.for( 'upcast' )
		// Support for the `vertical-align:*;` CSS definition for the table cell alignment.
		.attributeToAttribute( {
			view: {
				name: /^(td|th)$/,
				styles: {
					'vertical-align': VALIGN_VALUES_REG_EXP
				}
			},
			model: {
				key: 'tableCellVerticalAlignment',
				value: viewElement => {
					const align = viewElement.getStyle( 'vertical-align' );

					return align === defaultValue ? null : align;
				}
			}
		} )
		// Support for the `align` attribute as the backward compatibility while pasting from other sources.
		.attributeToAttribute( {
			view: {
				name: /^(td|th)$/,
				attributes: {
					valign: VALIGN_VALUES_REG_EXP
				}
			},
			model: {
				key: 'tableCellVerticalAlignment',
				value: viewElement => {
					const valign = viewElement.getAttribute( 'valign' );

					return valign === defaultValue ? null : valign;
				}
			}
		} );
}

// Enables conversion for an attribute for simple view-model mappings.
//
// @param {module:engine/model/schema~Schema} schema
// @param {module:engine/conversion/conversion~Conversion} conversion
// @param {Object} options
// @param {String} options.modelAttribute
// @param {String} options.styleName
// @param {String} options.defaultValue The default value for the specified `modelAttribute`.
// @param {Boolean} [options.reduceBoxSides=false]
function enableProperty( schema, conversion, options ) {
	const { modelAttribute } = options;

	schema.extend( 'tableCell', {
		allowAttributes: [ modelAttribute ]
	} );

	upcastStyleToAttribute( conversion, { viewElement: /^(td|th)$/, ...options } );
	downcastAttributeToStyle( conversion, { modelElement: 'tableCell', ...options } );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The table cell properties feature. Enables support for setting properties of table cells (size, border, background, etc.).
 *
 * Read more in the {@glink features/table#table-and-cell-styling-tools Table and cell styling tools} section.
 * See also the {@link module:table/tableproperties~TableProperties} plugin.
 *
 * This is a "glue" plugin that loads the
 * {@link module:table/tablecellproperties/tablecellpropertiesediting~TableCellPropertiesEditing table cell properties editing feature} and
 * the {@link module:table/tablecellproperties/tablecellpropertiesui~TableCellPropertiesUI table cell properties UI feature}.
 *
 * @extends module:core/plugin~Plugin
 */
class TableCellProperties extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TableCellProperties';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ TableCellPropertiesEditing, TableCellPropertiesUI ];
	}
}

/**
 * The configuration of the table cell properties user interface (balloon). It allows to define:
 *
 * * The color palette for the cell border color style field (`tableCellProperties.borderColors`),
 * * The color palette for the cell background style field (`tableCellProperties.backgroundColors`).
 *
 *		const tableConfig = {
 *			tableCellProperties: {
 *				borderColors: [
 *					{
 *						color: 'hsl(0, 0%, 90%)',
 *						label: 'Light grey'
 *					},
 *					// ...
 *				],
 *				backgroundColors: [
 *					{
 *						color: 'hsl(120, 75%, 60%)',
 *						label: 'Green'
 *					},
 *					// ...
 *				]
 *			}
 *		};
 *
 * * The default styles for table cells (`tableCellProperties.defaultProperties`):
 *
 *		const tableConfig = {
 *			tableCellProperties: {
 *				defaultProperties: {
 *					horizontalAlignment: 'right',
 *					verticalAlignment: 'bottom',
 *					padding: '5px'
 *				}
 *			}
 *		}
 *
 * 	 {@link module:table/tableproperties~TablePropertiesOptions Read more about the supported properties.}
 *
 * **Note**: The `borderColors` and `backgroundColors` options do not impact the data loaded into the editor,
 * i.e. they do not limit or filter the colors in the data. They are used only in the user interface
 * allowing users to pick colors in a more convenient way. The `defaultProperties` option does impact the data.
 * Default values will not be kept in the editor model.
 *
 * The default color palettes for the cell background and the cell border are the same
 * ({@link module:table/utils/ui/table-properties~defaultColors check out their content}).
 *
 * Both color palette configurations must follow the
 * {@link module:table/table~TableColorConfig table color configuration format}.
 *
 * Read more about configuring the table feature in {@link module:table/table~TableConfig}.
 *
 * @member {Object} module:table/table~TableConfig#tableCellProperties
 */

/**
 * The configuration of the table cell default properties feature.
 *
 * @typedef {Object} module:table/tablecellproperties~TableCellPropertiesOptions
 *
 * @property {String} width The default `width` of the table cell.
 *
 * @property {String} height The default `height` of the table cell.
 *
 * @property {String} padding The default `padding` of the table cell.
 *
 * @property {String} backgroundColor The default `background-color` of the table cell.
 *
 * @property {String} borderColor The default `border-color` of the table cell.
 *
 * @property {String} borderWidth The default `border-width` of the table cell.
 *
 * @property {String} [borderStyle='none'] The default `border-style` of the table cell.
 *
 * @property {String} [horizontalAlignment='center'] The default `horizontalAlignment` of the table cell.
 *
 * @property {String} [verticalAlignment='middle'] The default `verticalAlignment` of the table cell.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module source-editing/utils/formathtml
 */

/**
 * A simple (and naive) HTML code formatter that returns a formatted HTML markup that can be easily
 * parsed by human eyes. It beautifies the HTML code by adding new lines between elements that behave like block elements
 * (https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
 * and a few more like `tr`, `td`, and similar ones) and inserting indents for nested content.
 *
 * WARNING: This function works only on a text that does not contain any indentations or new lines.
 * Calling this function on the already formatted text will damage the formatting.
 *
 * @param {String} input An HTML string to format.
 * @returns {String}
 */
function formatHtml( input ) {
	// A list of block-like elements around which the new lines should be inserted, and within which
	// the indentation of their children should be increased.
	// The list is partially based on https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements that contains
	// a full list of HTML block-level elements.
	// A void element is an element that cannot have any child - https://html.spec.whatwg.org/multipage/syntax.html#void-elements.
	// Note that <pre> element is not listed on this list to avoid breaking whitespace formatting.
	const elementsToFormat = [
		{ name: 'address', isVoid: false },
		{ name: 'article', isVoid: false },
		{ name: 'aside', isVoid: false },
		{ name: 'blockquote', isVoid: false },
		{ name: 'br', isVoid: true },
		{ name: 'details', isVoid: false },
		{ name: 'dialog', isVoid: false },
		{ name: 'dd', isVoid: false },
		{ name: 'div', isVoid: false },
		{ name: 'dl', isVoid: false },
		{ name: 'dt', isVoid: false },
		{ name: 'fieldset', isVoid: false },
		{ name: 'figcaption', isVoid: false },
		{ name: 'figure', isVoid: false },
		{ name: 'footer', isVoid: false },
		{ name: 'form', isVoid: false },
		{ name: 'h1', isVoid: false },
		{ name: 'h2', isVoid: false },
		{ name: 'h3', isVoid: false },
		{ name: 'h4', isVoid: false },
		{ name: 'h5', isVoid: false },
		{ name: 'h6', isVoid: false },
		{ name: 'header', isVoid: false },
		{ name: 'hgroup', isVoid: false },
		{ name: 'hr', isVoid: true },
		{ name: 'input', isVoid: true },
		{ name: 'li', isVoid: false },
		{ name: 'main', isVoid: false },
		{ name: 'nav', isVoid: false },
		{ name: 'ol', isVoid: false },
		{ name: 'p', isVoid: false },
		{ name: 'section', isVoid: false },
		{ name: 'table', isVoid: false },
		{ name: 'tbody', isVoid: false },
		{ name: 'td', isVoid: false },
		{ name: 'textarea', isVoid: false },
		{ name: 'th', isVoid: false },
		{ name: 'thead', isVoid: false },
		{ name: 'tr', isVoid: false },
		{ name: 'ul', isVoid: false }
	];

	const elementNamesToFormat = elementsToFormat.map( element => element.name ).join( '|' );

	// It is not the fastest way to format the HTML markup but the performance should be good enough.
	const lines = input
		// Add new line before and after `<tag>` and `</tag>`.
		// It may separate individual elements with two new lines, but this will be fixed below.
		.replace( new RegExp( `</?(${ elementNamesToFormat })( .*?)?>`, 'g' ), '\n$&\n' )
		// Divide input string into lines, which start with either an opening tag, a closing tag, or just a text.
		.split( '\n' );

	let indentCount = 0;

	return lines
		.filter( line => line.length )
		.map( line => {
			if ( isNonVoidOpeningTag( line, elementsToFormat ) ) {
				return indentLine( line, indentCount++ );
			}

			if ( isClosingTag( line, elementsToFormat ) ) {
				return indentLine( line, --indentCount );
			}

			return indentLine( line, indentCount );
		} )
		.join( '\n' );
}

// Checks, if an argument is an opening tag of a non-void element to be formatted.
//
// @param {String} line String to check.
// @param {Array} elementsToFormat Elements to be formatted.
// @param {String} elementsToFormat.name Element name.
// @param {Boolean} elementsToFormat.isVoid Flag indicating whether element is a void one.
// @returns {Boolean}
function isNonVoidOpeningTag( line, elementsToFormat ) {
	return elementsToFormat.some( element => {
		if ( element.isVoid ) {
			return false;
		}

		if ( !new RegExp( `<${ element.name }( .*?)?>` ).test( line ) ) {
			return false;
		}

		return true;
	} );
}

// Checks, if an argument is a closing tag.
//
// @param {String} line String to check.
// @param {Array} elementsToFormat Elements to be formatted.
// @param {String} elementsToFormat.name Element name.
// @param {Boolean} elementsToFormat.isVoid Flag indicating whether element is a void one.
// @returns {Boolean}
function isClosingTag( line, elementsToFormat ) {
	return elementsToFormat.some( element => {
		return new RegExp( `</${ element.name }>` ).test( line );
	} );
}

// Indents a line by a specified number of characters.
//
// @param {String} line Line to indent.
// @param {Number} indentCount Number of characters to use for indentation.
// @param {String} [indentChar] Indentation character(s). 4 spaces by default.
// @returns {String}
function indentLine( line, indentCount, indentChar = '    ' ) {
	// More about Math.max() here in https://github.com/ckeditor/ckeditor5/issues/10698.
	return `${ indentChar.repeat( Math.max( 0, indentCount ) ) }${ line }`;
}var css_248z$b = ".ck-source-editing-area{overflow:hidden;position:relative}.ck-source-editing-area textarea,.ck-source-editing-area:after{border:1px solid transparent;font-family:monospace;font-size:var(--ck-font-size-normal);line-height:var(--ck-line-height-base);margin:0;padding:var(--ck-spacing-large);white-space:pre-wrap}.ck-source-editing-area:after{content:attr(data-value) \" \";display:block;visibility:hidden}.ck-source-editing-area textarea{border-color:var(--ck-color-base-border);border-radius:0;box-sizing:border-box;height:100%;outline:none;overflow:hidden;position:absolute;resize:none;width:100%}.ck-rounded-corners .ck-source-editing-area textarea,.ck-source-editing-area textarea.ck-rounded-corners{border-radius:var(--ck-border-radius);border-top-left-radius:0;border-top-right-radius:0}.ck-source-editing-area textarea:not([readonly]):focus{border:var(--ck-focus-ring);box-shadow:var(--ck-inner-shadow),0 0;outline:none}";
styleInject(css_248z$b);var sourceEditingIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m12.5 0 5 4.5v15.003h-16V0h11zM3 1.5v3.25l-1.497 1-.003 8 1.5 1v3.254L7.685 18l-.001 1.504H17.5V8.002L16 9.428l-.004-4.22-4.222-3.692L3 1.5z\"/><path d=\"M4.06 6.64a.75.75 0 0 1 .958 1.15l-.085.07L2.29 9.75l2.646 1.89c.302.216.4.62.232.951l-.058.095a.75.75 0 0 1-.951.232l-.095-.058-3.5-2.5V9.14l3.496-2.5zm4.194 6.22a.75.75 0 0 1-.958-1.149l.085-.07 2.643-1.89-2.646-1.89a.75.75 0 0 1-.232-.952l.058-.095a.75.75 0 0 1 .95-.232l.096.058 3.5 2.5v1.22l-3.496 2.5zm7.644-.836 2.122 2.122-5.825 5.809-2.125-.005.003-2.116zm2.539-1.847 1.414 1.414a.5.5 0 0 1 0 .707l-1.06 1.06-2.122-2.12 1.061-1.061a.5.5 0 0 1 .707 0z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const COMMAND_FORCE_DISABLE_ID = 'SourceEditingMode';

/**
 * The source editing feature.
 *
 * It provides the possibility to view and edit the source of the document.
 *
 * For a detailed overview, check the {@glink features/source-editing source editing feature documentation} and the
 * {@glink api/source-editing package page}.
 *
 * @extends module:core/plugin~Plugin
 */
class SourceEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SourceEditing';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ PendingActions ];
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		/**
		 * Flag indicating whether the document source mode is active.
		 *
		 * @observable
		 * @member {Boolean}
		 */
		this.set( 'isSourceEditingMode', false );

		/**
		 * The element replacer instance used to replace the editing roots with the wrapper elements containing the document source.
		 *
		 * @private
		 * @member {module:utils/elementreplacer~ElementReplacer}
		 */
		this._elementReplacer = new ElementReplacer();

		/**
		 * Maps all root names to wrapper elements containing the document source.
		 *
		 * @private
		 * @member {Map.<String,HTMLElement>}
		 */
		this._replacedRoots = new Map();

		/**
		 * Maps all root names to their document data.
		 *
		 * @private
		 * @member {Map.<String,String>}
		 */
		this._dataFromRoots = new Map();
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		editor.ui.componentFactory.add( 'sourceEditing', locale => {
			const buttonView = new ButtonView( locale );

			buttonView.set( {
				label: t( 'Source' ),
				icon: sourceEditingIcon,
				tooltip: true,
				withText: true,
				class: 'ck-source-editing-button'
			} );

			buttonView.bind( 'isOn' ).to( this, 'isSourceEditingMode' );

			// The button should be disabled if one of the following conditions is met:
			buttonView.bind( 'isEnabled' ).to(
				this, 'isEnabled',
				editor, 'isReadOnly',
				editor.plugins.get( PendingActions ), 'hasAny',
				( isEnabled, isEditorReadOnly, hasAnyPendingActions ) => {
					// (1) The plugin itself is disabled.
					if ( !isEnabled ) {
						return false;
					}

					// (2) The editor is in read-only mode.
					if ( isEditorReadOnly ) {
						return false;
					}

					// (3) Any pending action is scheduled. It may change the model, so modifying the document source should be prevented
					// until the model is finally set.
					if ( hasAnyPendingActions ) {
						return false;
					}

					return true;
				}
			);

			this.listenTo( buttonView, 'execute', () => {
				this.isSourceEditingMode = !this.isSourceEditingMode;
			} );

			return buttonView;
		} );

		// Currently, the plugin handles the source editing mode by itself only for the classic editor. To use this plugin with other
		// integrations, listen to the `change:isSourceEditingMode` event and act accordingly.
		if ( this._isAllowedToHandleSourceEditingMode() ) {
			this.on( 'change:isSourceEditingMode', ( evt, name, isSourceEditingMode ) => {
				if ( isSourceEditingMode ) {
					this._showSourceEditing();
					this._disableCommands();
				} else {
					this._hideSourceEditing();
					this._enableCommands();
				}
			} );

			this.on( 'change:isEnabled', ( evt, name, isEnabled ) => this._handleReadOnlyMode( !isEnabled ) );

			this.listenTo( editor, 'change:isReadOnly', ( evt, name, isReadOnly ) => this._handleReadOnlyMode( isReadOnly ) );
		}

		// Update the editor data while calling editor.getData() in the source editing mode.
		editor.data.on( 'get', () => {
			if ( this.isSourceEditingMode ) {
				this._updateEditorData();
			}
		}, { priority: 'high' } );
	}

	/**
	 * @inheritDoc
	 */
	afterInit() {
		const editor = this.editor;

		const collaborationPluginNamesToWarn = [
			'RealTimeCollaborativeEditing',
			'CommentsEditing',
			'TrackChangesEditing',
			'RevisionHistory'
		];

		// Currently, the basic integration with Collaboration Features is to display a warning in the console.
		if ( collaborationPluginNamesToWarn.some( pluginName => editor.plugins.has( pluginName ) ) ) {
			console.warn(
				'You initialized the editor with the source editing feature and at least one of the collaboration features. ' +
				'Please be advised that the source editing feature may not work, and be careful when editing document source ' +
				'that contains markers created by the collaboration features.'
			);
		}

		// Restricted Editing integration can also lead to problems. Warn the user accordingly.
		if ( editor.plugins.has( 'RestrictedEditingModeEditing' ) ) {
			console.warn(
				'You initialized the editor with the source editing feature and restricted editing feature. ' +
				'Please be advised that the source editing feature may not work, and be careful when editing document source ' +
				'that contains markers created by the restricted editing feature.'
			);
		}
	}

	/**
	 * Creates source editing wrappers that replace each editing root. Each wrapper contains the document source from the corresponding
	 * root.
	 *
	 * The wrapper element contains a textarea and it solves the problem, that the textarea element cannot auto expand its height based on
	 * the content it contains. The solution is to make the textarea more like a plain div element, which expands in height as much as it
	 * needs to, in order to display the whole document source without scrolling. The wrapper element is a parent for the textarea and for
	 * the pseudo-element `::after`, that replicates the look, content, and position of the textarea. The pseudo-element replica is hidden,
	 * but it is styled to be an identical visual copy of the textarea with the same content. Then, the wrapper is a grid container and both
	 * of its children (the textarea and the `::after` pseudo-element) are positioned within a CSS grid to occupy the same grid cell. The
	 * content in the pseudo-element `::after` is set in CSS and it stretches the grid to the appropriate size based on the textarea value.
	 * Since both children occupy the same grid cell, both have always the same height.
	 *
	 * @private
	 */
	_showSourceEditing() {
		const editor = this.editor;
		const editingView = editor.editing.view;
		const model = editor.model;

		model.change( writer => {
			writer.setSelection( null );
			writer.removeSelectionAttribute( model.document.selection.getAttributeKeys() );
		} );

		// It is not needed to iterate through all editing roots, as currently the plugin supports only the Classic Editor with a single
		// main root, but this code may help understand and use this feature in external integrations.
		for ( const [ rootName, domRootElement ] of editingView.domRoots ) {
			const data = formatSource( editor.data.get( { rootName } ) );

			const domSourceEditingElementTextarea = createElement( domRootElement.ownerDocument, 'textarea', {
				rows: '1',
				'aria-label': 'Source code editing area'
			} );

			const domSourceEditingElementWrapper = createElement( domRootElement.ownerDocument, 'div', {
				class: 'ck-source-editing-area',
				'data-value': data
			}, [ domSourceEditingElementTextarea ] );

			domSourceEditingElementTextarea.value = data;

			// Setting a value to textarea moves the input cursor to the end. We want the selection at the beginning.
			domSourceEditingElementTextarea.setSelectionRange( 0, 0 );

			// Bind the textarea's value to the wrapper's `data-value` property. Each change of the textarea's value updates the
			// wrapper's `data-value` property.
			domSourceEditingElementTextarea.addEventListener( 'input', () => {
				domSourceEditingElementWrapper.dataset.value = domSourceEditingElementTextarea.value;
			} );

			editingView.change( writer => {
				const viewRoot = editingView.document.getRoot( rootName );

				writer.addClass( 'ck-hidden', viewRoot );
			} );

			// Register the element so it becomes available for Alt+F10 and Esc navigation.
			editor.ui.setEditableElement( 'sourceEditing:' + rootName, domSourceEditingElementTextarea );

			this._replacedRoots.set( rootName, domSourceEditingElementWrapper );

			this._elementReplacer.replace( domRootElement, domSourceEditingElementWrapper );

			this._dataFromRoots.set( rootName, data );
		}

		this._focusSourceEditing();
	}

	/**
	 * Restores all hidden editing roots and sets the source data in them.
	 *
	 * @private
	 */
	_hideSourceEditing() {
		const editor = this.editor;
		const editingView = editor.editing.view;

		this._updateEditorData();

		editingView.change( writer => {
			for ( const [ rootName ] of this._replacedRoots ) {
				writer.removeClass( 'ck-hidden', editingView.document.getRoot( rootName ) );
			}
		} );

		this._elementReplacer.restore();

		this._replacedRoots.clear();
		this._dataFromRoots.clear();

		editingView.focus();
	}

	/**
	 * Updates the source data in all hidden editing roots.
	 *
	 * @private
	 */
	_updateEditorData() {
		const editor = this.editor;
		const data = {};

		for ( const [ rootName, domSourceEditingElementWrapper ] of this._replacedRoots ) {
			const oldData = this._dataFromRoots.get( rootName );
			const newData = domSourceEditingElementWrapper.dataset.value;

			// Do not set the data unless some changes have been made in the meantime.
			// This prevents empty undo steps after switching to the normal editor.
			if ( oldData !== newData ) {
				data[ rootName ] = newData;
			}
		}

		if ( Object.keys( data ).length ) {
			editor.data.set( data, { batchType: { isUndoable: true } } );
		}
	}

	/**
	 * Focuses the textarea containing document source from the first editing root.
	 *
	 * @private
	 */
	_focusSourceEditing() {
		const editor = this.editor;
		const [ domSourceEditingElementWrapper ] = this._replacedRoots.values();
		const textarea = domSourceEditingElementWrapper.querySelector( 'textarea' );

		// The FocusObserver was disabled by View.render() while the DOM root was getting hidden and the replacer
		// revealed the textarea. So it couldn't notice that the DOM root got blurred in the process.
		// Let's sync this state manually here because otherwise Renderer will attempt to render selection
		// in an invisible DOM root.
		editor.editing.view.document.isFocused = false;

		textarea.focus();
	}

	/**
	 * Disables all commands.
	 *
	 * @private
	 */
	_disableCommands() {
		const editor = this.editor;

		for ( const command of editor.commands.commands() ) {
			command.forceDisabled( COMMAND_FORCE_DISABLE_ID );
		}
	}

	/**
	 * Clears forced disable for all commands, that was previously set through {@link #_disableCommands}.
	 *
	 * @private
	 */
	_enableCommands() {
		const editor = this.editor;

		for ( const command of editor.commands.commands() ) {
			command.clearForceDisabled( COMMAND_FORCE_DISABLE_ID );
		}
	}

	/**
	 * Adds or removes the `readonly` attribute from the textarea from all roots, if document source mode is active.
	 *
	 * @param {Boolean} isReadOnly Indicates whether all textarea elements should be read-only.
	 */
	_handleReadOnlyMode( isReadOnly ) {
		if ( !this.isSourceEditingMode ) {
			return;
		}

		for ( const [ , domSourceEditingElementWrapper ] of this._replacedRoots ) {
			domSourceEditingElementWrapper.querySelector( 'textarea' ).readOnly = isReadOnly;
		}
	}

	/**
	 * Checks, if the plugin is allowed to handle the source editing mode by itself. Currently, the source editing mode is supported only
	 * for the {@link module:editor-classic/classiceditor~ClassicEditor classic editor}.
	 *
	 * @private
	 * @returns {Boolean}
	 */
	_isAllowedToHandleSourceEditingMode() {
		const editor = this.editor;
		const editable = editor.ui.view.editable;

		// Checks, if the editor's editable belongs to the editor's DOM tree.
		return editable && !editable._hasExternalElement;
	}
}

// Formats the content for a better readability.
//
// For a non-HTML source the unchanged input string is returned.
//
// @param {String} input Input string to check.
// @returns {Boolean}
function formatSource( input ) {
	if ( !isHtml( input ) ) {
		return input;
	}

	return formatHtml( input );
}

// Checks, if the document source is HTML. It is sufficient to just check the first character from the document data.
//
// @param {String} input Input string to check.
// @returns {Boolean}
function isHtml( input ) {
	return input.startsWith( '<' );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module alignment/utils
 */

/**
 * The list of supported alignment options:
 *
 * * `'left'`,
 * * `'right'`,
 * * `'center'`,
 * * `'justify'`
 */
const supportedOptions = [ 'left', 'right', 'center', 'justify' ];

/**
 * Checks whether the passed option is supported by {@link module:alignment/alignmentediting~AlignmentEditing}.
 *
 * @param {String} option The option value to check.
 * @returns {Boolean}
 */
function isSupported( option ) {
	return supportedOptions.includes( option );
}

/**
 * Checks whether alignment is the default one considering the direction
 * of the editor content.
 *
 * @param {String} alignment The name of the alignment to check.
 * @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
 * @returns {Boolean}
 */
function isDefault( alignment, locale ) {
	// Right now only LTR is supported so the 'left' value is always the default one.

	if ( locale.contentLanguageDirection == 'rtl' ) {
		return alignment === 'right';
	} else {
		return alignment === 'left';
	}
}

/**
 * Brings the configuration to the common form, an array of objects.
 *
 * @param {Array.<String|module:alignment/alignmentediting~AlignmentFormat>} configuredOptions Alignment plugin configuration.
 * @returns {Array.<module:alignment/alignmentediting~AlignmentFormat>} Normalized object holding the configuration.
 */
function normalizeAlignmentOptions( configuredOptions ) {
	const normalizedOptions = configuredOptions
		.map( option => {
			let result;

			if ( typeof option == 'string' ) {
				result = { name: option };
			} else {
				result = option;
			}

			return result;
		} )
		// Remove all unknown options.
		.filter( option => {
			const isNameValid = !!supportedOptions.includes( option.name );
			if ( !isNameValid ) {
				/**
				 * The `name` in one of the `alignment.options` is not recognized.
				 * The available options are: `'left'`, `'right'`, `'center'` and `'justify'`.
				 *
				 * @error alignment-config-name-not-recognized
				 * @param {Object} option Options with unknown value of the `name` property.
				 */
				logWarning( 'alignment-config-name-not-recognized', { option } );
			}

			return isNameValid;
		} );

	const classNameCount = normalizedOptions.filter( option => !!option.className ).length;

	// We either use classes for all styling options or for none.
	if ( classNameCount && classNameCount < normalizedOptions.length ) {
		/**
		 * The `className` property has to be defined for all options once at least one option declares `className`.
		 *
		 * @error alignment-config-classnames-are-missing
		 * @param {Array.<String|module:alignment/alignmentediting~AlignmentFormat>} configuredOptions Contents of `alignment.options`.
		 */
		throw new CKEditorError( 'alignment-config-classnames-are-missing', { configuredOptions } );
	}

	// Validate resulting config.
	normalizedOptions.forEach( ( option, index, allOptions ) => {
		const succeedingOptions = allOptions.slice( index + 1 );
		const nameAlreadyExists = succeedingOptions.some( item => item.name == option.name );

		if ( nameAlreadyExists ) {
			/**
			 * The same `name` in one of the `alignment.options` was already declared.
			 * Each `name` representing one alignment option can be set exactly once.
			 *
			 * @error alignment-config-name-already-defined
			 * @param {Object} option First option that declares given `name`.
			 * @param {Array.<String|module:alignment/alignmentediting~AlignmentFormat>} configuredOptions Contents of `alignment.options`.
			 */
			throw new CKEditorError( 'alignment-config-name-already-defined', { option, configuredOptions } );
		}

		// The `className` property is present. Check for duplicates then.
		if ( option.className ) {
			const classNameAlreadyExists = succeedingOptions.some( item => item.className == option.className );

			if ( classNameAlreadyExists ) {
				/**
				 * The same `className` in one of the `alignment.options` was already declared.
				 *
				 * @error alignment-config-classname-already-defined
				 * @param {Object} option First option that declares given `className`.
				 * @param {Array.<String|module:alignment/alignmentediting~AlignmentFormat>} configuredOptions
				 * Contents of `alignment.options`.
				 */
				throw new CKEditorError( 'alignment-config-classname-already-defined', { option, configuredOptions } );
			}
		}
	} );

	return normalizedOptions;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const ALIGNMENT = 'alignment';

/**
 * The alignment command plugin.
 *
 * @extends module:core/command~Command
 */
class AlignmentCommand extends Command {
	/**
	 * @inheritDoc
	 */
	refresh() {
		const editor = this.editor;
		const locale = editor.locale;
		const firstBlock = first( this.editor.model.document.selection.getSelectedBlocks() );

		// As first check whether to enable or disable the command as the value will always be false if the command cannot be enabled.
		this.isEnabled = !!firstBlock && this._canBeAligned( firstBlock );

		/**
		 * A value of the current block's alignment.
		 *
		 * @observable
		 * @readonly
		 * @member {String} #value
		 */
		if ( this.isEnabled && firstBlock.hasAttribute( 'alignment' ) ) {
			this.value = firstBlock.getAttribute( 'alignment' );
		} else {
			this.value = locale.contentLanguageDirection === 'rtl' ? 'right' : 'left';
		}
	}

	/**
	 * Executes the command. Applies the alignment `value` to the selected blocks.
	 * If no `value` is passed, the `value` is the default one or it is equal to the currently selected block's alignment attribute,
	 * the command will remove the attribute from the selected blocks.
	 *
	 * @param {Object} [options] Options for the executed command.
	 * @param {String} [options.value] The value to apply.
	 * @fires execute
	 */
	execute( options = {} ) {
		const editor = this.editor;
		const locale = editor.locale;
		const model = editor.model;
		const doc = model.document;

		const value = options.value;

		model.change( writer => {
			// Get only those blocks from selected that can have alignment set
			const blocks = Array.from( doc.selection.getSelectedBlocks() ).filter( block => this._canBeAligned( block ) );
			const currentAlignment = blocks[ 0 ].getAttribute( 'alignment' );

			// Remove alignment attribute if current alignment is:
			// - default (should not be stored in model as it will bloat model data)
			// - equal to currently set
			// - or no value is passed - denotes default alignment.
			const removeAlignment = isDefault( value, locale ) || currentAlignment === value || !value;

			if ( removeAlignment ) {
				removeAlignmentFromSelection( blocks, writer );
			} else {
				setAlignmentOnSelection( blocks, writer, value );
			}
		} );
	}

	/**
	 * Checks whether a block can have alignment set.
	 *
	 * @private
	 * @param {module:engine/model/element~Element} block The block to be checked.
	 * @returns {Boolean}
	 */
	_canBeAligned( block ) {
		return this.editor.model.schema.checkAttribute( block, ALIGNMENT );
	}
}

// Removes the alignment attribute from blocks.
// @private
function removeAlignmentFromSelection( blocks, writer ) {
	for ( const block of blocks ) {
		writer.removeAttribute( ALIGNMENT, block );
	}
}

// Sets the alignment attribute on blocks.
// @private
function setAlignmentOnSelection( blocks, writer, alignment ) {
	for ( const block of blocks ) {
		writer.setAttribute( ALIGNMENT, alignment, block );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The alignment editing feature. It introduces the {@link module:alignment/alignmentcommand~AlignmentCommand command} and adds
 * the `alignment` attribute for block elements in the {@link module:engine/model/model~Model model}.
 * @extends module:core/plugin~Plugin
 */
class AlignmentEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'AlignmentEditing';
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		editor.config.define( 'alignment', {
			options: [ ...supportedOptions.map( option => ( { name: option } ) ) ]
		} );
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const locale = editor.locale;
		const schema = editor.model.schema;

		const options = normalizeAlignmentOptions( editor.config.get( 'alignment.options' ) );

		// Filter out unsupported options and those that are redundant, e.g. `left` in LTR / `right` in RTL mode.
		const optionsToConvert = options.filter(
			option => isSupported( option.name ) && !isDefault( option.name, locale )
		);

		// Once there is at least one `className` defined, we switch to alignment with classes.
		const shouldUseClasses = optionsToConvert.some( option => !!option.className );

		// Allow alignment attribute on all blocks.
		schema.extend( '$block', { allowAttributes: 'alignment' } );
		editor.model.schema.setAttributeProperties( 'alignment', { isFormatting: true } );

		if ( shouldUseClasses ) {
			editor.conversion.attributeToAttribute( buildClassDefinition( optionsToConvert ) );
		} else {
			// Downcast inline styles.
			editor.conversion.for( 'downcast' ).attributeToAttribute( buildDowncastInlineDefinition( optionsToConvert ) );
		}

		const upcastInlineDefinitions = buildUpcastInlineDefinitions( optionsToConvert );

		// Always upcast from inline styles.
		for ( const definition of upcastInlineDefinitions ) {
			editor.conversion.for( 'upcast' ).attributeToAttribute( definition );
		}

		const upcastCompatibilityDefinitions = buildUpcastCompatibilityDefinitions( optionsToConvert );

		// Always upcast from deprecated `align` attribute.
		for ( const definition of upcastCompatibilityDefinitions ) {
			editor.conversion.for( 'upcast' ).attributeToAttribute( definition );
		}

		editor.commands.add( 'alignment', new AlignmentCommand( editor ) );
	}
}

// Prepare downcast conversion definition for inline alignment styling.
// @private
function buildDowncastInlineDefinition( options ) {
	const definition = {
		model: {
			key: 'alignment',
			values: options.map( option => option.name )
		},
		view: {}
	};

	for ( const { name } of options ) {
		definition.view[ name ] = {
			key: 'style',
			value: {
				'text-align': name
			}
		};
	}

	return definition;
}

// Prepare upcast definitions for inline alignment styles.
// @private
function buildUpcastInlineDefinitions( options ) {
	const definitions = [];

	for ( const { name } of options ) {
		definitions.push( {
			view: {
				key: 'style',
				value: {
					'text-align': name
				}
			},
			model: {
				key: 'alignment',
				value: name
			}
		} );
	}

	return definitions;
}

// Prepare upcast definitions for deprecated `align` attribute.
// @private
function buildUpcastCompatibilityDefinitions( options ) {
	const definitions = [];

	for ( const { name } of options ) {
		definitions.push( {
			view: {
				key: 'align',
				value: name
			},
			model: {
				key: 'alignment',
				value: name
			}
		} );
	}

	return definitions;
}

// Prepare conversion definitions for upcast and downcast alignment with classes.
// @private
function buildClassDefinition( options ) {
	const definition = {
		model: {
			key: 'alignment',
			values: options.map( option => option.name )
		},
		view: {}
	};

	for ( const option of options ) {
		definition.view[ option.name ] = {
			key: 'class',
			value: option.className
		};
	}

	return definition;
}

/**
 * The alignment configuration format descriptor.
 *
 *		const alignmentFormat = {
 *			name: 'right',
 *			className: 'my-align-right-class'
 *		}
 *
 * @typedef {Object} module:alignment/alignmentediting~AlignmentFormat
 *
 * @property {'left'|'right'|'center'|'justify'} name One of the alignment names options.
 *
 * @property {String} className The CSS class used to represent the style in the view.
 * Used to override default, inline styling for alignment.
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const iconsMap = new Map( [
	[ 'left', icons.alignLeft ],
	[ 'right', icons.alignRight ],
	[ 'center', icons.alignCenter ],
	[ 'justify', icons.alignJustify ]
] );

/**
 * The default alignment UI plugin.
 *
 * It introduces the `'alignment:left'`, `'alignment:right'`, `'alignment:center'` and `'alignment:justify'` buttons
 * and the `'alignment'` dropdown.
 *
 * @extends module:core/plugin~Plugin
 */
class AlignmentUI extends Plugin {
	/**
	 * Returns the localized option titles provided by the plugin.
	 *
	 * The following localized titles corresponding with
	 * {@link module:alignment/alignment~AlignmentConfig#options} are available:
	 *
	 * * `'left'`,
	 * * `'right'`,
	 * * `'center'`,
	 * * `'justify'`.
	 *
	 * @readonly
	 * @type {Object.<String,String>}
	 */
	get localizedOptionTitles() {
		const t = this.editor.t;

		return {
			'left': t( 'Align left' ),
			'right': t( 'Align right' ),
			'center': t( 'Align center' ),
			'justify': t( 'Justify' )
		};
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'AlignmentUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const componentFactory = editor.ui.componentFactory;
		const t = editor.t;
		const options = normalizeAlignmentOptions( editor.config.get( 'alignment.options' ) );

		options
			.map( option => option.name )
			.filter( isSupported )
			.forEach( option => this._addButton( option ) );

		componentFactory.add( 'alignment', locale => {
			const dropdownView = createDropdown( locale );

			// Add existing alignment buttons to dropdown's toolbar.
			const buttons = options.map( option => componentFactory.create( `alignment:${ option.name }` ) );
			addToolbarToDropdown( dropdownView, buttons, { enableActiveItemFocusOnDropdownOpen: true } );

			// Configure dropdown properties an behavior.
			dropdownView.buttonView.set( {
				label: t( 'Text alignment' ),
				tooltip: true
			} );

			dropdownView.toolbarView.isVertical = true;
			dropdownView.toolbarView.ariaLabel = t( 'Text alignment toolbar' );

			dropdownView.extendTemplate( {
				attributes: {
					class: 'ck-alignment-dropdown'
				}
			} );

			// The default icon depends on the direction of the content.
			const defaultIcon = locale.contentLanguageDirection === 'rtl' ? iconsMap.get( 'right' ) : iconsMap.get( 'left' );

			// Change icon to reflect current selection's alignment.
			dropdownView.buttonView.bind( 'icon' ).toMany( buttons, 'isOn', ( ...areActive ) => {
				// Get the index of an active button.
				const index = areActive.findIndex( value => value );

				// If none of the commands is active, display either defaultIcon or the first button's icon.
				if ( index < 0 ) {
					return defaultIcon;
				}

				// Return active button's icon.
				return buttons[ index ].icon;
			} );

			// Enable button if any of the buttons is enabled.
			dropdownView.bind( 'isEnabled' ).toMany( buttons, 'isEnabled', ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) );

			// Focus the editable after executing the command.
			// Overrides a default behaviour where the focus is moved to the dropdown button (#12125).
			this.listenTo( dropdownView, 'execute', () => {
				editor.editing.view.focus();
			} );

			return dropdownView;
		} );
	}

	/**
	 * Helper method for initializing the button and linking it with an appropriate command.
	 *
	 * @private
	 * @param {String} option The name of the alignment option for which the button is added.
	 */
	_addButton( option ) {
		const editor = this.editor;

		editor.ui.componentFactory.add( `alignment:${ option }`, locale => {
			const command = editor.commands.get( 'alignment' );
			const buttonView = new ButtonView( locale );

			buttonView.set( {
				label: this.localizedOptionTitles[ option ],
				icon: iconsMap.get( option ),
				tooltip: true,
				isToggleable: true
			} );

			// Bind button model to command.
			buttonView.bind( 'isEnabled' ).to( command );
			buttonView.bind( 'isOn' ).to( command, 'value', value => value === option );

			// Execute command.
			this.listenTo( buttonView, 'execute', () => {
				editor.execute( 'alignment', { value: option } );
				editor.editing.view.focus();
			} );

			return buttonView;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The text alignment plugin.
 *
 * For a detailed overview, check the {@glink features/text-alignment Text alignment feature documentation}
 * and the {@glink api/alignment package page}.
 *
 * This is a "glue" plugin which loads the {@link module:alignment/alignmentediting~AlignmentEditing} and
 * {@link module:alignment/alignmentui~AlignmentUI} plugins.
 *
 * @extends module:core/plugin~Plugin
 */
class Alignment extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ AlignmentEditing, AlignmentUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Alignment';
	}
}

/**
 * The configuration of the {@link module:alignment/alignment~Alignment alignment feature}.
 *
 * Read more in {@link module:alignment/alignment~AlignmentConfig}.
 *
 * @member {module:alignment/alignment~AlignmentConfig} module:core/editor/editorconfig~EditorConfig#alignment
 */

/**
 * The configuration of the {@link module:alignment/alignment~Alignment alignment feature}.
 *
 *		ClassicEditor
 *			.create( editorElement, {
 *				alignment: {
 *					options: [ 'left', 'right' ]
 *				}
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * See {@link module:core/editor/editorconfig~EditorConfig all editor configuration options}.
 *
 * @interface AlignmentConfig
 */

/**
 * Available alignment options.
 *
 * The available options are: `'left'`, `'right'`, `'center'` and `'justify'`. Other values are ignored.
 *
 * **Note:** It is recommended to always use `'left'` or `'right'` as these are default values which the user should
 * normally be able to choose depending on the
 * {@glink features/ui-language#setting-the-language-of-the-content language of the editor content}.
 *
 *		ClassicEditor
 *			.create( editorElement, {
 *				alignment: {
 *					options: [ 'left', 'right' ]
 *				}
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * By default the alignment is set inline using the `text-align` CSS property. To further customize the alignment,
 * you can provide names of classes for each alignment option using the `className` property.
 *
 * **Note:** Once you define the `className` property for one option, you need to specify it for all other options.
 *
 *		ClassicEditor
 *			.create( editorElement, {
 *				alignment: {
 *					options: [
 *						{ name: 'left', className: 'my-align-left' },
 *						{ name: 'right', className: 'my-align-right' }
 *					]
 *				}
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * See the demo of {@glink features/text-alignment#configuring-alignment-options custom alignment options}.
 *
 * @member {Array.<String|module:alignment/alignmentediting~AlignmentFormat>} module:alignment/alignment~AlignmentConfig#options
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

// These are intermediate element names that can't be rendered as style preview because they don't make sense standalone.
const NON_PREVIEWABLE_ELEMENT_NAMES = [
	'caption', 'colgroup', 'dd', 'dt', 'figcaption', 'legend', 'li', 'optgroup', 'option', 'rp',
	'rt', 'summary', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr'
];

/**
 * A class representing an individual button (style) in the grid. Renders a rich preview of the style.
 *
 * @protected
 * @extends {module:ui/button/buttonview~ButtonView}
 */
class StyleGridButtonView extends ButtonView {
	/**
	 * Creates an instance of the {@link module:style/ui/stylegridbuttonview~StyleGridButtonView} class.
	 *
	 * @param {module:utils/locale~Locale} locale The localization services instance.
	 * @param {module:style/style~StyleDefinition} styleDefinition Definition of the style.
	 */
	constructor( locale, styleDefinition ) {
		super( locale );

		/**
		 * Definition of the style the button will apply when executed.
		 *
		 * @readonly
		 * @member {module:style/style~StyleDefinition} #styleDefinition
		 */
		this.styleDefinition = styleDefinition;

		/**
		 * The view rendering the preview of the style.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/view~View} #previewView
		 */
		this.previewView = this._createPreview();

		this.set( {
			label: styleDefinition.name,
			class: 'ck-style-grid__button',
			withText: true
		} );

		this.extendTemplate( {
			attributes: {
				role: 'option'
			}
		} );

		this.children.add( this.previewView, 0 );
	}

	/**
	 * Creates the view representing the preview of the style.
	 *
	 * @private
	 * @returns {module:ui/view~View}
	 */
	_createPreview() {
		const { element, classes } = this.styleDefinition;
		const previewView = new View( this.locale );

		previewView.setTemplate( {
			tag: 'div',

			attributes: {
				class: [
					'ck',
					'ck-reset_all-excluded',
					'ck-style-grid__button__preview',
					'ck-content'
				]
			},

			children: [
				{
					tag: this._isPreviewable( element ) ? element : 'div',
					attributes: {
						class: classes
					},
					children: [
						{ text: 'AaBbCcDdEeFfGgHhIiJj' }
					]
				}
			]
		} );

		return previewView;
	}

	/**
	 * Decides whether an element should be created in the preview or a substitute `<div>` should
	 * be used instead. This avoids previewing a standalone `<td>`, `<li>`, etc. without a parent.
	 *
	 * @private
	 * @param {String} elementName
	 * @returns {Boolean} `true` when the element can be rendered. `false` otherwise.
	 */
	_isPreviewable( elementName ) {
		return !NON_PREVIEWABLE_ELEMENT_NAMES.includes( elementName );
	}
}var css_248z$a = ":root{--ck-style-panel-columns:3}.ck.ck-style-panel .ck-style-grid{display:grid;grid-template-columns:repeat(var(--ck-style-panel-columns),auto);justify-content:start}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button{display:flex;flex-direction:column;justify-content:space-between}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button .ck-style-grid__button__preview{align-content:center;align-items:center;display:flex;flex-basis:100%;flex-grow:1;justify-content:flex-start}:root{--ck-style-panel-button-width:120px;--ck-style-panel-button-height:80px;--ck-style-panel-button-label-background:#f0f0f0;--ck-style-panel-button-hover-label-background:#ebebeb;--ck-style-panel-button-hover-border-color:#b3b3b3}.ck.ck-style-panel .ck-style-grid{column-gap:var(--ck-spacing-large);row-gap:var(--ck-spacing-large)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button{--ck-color-button-default-hover-background:var(--ck-color-base-background);--ck-color-button-default-active-background:var(--ck-color-base-background);height:var(--ck-style-panel-button-height);padding:0;width:var(--ck-style-panel-button-width)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button:not(:focus){border:1px solid var(--ck-color-base-border)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button .ck-button__label{flex-shrink:0;height:22px;line-height:22px;overflow:hidden;padding:0 var(--ck-spacing-medium);text-overflow:ellipsis;width:100%}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button .ck-style-grid__button__preview{background:var(--ck-color-base-background);border:2px solid var(--ck-color-base-background);opacity:.9;overflow:hidden;padding:var(--ck-spacing-medium);width:100%}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-disabled{--ck-color-button-default-disabled-background:var(--ck-color-base-foreground)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-disabled:not(:focus){border-color:var(--ck-style-panel-button-label-background)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-disabled .ck-style-grid__button__preview{border-color:var(--ck-color-base-foreground);filter:saturate(.3);opacity:.4}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-on{border-color:var(--ck-color-base-active)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-on .ck-button__label{box-shadow:0 -1px 0 var(--ck-color-base-active);z-index:1}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button.ck-on:hover{border-color:var(--ck-color-base-active-focus)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button:not(.ck-on) .ck-button__label{background:var(--ck-style-panel-button-label-background)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button:not(.ck-on):hover .ck-button__label{background:var(--ck-style-panel-button-hover-label-background)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button:hover:not(.ck-disabled):not(.ck-on){border-color:var(--ck-style-panel-button-hover-border-color)}.ck.ck-style-panel .ck-style-grid .ck-style-grid__button:hover:not(.ck-disabled):not(.ck-on) .ck-style-grid__button__preview{opacity:1}";
styleInject(css_248z$a);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A class representing a grid of styles ({@link module:style/ui/stylegridbuttonview~StyleGridButtonView buttons}).
 * Allows users to select a style.
 *
 * @protected
 * @extends module:ui/view~View
 */
class StyleGridView extends View {
	/**
	 * Creates an instance of the {@link module:style/ui/stylegridview~StyleGridView} class.
	 *
	 * @param {module:utils/locale~Locale} locale The localization services instance.
	 * @param {Array.<module:style/style~StyleDefinition>} styleDefinitions Definitions of the styles.
	 */
	constructor( locale, styleDefinitions ) {
		super( locale );

		/**
		 * Array of active style names. They must correspond to the names of styles from
		 * definitions passed to the {@link #constructor}.
		 *
		 * @observable
		 * @readonly
		 * @default []
		 * @member {Array.<String>} #activeStyles
		 */
		this.set( 'activeStyles', [] );

		/**
		 * Array of enabled style names. They must correspond to the names of styles from
		 * definitions passed to the {@link #constructor}.
		 *
		 * @observable
		 * @readonly
		 * @default []
		 * @member {Array.<String>} #enabledStyles
		 */
		this.set( 'enabledStyles', [] );

		/**
		 * A collection of style {@link module:style/ui/stylegridbuttonview~StyleGridButtonView buttons}.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.children = this.createCollection();
		this.children.delegate( 'execute' ).to( this );

		for ( const definition of styleDefinitions ) {
			const gridTileView = new StyleGridButtonView( locale, definition );

			this.children.add( gridTileView );
		}

		this.on( 'change:activeStyles', () => {
			for ( const child of this.children ) {
				child.isOn = this.activeStyles.includes( child.styleDefinition.name );
			}
		} );

		this.on( 'change:enabledStyles', () => {
			for ( const child of this.children ) {
				child.isEnabled = this.enabledStyles.includes( child.styleDefinition.name );
			}
		} );

		this.setTemplate( {
			tag: 'div',

			attributes: {
				class: [
					'ck',
					'ck-style-grid'
				],
				role: 'listbox'
			},

			children: this.children
		} );

		/**
		 * Fired when a {@link module:style/ui/stylegridbuttonview~StyleGridButtonView style} was selected (clicked) by the user.
		 *
		 * @event execute
		 */
	}
}var css_248z$9 = ".ck.ck-style-panel .ck-style-panel__style-group>.ck-label{margin:var(--ck-spacing-large) 0}.ck.ck-style-panel .ck-style-panel__style-group:first-child>.ck-label{margin-top:0}";
styleInject(css_248z$9);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A class representing a group of styles (e.g. "block" or "inline").
 *
 * Renders a {@link module:style/ui/stylegridview~StyleGridView style grid} and a label.
 *
 * @protected
 * @extends module:ui/view~View
 */
class StyleGroupView extends View {
	/**
	 * Creates an instance of the {@link module:style/ui/stylegroupview~StyleGroupView} class.
	 *
	 * @param {module:utils/locale~Locale} locale The localization services instance.
	 * @param {String} label The localized label of the group.
	 * @param {Array.<module:style/style~StyleDefinition>} styleDefinitions Definitions of the styles in the group.
	 */
	constructor( locale, label, styleDefinitions ) {
		super( locale );

		/**
		 * The label of the group.
		 *
		 * @protected
		 * @readonly
		 * @member {module:ui/label~LabelView} #labelView
		 */
		this.labelView = new LabelView( locale );
		this.labelView.text = label;

		/**
		 * The styles grid of the group.
		 *
		 * @readonly
		 * @member {module:style/ui/stylegridview~StyleGridView} #gridView
		 */
		this.gridView = new StyleGridView( locale, styleDefinitions );

		this.setTemplate( {
			tag: 'div',

			attributes: {
				class: [
					'ck',
					'ck-style-panel__style-group'
				],
				role: 'group',
				'aria-labelledby': this.labelView.id
			},

			children: [
				this.labelView,
				this.gridView
			]
		} );
	}
}var css_248z$8 = ":root{--ck-style-panel-max-height:470px}.ck.ck-style-panel{max-height:var(--ck-style-panel-max-height);overflow-y:auto;padding:var(--ck-spacing-large)}";
styleInject(css_248z$8);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A class representing a panel with available content styles. It renders styles in button grids, grouped
 * in categories.
 *
 * @protected
 * @extends module:ui/view~View
 */
class StylePanelView extends View {
	/**
	 * Creates an instance of the {@link module:style/ui/stylegroupview~StyleGroupView} class.
	 *
	 * @param {module:utils/locale~Locale} locale The localization services instance.
	 * @param {Object} styleDefinitions Normalized definitions of the styles.
	 * @param {Array.<module:style/style~StyleDefinition>} styleDefinitions.block Definitions of block styles.
	 * @param {Array.<module:style/style~StyleDefinition>} styleDefinitions.inline Definitions of inline styles.
	 */
	constructor( locale, styleDefinitions ) {
		super( locale );

		const t = locale.t;

		/**
		 * Tracks information about DOM focus in the panel.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
		 *
		 * @readonly
		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
		 */
		this.keystrokes = new KeystrokeHandler();

		/**
		 * A collection of panel children.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.children = this.createCollection();

		/**
		 * A view representing block styles group.
		 *
		 * @readonly
		 * @member {module:style/ui/stylegroupview~StyleGroupView}
		 */
		this.blockStylesGroupView = new StyleGroupView( locale, t( 'Block styles' ), styleDefinitions.block );

		/**
		 * A view representing inline styles group.
		 *
		 * @readonly
		 * @member {module:style/ui/stylegroupview~StyleGroupView}
		 */
		this.inlineStylesGroupView = new StyleGroupView( locale, t( 'Text styles' ), styleDefinitions.inline );

		/**
		 * Array of active style names. They must correspond to the names of styles from
		 * definitions passed to the {@link #constructor}.
		 *
		 * @observable
		 * @readonly
		 * @default []
		 * @member {Array.<String>} #activeStyles
		 */
		this.set( 'activeStyles', [] );

		/**
		 * Array of enabled style names. They must correspond to the names of styles from
		 * definitions passed to the {@link #constructor}.
		 *
		 * @observable
		 * @readonly
		 * @default []
		 * @member {Array.<String>} #enabledStyles
		 */
		this.set( 'enabledStyles', [] );

		/**
		 * A collection of views that can be focused in the panel.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this._focusables = new ViewCollection();

		/**
		 * Helps cycling over {@link #_focusables} in the panel.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/focuscycler~FocusCycler}
		 */
		this._focusCycler = new FocusCycler( {
			focusables: this._focusables,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				// Navigate style buttons backwards using the ↑ or ← keystrokes.
				focusPrevious: [ 'arrowup', 'arrowleft' ],

				// Navigate style buttons forward using the Arrow ↓ or → keystrokes.
				focusNext: [ 'arrowdown', 'arrowright' ]
			}
		} );

		if ( styleDefinitions.block.length ) {
			this.children.add( this.blockStylesGroupView );
		}

		if ( styleDefinitions.inline.length ) {
			this.children.add( this.inlineStylesGroupView );
		}

		this.blockStylesGroupView.gridView.delegate( 'execute' ).to( this );
		this.inlineStylesGroupView.gridView.delegate( 'execute' ).to( this );

		this.blockStylesGroupView.gridView.bind( 'activeStyles', 'enabledStyles' ).to( this );
		this.inlineStylesGroupView.gridView.bind( 'activeStyles', 'enabledStyles' ).to( this );

		this.setTemplate( {
			tag: 'div',

			attributes: {
				class: [
					'ck',
					'ck-style-panel'
				]
			},

			children: this.children
		} );

		/**
		 * Fired when a style was selected (clicked) by the user.
		 *
		 * @event execute
		 */
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		const childViews = [
			...this.blockStylesGroupView.gridView.children,
			...this.inlineStylesGroupView.gridView.children
		];

		childViews.forEach( v => {
			// Register the view as focusable.
			this._focusables.add( v );

			// Register the view in the focus tracker.
			this.focusTracker.add( v.element );
		} );

		this.keystrokes.listenTo( this.element );
	}

	/**
	 * Focuses the first focusable element in the panel.
	 */
	focus() {
		this._focusCycler.focusFirst();
	}

	/**
	 * Focuses the last focusable element in the panel.
	 */
	focusLast() {
		this._focusCycler.focusLast();
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module style/utils
 */

/**
 * Normalizes {@link module:style/style~StyleConfig#definitions} in the configuration of the styles feature.
 * The structure of normalized styles looks as follows:
 *
 *		{
 *			block: [
 *				<module:style/style~StyleDefinition>,
 *				<module:style/style~StyleDefinition>,
 *				...
 *			],
 *			inline: [
 *				<module:style/style~StyleDefinition>,
 *				<module:style/style~StyleDefinition>,
 *				...
 *			]
 *		}
 *
 * @protected
 * @param {module:html-support/dataschema~DataSchema} dataSchema
 * @param {Array.<module:style/style~StyleDefinition>} styleDefinitions
 * @returns {Object} An object with normalized style definitions grouped into `block` and `inline` categories (arrays).
 */
function normalizeConfig( dataSchema, styleDefinitions = [] ) {
	const normalizedDefinitions = {
		block: [],
		inline: []
	};

	for ( const definition of styleDefinitions ) {
		const modelElements = [];
		const ghsAttributes = [];

		for ( const ghsDefinition of dataSchema.getDefinitionsForView( definition.element ) ) {
			if ( ghsDefinition.isBlock ) {
				modelElements.push( ghsDefinition.model );
			} else {
				ghsAttributes.push( ghsDefinition.model );
			}
		}

		if ( modelElements.length ) {
			normalizedDefinitions.block.push( { ...definition, modelElements, isBlock: true } );
		} else {
			normalizedDefinitions.inline.push( { ...definition, ghsAttributes } );
		}
	}
	return normalizedDefinitions;
}var css_248z$7 = ".ck.ck-dropdown.ck-style-dropdown.ck-style-dropdown_multiple-active>.ck-button>.ck-button__label{font-style:italic}";
styleInject(css_248z$7);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The UI plugin of the style feature .
 *
 * It registers the `'style'` UI dropdown in the editor's {@link module:ui/componentfactory~ComponentFactory component factory}
 * that displays a grid of styles and allows changing styles of the content.
 *
 * @extends module:core/plugin~Plugin
 */
class StyleUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'StyleUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const dataSchema = editor.plugins.get( 'DataSchema' );
		const normalizedStyleDefinitions = normalizeConfig( dataSchema, editor.config.get( 'style.definitions' ) );

		// Add the dropdown to the component factory.
		editor.ui.componentFactory.add( 'style', locale => {
			const t = locale.t;
			const dropdown = createDropdown( locale );
			const panelView = new StylePanelView( locale, normalizedStyleDefinitions );
			const styleCommand = editor.commands.get( 'style' );

			// The entire dropdown will be disabled together with the command (e.g. when the editor goes read-only).
			dropdown.bind( 'isEnabled' ).to( styleCommand );

			// Put the styles panel is the dropdown.
			dropdown.panelView.children.add( panelView );

			// This dropdown has no icon. It displays text label depending on the selection.
			dropdown.buttonView.withText = true;

			// The label of the dropdown is dynamic and depends on how many styles are active at a time.
			dropdown.buttonView.bind( 'label' ).to( styleCommand, 'value', value => {
				if ( value.length > 1 ) {
					return t( 'Multiple styles' );
				} else if ( value.length === 1 ) {
					return value[ 0 ];
				} else {
					return t( 'Styles' );
				}
			} );

			// The dropdown has a static CSS class for easy customization. There's another CSS class
			// that gets displayed when multiple styles are active at a time allowing visual customization of
			// the label.
			dropdown.bind( 'class' ).to( styleCommand, 'value', value => {
				const classes = [
					'ck-style-dropdown'
				];

				if ( value.length > 1 ) {
					classes.push( 'ck-style-dropdown_multiple-active' );
				}

				return classes.join( ' ' );
			} );

			// Close the dropdown when a style is selected in the styles panel.
			panelView.delegate( 'execute' ).to( dropdown );

			// Execute the command when a style is selected in the styles panel.
			// Also focus the editable after executing the command.
			// It overrides a default behaviour where the focus is moved to the dropdown button (#12125).
			dropdown.on( 'execute', evt => {
				editor.execute( 'style', { styleName: evt.source.styleDefinition.name } );
				editor.editing.view.focus();
			} );

			// Bind the state of the styles panel to the command.
			panelView.bind( 'activeStyles' ).to( styleCommand, 'value' );
			panelView.bind( 'enabledStyles' ).to( styleCommand, 'enabledStyles' );

			return dropdown;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Style command.
 *
 * Applies and removes styles from selection and elements.
 *
 * @extends module:core/command~Command
 */
class StyleCommand extends Command {
	/**
	 * Creates an instance of the command.
	 *
	 * @param {module:core/editor/editor~Editor} editor Editor on which this command will be used.
	 * @param {Object} styleDefinitions Normalized definitions of the styles.
	 * @param {Array.<module:style/style~StyleDefinition>} styleDefinitions.block Definitions of block styles.
	 * @param {Array.<module:style/style~StyleDefinition>} styleDefinitions.inline Definitions of inline styles.
	 */
	constructor( editor, styleDefinitions ) {
		super( editor );

		/**
		 * Set of currently applied styles on the current selection.
		 *
		 * Names of styles correspond to the `name` property of
		 * {@link module:style/style~StyleDefinition configured definitions}.
		 *
		 * @readonly
		 * @observable
		 * @member {Array.<String>} #value
		 */
		this.set( 'value', [] );

		/**
		 * Names of enabled styles (styles that can be applied to the current selection).
		 *
		 * Names of enabled styles correspond to the `name` property of
		 * {@link module:style/style~StyleDefinition configured definitions}.
		 *
		 * @readonly
		 * @observable
		 * @member {Array.<String>} #enabledStyles
		 */
		this.set( 'enabledStyles', [] );

		/**
		 * Normalized definitions of the styles.
		 *
		 * @private
		 * @readonly
		 * @member {Object} #styleDefinitions
		 */
		this._styleDefinitions = styleDefinitions;
	}

	/**
	 * @inheritDoc
	 */
	refresh() {
		const model = this.editor.model;
		const selection = model.document.selection;

		const value = new Set();
		const enabledStyles = new Set();

		// Inline styles.
		for ( const definition of this._styleDefinitions.inline ) {
			for ( const ghsAttributeName of definition.ghsAttributes ) {
				// Check if this inline style is enabled.
				if ( model.schema.checkAttributeInSelection( selection, ghsAttributeName ) ) {
					enabledStyles.add( definition.name );
				}

				// Check if this inline style is active.
				const ghsAttributeValue = this._getValueFromFirstAllowedNode( ghsAttributeName );

				if ( hasAllClasses( ghsAttributeValue, definition.classes ) ) {
					value.add( definition.name );
				}
			}
		}

		// Block styles.
		const firstBlock = first( selection.getSelectedBlocks() );

		if ( firstBlock ) {
			const ancestorBlocks = firstBlock.getAncestors( { includeSelf: true, parentFirst: true } );

			for ( const block of ancestorBlocks ) {
				// E.g. reached a model table when the selection is in a cell. The command should not modify
				// ancestors of a table.
				if ( model.schema.isLimit( block ) ) {
					break;
				}

				if ( !model.schema.checkAttribute( block, 'htmlAttributes' ) ) {
					continue;
				}

				for ( const definition of this._styleDefinitions.block ) {
					// Check if this block style is enabled.
					if ( !definition.modelElements.includes( block.name ) ) {
						continue;
					}

					enabledStyles.add( definition.name );

					// Check if this block style is active.
					const ghsAttributeValue = block.getAttribute( 'htmlAttributes' );

					if ( hasAllClasses( ghsAttributeValue, definition.classes ) ) {
						value.add( definition.name );
					}
				}
			}
		}

		this.enabledStyles = Array.from( enabledStyles ).sort();
		this.isEnabled = this.enabledStyles.length > 0;
		this.value = this.isEnabled ? Array.from( value ).sort() : [];
	}

	/**
	 * Executes the command &mdash; applies the style classes to the selection or removes it from the selection.
	 *
	 * If the command value already contains the requested style, it will remove the style classes. Otherwise, it will set it.
	 *
	 * The execution result differs, depending on the {@link module:engine/model/document~Document#selection} and the
	 * style type (inline or block):
	 *
	 * * When applying inline styles:
	 *   * If the selection is on a range, the command applies the style classes to all nodes in that range.
	 *   * If the selection is collapsed in a non-empty node, the command applies the style classes to the
	 * {@link module:engine/model/document~Document#selection}.
	 *
	 * * When applying block styles:
	 *   * If the selection is on a range, the command applies the style classes to the nearest block parent element.
	 *
	 * @fires execute
	 * @param {Object} [options] Command options.
	 * @param {String} options.styleName Style name matching the one defined in the
	 * {@link module:style/style~StyleConfig#definitions configuration}.
	 * @param {Boolean} [options.forceValue] Whether the command should add given style (`true`) or remove it (`false`) from the selection.
	 * If not set (default), the command will toggle the style basing on the first selected node. Note, that this will not force
	 * setting a style on an element that cannot receive given style.
	 */
	execute( { styleName, forceValue } ) {
		if ( !this.enabledStyles.includes( styleName ) ) {
			/**
			 * Style command can be executed only with a correct style name.
			 *
			 * This warning may be caused by:
			 *
			 * * passing a name that is not specified in the {@link module:style/style~StyleConfig#definitions configuration}
			 * (e.g. a CSS class name),
			 * * when trying to apply a style that is not allowed on a given element.
			 *
			 * @error style-command-executed-with-incorrect-style-name
			 */
			logWarning( 'style-command-executed-with-incorrect-style-name' );

			return;
		}

		const model = this.editor.model;
		const selection = model.document.selection;
		const htmlSupport = this.editor.plugins.get( 'GeneralHtmlSupport' );

		const definition = [
			...this._styleDefinitions.inline,
			...this._styleDefinitions.block
		].find( ( { name } ) => name == styleName );

		const shouldAddStyle = forceValue === undefined ? !this.value.includes( definition.name ) : forceValue;

		model.change( () => {
			let selectables;

			if ( definition.isBlock ) {
				selectables = getAffectedBlocks( selection.getSelectedBlocks(), definition.modelElements, model.schema );
			} else {
				selectables = [ selection ];
			}

			for ( const selectable of selectables ) {
				if ( shouldAddStyle ) {
					htmlSupport.addModelHtmlClass( definition.element, definition.classes, selectable );
				} else {
					htmlSupport.removeModelHtmlClass( definition.element, definition.classes, selectable );
				}
			}
		} );
	}

	/**
	 * Checks the attribute value of the first node in the selection that allows the attribute.
	 * For the collapsed selection, returns the selection attribute.
	 *
	 * @private
	 * @param {String} attributeName Name of the GHS attribute.
	 * @returns {Object|null} The attribute value.
	 */
	_getValueFromFirstAllowedNode( attributeName ) {
		const model = this.editor.model;
		const schema = model.schema;
		const selection = model.document.selection;

		if ( selection.isCollapsed ) {
			return selection.getAttribute( attributeName );
		}

		for ( const range of selection.getRanges() ) {
			for ( const item of range.getItems() ) {
				if ( schema.checkAttribute( item, attributeName ) ) {
					return item.getAttribute( attributeName );
				}
			}
		}

		return null;
	}
}

// Verifies if all classes are present in the given GHS attribute.
function hasAllClasses( ghsAttributeValue, classes ) {
	if ( !ghsAttributeValue || !ghsAttributeValue.classes ) {
		return false;
	}

	return classes.every( className => ghsAttributeValue.classes.includes( className ) );
}

// Returns a set of elements that should be affected by the block-style change.
function getAffectedBlocks( selectedBlocks, elementNames, schema ) {
	const blocks = new Set();

	for ( const selectedBlock of selectedBlocks ) {
		const ancestorBlocks = selectedBlock.getAncestors( { includeSelf: true, parentFirst: true } );

		for ( const block of ancestorBlocks ) {
			if ( schema.isLimit( block ) ) {
				break;
			}

			if ( elementNames.includes( block.name ) ) {
				blocks.add( block );

				break;
			}
		}
	}

	return blocks;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The style engine feature.
 *
 * It configures the {@glink features/general-html-support General HTML Support feature} based on
 * {@link module:style/style~StyleConfig#definitions configured style definitions} and introduces the
 * {@link module:style/stylecommand~StyleCommand style command} that applies styles to the content of the document.
 *
 * @extends module:core/plugin~Plugin
 */
class StyleEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'StyleEditing';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ 'GeneralHtmlSupport' ];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const dataSchema = editor.plugins.get( 'DataSchema' );
		const normalizedStyleDefinitions = normalizeConfig( dataSchema, editor.config.get( 'style.definitions' ) );

		editor.commands.add( 'style', new StyleCommand( editor, normalizedStyleDefinitions ) );

		this._configureGHSDataFilter( normalizedStyleDefinitions );
	}

	/**
	 * This is where the styles feature configures the GHS feature. This method translates normalized
	 * {@link module:style/style~StyleDefinition style definitions} to {@link module:engine/view/matcher~MatcherPattern matcher patterns}
	 * and feeds them to the GHS {@link module:html-support/datafilter~DataFilter} plugin.
	 *
	 * @private
	 * @param {Object} normalizedStyleDefinitions
	 */
	_configureGHSDataFilter( { block: blockDefinitions, inline: inlineDefinitions } ) {
		const ghsDataFilter = this.editor.plugins.get( 'DataFilter' );

		ghsDataFilter.loadAllowedConfig( blockDefinitions.map( normalizedStyleDefinitionToMatcherPattern ) );
		ghsDataFilter.loadAllowedConfig( inlineDefinitions.map( normalizedStyleDefinitionToMatcherPattern ) );
	}
}

// Translates a normalized style definition to a view matcher pattern.
//
// @param {Object} definition A normalized style definition.
// @returns {module:engine/view/matcher~MatcherPattern}
function normalizedStyleDefinitionToMatcherPattern( { element, classes } ) {
	return {
		name: element,
		classes
	};
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The style plugin.
 *
 * This is a "glue" plugin that loads the {@link module:style/styleediting~StyleEditing style editing feature}
 * and {@link module:style/styleui~StyleUI style UI feature}.
 *
 * @extends module:core/plugin~Plugin
 */
class Style extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Style';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ StyleEditing, StyleUI ];
	}
}

/**
 * The configuration of the {@link module:style/style~Style} feature.
 *
 * Read more in {@link module:style/style~StyleConfig}.
 *
 * @member {module:style/style~StyleConfig} module:core/editor/editorconfig~EditorConfig#style
 */

/**
 * The configuration of the style feature.
 *
 *		ClassicEditor
 *			.create( {
 * 				style: ... // Style feature config.
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
 *
 * @interface StyleConfig
 */

/**
 * The available style definitions.
 *
 * Style definitions are displayed in the `'style'` UI dropdown and get applied by the
 * {@link module:style/stylecommand~StyleCommand style command} to the content of the document.
 *
 * In the `'style'` UI dropdown, definitions are automatically grouped into two categories based on the of the `element` property:
 *
 * * **Block styles**: Can be applied to entire {@link module:html-support/dataschema~DataSchema#registerBlockElement block elements}
 * only (e.g. headings, paragraphs, divs).
 * * **Text styles**: Can by applied to any {@link module:html-support/dataschema~DataSchema#registerInlineElement text} in any element
 * in the document.
 *
 * An example configuration:
 *
 *		[
 *			// Definitions of block styles.
 *			{
 *				name: 'Red heading',
 *				element: 'h2',
 *				classes: [ 'red-heading' ]
 *			},
 *			{
 *				name: 'Vibrant code',
 *				element: 'pre',
 *				classes: [ 'vibrant-code' ]
 *			},
 *
 *			// Definitions of text (inline) styles.
 *			{
 *				name: 'Marker',
 *				element: 'span',
 *				classes: [ 'marker' ]
 *			},
 *			// ...
 *		]
 *
 * **Note**: Configuring style definitions will automatically configure the
 * {@glink features/general-html-support General HTML Support feature}. **You do not need to repeat the configuration in
 * {@link module:html-support/generalhtmlsupport~GeneralHtmlSupportConfig}**.
 *
 * @member {Array.<module:style/style~StyleDefinition>} module:style/style~StyleConfig#definitions
 */

/**
 * Style definition.
 *
 * An object describing a style definition. It contains the style `name`, `element` name and an array of CSS `classes`.
 *
 *		// This style will create <h2 class="foo">...</h2> in the document data.
 *		{
 *			name: 'Example style',
 *			element: 'h2',
 *			classes: [ 'foo' ]
 *		}
 *
 * @typedef {Object} module:style/style~StyleDefinition
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module html-support/schemadefinitions
 */

// Skipped elements due to HTML deprecation:
// * noframes (not sure if we should provide support for this element. CKE4 is not supporting frameset and frame,
//   but it will unpack <frameset><noframes>foobar</noframes></frameset> to <noframes>foobar</noframes>, so there
//   may be some content loss. Although using noframes as a standalone element seems invalid)
// * keygen (this one is also empty)
// * applet (support is limited mostly to old IE)
// * basefont (this one is also empty)
// * isindex (basically no support for modern browsers at all)
//
// Skipped elements due to lack empty element support:
// * hr
// * area
// * br
// * command
// * map
// * wbr
// * colgroup -> col
//
// Skipped elements due to complexity:
// * datalist with option elements used as a data source for input[list] element
//
// Skipped elements as they are handled as an object content:
// * track
// * source
// * option
// * param
// * optgroup
//
// Skipped full page HTML elements:
// * body
// * html
// * title
// * head
// * meta
// * link
// * etc...
//
// Skipped hidden elements:
// noscript

var defaultConfig = {
	block: [
		// Existing features
		{
			model: 'codeBlock',
			view: 'pre'
		},
		{
			model: 'paragraph',
			view: 'p'
		},
		{
			model: 'blockQuote',
			view: 'blockquote'
		},
		{
			model: 'listItem',
			view: 'li'
		},
		{
			model: 'pageBreak',
			view: 'div'
		},
		{
			model: 'rawHtml',
			view: 'div'
		},
		{
			model: 'table',
			view: 'table'
		},
		{
			model: 'tableRow',
			view: 'tr'
		},
		{
			model: 'tableCell',
			view: 'td'
		},
		{
			model: 'tableCell',
			view: 'th'
		},
		{
			model: 'caption',
			view: 'caption'
		},
		{
			model: 'caption',
			view: 'figcaption'
		},
		{
			model: 'imageBlock',
			view: 'img'
		},
		{
			model: 'imageInline',
			view: 'img'
		},

		// Compatibility features
		{
			model: 'htmlP',
			view: 'p',
			modelSchema: {
				inheritAllFrom: '$block'
			}
		},
		{
			model: 'htmlBlockquote',
			view: 'blockquote',
			modelSchema: {
				inheritAllFrom: '$container'
			}
		},
		{
			model: 'htmlTable',
			view: 'table',
			modelSchema: {
				allowWhere: '$block',
				isBlock: true
			}
		},
		{
			model: 'htmlTbody',
			view: 'tbody',
			modelSchema: {
				allowIn: 'htmlTable',
				isBlock: true
			}
		},
		{
			model: 'htmlThead',
			view: 'thead',
			modelSchema: {
				allowIn: 'htmlTable',
				isBlock: true
			}
		},
		{
			model: 'htmlTfoot',
			view: 'tfoot',
			modelSchema: {
				allowIn: 'htmlTable',
				isBlock: true
			}
		},
		{
			model: 'htmlCaption',
			view: 'caption',
			modelSchema: {
				allowIn: 'htmlTable',
				allowChildren: '$text',
				isBlock: true
			}
		},
		{
			model: 'htmlColgroup',
			view: 'colgroup',
			modelSchema: {
				allowIn: 'htmlTable',
				allowChildren: 'col',
				isBlock: true
			}
		},
		{
			model: 'htmlCol',
			view: 'col',
			modelSchema: {
				allowIn: 'htmlColgroup',
				isBlock: true
			}
		},
		{
			model: 'htmlTr',
			view: 'tr',
			modelSchema: {
				allowIn: [ 'htmlTable', 'htmlThead', 'htmlTbody' ]
			}
		},
		// TODO can also include text.
		{
			model: 'htmlTd',
			view: 'td',
			modelSchema: {
				allowIn: 'htmlTr',
				allowContentOf: '$container'
			}
		},
		// TODO can also include text.
		{
			model: 'htmlTh',
			view: 'th',
			modelSchema: {
				allowIn: 'htmlTr',
				allowContentOf: '$container'
			}
		},
		// TODO can also include text.
		{
			model: 'htmlFigure',
			view: 'figure',
			modelSchema: {
				inheritAllFrom: '$container',
				isBlock: true
			}
		},
		// TODO can also include other block elements.
		{
			model: 'htmlFigcaption',
			view: 'figcaption',
			modelSchema: {
				allowIn: 'htmlFigure',
				allowChildren: '$text',
				isBlock: true
			}
		},
		// TODO can also include text.
		{
			model: 'htmlAddress',
			view: 'address',
			modelSchema: {
				inheritAllFrom: '$container',
				isBlock: true
			}
		},
		// TODO can also include text.
		{
			model: 'htmlAside',
			view: 'aside',
			modelSchema: {
				inheritAllFrom: '$container',
				isBlock: true
			}
		},
		// TODO can also include text.
		{
			model: 'htmlMain',
			view: 'main',
			modelSchema: {
				inheritAllFrom: '$container',
				isBlock: true
			}
		},
		// TODO can also include text.
		{
			model: 'htmlDetails',
			view: 'details',
			modelSchema: {
				inheritAllFrom: '$container',
				isBlock: true
			}
		},
		{
			model: 'htmlSummary',
			view: 'summary',
			modelSchema: {
				allowChildren: '$text',
				allowIn: 'htmlDetails',
				isBlock: true
			}
		},
		{
			model: 'htmlDiv',
			view: 'div',
			paragraphLikeModel: 'htmlDivParagraph',
			modelSchema: {
				inheritAllFrom: '$container'
			}
		},
		// TODO can also include text.
		{
			model: 'htmlFieldset',
			view: 'fieldset',
			modelSchema: {
				inheritAllFrom: '$container',
				isBlock: true
			}
		},
		// TODO can also include h1-h6.
		{
			model: 'htmlLegend',
			view: 'legend',
			modelSchema: {
				allowIn: 'htmlFieldset',
				allowChildren: '$text'
			}
		},
		// TODO can also include text.
		{
			model: 'htmlHeader',
			view: 'header',
			modelSchema: {
				inheritAllFrom: '$container',
				isBlock: true
			}
		},
		// TODO can also include text.
		{
			model: 'htmlFooter',
			view: 'footer',
			modelSchema: {
				inheritAllFrom: '$container',
				isBlock: true
			}
		},
		// TODO can also include text.
		{
			model: 'htmlForm',
			view: 'form',
			modelSchema: {
				inheritAllFrom: '$container',
				isBlock: true
			}
		},
		{
			model: 'htmlHgroup',
			view: 'hgroup',
			modelSchema: {
				allowChildren: [
					'htmlH1',
					'htmlH2',
					'htmlH3',
					'htmlH4',
					'htmlH5',
					'htmlH6'
				],
				isBlock: true
			}
		},
		{
			model: 'htmlH1',
			view: 'h1',
			modelSchema: {
				inheritAllFrom: '$block'
			}
		},
		{
			model: 'htmlH2',
			view: 'h2',
			modelSchema: {
				inheritAllFrom: '$block'
			}
		},
		{
			model: 'htmlH3',
			view: 'h3',
			modelSchema: {
				inheritAllFrom: '$block'
			}
		},
		{
			model: 'htmlH4',
			view: 'h4',
			modelSchema: {
				inheritAllFrom: '$block'
			}
		},
		{
			model: 'htmlH5',
			view: 'h5',
			modelSchema: {
				inheritAllFrom: '$block'
			}
		},
		{
			model: 'htmlH6',
			view: 'h6',
			modelSchema: {
				inheritAllFrom: '$block'
			}
		},
		{
			model: '$htmlList',
			modelSchema: {
				allowWhere: '$container',
				allowChildren: [ '$htmlList', 'htmlLi' ],
				isBlock: true
			}
		},
		{
			model: 'htmlDir',
			view: 'dir',
			modelSchema: {
				inheritAllFrom: '$htmlList'
			}
		},
		{
			model: 'htmlMenu',
			view: 'menu',
			modelSchema: {
				inheritAllFrom: '$htmlList'
			}
		},
		{
			model: 'htmlUl',
			view: 'ul',
			modelSchema: {
				inheritAllFrom: '$htmlList'
			}
		},
		{
			model: 'htmlOl',
			view: 'ol',
			modelSchema: {
				inheritAllFrom: '$htmlList'
			}
		},
		// TODO can also include other block elements.
		{
			model: 'htmlLi',
			view: 'li',
			modelSchema: {
				allowIn: '$htmlList',
				allowChildren: '$text',
				isBlock: true
			}
		},
		{
			model: 'htmlPre',
			view: 'pre',
			modelSchema: {
				inheritAllFrom: '$block'
			}
		},
		{
			model: 'htmlArticle',
			view: 'article',
			modelSchema: {
				inheritAllFrom: '$container',
				isBlock: true
			}
		},
		{
			model: 'htmlSection',
			view: 'section',
			modelSchema: {
				inheritAllFrom: '$container',
				isBlock: true
			}
		},
		// TODO can also include text.
		{
			model: 'htmlNav',
			view: 'nav',
			modelSchema: {
				inheritAllFrom: '$container',
				isBlock: true
			}
		},
		{
			model: 'htmlDl',
			view: 'dl',
			modelSchema: {
				allowWhere: '$container',
				allowChildren: [ 'htmlDt', 'htmlDd' ],
				isBlock: true
			}
		},
		{
			model: 'htmlDt',
			view: 'dt',
			modelSchema: {
				allowChildren: '$block',
				isBlock: true
			}
		},
		{
			model: 'htmlDd',
			view: 'dd',
			modelSchema: {
				allowChildren: '$block',
				isBlock: true
			}
		},
		{
			model: 'htmlCenter',
			view: 'center',
			modelSchema: {
				inheritAllFrom: '$container',
				isBlock: true
			}
		}
	],
	inline: [
		{
			model: 'htmlAcronym',
			view: 'acronym',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlTt',
			view: 'tt',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlFont',
			view: 'font',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlTime',
			view: 'time',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlVar',
			view: 'var',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlBig',
			view: 'big',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlSmall',
			view: 'small',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlSamp',
			view: 'samp',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlQ',
			view: 'q',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlOutput',
			view: 'output',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlKbd',
			view: 'kbd',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlBdi',
			view: 'bdi',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlBdo',
			view: 'bdo',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlAbbr',
			view: 'abbr',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlA',
			view: 'a',
			priority: 5,
			coupledAttribute: 'linkHref',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlStrong',
			view: 'strong',
			coupledAttribute: 'bold',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlB',
			view: 'b',
			coupledAttribute: 'bold',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlI',
			view: 'i',
			coupledAttribute: 'italic',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlEm',
			view: 'em',
			coupledAttribute: 'italic',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlS',
			view: 's',
			coupledAttribute: 'strikethrough',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		// TODO According to HTML-spec can behave as div-like element, although CKE4 only handles it as an inline element.
		{
			model: 'htmlDel',
			view: 'del',
			coupledAttribute: 'strikethrough',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		// TODO According to HTML-spec can behave as div-like element, although CKE4 only handles it as an inline element.
		{
			model: 'htmlIns',
			view: 'ins',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlU',
			view: 'u',
			coupledAttribute: 'underline',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlSub',
			view: 'sub',
			coupledAttribute: 'subscript',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlSup',
			view: 'sup',
			coupledAttribute: 'superscript',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlCode',
			view: 'code',
			coupledAttribute: 'code',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlMark',
			view: 'mark',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlSpan',
			view: 'span',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlCite',
			view: 'cite',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlLabel',
			view: 'label',
			attributeProperties: {
				copyOnEnter: true
			}
		},
		{
			model: 'htmlDfn',
			view: 'dfn',
			attributeProperties: {
				copyOnEnter: true
			}
		},

		// Objects
		{
			model: 'htmlObject',
			view: 'object',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		{
			model: 'htmlIframe',
			view: 'iframe',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		{
			model: 'htmlInput',
			view: 'input',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		{
			model: 'htmlButton',
			view: 'button',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		{
			model: 'htmlTextarea',
			view: 'textarea',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		{
			model: 'htmlSelect',
			view: 'select',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		{
			model: 'htmlVideo',
			view: 'video',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		{
			model: 'htmlEmbed',
			view: 'embed',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		{
			model: 'htmlOembed',
			view: 'oembed',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		{
			model: 'htmlAudio',
			view: 'audio',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		{
			model: 'htmlImg',
			view: 'img',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		{
			model: 'htmlCanvas',
			view: 'canvas',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		// TODO it could be probably represented as non-object element, although it has graphical representation,
		// so probably makes more sense to keep it as an object.
		{
			model: 'htmlMeter',
			view: 'meter',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		// TODO it could be probably represented as non-object element, although it has graphical representation,
		// so probably makes more sense to keep it as an object.
		{
			model: 'htmlProgress',
			view: 'progress',
			isObject: true,
			modelSchema: {
				inheritAllFrom: '$inlineObject'
			}
		},
		{
			model: 'htmlScript',
			view: 'script',
			modelSchema: {
				allowWhere: [ '$text', '$block' ],
				isInline: true
			}
		},
		{
			model: 'htmlStyle',
			view: 'style',
			modelSchema: {
				allowWhere: [ '$text', '$block' ],
				isInline: true
			}
		},
		{
			model: 'htmlCustomElement',
			view: '$customElement',
			modelSchema: {
				allowWhere: [ '$text', '$block' ],
				isInline: true
			}
		}
	]
};/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Holds representation of the extended HTML document type definitions to be used by the
 * editor in HTML support.
 *
 * Data schema is represented by data schema definitions.
 *
 * To add new definition for block element,
 * use {@link module:html-support/dataschema~DataSchema#registerBlockElement} method:
 *
 *		dataSchema.registerBlockElement( {
 *			view: 'section',
 *			model: 'my-section',
 *			modelSchema: {
 *				inheritAllFrom: '$block'
 *			}
 *		} );
 *
 * To add new definition for inline element,
 * use {@link module:html-support/dataschema~DataSchema#registerInlineElement} method:
 *
 *		dataSchema.registerInlineElement( {
 *			view: 'span',
 *			model: 'my-span',
 *			attributeProperties: {
 *				copyOnEnter: true
 *			}
 *		} );
 *
 * @extends module:core/plugin~Plugin
 */
class DataSchema extends Plugin {
	constructor( editor ) {
		super( editor );

		/**
		 * A map of registered data schema definitions.
		 *
		 * @readonly
		 * @private
		 * @member {Map.<String, module:html-support/dataschema~DataSchemaDefinition>} #_definitions
		 */
		this._definitions = new Map();
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'DataSchema';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		for ( const definition of defaultConfig.block ) {
			this.registerBlockElement( definition );
		}

		for ( const definition of defaultConfig.inline ) {
			this.registerInlineElement( definition );
		}
	}

	/**
	 * Add new data schema definition describing block element.
	 *
	 * @param {module:html-support/dataschema~DataSchemaBlockElementDefinition} definition
	 */
	registerBlockElement( definition ) {
		this._definitions.set( definition.model, { ...definition, isBlock: true } );
	}

	/**
	 * Add new data schema definition describing inline element.
	 *
	 * @param {module:html-support/dataschema~DataSchemaInlineElementDefinition} definition
	 */
	registerInlineElement( definition ) {
		this._definitions.set( definition.model, { ...definition, isInline: true } );
	}

	/**
	 * Updates schema definition describing block element with new properties.
	 *
	 * Creates new scheme if it doesn't exist.
	 * Array properties are concatenated with original values.
	 *
	 * @param {module:html-support/dataschema~DataSchemaBlockElementDefinition} definition Definition update.
	 */
	extendBlockElement( definition ) {
		this._extendDefinition( { ...definition, isBlock: true } );
	}

	/**
	 * Updates schema definition describing inline element with new properties.
	 *
	 * Creates new scheme if it doesn't exist.
	 * Array properties are concatenated with original values.
	 *
	 * @param {module:html-support/dataschema~DataSchemaInlineElementDefinition} definition Definition update.
	 */
	extendInlineElement( definition ) {
		this._extendDefinition( { ...definition, isInline: true } );
	}

	/**
	 * Returns all definitions matching the given view name.
	 *
	 * @param {String|RegExp} viewName
	 * @param {Boolean} [includeReferences] Indicates if this method should also include definitions of referenced models.
	 * @returns {Set.<module:html-support/dataschema~DataSchemaDefinition>}
	 */
	getDefinitionsForView( viewName, includeReferences ) {
		const definitions = new Set();

		for ( const definition of this._getMatchingViewDefinitions( viewName ) ) {
			if ( includeReferences ) {
				for ( const reference of this._getReferences( definition.model ) ) {
					definitions.add( reference );
				}
			}

			definitions.add( definition );
		}

		return definitions;
	}

	/**
	 * Returns definitions matching the given view name.
	 *
	 * @private
	 * @param {String|RegExp} viewName
	 * @returns {Array.<module:html-support/dataschema~DataSchemaDefinition>}
	 */
	_getMatchingViewDefinitions( viewName ) {
		return Array.from( this._definitions.values() )
			.filter( def => def.view && testViewName( viewName, def.view ) );
	}

	/**
	 * Resolves all definition references registered for the given data schema definition.
	 *
	 * @private
	 * @param {String} modelName Data schema model name.
	 * @returns {Iterable.<module:html-support/dataschema~DataSchemaDefinition>}
	 */
	* _getReferences( modelName ) {
		const { modelSchema } = this._definitions.get( modelName );

		if ( !modelSchema ) {
			return;
		}

		const inheritProperties = [ 'inheritAllFrom', 'inheritTypesFrom', 'allowWhere', 'allowContentOf', 'allowAttributesOf' ];

		for ( const property of inheritProperties ) {
			for ( const referenceName of toArray( modelSchema[ property ] || [] ) ) {
				const definition = this._definitions.get( referenceName );

				if ( referenceName !== modelName && definition ) {
					yield* this._getReferences( definition.model );
					yield definition;
				}
			}
		}
	}

	/**
	 * Updates schema definition with new properties.
	 *
	 * Creates new scheme if it doesn't exist.
	 * Array properties are concatenated with original values.
	 *
	 * @private
	 * @param {module:html-support/dataschema~DataSchemaDefinition} definition Definition update.
	 */
	_extendDefinition( definition ) {
		const currentDefinition = this._definitions.get( definition.model );

		const mergedDefinition = mergeWith$1( {}, currentDefinition, definition, ( target, source ) => {
			return Array.isArray( target ) ? target.concat( source ) : undefined;
		} );

		this._definitions.set( definition.model, mergedDefinition );
	}
}

// Test view name against the given pattern.
//
// @private
// @param {String|RegExp} pattern
// @param {String} viewName
// @returns {Boolean}
function testViewName( pattern, viewName ) {
	if ( typeof pattern === 'string' ) {
		return pattern === viewName;
	}

	if ( pattern instanceof RegExp ) {
		return pattern.test( viewName );
	}

	return false;
}

/**
 * A base definition of {@link module:html-support/dataschema~DataSchema data schema}.
 *
 * @typedef {Object} module:html-support/dataschema~DataSchemaDefinition
 * @property {String} model Name of the model.
 * @property {String} [view] Name of the view element.
 * @property {Boolean} [isObject] Indicates that the definition describes object element.
 * @property {module:engine/model/schema~SchemaItemDefinition} [modelSchema] The model schema item definition describing registered model.
 */

/**
 * A definition of {@link module:html-support/dataschema~DataSchema data schema} for block elements.
 *
 * @typedef {Object} module:html-support/dataschema~DataSchemaBlockElementDefinition
 * @property {Boolean} isBlock Indicates that the definition describes block element.
 * Set by {@link module:html-support/dataschema~DataSchema#registerBlockElement} method.
 * @property {String} [paragraphLikeModel] Should be used when an element can behave both as a sectioning element (e.g. article) and
 * element accepting only inline content (e.g. paragraph).
 * If an element contains only inline content, this option will be used as a model
 * name.
 * @extends module:html-support/dataschema~DataSchemaDefinition
 */

/**
 * A definition of {@link module:html-support/dataschema~DataSchema data schema} for inline elements.
 *
 * @typedef {Object} module:html-support/dataschema~DataSchemaInlineElementDefinition
 * @property {module:engine/model/schema~AttributeProperties} [attributeProperties] Additional metadata describing the model attribute.
 * @property {Boolean} isInline Indicates that the definition describes inline element.
 * @property {Number} [priority] Element priority. Decides in what order elements are wrapped by
 * {@link module:engine/view/downcastwriter~DowncastWriter}.
 * Set by {@link module:html-support/dataschema~DataSchema#registerInlineElement} method.
 * @property {String} [coupledAttribute] The name of the model attribute that generates the same view element. GHS inline attribute
 * will be removed from the model tree as soon as the coupled attribute is removed. See
 * {@link module:html-support/datafilter~DataFilter#_registerModelPostFixer GHS post-fixer} for more details.
 * @extends module:html-support/dataschema~DataSchemaDefinition
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
* Helper function for the downcast converter. Updates attributes on the given view element.
*
* @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer.
* @param {Object} oldViewAttributes The previous GHS attribute value.
* @param {Object} newViewAttributes The current GHS attribute value.
* @param {module:engine/view/element~Element} viewElement The view element to update.
*/
function updateViewAttributes( writer, oldViewAttributes, newViewAttributes, viewElement ) {
	if ( oldViewAttributes ) {
		removeViewAttributes( writer, oldViewAttributes, viewElement );
	}

	if ( newViewAttributes ) {
		setViewAttributes( writer, newViewAttributes, viewElement );
	}
}

/**
 * Helper function for the downcast converter. Sets attributes on the given view element.
 *
 * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer.
 * @param {Object} viewAttributes The GHS attribute value.
 * @param {module:engine/view/element~Element} viewElement The view element to update.
 */
function setViewAttributes( writer, viewAttributes, viewElement ) {
	if ( viewAttributes.attributes ) {
		for ( const [ key, value ] of Object.entries( viewAttributes.attributes ) ) {
			writer.setAttribute( key, value, viewElement );
		}
	}

	if ( viewAttributes.styles ) {
		writer.setStyle( viewAttributes.styles, viewElement );
	}

	if ( viewAttributes.classes ) {
		writer.addClass( viewAttributes.classes, viewElement );
	}
}

/**
 * Helper function for the downcast converter. Removes attributes on the given view element.
 *
 * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer.
 * @param {Object} viewAttributes The GHS attribute value.
 * @param {module:engine/view/element~Element} viewElement The view element to update.
 */
function removeViewAttributes( writer, viewAttributes, viewElement ) {
	if ( viewAttributes.attributes ) {
		for ( const [ key ] of Object.entries( viewAttributes.attributes ) ) {
			writer.removeAttribute( key, viewElement );
		}
	}

	if ( viewAttributes.styles ) {
		for ( const style of Object.keys( viewAttributes.styles ) ) {
			writer.removeStyle( style, viewElement );
		}
	}

	if ( viewAttributes.classes ) {
		writer.removeClass( viewAttributes.classes, viewElement );
	}
}

/**
* Merges view element attribute objects.
*
* @param {Object} target
* @param {Object} source
* @returns {Object}
*/
function mergeViewElementAttributes( target, source ) {
	const result = cloneDeep( target );

	for ( const key in source ) {
		// Merge classes.
		if ( Array.isArray( source[ key ] ) ) {
			result[ key ] = Array.from( new Set( [ ...( target[ key ] || [] ), ...source[ key ] ] ) );
		}

		// Merge attributes or styles.
		else {
			result[ key ] = { ...target[ key ], ...source[ key ] };
		}
	}

	return result;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * View-to-model conversion helper for object elements.
 *
 * Preserves object element content in `htmlContent` attribute.
 *
 * @param {module:html-support/dataschema~DataSchemaDefinition} definition
 * @returns {Function} Returns a conversion callback.
*/
function viewToModelObjectConverter( { model: modelName } ) {
	return ( viewElement, conversionApi ) => {
		// Let's keep element HTML and its attributes, so we can rebuild element in downcast conversions.
		return conversionApi.writer.createElement( modelName, {
			htmlContent: viewElement.getCustomProperty( '$rawContent' )
		} );
	};
}

/**
 * Conversion helper converting an object element to an HTML object widget.
 *
 * @param {module:core/editor/editor~Editor} editor
 * @param {module:html-support/dataschema~DataSchemaInlineElementDefinition} definition
 * @returns {Function} Returns a conversion callback.
*/
function toObjectWidgetConverter( editor, { view: viewName, isInline } ) {
	const t = editor.t;

	return ( modelElement, { writer } ) => {
		const widgetLabel = t( 'HTML object' );

		const viewElement = createObjectView( viewName, modelElement, writer );
		const viewAttributes = modelElement.getAttribute( 'htmlAttributes' );

		writer.addClass( 'html-object-embed__content', viewElement );

		if ( viewAttributes ) {
			setViewAttributes( writer, viewAttributes, viewElement );
		}

		// Widget cannot be a raw element because the widget system would not be able
		// to add its UI to it. Thus, we need separate view container.
		const viewContainer = writer.createContainerElement( isInline ? 'span' : 'div',
			{
				class: 'html-object-embed',
				'data-html-object-embed-label': widgetLabel
			},
			viewElement
		);

		return toWidget( viewContainer, writer, { widgetLabel } );
	};
}

/**
* Creates object view element from the given model element.
*
* @param {String} viewName
* @param {module:engine/model/element~Element} modelElement
* @param {module:engine/view/downcastwriter~DowncastWriter} writer
* @returns {module:engine/view/element~Element}
*/
function createObjectView( viewName, modelElement, writer ) {
	return writer.createRawElement( viewName, null, ( domElement, domConverter ) => {
		domConverter.setContentOf( domElement, modelElement.getAttribute( 'htmlContent' ) );
	} );
}

/**
 * View-to-attribute conversion helper preserving inline element attributes on `$text`.
 *
 * @param {module:html-support/dataschema~DataSchemaInlineElementDefinition} definition
 * @param {module:html-support/datafilter~DataFilter} dataFilter
 * @returns {Function} Returns a conversion callback.
*/
function viewToAttributeInlineConverter( { view: viewName, model: attributeKey }, dataFilter ) {
	return dispatcher => {
		dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => {
			let viewAttributes = dataFilter.processViewAttributes( data.viewItem, conversionApi );

			// Do not apply the attribute if the element itself is already consumed and there are no view attributes to store.
			if ( !viewAttributes && !conversionApi.consumable.test( data.viewItem, { name: true } ) ) {
				return;
			}

			// Otherwise, we might need to convert it to an empty object just to preserve element itself,
			// for example `<cite>` => <$text htmlCite="{}">.
			viewAttributes = viewAttributes || {};

			// Consume the element itself if it wasn't consumed by any other converter.
			conversionApi.consumable.consume( data.viewItem, { name: true } );

			// Since we are converting to attribute we need a range on which we will set the attribute.
			// If the range is not created yet, we will create it.
			if ( !data.modelRange ) {
				data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) );
			}

			// Set attribute on each item in range according to the schema.
			for ( const node of data.modelRange.getItems() ) {
				if ( conversionApi.schema.checkAttribute( node, attributeKey ) ) {
					// Node's children are converted recursively, so node can already include model attribute.
					// We want to extend it, not replace.
					const nodeAttributes = node.getAttribute( attributeKey );
					const attributesToAdd = mergeViewElementAttributes( viewAttributes, nodeAttributes || {} );

					conversionApi.writer.setAttribute( attributeKey, attributesToAdd, node );
				}
			}
		}, { priority: 'low' } );
	};
}

/**
 * Attribute-to-view conversion helper applying attributes to view element preserved on `$text`.
 *
 * @param {module:html-support/dataschema~DataSchemaInlineElementDefinition} definition
 * @returns {Function} Returns a conversion callback.
*/
function attributeToViewInlineConverter( { priority, view: viewName } ) {
	return ( attributeValue, conversionApi ) => {
		if ( !attributeValue ) {
			return;
		}

		const { writer } = conversionApi;
		const viewElement = writer.createAttributeElement( viewName, null, { priority } );

		setViewAttributes( writer, attributeValue, viewElement );

		return viewElement;
	};
}

/**
 * View-to-model conversion helper preserving allowed attributes on block element.
 *
 * All matched attributes will be preserved on `htmlAttributes` attribute.
 *
 * @param {module:html-support/dataschema~DataSchemaBlockElementDefinition} definition
 * @param {module:html-support/datafilter~DataFilter} dataFilter
 * @returns {Function} Returns a conversion callback.
*/
function viewToModelBlockAttributeConverter( { view: viewName }, dataFilter ) {
	return dispatcher => {
		dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => {
			// Converting an attribute of an element that has not been converted to anything does not make sense
			// because there will be nowhere to set that attribute on. At this stage, the element should've already
			// been converted. A collapsed range can show up in to-do lists (<input>) or complex widgets (e.g. table).
			// (https://github.com/ckeditor/ckeditor5/issues/11000).
			if ( !data.modelRange || data.modelRange.isCollapsed ) {
				return;
			}

			const viewAttributes = dataFilter.processViewAttributes( data.viewItem, conversionApi );

			if ( viewAttributes ) {
				conversionApi.writer.setAttribute( 'htmlAttributes', viewAttributes, data.modelRange );
			}
		}, { priority: 'low' } );
	};
}

/**
 * Model-to-view conversion helper applying attributes preserved in `htmlAttributes` attribute
 * for block elements.
 *
 * @param {module:html-support/dataschema~DataSchemaBlockElementDefinition} definition
 * @returns {Function} Returns a conversion callback.
*/
function modelToViewBlockAttributeConverter( { model: modelName } ) {
	return dispatcher => {
		dispatcher.on( `attribute:htmlAttributes:${ modelName }`, ( evt, data, conversionApi ) => {
			if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
				return;
			}

			const { attributeOldValue, attributeNewValue } = data;
			const viewWriter = conversionApi.writer;
			const viewElement = conversionApi.mapper.toViewElement( data.item );

			updateViewAttributes( viewWriter, attributeOldValue, attributeNewValue, viewElement );
		} );
	};
}var css_248z$6 = ":root{--ck-html-object-embed-unfocused-outline-width:1px}.ck-widget.html-object-embed{background-color:var(--ck-color-base-foreground);font-size:var(--ck-font-size-base);min-width:calc(76px + var(--ck-spacing-standard));padding:var(--ck-spacing-small);padding-top:calc(var(--ck-font-size-tiny) + var(--ck-spacing-large))}.ck-widget.html-object-embed:not(.ck-widget_selected):not(:hover){outline:var(--ck-html-object-embed-unfocused-outline-width) dashed var(--ck-color-widget-blurred-border)}.ck-widget.html-object-embed:before{background:#999;border-radius:0 0 var(--ck-border-radius) var(--ck-border-radius);color:var(--ck-color-base-background);content:attr(data-html-object-embed-label);font-family:var(--ck-font-face);font-size:var(--ck-font-size-tiny);font-style:normal;font-weight:400;left:var(--ck-spacing-standard);padding:calc(var(--ck-spacing-tiny) + var(--ck-html-object-embed-unfocused-outline-width)) var(--ck-spacing-small) var(--ck-spacing-tiny);position:absolute;top:0;transition:background var(--ck-widget-handler-animation-duration) var(--ck-widget-handler-animation-curve)}.ck-widget.html-object-embed .ck-widget__type-around .ck-widget__type-around__button.ck-widget__type-around__button_before{margin-left:50px}.ck-widget.html-object-embed .html-object-embed__content{pointer-events:none}div.ck-widget.html-object-embed{margin:1em auto}span.ck-widget.html-object-embed{display:inline-block}";
styleInject(css_248z$6);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Allows to validate elements and element attributes registered by {@link module:html-support/dataschema~DataSchema}.
 *
 * To enable registered element in the editor, use {@link module:html-support/datafilter~DataFilter#allowElement} method:
 *
 *		dataFilter.allowElement( 'section' );
 *
 * You can also allow or disallow specific element attributes:
 *
 *		// Allow `data-foo` attribute on `section` element.
 *		dataFilter.allowAttributes( {
 *			name: 'section',
 *			attributes: {
 *				'data-foo': true
 *			}
 *		} );
 *
 *		// Disallow `color` style attribute on 'section' element.
 *		dataFilter.disallowAttributes( {
 *			name: 'section',
 *			styles: {
 *				color: /[\s\S]+/
 *			}
 *		} );
 *
 * To apply the information about allowed and disallowed attributes in custom integration plugin,
 * use the {@link module:html-support/datafilter~DataFilter#processViewAttributes `processViewAttributes()`} method.
 *
 * @extends module:core/plugin~Plugin
 */
class DataFilter extends Plugin {
	constructor( editor ) {
		super( editor );

		/**
		 * An instance of the {@link module:html-support/dataschema~DataSchema}.
		 *
		 * @readonly
		 * @private
		 * @member {module:html-support/dataschema~DataSchema} #_dataSchema
		 */
		this._dataSchema = editor.plugins.get( 'DataSchema' );

		/**
		 * {@link module:engine/view/matcher~Matcher Matcher} instance describing rules upon which
		 * content attributes should be allowed.
		 *
		 * @readonly
		 * @private
		 * @member {module:engine/view/matcher~Matcher} #_allowedAttributes
		 */
		this._allowedAttributes = new Matcher();

		/**
		 * {@link module:engine/view/matcher~Matcher Matcher} instance describing rules upon which
		 * content attributes should be disallowed.
		 *
		 * @readonly
		 * @private
		 * @member {module:engine/view/matcher~Matcher} #_disallowedAttributes
		 */
		this._disallowedAttributes = new Matcher();

		/**
		 * Allowed element definitions by {@link module:html-support/datafilter~DataFilter#allowElement} method.
		 *
		 * @readonly
		 * @private
		 * @member {Set.<module:html-support/dataschema~DataSchemaDefinition>} #_allowedElements
		*/
		this._allowedElements = new Set();

		/**
		 * Indicates if {@link module:engine/controller/datacontroller~DataController editor's data controller}
		 * data has been already initialized.
		 *
		 * @private
		 * @member {Boolean} [#_dataInitialized=false]
		*/
		this._dataInitialized = false;

		/**
		 * Cached map of coupled attributes. Keys are the feature attributes names
		 * and values are arrays with coupled GHS attributes names.
		 *
		 * @private
		 * @member {Map.<String,Array>}
		 */
		this._coupledAttributes = null;

		this._registerElementsAfterInit();
		this._registerElementHandlers();
		this._registerModelPostFixer();
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'DataFilter';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ DataSchema, Widget ];
	}

	/**
	 * Load a configuration of one or many elements, where their attributes should be allowed.
	 *
	 * @param {Array.<module:engine/view/matcher~MatcherPattern>} config Configuration of elements
	 * that should have their attributes accepted in the editor.
	 */
	loadAllowedConfig( config ) {
		this._loadConfig( config, pattern => this.allowAttributes( pattern ) );
	}

	/**
	 * Load a configuration of one or many elements, where their attributes should be disallowed.
	 *
	 * @param {Array.<module:engine/view/matcher~MatcherPattern>} config Configuration of elements
	 * that should have their attributes rejected from the editor.
	 */
	loadDisallowedConfig( config ) {
		this._loadConfig( config, pattern => this.disallowAttributes( pattern ) );
	}

	/**
	 * Allow the given element in the editor context.
	 *
	 * This method will only allow elements described by the {@link module:html-support/dataschema~DataSchema} used
	 * to create data filter.
	 *
	 * @param {String|RegExp} viewName String or regular expression matching view name.
	 */
	allowElement( viewName ) {
		for ( const definition of this._dataSchema.getDefinitionsForView( viewName, true ) ) {
			if ( this._allowedElements.has( definition ) ) {
				continue;
			}

			this._allowedElements.add( definition );

			// We need to wait for all features to be initialized before we can register
			// element, so we can access existing features model schemas.
			// If the data has not been initialized yet, _registerElementsAfterInit() method will take care of
			// registering elements.
			if ( this._dataInitialized ) {
				this._fireRegisterEvent( definition );
			}

			// Reset cached map to recalculate it on the next usage.
			this._coupledAttributes = null;
		}
	}

	/**
	 * Allow the given attributes for view element allowed by {@link #allowElement} method.
	 *
	 * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all attributes which should be allowed.
	 */
	allowAttributes( config ) {
		this._allowedAttributes.add( config );
	}

	/**
	 * Disallow the given attributes for view element allowed by {@link #allowElement} method.
	 *
	 * @param {module:engine/view/matcher~MatcherPattern} config Pattern matching all attributes which should be disallowed.
	 */
	disallowAttributes( config ) {
		this._disallowedAttributes.add( config );
	}

	/**
	 * Batch load of the filtering configuration.
	 *
	 * @private
	 * @param {Array.<module:engine/view/matcher~MatcherPattern>} config Filtering configuration.
	 * @param {Function} handleAttributes Callback handling the way the attributes should be processed.
	 */
	_loadConfig( config, handleAttributes ) {
		for ( const pattern of config ) {
			// MatcherPattern allows omitting `name` to widen the search of elements.
			// Let's keep it consistent and match every element if a `name` has not been provided.
			const elementName = pattern.name || /[\s\S]+/;

			this.allowElement( elementName );

			splitRules( pattern ).forEach( handleAttributes );
		}
	}

	/**
	 * Processes all allowed and disallowed attributes on the view element by consuming them and returning the allowed ones.
	 *
	 * This method applies the configuration set up by {@link #allowAttributes `allowAttributes()`}
	 * and {@link #disallowAttributes `disallowAttributes()`} over the given view element by consuming relevant attributes.
	 * It returns the allowed attributes that were found on the given view element for further processing by integration code.
	 *
	 *		dispatcher.on( 'element:myElement', ( evt, data, conversionApi ) => {
	 *			// Get rid of disallowed and extract all allowed attributes from a viewElement.
	 *			const viewAttributes = dataFilter.processViewAttributes( data.viewItem, conversionApi );
	 *			// Do something with them, i.e. store inside a model as a dictionary.
	 *			if ( viewAttributes ) {
	 *				conversionApi.writer.setAttribute( 'htmlAttributesOfMyElement', viewAttributes, data.modelRange );
	 *			}
	 *		} );
	 *
	 * @see module:engine/conversion/viewconsumable~ViewConsumable#consume
	 * @param {module:engine/view/element~Element} viewElement
	 * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi
	 * @returns {Object} [result]
	 * @returns {Object} result.attributes Set with matched attribute names.
	 * @returns {Object} result.styles Set with matched style names.
	 * @returns {Array.<String>} result.classes Set with matched class names.
	 */
	processViewAttributes( viewElement, conversionApi ) {
		// Make sure that the disabled attributes are handled before the allowed attributes are called.
		// For example, for block images the <figure> converter triggers conversion for <img> first and then for other elements, i.e. <a>.
		consumeAttributes( viewElement, conversionApi, this._disallowedAttributes );

		return consumeAttributes( viewElement, conversionApi, this._allowedAttributes );
	}

	/**
	 * Registers elements allowed by {@link module:html-support/datafilter~DataFilter#allowElement} method
	 * once {@link module:engine/controller/datacontroller~DataController editor's data controller} is initialized.
	 *
	 * @private
	*/
	_registerElementsAfterInit() {
		this.editor.data.on( 'init', () => {
			this._dataInitialized = true;

			for ( const definition of this._allowedElements ) {
				this._fireRegisterEvent( definition );
			}
		}, {
			// With highest priority listener we are able to register elements right before
			// running data conversion. Also:
			// * Make sure that priority is higher than the one used by `RealTimeCollaborationClient`,
			// as RTC is stopping event propagation.
			// * Make sure no other features hook into this event before GHS because otherwise the
			// downcast conversion (for these features) could run before GHS registered its converters
			// (https://github.com/ckeditor/ckeditor5/issues/11356).
			priority: priorities.get( 'highest' ) + 1
		} );
	}

	/**
	 * Registers default element handlers.
	 *
	 * @private
	 */
	_registerElementHandlers() {
		this.on( 'register', ( evt, definition ) => {
			const schema = this.editor.model.schema;

			// Object element should be only registered for new features.
			// If the model schema is already registered, it should be handled by
			// #_registerBlockElement() or #_registerObjectElement() attribute handlers.
			if ( definition.isObject && !schema.isRegistered( definition.model ) ) {
				this._registerObjectElement( definition );
			} else if ( definition.isBlock ) {
				this._registerBlockElement( definition );
			} else if ( definition.isInline ) {
				this._registerInlineElement( definition );
			} else {
				/**
				 * The definition cannot be handled by the data filter.
				 *
				 * Make sure that the registered definition is correct.
				 *
				 * @error data-filter-invalid-definition
				 */
				throw new CKEditorError(
					'data-filter-invalid-definition',
					null,
					definition
				);
			}

			evt.stop();
		}, { priority: 'lowest' } );
	}

	/**
	 * Registers a model post-fixer that is removing coupled GHS attributes of inline elements. Those attributes
	 * are removed if a coupled feature attribute is removed.
	 *
	 * For example, consider following HTML:
	 *
	 *		<a href="foo.html" id="myId">bar</a>
	 *
	 * Which would be upcasted to following text node in the model:
	 *
	 *		<$text linkHref="foo.html" htmlA="{ attributes: { id: 'myId' } }">bar</$text>
	 *
	 * When the user removes the link from that text (using UI), only `linkHref` attribute would be removed:
	 *
	 *		<$text htmlA="{ attributes: { id: 'myId' } }">bar</$text>
	 *
	 * The `htmlA` attribute would stay in the model and would cause GHS to generate an `<a>` element.
	 * This is incorrect from UX point of view, as the user wanted to remove the whole link (not only `href`).
	 *
	 * @private
	 */
	_registerModelPostFixer() {
		const model = this.editor.model;

		model.document.registerPostFixer( writer => {
			const changes = model.document.differ.getChanges();
			let changed = false;

			const coupledAttributes = this._getCoupledAttributesMap();

			for ( const change of changes ) {
				// Handle only attribute removals.
				if ( change.type != 'attribute' || change.attributeNewValue !== null ) {
					continue;
				}

				// Find a list of coupled GHS attributes.
				const attributeKeys = coupledAttributes.get( change.attributeKey );

				if ( !attributeKeys ) {
					continue;
				}

				// Remove the coupled GHS attributes on the same range as the feature attribute was removed.
				for ( const { item } of change.range.getWalker( { shallow: true } ) ) {
					for ( const attributeKey of attributeKeys ) {
						if ( item.hasAttribute( attributeKey ) ) {
							writer.removeAttribute( attributeKey, item );
							changed = true;
						}
					}
				}
			}

			return changed;
		} );
	}

	/**
	 * Collects the map of coupled attributes. The returned map is keyed by the feature attribute name
	 * and coupled GHS attribute names are stored in the value array .
	 *
	 * @private
	 * @returns {Map.<String,Array>}
	 */
	_getCoupledAttributesMap() {
		if ( this._coupledAttributes ) {
			return this._coupledAttributes;
		}

		this._coupledAttributes = new Map();

		for ( const definition of this._allowedElements ) {
			if ( definition.coupledAttribute && definition.model ) {
				const attributeNames = this._coupledAttributes.get( definition.coupledAttribute );

				if ( attributeNames ) {
					attributeNames.push( definition.model );
				} else {
					this._coupledAttributes.set( definition.coupledAttribute, [ definition.model ] );
				}
			}
		}
	}

	/**
	 * Fires `register` event for the given element definition.
	 *
	 * @private
	 * @param {module:html-support/dataschema~DataSchemaDefinition} definition
	 */
	_fireRegisterEvent( definition ) {
		this.fire( definition.view ? `register:${ definition.view }` : 'register', definition );
	}

	/**
	 * Registers object element and attribute converters for the given data schema definition.
	 *
	 * @private
	 * @param {module:html-support/dataschema~DataSchemaDefinition} definition
	 */
	_registerObjectElement( definition ) {
		const editor = this.editor;
		const schema = editor.model.schema;
		const conversion = editor.conversion;
		const { view: viewName, model: modelName } = definition;

		schema.register( modelName, definition.modelSchema );

		/* istanbul ignore next: paranoid check */
		if ( !viewName ) {
			return;
		}

		schema.extend( definition.model, {
			allowAttributes: [ 'htmlAttributes', 'htmlContent' ]
		} );

		// Store element content in special `$rawContent` custom property to
		// avoid editor's data filtering mechanism.
		editor.data.registerRawContentMatcher( {
			name: viewName
		} );

		conversion.for( 'upcast' ).elementToElement( {
			view: viewName,
			model: viewToModelObjectConverter( definition ),
			// With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure
			// this listener is called before it. If not, some elements will be transformed into a paragraph.
			converterPriority: priorities.get( 'low' ) + 1
		} );
		conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, this ) );

		conversion.for( 'editingDowncast' ).elementToStructure( {
			model: {
				name: modelName,
				attributes: [
					'htmlAttributes'
				]
			},
			view: toObjectWidgetConverter( editor, definition )
		} );

		conversion.for( 'dataDowncast' ).elementToElement( {
			model: modelName,
			view: ( modelElement, { writer } ) => {
				return createObjectView( viewName, modelElement, writer );
			}
		} );
		conversion.for( 'dataDowncast' ).add( modelToViewBlockAttributeConverter( definition ) );
	}

	/**
	 * Registers block element and attribute converters for the given data schema definition.
	 *
	 * @private
	 * @param {module:html-support/dataschema~DataSchemaBlockElementDefinition} definition
	 */
	_registerBlockElement( definition ) {
		const editor = this.editor;
		const schema = editor.model.schema;
		const conversion = editor.conversion;
		const { view: viewName, model: modelName } = definition;

		if ( !schema.isRegistered( definition.model ) ) {
			schema.register( definition.model, definition.modelSchema );

			if ( !viewName ) {
				return;
			}

			conversion.for( 'upcast' ).elementToElement( {
				model: modelName,
				view: viewName,
				// With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure
				// this listener is called before it. If not, some elements will be transformed into a paragraph.
				converterPriority: priorities.get( 'low' ) + 1
			} );

			conversion.for( 'downcast' ).elementToElement( {
				model: modelName,
				view: viewName
			} );
		}

		if ( !viewName ) {
			return;
		}

		schema.extend( definition.model, {
			allowAttributes: 'htmlAttributes'
		} );

		conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, this ) );
		conversion.for( 'downcast' ).add( modelToViewBlockAttributeConverter( definition ) );
	}

	/**
	 * Registers inline element and attribute converters for the given data schema definition.
	 *
	 * Extends `$text` model schema to allow the given definition model attribute and its properties.
	 *
	 * @private
	 * @param {module:html-support/dataschema~DataSchemaInlineElementDefinition} definition
	 */
	_registerInlineElement( definition ) {
		const editor = this.editor;
		const schema = editor.model.schema;
		const conversion = editor.conversion;
		const attributeKey = definition.model;

		schema.extend( '$text', {
			allowAttributes: attributeKey
		} );

		if ( definition.attributeProperties ) {
			schema.setAttributeProperties( attributeKey, definition.attributeProperties );
		}

		conversion.for( 'upcast' ).add( viewToAttributeInlineConverter( definition, this ) );

		conversion.for( 'downcast' ).attributeToElement( {
			model: attributeKey,
			view: attributeToViewInlineConverter( definition )
		} );
	}

	/**
	 * Fired when {@link module:html-support/datafilter~DataFilter} is registering element and attribute
	 * converters for the {@link module:html-support/dataschema~DataSchemaDefinition element definition}.
	 *
	 * The event also accepts {@link module:html-support/dataschema~DataSchemaDefinition#view} value
	 * as an event namespace, e.g. `register:span`.
	 *
	 * 		dataFilter.on( 'register', ( evt, definition ) => {
	 * 			editor.model.schema.register( definition.model, definition.modelSchema );
	 * 			editor.conversion.elementToElement( { model: definition.model, view: definition.view } );
	 *
	 * 			evt.stop();
	 * 		} );
	 *
	 * 		dataFilter.on( 'register:span', ( evt, definition ) => {
	 * 			editor.model.schema.extend( '$text', { allowAttributes: 'htmlSpan' } );
	 *
	 * 			editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'span', model: 'htmlSpan' } );
	 * 			editor.conversion.for( 'downcast' ).attributeToElement( { view: 'span', model: 'htmlSpan' } );
	 *
	 * 			evt.stop();
	 * 		}, { priority: 'high' } )
	 *
	 * @event register
	 * @param {module:html-support/dataschema~DataSchemaDefinition} definition
	 */
}

// Matches and consumes the given view attributes.
//
// @private
// @param {module:engine/view/element~Element} viewElement
// @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi
// @param {module:engine/view/matcher~Matcher Matcher} matcher
// @returns {Object} [result]
// @returns {Object} result.attributes
// @returns {Object} result.styles
// @returns {Array.<String>} result.classes
function consumeAttributes( viewElement, conversionApi, matcher ) {
	const matches = consumeAttributeMatches( viewElement, conversionApi, matcher );
	const { attributes, styles, classes } = mergeMatchResults( matches );
	const viewAttributes = {};

	// Remove invalid DOM element attributes.
	if ( attributes.size ) {
		for ( const key of attributes ) {
			if ( !isValidAttributeName( key ) ) {
				attributes.delete( key );
			}
		}
	}

	if ( attributes.size ) {
		viewAttributes.attributes = iterableToObject( attributes, key => viewElement.getAttribute( key ) );
	}

	if ( styles.size ) {
		viewAttributes.styles = iterableToObject( styles, key => viewElement.getStyle( key ) );
	}

	if ( classes.size ) {
		viewAttributes.classes = Array.from( classes );
	}

	if ( !Object.keys( viewAttributes ).length ) {
		return null;
	}

	return viewAttributes;
}

// Consumes matched attributes.
//
// @private
// @param {module:engine/view/element~Element} viewElement
// @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi
// @param {module:engine/view/matcher~Matcher Matcher} matcher
// @returns {Array.<Object>} Array with match information about found attributes.
function consumeAttributeMatches( viewElement, { consumable }, matcher ) {
	const matches = matcher.matchAll( viewElement ) || [];
	const consumedMatches = [];

	for ( const match of matches ) {
		removeConsumedAttributes( consumable, viewElement, match );

		// We only want to consume attributes, so element can be still processed by other converters.
		delete match.match.name;

		consumable.consume( viewElement, match.match );
		consumedMatches.push( match );
	}

	return consumedMatches;
}

// Removes attributes from the given match that were already consumed by other converters.
//
// @private
// @param {module:engine/view/element~Element} viewElement
// @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable
// @param {Object} match
function removeConsumedAttributes( consumable, viewElement, match ) {
	for ( const key of [ 'attributes', 'classes', 'styles' ] ) {
		const attributes = match.match[ key ];

		if ( !attributes ) {
			continue;
		}

		// Iterating over a copy of an array so removing items doesn't influence iteration.
		for ( const value of Array.from( attributes ) ) {
			if ( !consumable.test( viewElement, ( { [ key ]: [ value ] } ) ) ) {
				removeItemFromArray( attributes, value );
			}
		}
	}
}

// Merges the result of {@link module:engine/view/matcher~Matcher#matchAll} method.
//
// @private
// @param {Array.<Object>} matches
// @returns {Object} result
// @returns {Set.<Object>} result.attributes Set with matched attribute names.
// @returns {Set.<Object>} result.styles Set with matched style names.
// @returns {Set.<String>} result.classes Set with matched class names.
function mergeMatchResults( matches ) {
	const matchResult = {
		attributes: new Set(),
		classes: new Set(),
		styles: new Set()
	};

	for ( const match of matches ) {
		for ( const key in matchResult ) {
			const values = match.match[ key ] || [];

			values.forEach( value => matchResult[ key ].add( value ) );
		}
	}

	return matchResult;
}

// Converts the given iterable object into an object.
//
// @private
// @param {Iterable.<String>} iterable
// @param {Function} getValue Should result with value for the given object key.
// @returns {Object}
function iterableToObject( iterable, getValue ) {
	const attributesObject = {};

	for ( const prop of iterable ) {
		const value = getValue( prop );
		if ( value !== undefined ) {
			attributesObject[ prop ] = getValue( prop );
		}
	}

	return attributesObject;
}

// Matcher by default has to match **all** patterns to count it as an actual match. Splitting the pattern
// into separate patterns means that any matched pattern will be count as a match.
//
// @private
// @param {module:engine/view/matcher~MatcherPattern} pattern Pattern to split.
// @param {String} attributeName Name of the attribute to split (e.g. 'attributes', 'classes', 'styles').
// @returns {Array.<module:engine/view/matcher~MatcherPattern>}
function splitPattern( pattern, attributeName ) {
	const { name } = pattern;

	if ( isPlainObject( pattern[ attributeName ] ) ) {
		return Object.entries( pattern[ attributeName ] ).map(
			( [ key, value ] ) => ( {
				name,
				[ attributeName ]: {
					[ key ]: value
				}
			} ) );
	}

	if ( Array.isArray( pattern[ attributeName ] ) ) {
		return pattern[ attributeName ].map(
			value => ( {
				name,
				[ attributeName ]: [ value ]
			} )
		);
	}

	return [ pattern ];
}

// Rules are matched in conjunction (AND operation), but we want to have a match if *any* of the rules is matched (OR operation).
// By splitting the rules we force the latter effect.
//
// @private
// @param {module:engine/view/matcher~MatcherPattern} rules
// @returns {Array.<module:engine/view/matcher~MatcherPattern>}
function splitRules( rules ) {
	const { name, attributes, classes, styles } = rules;
	const splittedRules = [];

	if ( attributes ) {
		splittedRules.push( ...splitPattern( { name, attributes }, 'attributes' ) );
	}
	if ( classes ) {
		splittedRules.push( ...splitPattern( { name, classes }, 'classes' ) );
	}
	if ( styles ) {
		splittedRules.push( ...splitPattern( { name, styles }, 'styles' ) );
	}

	return splittedRules;
}

// Returns true if name is valid for a DOM attribute name.
function isValidAttributeName( name ) {
	try {
		document.createAttribute( name );
	} catch ( error ) {
		return false;
	}

	return true;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Provides the General HTML Support integration with {@link module:code-block/codeblock~CodeBlock Code Block} feature.
 *
 * @extends module:core/plugin~Plugin
 */
class CodeBlockElementSupport extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ DataFilter ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'CodeBlockElementSupport';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		if ( !this.editor.plugins.has( 'CodeBlockEditing' ) ) {
			return;
		}

		const dataFilter = this.editor.plugins.get( DataFilter );

		dataFilter.on( 'register:pre', ( evt, definition ) => {
			if ( definition.model !== 'codeBlock' ) {
				return;
			}

			const editor = this.editor;
			const schema = editor.model.schema;
			const conversion = editor.conversion;

			// Extend codeBlock to allow attributes required by attribute filtration.
			schema.extend( 'codeBlock', {
				allowAttributes: [ 'htmlAttributes', 'htmlContentAttributes' ]
			} );

			conversion.for( 'upcast' ).add( viewToModelCodeBlockAttributeConverter( dataFilter ) );
			conversion.for( 'downcast' ).add( modelToViewCodeBlockAttributeConverter() );

			evt.stop();
		} );
	}
}

// View-to-model conversion helper preserving allowed attributes on {@link module:code-block/codeblock~CodeBlock Code Block}
// feature model element.
//
// Attributes are preserved as a value of `htmlAttributes` model attribute.
//
// @private
// @param {module:html-support/datafilter~DataFilter} dataFilter
// @returns {Function} Returns a conversion callback.
function viewToModelCodeBlockAttributeConverter( dataFilter ) {
	return dispatcher => {
		dispatcher.on( 'element:code', ( evt, data, conversionApi ) => {
			const viewCodeElement = data.viewItem;
			const viewPreElement = viewCodeElement.parent;

			if ( !viewPreElement || !viewPreElement.is( 'element', 'pre' ) ) {
				return;
			}

			preserveElementAttributes( viewPreElement, 'htmlAttributes' );
			preserveElementAttributes( viewCodeElement, 'htmlContentAttributes' );

			function preserveElementAttributes( viewElement, attributeName ) {
				const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );

				if ( viewAttributes ) {
					conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange );
				}
			}
		}, { priority: 'low' } );
	};
}

// Model-to-view conversion helper applying attributes from {@link module:code-block/codeblock~CodeBlock Code Block}
// feature model element.
//
// @private
// @returns {Function} Returns a conversion callback.
function modelToViewCodeBlockAttributeConverter() {
	return dispatcher => {
		dispatcher.on( 'attribute:htmlAttributes:codeBlock', ( evt, data, conversionApi ) => {
			if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
				return;
			}

			const { attributeOldValue, attributeNewValue } = data;
			const viewCodeElement = conversionApi.mapper.toViewElement( data.item );
			const viewPreElement = viewCodeElement.parent;

			updateViewAttributes( conversionApi.writer, attributeOldValue, attributeNewValue, viewPreElement );
		} );

		dispatcher.on( 'attribute:htmlContentAttributes:codeBlock', ( evt, data, conversionApi ) => {
			if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
				return;
			}

			const { attributeOldValue, attributeNewValue } = data;
			const viewCodeElement = conversionApi.mapper.toViewElement( data.item );

			updateViewAttributes( conversionApi.writer, attributeOldValue, attributeNewValue, viewCodeElement );
		} );
	};
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Provides the General HTML Support integration for elements which can behave like sectioning element (e.g. article) or
 * element accepting only inline content (e.g. paragraph).
 *
 * The distinction between this two content models is important for choosing correct schema model and proper content conversion.
 * As an example, it ensures that:
 *
 * * children elements paragraphing is enabled for sectioning elements only,
 * * element and its content can be correctly handled by editing view (splitting and merging elements),
 * * model element HTML is semantically correct and easier to work with.
 *
 * If element contains any block element, it will be treated as a sectioning element and registered using
 * {@link module:html-support/dataschema~DataSchemaDefinition#model} and
 * {@link module:html-support/dataschema~DataSchemaDefinition#modelSchema} in editor schema.
 * Otherwise, it will be registered under {@link module:html-support/dataschema~DataSchemaBlockElementDefinition#paragraphLikeModel} model
 * name with model schema accepting only inline content (inheriting from `$block`).
 *
 * @extends module:core/plugin~Plugin
 */
class DualContentModelElementSupport extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ DataFilter ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'DualContentModelElementSupport';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const dataFilter = this.editor.plugins.get( DataFilter );

		dataFilter.on( 'register', ( evt, definition ) => {
			const editor = this.editor;
			const schema = editor.model.schema;
			const conversion = editor.conversion;

			if ( !definition.paragraphLikeModel ) {
				return;
			}

			// Can only apply to newly registered features.
			if ( schema.isRegistered( definition.model ) || schema.isRegistered( definition.paragraphLikeModel ) ) {
				return;
			}

			const paragraphLikeModelDefinition = {
				model: definition.paragraphLikeModel,
				view: definition.view
			};

			schema.register( definition.model, definition.modelSchema );
			schema.register( paragraphLikeModelDefinition.model, {
				inheritAllFrom: '$block'
			} );

			conversion.for( 'upcast' ).elementToElement( {
				view: definition.view,
				model: ( viewElement, { writer } ) => {
					if ( this._hasBlockContent( viewElement ) ) {
						return writer.createElement( definition.model );
					}

					return writer.createElement( paragraphLikeModelDefinition.model );
				},
				// With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure
				// this listener is called before it. If not, some elements will be transformed into a paragraph.
				converterPriority: priorities.get( 'low' ) + 1
			} );

			conversion.for( 'downcast' ).elementToElement( {
				view: definition.view,
				model: definition.model
			} );
			this._addAttributeConversion( definition );

			conversion.for( 'downcast' ).elementToElement( {
				view: paragraphLikeModelDefinition.view,
				model: paragraphLikeModelDefinition.model
			} );
			this._addAttributeConversion( paragraphLikeModelDefinition );

			evt.stop();
		} );
	}

	/**
	 * Checks whether the given view element includes any other block element.
	 *
	 * @private
	 * @param {module:engine/view/element~Element} viewElement
	 * @returns {Boolean}
	 */
	_hasBlockContent( viewElement ) {
		const view = this.editor.editing.view;
		const blockElements = view.domConverter.blockElements;

		// Traversing the viewElement subtree looking for block elements.
		// Especially for the cases like <div><a href="#"><p>foo</p></a></div>.
		// https://github.com/ckeditor/ckeditor5/issues/11513
		for ( const viewItem of view.createRangeIn( viewElement ).getItems() ) {
			if ( viewItem.is( 'element' ) && blockElements.includes( viewItem.name ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Adds attribute filtering conversion for the given data schema.
	 *
	 * @private
	 * @param {module:html-support/dataschema~DataSchemaBlockElementDefinition} definition
	 */
	_addAttributeConversion( definition ) {
		const editor = this.editor;
		const conversion = editor.conversion;
		const dataFilter = editor.plugins.get( DataFilter );

		editor.model.schema.extend( definition.model, {
			allowAttributes: 'htmlAttributes'
		} );

		conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, dataFilter ) );
		conversion.for( 'downcast' ).add( modelToViewBlockAttributeConverter( definition ) );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Provides the General HTML Support integration with {@link module:heading/heading~Heading Heading} feature.
 *
 * @extends module:core/plugin~Plugin
 */
class HeadingElementSupport extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ DataSchema ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'HeadingElementSupport';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		if ( !editor.plugins.has( 'HeadingEditing' ) ) {
			return;
		}

		const dataSchema = editor.plugins.get( DataSchema );
		const options = editor.config.get( 'heading.options' );
		const headerModels = [];

		// We are registering all elements supported by HeadingEditing
		// to enable custom attributes for those elements.
		for ( const option of options ) {
			if ( 'model' in option && 'view' in option ) {
				dataSchema.registerBlockElement( {
					view: option.view,
					model: option.model
				} );

				headerModels.push( option.model );
			}
		}

		dataSchema.extendBlockElement( {
			model: 'htmlHgroup',
			modelSchema: {
				allowChildren: headerModels
			}
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Provides the General HTML Support integration with the {@link module:image/image~Image Image} feature.
 *
 * @extends module:core/plugin~Plugin
 */
class ImageElementSupport extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ DataFilter ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'ImageElementSupport';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		// At least one image plugin should be loaded for the integration to work properly.
		if ( !editor.plugins.has( 'ImageInlineEditing' ) && !editor.plugins.has( 'ImageBlockEditing' ) ) {
			return;
		}

		const schema = editor.model.schema;
		const conversion = editor.conversion;
		const dataFilter = editor.plugins.get( DataFilter );

		dataFilter.on( 'register:figure', () => {
			conversion.for( 'upcast' ).add( viewToModelFigureAttributeConverter$1( dataFilter ) );
		} );

		dataFilter.on( 'register:img', ( evt, definition ) => {
			if ( definition.model !== 'imageBlock' && definition.model !== 'imageInline' ) {
				return;
			}

			if ( schema.isRegistered( 'imageBlock' ) ) {
				schema.extend( 'imageBlock', {
					allowAttributes: [
						'htmlAttributes',
						// Figure and Link don't have model counterpart.
						// We will preserve attributes on image model element using these attribute keys.
						'htmlFigureAttributes',
						'htmlLinkAttributes'
					]
				} );
			}

			if ( schema.isRegistered( 'imageInline' ) ) {
				schema.extend( 'imageInline', {
					allowAttributes: [
						// `htmlA` is needed for standard GHS link integration.
						'htmlA',
						'htmlAttributes'
					]
				} );
			}

			conversion.for( 'upcast' ).add( viewToModelImageAttributeConverter( dataFilter ) );
			conversion.for( 'downcast' ).add( modelToViewImageAttributeConverter() );

			evt.stop();
		} );
	}
}

// View-to-model conversion helper preserving allowed attributes on the {@link module:image/image~Image Image}
// feature model element.
//
// @private
// @param {module:html-support/datafilter~DataFilter} dataFilter
// @returns {Function} Returns a conversion callback.
function viewToModelImageAttributeConverter( dataFilter ) {
	return dispatcher => {
		dispatcher.on( 'element:img', ( evt, data, conversionApi ) => {
			if ( !data.modelRange ) {
				return;
			}

			const viewImageElement = data.viewItem;
			const viewContainerElement = viewImageElement.parent;

			preserveElementAttributes( viewImageElement, 'htmlAttributes' );

			if ( viewContainerElement.is( 'element', 'a' ) ) {
				preserveLinkAttributes( viewContainerElement );
			}

			function preserveElementAttributes( viewElement, attributeName ) {
				const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );

				if ( viewAttributes ) {
					conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange );
				}
			}

			function preserveLinkAttributes( viewContainerElement ) {
				if ( data.modelRange && data.modelRange.getContainedElement().is( 'element', 'imageBlock' ) ) {
					preserveElementAttributes( viewContainerElement, 'htmlLinkAttributes' );
				}
			}
		}, { priority: 'low' } );
	};
}

// View-to-model conversion helper preserving allowed attributes on {@link module:image/image~Image Image}
// feature model element from figure view element.
//
// @private
// @param {module:html-support/datafilter~DataFilter} dataFilter
// @returns {Function} Returns a conversion callback.
function viewToModelFigureAttributeConverter$1( dataFilter ) {
	return dispatcher => {
		dispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
			const viewFigureElement = data.viewItem;

			if ( !data.modelRange || !viewFigureElement.hasClass( 'image' ) ) {
				return;
			}

			const viewAttributes = dataFilter.processViewAttributes( viewFigureElement, conversionApi );

			if ( viewAttributes ) {
				conversionApi.writer.setAttribute( 'htmlFigureAttributes', viewAttributes, data.modelRange );
			}
		}, { priority: 'low' } );
	};
}

// A model-to-view conversion helper applying attributes from the {@link module:image/image~Image Image}
// feature.
//
// @private
// @returns {Function} Returns a conversion callback.
function modelToViewImageAttributeConverter() {
	return dispatcher => {
		addInlineAttributeConversion( 'htmlAttributes' );

		addBlockAttributeConversion( 'img', 'htmlAttributes' );
		addBlockAttributeConversion( 'figure', 'htmlFigureAttributes' );
		addBlockAttributeConversion( 'a', 'htmlLinkAttributes' );

		function addInlineAttributeConversion( attributeName ) {
			dispatcher.on( `attribute:${ attributeName }:imageInline`, ( evt, data, conversionApi ) => {
				if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
					return;
				}

				const { attributeOldValue, attributeNewValue } = data;
				const viewElement = conversionApi.mapper.toViewElement( data.item );

				updateViewAttributes( conversionApi.writer, attributeOldValue, attributeNewValue, viewElement );
			}, { priority: 'low' } );
		}

		function addBlockAttributeConversion( elementName, attributeName ) {
			dispatcher.on( `attribute:${ attributeName }:imageBlock`, ( evt, data, conversionApi ) => {
				if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
					return;
				}

				const { attributeOldValue, attributeNewValue } = data;
				const containerElement = conversionApi.mapper.toViewElement( data.item );
				const viewElement = getDescendantElement$2( conversionApi.writer, containerElement, elementName );

				if ( viewElement ) {
					updateViewAttributes( conversionApi.writer, attributeOldValue, attributeNewValue, viewElement );
					conversionApi.consumable.consume( data.item, evt.name );
				}
			}, { priority: 'low' } );

			if ( elementName === 'a' ) {
				// To have a link element in the view, we need to attach a converter to the `linkHref` attribute as well.
				dispatcher.on( 'attribute:linkHref:imageBlock', ( evt, data, conversionApi ) => {
					if ( !conversionApi.consumable.consume( data.item, 'attribute:htmlLinkAttributes:imageBlock' ) ) {
						return;
					}

					const containerElement = conversionApi.mapper.toViewElement( data.item );
					const viewElement = getDescendantElement$2( conversionApi.writer, containerElement, 'a' );

					setViewAttributes( conversionApi.writer, data.item.getAttribute( 'htmlLinkAttributes' ), viewElement );
				}, { priority: 'low' } );
			}
		}
	};
}

// Returns the first view element descendant matching the given view name.
// Includes view element itself.
//
// @private
// @param {module:engine/view/downcastwriter~DowncastWriter} writer
// @param {module:engine/view/element~Element} containerElement
// @param {String} elementName
// @returns {module:engine/view/element~Element|null}
function getDescendantElement$2( writer, containerElement, elementName ) {
	const range = writer.createRangeOn( containerElement );

	for ( const { item } of range.getWalker() ) {
		if ( item.is( 'element', elementName ) ) {
			return item;
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Provides the General HTML Support integration with {@link module:media-embed/mediaembed~MediaEmbed Media Embed} feature.
 *
 * @extends module:core/plugin~Plugin
 */
class MediaEmbedElementSupport extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ DataFilter ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'MediaEmbedElementSupport';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		// Stop here if MediaEmbed plugin is not provided or the integrator wants to output markup with previews as
		// we do not support filtering previews.
		if ( !editor.plugins.has( 'MediaEmbed' ) || editor.config.get( 'mediaEmbed.previewsInData' ) ) {
			return;
		}

		const schema = editor.model.schema;
		const conversion = editor.conversion;
		const dataFilter = this.editor.plugins.get( DataFilter );
		const dataSchema = this.editor.plugins.get( DataSchema );
		const mediaElementName = editor.config.get( 'mediaEmbed.elementName' );

		// Overwrite GHS schema definition for a given elementName.
		dataSchema.registerBlockElement( {
			model: 'media',
			view: mediaElementName
		} );

		dataFilter.on( 'register:figure', ( ) => {
			conversion.for( 'upcast' ).add( viewToModelFigureAttributesConverter( dataFilter ) );
		} );

		dataFilter.on( `register:${ mediaElementName }`, ( evt, definition ) => {
			if ( definition.model !== 'media' ) {
				return;
			}

			schema.extend( 'media', {
				allowAttributes: [
					'htmlAttributes',
					'htmlFigureAttributes'
				]
			} );

			conversion.for( 'upcast' ).add( viewToModelMediaAttributesConverter( dataFilter, mediaElementName ) );
			conversion.for( 'dataDowncast' ).add( modelToViewMediaAttributeConverter( mediaElementName ) );

			evt.stop();
		} );
	}
}

function viewToModelMediaAttributesConverter( dataFilter, mediaElementName ) {
	return dispatcher => {
		dispatcher.on( `element:${ mediaElementName }`, upcastMedia );
	};

	function upcastMedia( evt, data, conversionApi ) {
		const viewMediaElement = data.viewItem;

		preserveElementAttributes( viewMediaElement, 'htmlAttributes' );

		function preserveElementAttributes( viewElement, attributeName ) {
			const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );

			if ( viewAttributes ) {
				conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange );
			}
		}
	}
}

// View-to-model conversion helper preserving allowed attributes on {@link module:media-embed/mediaembed~MediaEmbed MediaEmbed}
// feature model element from figure view element.
//
// @private
// @param {module:html-support/datafilter~DataFilter} dataFilter
// @returns {Function} Returns a conversion callback.
function viewToModelFigureAttributesConverter( dataFilter ) {
	return dispatcher => {
		dispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
			const viewFigureElement = data.viewItem;

			if ( !data.modelRange || !viewFigureElement.hasClass( 'media' ) ) {
				return;
			}

			const viewAttributes = dataFilter.processViewAttributes( viewFigureElement, conversionApi );

			if ( viewAttributes ) {
				conversionApi.writer.setAttribute( 'htmlFigureAttributes', viewAttributes, data.modelRange );
			}
		}, { priority: 'low' } );
	};
}

function modelToViewMediaAttributeConverter( mediaElementName ) {
	return dispatcher => {
		addAttributeConversionDispatcherHandler( mediaElementName, 'htmlAttributes' );
		addAttributeConversionDispatcherHandler( 'figure', 'htmlFigureAttributes' );

		function addAttributeConversionDispatcherHandler( elementName, attributeName ) {
			dispatcher.on( `attribute:${ attributeName }:media`, ( evt, data, conversionApi ) => {
				if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
					return;
				}

				const { attributeOldValue, attributeNewValue } = data;
				const containerElement = conversionApi.mapper.toViewElement( data.item );
				const viewElement = getDescendantElement$1( conversionApi.writer, containerElement, elementName );

				updateViewAttributes( conversionApi.writer, attributeOldValue, attributeNewValue, viewElement );
			} );
		}
	};
}

// Returns the first view element descendant matching the given view name.
// Includes view element itself.
//
// @private
// @param {module:engine/view/downcastwriter~DowncastWriter} writer
// @param {module:engine/view/element~Element} containerElement
// @param {String} elementName
// @returns {module:engine/view/element~Element|null}
function getDescendantElement$1( writer, containerElement, elementName ) {
	const range = writer.createRangeOn( containerElement );

	for ( const { item } of range.getWalker() ) {
		if ( item.is( 'element', elementName ) ) {
			return item;
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Provides the General HTML Support for `script` elements.
 *
 * @extends module:core/plugin~Plugin
 */
class ScriptElementSupport extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ DataFilter ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'ScriptElementSupport';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const dataFilter = this.editor.plugins.get( DataFilter );

		dataFilter.on( 'register:script', ( evt, definition ) => {
			const editor = this.editor;
			const schema = editor.model.schema;
			const conversion = editor.conversion;

			schema.register( 'htmlScript', definition.modelSchema );

			schema.extend( 'htmlScript', {
				allowAttributes: [ 'htmlAttributes', 'htmlContent' ],
				isContent: true
			} );

			editor.data.registerRawContentMatcher( {
				name: 'script'
			} );

			conversion.for( 'upcast' ).elementToElement( {
				view: 'script',
				model: viewToModelObjectConverter( definition )
			} );

			conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, dataFilter ) );

			conversion.for( 'downcast' ).elementToElement( {
				model: 'htmlScript',
				view: ( modelElement, { writer } ) => {
					return createObjectView( 'script', modelElement, writer );
				}
			} );

			conversion.for( 'downcast' ).add( modelToViewBlockAttributeConverter( definition ) );

			evt.stop();
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Provides the General HTML Support integration with {@link module:table/table~Table Table} feature.
 *
 * @extends module:core/plugin~Plugin
 */
class TableElementSupport extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ DataFilter ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'TableElementSupport';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		if ( !editor.plugins.has( 'TableEditing' ) ) {
			return;
		}

		const schema = editor.model.schema;
		const conversion = editor.conversion;
		const dataFilter = editor.plugins.get( DataFilter );

		dataFilter.on( 'register:figure', ( ) => {
			conversion.for( 'upcast' ).add( viewToModelFigureAttributeConverter( dataFilter ) );
		} );

		dataFilter.on( 'register:table', ( evt, definition ) => {
			if ( definition.model !== 'table' ) {
				return;
			}

			schema.extend( 'table', {
				allowAttributes: [
					'htmlAttributes',
					// Figure, thead and tbody elements don't have model counterparts.
					// We will be preserving attributes on table element using these attribute keys.
					'htmlFigureAttributes', 'htmlTheadAttributes', 'htmlTbodyAttributes'
				]
			} );

			conversion.for( 'upcast' ).add( viewToModelTableAttributeConverter( dataFilter ) );
			conversion.for( 'downcast' ).add( modelToViewTableAttributeConverter() );

			evt.stop();
		} );
	}
}

// View-to-model conversion helper preserving allowed attributes on {@link module:table/table~Table Table}
// feature model element.
//
// @private
// @param {module:html-support/datafilter~DataFilter} dataFilter
// @returns {Function} Returns a conversion callback.
function viewToModelTableAttributeConverter( dataFilter ) {
	return dispatcher => {
		dispatcher.on( 'element:table', ( evt, data, conversionApi ) => {
			const viewTableElement = data.viewItem;

			preserveElementAttributes( viewTableElement, 'htmlAttributes' );

			for ( const childNode of viewTableElement.getChildren() ) {
				if ( childNode.is( 'element', 'thead' ) ) {
					preserveElementAttributes( childNode, 'htmlTheadAttributes' );
				}

				if ( childNode.is( 'element', 'tbody' ) ) {
					preserveElementAttributes( childNode, 'htmlTbodyAttributes' );
				}
			}

			function preserveElementAttributes( viewElement, attributeName ) {
				const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );

				if ( viewAttributes ) {
					conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange );
				}
			}
		} );
	};
}

// View-to-model conversion helper preserving allowed attributes on {@link module:table/table~Table Table}
// feature model element from figure view element.
//
// @private
// @param {module:html-support/datafilter~DataFilter} dataFilter
// @returns {Function} Returns a conversion callback.
function viewToModelFigureAttributeConverter( dataFilter ) {
	return dispatcher => {
		dispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
			const viewFigureElement = data.viewItem;

			if ( !data.modelRange || !viewFigureElement.hasClass( 'table' ) ) {
				return;
			}

			const viewAttributes = dataFilter.processViewAttributes( viewFigureElement, conversionApi );

			if ( viewAttributes ) {
				conversionApi.writer.setAttribute( 'htmlFigureAttributes', viewAttributes, data.modelRange );
			}
		}, { priority: 'low' } );
	};
}

// Model-to-view conversion helper applying attributes from {@link module:table/table~Table Table}
// feature.
//
// @private
// @returns {Function} Returns a conversion callback.
function modelToViewTableAttributeConverter() {
	return dispatcher => {
		addAttributeConversionDispatcherHandler( 'table', 'htmlAttributes' );
		addAttributeConversionDispatcherHandler( 'figure', 'htmlFigureAttributes' );
		addAttributeConversionDispatcherHandler( 'thead', 'htmlTheadAttributes' );
		addAttributeConversionDispatcherHandler( 'tbody', 'htmlTbodyAttributes' );

		function addAttributeConversionDispatcherHandler( elementName, attributeName ) {
			dispatcher.on( `attribute:${ attributeName }:table`, ( evt, data, conversionApi ) => {
				if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
					return;
				}

				const containerElement = conversionApi.mapper.toViewElement( data.item );
				const viewElement = getDescendantElement( conversionApi.writer, containerElement, elementName );

				setViewAttributes( conversionApi.writer, data.attributeNewValue, viewElement );
			} );
		}
	};
}

// Returns the first view element descendant matching the given view name.
// Includes view element itself.
//
// @private
// @param {module:engine/view/downcastwriter~DowncastWriter} writer
// @param {module:engine/view/element~Element} containerElement
// @param {String} elementName
// @returns {module:engine/view/element~Element|null}
function getDescendantElement( writer, containerElement, elementName ) {
	const range = writer.createRangeOn( containerElement );

	for ( const { item } of range.getWalker() ) {
		if ( item.is( 'element', elementName ) ) {
			return item;
		}
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Provides the General HTML Support for `style` elements.
 *
 * @extends module:core/plugin~Plugin
 */
class StyleElementSupport extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ DataFilter ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'StyleElementSupport';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const dataFilter = this.editor.plugins.get( DataFilter );

		dataFilter.on( 'register:style', ( evt, definition ) => {
			const editor = this.editor;
			const schema = editor.model.schema;
			const conversion = editor.conversion;

			schema.register( 'htmlStyle', definition.modelSchema );

			schema.extend( 'htmlStyle', {
				allowAttributes: [ 'htmlAttributes', 'htmlContent' ],
				isContent: true
			} );

			editor.data.registerRawContentMatcher( {
				name: 'style'
			} );

			conversion.for( 'upcast' ).elementToElement( {
				view: 'style',
				model: viewToModelObjectConverter( definition )
			} );

			conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, dataFilter ) );

			conversion.for( 'downcast' ).elementToElement( {
				model: 'htmlStyle',
				view: ( modelElement, { writer } ) => {
					return createObjectView( 'style', modelElement, writer );
				}
			} );

			conversion.for( 'downcast' ).add( modelToViewBlockAttributeConverter( definition ) );

			evt.stop();
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Provides the General HTML Support integration with the {@link module:list/documentlist~DocumentList Document List} feature.
 *
 * @extends module:core/plugin~Plugin
 */
class DocumentListElementSupport extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ DataFilter ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'DocumentListElementSupport';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		if ( !editor.plugins.has( 'DocumentListEditing' ) ) {
			return;
		}

		const schema = editor.model.schema;
		const conversion = editor.conversion;
		const dataFilter = editor.plugins.get( DataFilter );
		const documentListEditing = editor.plugins.get( 'DocumentListEditing' );

		// Register downcast strategy.
		// Note that this must be done before document list editing registers conversion in afterInit.
		documentListEditing.registerDowncastStrategy( {
			scope: 'item',
			attributeName: 'htmlLiAttributes',

			setAttributeOnDowncast( writer, attributeValue, viewElement ) {
				setViewAttributes( writer, attributeValue, viewElement );
			}
		} );

		documentListEditing.registerDowncastStrategy( {
			scope: 'list',
			attributeName: 'htmlListAttributes',

			setAttributeOnDowncast( writer, viewAttributes, viewElement ) {
				setViewAttributes( writer, viewAttributes, viewElement );
			}
		} );

		dataFilter.on( 'register', ( evt, definition ) => {
			if ( ![ 'ul', 'ol', 'li' ].includes( definition.view ) ) {
				return;
			}

			evt.stop();

			// Do not register same converters twice.
			if ( schema.checkAttribute( '$block', 'htmlListAttributes' ) ) {
				return;
			}

			schema.extend( '$block', { allowAttributes: [ 'htmlListAttributes', 'htmlLiAttributes' ] } );
			schema.extend( '$blockObject', { allowAttributes: [ 'htmlListAttributes', 'htmlLiAttributes' ] } );
			schema.extend( '$container', { allowAttributes: [ 'htmlListAttributes', 'htmlLiAttributes' ] } );

			conversion.for( 'upcast' ).add( dispatcher => {
				dispatcher.on( 'element:ul', viewToModelListAttributeConverter( 'htmlListAttributes', dataFilter ), { priority: 'low' } );
				dispatcher.on( 'element:ol', viewToModelListAttributeConverter( 'htmlListAttributes', dataFilter ), { priority: 'low' } );
				dispatcher.on( 'element:li', viewToModelListAttributeConverter( 'htmlLiAttributes', dataFilter ), { priority: 'low' } );
			} );
		} );

		// Make sure that all items in a single list (items at the same level & listType) have the same properties.
		// Note: This is almost an exact copy from DocumentListPropertiesEditing.
		documentListEditing.on( 'postFixer', ( evt, { listNodes, writer } ) => {
			const previousNodesByIndent = []; // Last seen nodes of lower indented lists.

			for ( const { node, previous } of listNodes ) {
				// For the first list block there is nothing to compare with.
				if ( !previous ) {
					continue;
				}

				const nodeIndent = node.getAttribute( 'listIndent' );
				const previousNodeIndent = previous.getAttribute( 'listIndent' );

				let previousNodeInList = null; // It's like `previous` but has the same indent as current node.

				// Let's find previous node for the same indent.
				// We're going to need that when we get back to previous indent.
				if ( nodeIndent > previousNodeIndent ) {
					previousNodesByIndent[ previousNodeIndent ] = previous;
				}
				// Restore the one for given indent.
				else if ( nodeIndent < previousNodeIndent ) {
					previousNodeInList = previousNodesByIndent[ nodeIndent ];
					previousNodesByIndent.length = nodeIndent;
				}
				// Same indent.
				else {
					previousNodeInList = previous;
				}

				// This is a first item of a nested list.
				if ( !previousNodeInList ) {
					continue;
				}

				if ( previousNodeInList.getAttribute( 'listType' ) == node.getAttribute( 'listType' ) ) {
					const value = previousNodeInList.getAttribute( 'htmlListAttributes' );

					if ( !isEqual( node.getAttribute( 'htmlListAttributes' ), value ) ) {
						writer.setAttribute( 'htmlListAttributes', value, node );
						evt.return = true;
					}
				}

				if ( previousNodeInList.getAttribute( 'listItemId' ) == node.getAttribute( 'listItemId' ) ) {
					const value = previousNodeInList.getAttribute( 'htmlLiAttributes' );

					if ( !isEqual( node.getAttribute( 'htmlLiAttributes' ), value ) ) {
						writer.setAttribute( 'htmlLiAttributes', value, node );
						evt.return = true;
					}
				}
			}
		} );
	}

	/**
	 * @inheritDoc
	 */
	afterInit() {
		const editor = this.editor;

		if ( !editor.commands.get( 'indentList' ) ) {
			return;
		}

		// Reset list attributes after indenting list items.
		this.listenTo( editor.commands.get( 'indentList' ), 'afterExecute', ( evt, changedBlocks ) => {
			editor.model.change( writer => {
				for ( const node of changedBlocks ) {
					// Just reset the attribute.
					// If there is a previous indented list that this node should be merged into,
					// the postfixer will unify all the attributes of both sub-lists.
					writer.setAttribute( 'htmlListAttributes', {}, node );
				}
			} );
		} );
	}
}

// View-to-model conversion helper preserving allowed attributes on {@link TODO}
// feature model element.
//
// @private
// @param {String} attributeName
// @param {module:html-support/datafilter~DataFilter} dataFilter
// @returns {Function} Returns a conversion callback.
function viewToModelListAttributeConverter( attributeName, dataFilter ) {
	return ( evt, data, conversionApi ) => {
		const viewElement = data.viewItem;

		if ( !data.modelRange ) {
			Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) );
		}

		const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );

		for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
			// Apply only to list item blocks.
			if ( !item.hasAttribute( 'listItemId' ) ) {
				continue;
			}

			// Set list attributes only on same level items, those nested deeper are already handled
			// by the recursive conversion.
			if ( item.hasAttribute( attributeName ) ) {
				continue;
			}

			conversionApi.writer.setAttribute( attributeName, viewAttributes || {}, item );
		}
	};
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * Provides the General HTML Support for custom elements (not registered in the {@link module:html-support/dataschema~DataSchema}).
 *
 * @extends module:core/plugin~Plugin
 */
class CustomElementSupport extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ DataFilter, DataSchema ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'CustomElementSupport';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const dataFilter = this.editor.plugins.get( DataFilter );
		const dataSchema = this.editor.plugins.get( DataSchema );

		dataFilter.on( 'register:$customElement', ( evt, definition ) => {
			evt.stop();

			const editor = this.editor;
			const schema = editor.model.schema;
			const conversion = editor.conversion;
			const unsafeElements = editor.editing.view.domConverter.unsafeElements;
			const preLikeElements = editor.data.htmlProcessor.domConverter.preElements;

			schema.register( definition.model, definition.modelSchema );
			schema.extend( definition.model, {
				allowAttributes: [ 'htmlElementName', 'htmlAttributes', 'htmlContent' ],
				isContent: true
			} );

			// Being executed on the low priority, it will catch all elements that were not caught by other converters.
			conversion.for( 'upcast' ).elementToElement( {
				view: /.*/,
				model: ( viewElement, conversionApi ) => {
					// Do not try to convert $comment fake element.
					if ( viewElement.name == '$comment' ) {
						return;
					}

					if ( !isValidElementName( viewElement.name ) ) {
						return;
					}

					// Allow for fallback only if this element is not defined in data schema to make sure
					// that this will handle only custom elements not registered in the data schema.
					if ( dataSchema.getDefinitionsForView( viewElement.name ).size ) {
						return;
					}

					// Make sure that this element will not render in the editing view.
					if ( !unsafeElements.includes( viewElement.name ) ) {
						unsafeElements.push( viewElement.name );
					}

					// Make sure that whitespaces will not be trimmed or replaced by nbsps while stringify content.
					if ( !preLikeElements.includes( viewElement.name ) ) {
						preLikeElements.push( viewElement.name );
					}

					const modelElement = conversionApi.writer.createElement( definition.model, {
						htmlElementName: viewElement.name
					} );

					const htmlAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );

					if ( htmlAttributes ) {
						conversionApi.writer.setAttribute( 'htmlAttributes', htmlAttributes, modelElement );
					}

					// Store the whole element in the attribute so that DomConverter will be able to use the pre like element context.
					const viewWriter = new UpcastWriter( viewElement.document );
					const documentFragment = viewWriter.createDocumentFragment( viewElement );
					const htmlContent = editor.data.processor.toData( documentFragment );

					conversionApi.writer.setAttribute( 'htmlContent', htmlContent, modelElement );

					// Consume the content of the element.
					for ( const { item } of editor.editing.view.createRangeIn( viewElement ) ) {
						conversionApi.consumable.consume( item, { name: true } );
					}

					return modelElement;
				},
				converterPriority: 'low'
			} );

			// Because this element is unsafe (DomConverter#unsafeElements), it will render as a transparent <span> but it must
			// be rendered anyway for the mapping between the model and the view to exist.
			conversion.for( 'editingDowncast' ).elementToElement( {
				model: {
					name: definition.model,
					attributes: [ 'htmlElementName', 'htmlAttributes', 'htmlContent' ]
				},
				view: ( modelElement, { writer } ) => {
					const viewName = modelElement.getAttribute( 'htmlElementName' );
					const viewElement = writer.createRawElement( viewName );

					if ( modelElement.hasAttribute( 'htmlAttributes' ) ) {
						setViewAttributes( writer, modelElement.getAttribute( 'htmlAttributes' ), viewElement );
					}

					return viewElement;
				}
			} );

			conversion.for( 'dataDowncast' ).elementToElement( {
				model: {
					name: definition.model,
					attributes: [ 'htmlElementName', 'htmlAttributes', 'htmlContent' ]
				},
				view: ( modelElement, { writer } ) => {
					const viewName = modelElement.getAttribute( 'htmlElementName' );
					const htmlContent = modelElement.getAttribute( 'htmlContent' );

					const viewElement = writer.createRawElement( viewName, null, ( domElement, domConverter ) => {
						domConverter.setContentOf( domElement, htmlContent );

						// Unwrap the custom element content (it was stored in the attribute as the whole custom element).
						// See the upcast conversion for the "htmlContent" attribute to learn more.
						const customElement = domElement.firstChild;

						customElement.remove();

						while ( customElement.firstChild ) {
							domElement.appendChild( customElement.firstChild );
						}
					} );

					if ( modelElement.hasAttribute( 'htmlAttributes' ) ) {
						setViewAttributes( writer, modelElement.getAttribute( 'htmlAttributes' ), viewElement );
					}

					return viewElement;
				}
			} );
		} );
	}
}

// Returns true if name is valid for a DOM element name.
function isValidElementName( name ) {
	try {
		document.createElement( name );
	} catch ( error ) {
		return false;
	}

	return true;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The General HTML Support feature.
 *
 * This is a "glue" plugin which initializes the {@link module:html-support/datafilter~DataFilter data filter} configuration
 * and features integration with the General HTML Support.
 *
 * @extends module:core/plugin~Plugin
 */
class GeneralHtmlSupport extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'GeneralHtmlSupport';
	}

	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [
			DataFilter,
			CodeBlockElementSupport,
			DualContentModelElementSupport,
			HeadingElementSupport,
			ImageElementSupport,
			MediaEmbedElementSupport,
			ScriptElementSupport,
			TableElementSupport,
			StyleElementSupport,
			DocumentListElementSupport,
			CustomElementSupport
		];
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const dataFilter = editor.plugins.get( DataFilter );

		// Load the filtering configuration.
		dataFilter.loadAllowedConfig( editor.config.get( 'htmlSupport.allow' ) || [] );
		dataFilter.loadDisallowedConfig( editor.config.get( 'htmlSupport.disallow' ) || [] );
	}

	/**
	 * Returns a GHS model attribute name related to a given view element name.
	 *
	 * @protected
	 * @param {String} viewElementName A view element name.
	 * @returns {String}
	 */
	getGhsAttributeNameForElement( viewElementName ) {
		const dataSchema = this.editor.plugins.get( 'DataSchema' );
		const definitions = Array.from( dataSchema.getDefinitionsForView( viewElementName, false ) );

		if ( definitions && definitions.length && definitions[ 0 ].isInline && !definitions[ 0 ].isObject ) {
			return definitions[ 0 ].model;
		}

		return 'htmlAttributes';
	}

	/**
	 * Updates GHS model attribute for a specified view element name, so it includes the given class name.
	 *
	 * @protected
	 * @param {String} viewElementName A view element name.
	 * @param {String|Array.<String>} className The css class to add.
	 * @param {module:engine/model/selection~Selectable} selectable The selection or element to update.
	 */
	addModelHtmlClass( viewElementName, className, selectable ) {
		const model = this.editor.model;
		const ghsAttributeName = this.getGhsAttributeNameForElement( viewElementName );

		model.change( writer => {
			for ( const item of getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) ) {
				modifyGhsAttribute( writer, item, ghsAttributeName, 'classes', classes => {
					for ( const value of toArray( className ) ) {
						classes.add( value );
					}
				} );
			}
		} );
	}

	/**
	 * Updates GHS model attribute for a specified view element name, so it does not include the given class name.
	 *
	 * @protected
	 * @param {String} viewElementName A view element name.
	 * @param {String|Array.<String>} className The css class to remove.
	 * @param {module:engine/model/selection~Selectable} selectable The selection or element to update.
	 */
	removeModelHtmlClass( viewElementName, className, selectable ) {
		const model = this.editor.model;
		const ghsAttributeName = this.getGhsAttributeNameForElement( viewElementName );

		model.change( writer => {
			for ( const item of getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) ) {
				modifyGhsAttribute( writer, item, ghsAttributeName, 'classes', classes => {
					for ( const value of toArray( className ) ) {
						classes.delete( value );
					}
				} );
			}
		} );
	}

	/**
	 * Updates GHS model attribute for a specified view element name, so it includes the given attribute.
	 *
	 * @protected
	 * @param {String} viewElementName A view element name.
	 * @param {Object} attributes The object with attributes to set.
	 * @param {module:engine/model/selection~Selectable} selectable The selection or element to update.
	 */
	setModelHtmlAttributes( viewElementName, attributes, selectable ) {
		const model = this.editor.model;
		const ghsAttributeName = this.getGhsAttributeNameForElement( viewElementName );

		model.change( writer => {
			for ( const item of getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) ) {
				modifyGhsAttribute( writer, item, ghsAttributeName, 'attributes', attributesMap => {
					for ( const [ key, value ] of Object.entries( attributes ) ) {
						attributesMap.set( key, value );
					}
				} );
			}
		} );
	}

	/**
	 * Updates GHS model attribute for a specified view element name, so it does not include the given attribute.
	 *
	 * @protected
	 * @param {String} viewElementName A view element name.
	 * @param {String|Array.<String>} attributeName The attribute name (or names) to remove.
	 * @param {module:engine/model/selection~Selectable} selectable The selection or element to update.
	 */
	removeModelHtmlAttributes( viewElementName, attributeName, selectable ) {
		const model = this.editor.model;
		const ghsAttributeName = this.getGhsAttributeNameForElement( viewElementName );

		model.change( writer => {
			for ( const item of getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) ) {
				modifyGhsAttribute( writer, item, ghsAttributeName, 'attributes', attributesMap => {
					for ( const key of toArray( attributeName ) ) {
						attributesMap.delete( key );
					}
				} );
			}
		} );
	}

	/**
	 * Updates GHS model attribute for a specified view element name, so it includes a given style.
	 *
	 * @protected
	 * @param {String} viewElementName A view element name.
	 * @param {Object} styles The object with styles to set.
	 * @param {module:engine/model/selection~Selectable} selectable The selection or element to update.
	 */
	setModelHtmlStyles( viewElementName, styles, selectable ) {
		const model = this.editor.model;
		const ghsAttributeName = this.getGhsAttributeNameForElement( viewElementName );

		model.change( writer => {
			for ( const item of getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) ) {
				modifyGhsAttribute( writer, item, ghsAttributeName, 'styles', stylesMap => {
					for ( const [ key, value ] of Object.entries( styles ) ) {
						stylesMap.set( key, value );
					}
				} );
			}
		} );
	}

	/**
	 * Updates GHS model attribute for a specified view element name, so it does not include a given style.
	 *
	 * @protected
	 * @param {String} viewElementName A view element name.
	 * @param {String|Array.<String>} properties The style (or styles list) to remove.
	 * @param {module:engine/model/selection~Selectable} selectable The selection or element to update.
	 */
	removeModelHtmlStyles( viewElementName, properties, selectable ) {
		const model = this.editor.model;
		const ghsAttributeName = this.getGhsAttributeNameForElement( viewElementName );

		model.change( writer => {
			for ( const item of getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) ) {
				modifyGhsAttribute( writer, item, ghsAttributeName, 'styles', stylesMap => {
					for ( const key of toArray( properties ) ) {
						stylesMap.delete( key );
					}
				} );
			}
		} );
	}
}

// Returns an iterator over an items in the selectable that accept given GHS attribute.
function* getItemsToUpdateGhsAttribute( model, selectable, ghsAttributeName ) {
	if ( selectable.is( 'documentSelection' ) && selectable.isCollapsed ) {
		if ( model.schema.checkAttributeInSelection( selectable, ghsAttributeName ) ) {
			yield selectable;
		}
	} else {
		for ( const range of getValidRangesForSelectable( model, selectable, ghsAttributeName ) ) {
			yield* range.getItems( { shallow: true } );
		}
	}
}

// Translates a given selectable to an iterable of ranges.
function getValidRangesForSelectable( model, selectable, ghsAttributeName ) {
	if ( selectable.is( 'node' ) || selectable.is( '$text' ) || selectable.is( '$textProxy' ) ) {
		if ( model.schema.checkAttribute( selectable, ghsAttributeName ) ) {
			return [ model.createRangeOn( selectable ) ];
		} else {
			return [];
		}
	} else {
		return model.schema.getValidRanges( model.createSelection( selectable ).getRanges(), ghsAttributeName );
	}
}

// Updates a GHS attribute on a specified item.
// @param {module:engine/model/writer~Writer} writer
// @param {module:engine/model/item~Item|module:engine/model/documentselection~DocumentSelection} item
// @param {String} ghsAttributeName
// @param {'classes'|'attributes'|'styles'} subject
// @param {Function} callback That receives a map or set as an argument and should modify it (add or remove entries).
function modifyGhsAttribute( writer, item, ghsAttributeName, subject, callback ) {
	const oldValue = item.getAttribute( ghsAttributeName );
	const newValue = {};

	for ( const kind of [ 'attributes', 'styles', 'classes' ] ) {
		if ( kind != subject ) {
			if ( oldValue && oldValue[ kind ] ) {
				newValue[ kind ] = oldValue[ kind ];
			}
		} else {
			const values = kind == 'classes' ?
				new Set( oldValue && oldValue[ kind ] || [] ) :
				new Map( Object.entries( oldValue && oldValue[ kind ] || {} ) );

			callback( values );

			if ( values.size ) {
				newValue[ kind ] = kind == 'classes' ? Array.from( values ) : Object.fromEntries( values );
			}
		}
	}

	if ( Object.keys( newValue ).length ) {
		if ( item.is( 'documentSelection' ) ) {
			writer.setSelectionAttribute( ghsAttributeName, newValue );
		} else {
			writer.setAttribute( ghsAttributeName, newValue, item );
		}
	} else if ( oldValue ) {
		if ( item.is( 'documentSelection' ) ) {
			writer.removeSelectionAttribute( ghsAttributeName );
		} else {
			writer.removeAttribute( ghsAttributeName, item );
		}
	}
}

/**
 * The configuration of the General HTML Support feature.
 * Introduced by the {@link module:html-support/generalhtmlsupport~GeneralHtmlSupport} feature.
 *
 * Read more in {@link module:html-support/generalhtmlsupport~GeneralHtmlSupportConfig}.
 *
 * @member {module:htmlsupport/generalhtmlsupport~GeneralHtmlSupportConfig} module:core/editor/editorconfig~EditorConfig#htmlSupport
 */

/**
 * The configuration of the General HTML Support feature.
 * The option is used by the {@link module:html-support/generalhtmlsupport~GeneralHtmlSupport} feature.
 *
 *		ClassicEditor
 *			.create( {
 * 				htmlSupport: ... // General HTML Support feature config.
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
 *
 * @interface GeneralHtmlSupportConfig
 */

/**
 * The configuration of allowed content rules used by General HTML Support.
 *
 * Setting this configuration option will enable HTML features that are not explicitly supported by any other dedicated CKEditor 5 features.
 *
 * 		const htmlSupportConfig.allow = [
 * 			{
 * 				name: 'div',                      // Enable 'div' element support,
 * 				classes: [ 'special-container' ], // allow 'special-container' class,
 * 				styles: 'background',             // allow 'background' style,
 * 				attributes: true                  // allow any attribute (can be empty).
 * 			},
 * 			{
 * 				name: 'p',                                   // Extend existing Paragraph feature,
 * 				classes: 'highlighted'                       // with 'highlighted' class,
 * 				attributes: [
 * 					{ key: 'data-i18n-context, value: true } // and i18n attribute.
 * 				]
 * 			}
 * 		];
 *
 * @member {Array.<module:engine/view/matcher~MatcherPattern>} module:html-support/generalhtmlsupport~GeneralHtmlSupportConfig#allow
 */

/**
 * The configuration of disallowed content rules used by General HTML Support.
 *
 * Setting this configuration option will disable listed HTML features.
 *
 * 		const htmlSupportConfig.disallow = [
 * 			{
 * 				name: /[\s\S]+/    // For every HTML feature,
 * 				attributes: {
 * 					key: /^on.*$/ // disable 'on*' attributes, like 'onClick', 'onError' etc.
 * 				}
 * 			}
 * 		];
 * @member {Array.<module:engine/view/matcher~MatcherPattern>} module:html-support/generalhtmlsupport~GeneralHtmlSupportConfig#disallow
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * An extension of the base {@link module:core/command~Command} class, which provides utilities for a command
 * that toggles a single attribute on a text or an element.
 *
 * `AttributeCommand` uses {@link module:engine/model/document~Document#selection}
 * to decide which nodes (if any) should be changed, and applies or removes the attribute from them.
 *
 * The command checks the {@link module:engine/model/model~Model#schema} to decide if it can be enabled
 * for the current selection and to which nodes the attribute can be applied.
 *
 * @extends module:core/command~Command
 */
class AttributeCommand extends Command {
	/**
	 * @param {module:core/editor/editor~Editor} editor
	 * @param {String} attributeKey Attribute that will be set by the command.
	 */
	constructor( editor, attributeKey ) {
		super( editor );

		/**
		 * The attribute that will be set by the command.
		 *
		 * @readonly
		 * @member {String}
		 */
		this.attributeKey = attributeKey;

		/**
		 * Flag indicating whether the command is active. The command is active when the
		 * {@link module:engine/model/selection~Selection#hasAttribute selection has the attribute} which means that:
		 *
		 * * If the selection is not empty &ndash; That the attribute is set on the first node in the selection that allows this attribute.
		 * * If the selection is empty &ndash; That the selection has the attribute itself (which means that newly typed
		 * text will have this attribute, too).
		 *
		 * @observable
		 * @readonly
		 * @member {Boolean} #value
		 */
	}

	/**
	 * Updates the command's {@link #value} and {@link #isEnabled} based on the current selection.
	 */
	refresh() {
		const model = this.editor.model;
		const doc = model.document;

		this.value = this._getValueFromFirstAllowedNode();
		this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, this.attributeKey );
	}

	/**
	 * Executes the command &mdash; applies the attribute to the selection or removes it from the selection.
	 *
	 * If the command is active (`value == true`), it will remove attributes. Otherwise, it will set attributes.
	 *
	 * The execution result differs, depending on the {@link module:engine/model/document~Document#selection}:
	 *
	 * * If the selection is on a range, the command applies the attribute to all nodes in that range
	 * (if they are allowed to have this attribute by the {@link module:engine/model/schema~Schema schema}).
	 * * If the selection is collapsed in a non-empty node, the command applies the attribute to the
	 * {@link module:engine/model/document~Document#selection} itself (note that typed characters copy attributes from the selection).
	 * * If the selection is collapsed in an empty node, the command applies the attribute to the parent node of the selection (note
	 * that the selection inherits all attributes from a node if it is in an empty node).
	 *
	 * @fires execute
	 * @param {Object} [options] Command options.
	 * @param {Boolean} [options.forceValue] If set, it will force the command behavior. If `true`, the command will apply the attribute,
	 * otherwise the command will remove the attribute.
	 * If not set, the command will look for its current value to decide what it should do.
	 */
	execute( options = {} ) {
		const model = this.editor.model;
		const doc = model.document;
		const selection = doc.selection;
		const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue;

		model.change( writer => {
			if ( selection.isCollapsed ) {
				if ( value ) {
					writer.setSelectionAttribute( this.attributeKey, true );
				} else {
					writer.removeSelectionAttribute( this.attributeKey );
				}
			} else {
				const ranges = model.schema.getValidRanges( selection.getRanges(), this.attributeKey );

				for ( const range of ranges ) {
					if ( value ) {
						writer.setAttribute( this.attributeKey, value, range );
					} else {
						writer.removeAttribute( this.attributeKey, range );
					}
				}
			}
		} );
	}

	/**
	 * Checks the attribute value of the first node in the selection that allows the attribute.
	 * For the collapsed selection returns the selection attribute.
	 *
	 * @private
	 * @returns {Boolean} The attribute value.
	 */
	_getValueFromFirstAllowedNode() {
		const model = this.editor.model;
		const schema = model.schema;
		const selection = model.document.selection;

		if ( selection.isCollapsed ) {
			return selection.hasAttribute( this.attributeKey );
		}

		for ( const range of selection.getRanges() ) {
			for ( const item of range.getItems() ) {
				if ( schema.checkAttribute( item, this.attributeKey ) ) {
					return item.hasAttribute( this.attributeKey );
				}
			}
		}

		return false;
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const BOLD$1 = 'bold';

/**
 * The bold editing feature.
 *
 * It registers the `'bold'` command and introduces the `bold` attribute in the model which renders to the view
 * as a `<strong>` element.
 *
 * @extends module:core/plugin~Plugin
 */
class BoldEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'BoldEditing';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		// Allow bold attribute on text nodes.
		editor.model.schema.extend( '$text', { allowAttributes: BOLD$1 } );
		editor.model.schema.setAttributeProperties( BOLD$1, {
			isFormatting: true,
			copyOnEnter: true
		} );

		// Build converter from model to view for data and editing pipelines.
		editor.conversion.attributeToElement( {
			model: BOLD$1,
			view: 'strong',
			upcastAlso: [
				'b',
				viewElement => {
					const fontWeight = viewElement.getStyle( 'font-weight' );

					if ( !fontWeight ) {
						return null;
					}

					// Value of the `font-weight` attribute can be defined as a string or a number.
					if ( fontWeight == 'bold' || Number( fontWeight ) >= 600 ) {
						return {
							name: true,
							styles: [ 'font-weight' ]
						};
					}
				}
			]
		} );

		// Create bold command.
		editor.commands.add( BOLD$1, new AttributeCommand( editor, BOLD$1 ) );

		// Set the Ctrl+B keystroke.
		editor.keystrokes.set( 'CTRL+B', BOLD$1 );
	}
}var boldIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M10.187 17H5.773c-.637 0-1.092-.138-1.364-.415-.273-.277-.409-.718-.409-1.323V4.738c0-.617.14-1.062.419-1.332.279-.27.73-.406 1.354-.406h4.68c.69 0 1.288.041 1.793.124.506.083.96.242 1.36.478.341.197.644.447.906.75a3.262 3.262 0 0 1 .808 2.162c0 1.401-.722 2.426-2.167 3.075C15.05 10.175 16 11.315 16 13.01a3.756 3.756 0 0 1-2.296 3.504 6.1 6.1 0 0 1-1.517.377c-.571.073-1.238.11-2 .11zm-.217-6.217H7v4.087h3.069c1.977 0 2.965-.69 2.965-2.072 0-.707-.256-1.22-.768-1.537-.512-.319-1.277-.478-2.296-.478zM7 5.13v3.619h2.606c.729 0 1.292-.067 1.69-.2a1.6 1.6 0 0 0 .91-.765c.165-.267.247-.566.247-.897 0-.707-.26-1.176-.778-1.409-.519-.232-1.31-.348-2.375-.348H7z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const BOLD = 'bold';

/**
 * The bold UI feature. It introduces the Bold button.
 *
 * @extends module:core/plugin~Plugin
 */
class BoldUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'BoldUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		// Add bold button to feature components.
		editor.ui.componentFactory.add( BOLD, locale => {
			const command = editor.commands.get( BOLD );
			const view = new ButtonView( locale );

			view.set( {
				label: t( 'Bold' ),
				icon: boldIcon,
				keystroke: 'CTRL+B',
				tooltip: true,
				isToggleable: true
			} );

			view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

			// Execute command.
			this.listenTo( view, 'execute', () => {
				editor.execute( BOLD );
				editor.editing.view.focus();
			} );

			return view;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The bold feature.
 *
 * For a detailed overview check the {@glink features/basic-styles Basic styles feature documentation}
 * and the {@glink api/basic-styles package page}.
 *
 * This is a "glue" plugin which loads the {@link module:basic-styles/bold/boldediting~BoldEditing bold editing feature}
 * and {@link module:basic-styles/bold/boldui~BoldUI bold UI feature}.
 *
 * @extends module:core/plugin~Plugin
 */
class Bold extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ BoldEditing, BoldUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Bold';
	}
}var css_248z$5 = ".ck-content code{background-color:hsla(0,0%,78%,.3);border-radius:2px;padding:.15em}.ck.ck-editor__editable .ck-code_selected{background-color:hsla(0,0%,78%,.5)}";
styleInject(css_248z$5);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const ITALIC$1 = 'italic';

/**
 * The italic editing feature.
 *
 * It registers the `'italic'` command, the <kbd>Ctrl+I</kbd> keystroke and introduces the `italic` attribute in the model
 * which renders to the view as an `<i>` element.
 *
 * @extends module:core/plugin~Plugin
 */
class ItalicEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'ItalicEditing';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		// Allow italic attribute on text nodes.
		editor.model.schema.extend( '$text', { allowAttributes: ITALIC$1 } );
		editor.model.schema.setAttributeProperties( ITALIC$1, {
			isFormatting: true,
			copyOnEnter: true
		} );

		editor.conversion.attributeToElement( {
			model: ITALIC$1,
			view: 'i',
			upcastAlso: [
				'em',
				{
					styles: {
						'font-style': 'italic'
					}
				}
			]
		} );

		// Create italic command.
		editor.commands.add( ITALIC$1, new AttributeCommand( editor, ITALIC$1 ) );

		// Set the Ctrl+I keystroke.
		editor.keystrokes.set( 'CTRL+I', ITALIC$1 );
	}
}var italicIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m9.586 14.633.021.004c-.036.335.095.655.393.962.082.083.173.15.274.201h1.474a.6.6 0 1 1 0 1.2H5.304a.6.6 0 0 1 0-1.2h1.15c.474-.07.809-.182 1.005-.334.157-.122.291-.32.404-.597l2.416-9.55a1.053 1.053 0 0 0-.281-.823 1.12 1.12 0 0 0-.442-.296H8.15a.6.6 0 0 1 0-1.2h6.443a.6.6 0 1 1 0 1.2h-1.195c-.376.056-.65.155-.823.296-.215.175-.423.439-.623.79l-2.366 9.347z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const ITALIC = 'italic';

/**
 * The italic UI feature. It introduces the Italic button.
 *
 * @extends module:core/plugin~Plugin
 */
class ItalicUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'ItalicUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		// Add bold button to feature components.
		editor.ui.componentFactory.add( ITALIC, locale => {
			const command = editor.commands.get( ITALIC );
			const view = new ButtonView( locale );

			view.set( {
				label: t( 'Italic' ),
				icon: italicIcon,
				keystroke: 'CTRL+I',
				tooltip: true,
				isToggleable: true
			} );

			view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

			// Execute command.
			this.listenTo( view, 'execute', () => {
				editor.execute( ITALIC );
				editor.editing.view.focus();
			} );

			return view;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The italic feature.
 *
 * For a detailed overview check the {@glink features/basic-styles Basic styles feature documentation}
 * and the {@glink api/basic-styles package page}.
 *
 * This is a "glue" plugin which loads the {@link module:basic-styles/italic/italicediting~ItalicEditing} and
 * {@link module:basic-styles/italic/italicui~ItalicUI} plugins.
 *
 * @extends module:core/plugin~Plugin
 */
class Italic extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ ItalicEditing, ItalicUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Italic';
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const STRIKETHROUGH$1 = 'strikethrough';

/**
 * The strikethrough editing feature.
 *
 * It registers the `'strikethrough'` command, the <kbd>Ctrl+Shift+X</kbd> keystroke and introduces the
 * `strikethroughsthrough` attribute in the model which renders to the view
 * as a `<s>` element.
 *
 * @extends module:core/plugin~Plugin
 */
class StrikethroughEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'StrikethroughEditing';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		// Allow strikethrough attribute on text nodes.
		editor.model.schema.extend( '$text', { allowAttributes: STRIKETHROUGH$1 } );
		editor.model.schema.setAttributeProperties( STRIKETHROUGH$1, {
			isFormatting: true,
			copyOnEnter: true
		} );

		editor.conversion.attributeToElement( {
			model: STRIKETHROUGH$1,
			view: 's',
			upcastAlso: [
				'del',
				'strike',
				{
					styles: {
						'text-decoration': 'line-through'
					}
				}
			]
		} );

		// Create strikethrough command.
		editor.commands.add( STRIKETHROUGH$1, new AttributeCommand( editor, STRIKETHROUGH$1 ) );

		// Set the Ctrl+Shift+X keystroke.
		editor.keystrokes.set( 'CTRL+SHIFT+X', 'strikethrough' );
	}
}var strikethroughIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M7 16.4c-.8-.4-1.5-.9-2.2-1.5a.6.6 0 0 1-.2-.5l.3-.6h1c1 1.2 2.1 1.7 3.7 1.7 1 0 1.8-.3 2.3-.6.6-.4.6-1.2.6-1.3.2-1.2-.9-2.1-.9-2.1h2.1c.3.7.4 1.2.4 1.7v.8l-.6 1.2c-.6.8-1.1 1-1.6 1.2a6 6 0 0 1-2.4.6c-1 0-1.8-.3-2.5-.6zM6.8 9 6 8.3c-.4-.5-.5-.8-.5-1.6 0-.7.1-1.3.5-1.8.4-.6 1-1 1.6-1.3a6.3 6.3 0 0 1 4.7 0 4 4 0 0 1 1.7 1l.3.7c0 .1.2.4-.2.7-.4.2-.9.1-1 0a3 3 0 0 0-1.2-1c-.4-.2-1-.3-2-.4-.7 0-1.4.2-2 .6-.8.6-1 .8-1 1.5 0 .8.5 1 1.2 1.5.6.4 1.1.7 1.9 1H6.8z\"/><path d=\"M3 10.5V9h14v1.5z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const STRIKETHROUGH = 'strikethrough';

/**
 * The strikethrough UI feature. It introduces the Strikethrough button.
 *
 * @extends module:core/plugin~Plugin
 */
class StrikethroughUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'StrikethroughUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		// Add strikethrough button to feature components.
		editor.ui.componentFactory.add( STRIKETHROUGH, locale => {
			const command = editor.commands.get( STRIKETHROUGH );
			const view = new ButtonView( locale );

			view.set( {
				label: t( 'Strikethrough' ),
				icon: strikethroughIcon,
				keystroke: 'CTRL+SHIFT+X',
				tooltip: true,
				isToggleable: true
			} );

			view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

			// Execute command.
			this.listenTo( view, 'execute', () => {
				editor.execute( STRIKETHROUGH );
				editor.editing.view.focus();
			} );

			return view;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The strikethrough feature.
 *
 * For a detailed overview check the {@glink features/basic-styles Basic styles feature documentation}
 * and the {@glink api/basic-styles package page}.
 *
 * This is a "glue" plugin which loads the {@link module:basic-styles/strikethrough/strikethroughediting~StrikethroughEditing} and
 * {@link module:basic-styles/strikethrough/strikethroughui~StrikethroughUI} plugins.
 *
 * @extends module:core/plugin~Plugin
 */
class Strikethrough extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ StrikethroughEditing, StrikethroughUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Strikethrough';
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const SUBSCRIPT$1 = 'subscript';

/**
 * The subscript editing feature.
 *
 * It registers the `sub` command and introduces the `sub` attribute in the model which renders to the view
 * as a `<sub>` element.
 *
 * @extends module:core/plugin~Plugin
 */
class SubscriptEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SubscriptEditing';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		// Allow sub attribute on text nodes.
		editor.model.schema.extend( '$text', { allowAttributes: SUBSCRIPT$1 } );
		editor.model.schema.setAttributeProperties( SUBSCRIPT$1, {
			isFormatting: true,
			copyOnEnter: true
		} );

		// Build converter from model to view for data and editing pipelines.

		editor.conversion.attributeToElement( {
			model: SUBSCRIPT$1,
			view: 'sub',
			upcastAlso: [
				{
					styles: {
						'vertical-align': 'sub'
					}
				}
			]
		} );

		// Create sub command.
		editor.commands.add( SUBSCRIPT$1, new AttributeCommand( editor, SUBSCRIPT$1 ) );
	}
}var subscriptIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"m7.03 10.349 3.818-3.819a.8.8 0 1 1 1.132 1.132L8.16 11.48l3.819 3.818a.8.8 0 1 1-1.132 1.132L7.03 12.61l-3.818 3.82a.8.8 0 1 1-1.132-1.132L5.9 11.48 2.08 7.662A.8.8 0 1 1 3.212 6.53l3.818 3.82zm8.147 7.829h2.549c.254 0 .447.05.58.152a.49.49 0 0 1 .201.413.54.54 0 0 1-.159.393c-.105.108-.266.162-.48.162h-3.594c-.245 0-.435-.066-.572-.197a.621.621 0 0 1-.205-.463c0-.114.044-.265.132-.453a1.62 1.62 0 0 1 .288-.444c.433-.436.824-.81 1.172-1.122.348-.312.597-.517.747-.615.267-.183.49-.368.667-.553.177-.185.312-.375.405-.57.093-.194.139-.384.139-.57a1.008 1.008 0 0 0-.554-.917 1.197 1.197 0 0 0-.56-.133c-.426 0-.761.182-1.005.546a2.332 2.332 0 0 0-.164.39 1.609 1.609 0 0 1-.258.488c-.096.114-.237.17-.423.17a.558.558 0 0 1-.405-.156.568.568 0 0 1-.161-.427c0-.218.05-.446.151-.683.101-.238.252-.453.452-.646s.454-.349.762-.467a2.998 2.998 0 0 1 1.081-.178c.498 0 .923.076 1.274.228a1.916 1.916 0 0 1 1.004 1.032 1.984 1.984 0 0 1-.156 1.794c-.2.32-.405.572-.613.754-.208.182-.558.468-1.048.857-.49.39-.826.691-1.008.906a2.703 2.703 0 0 0-.24.309z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const SUBSCRIPT = 'subscript';

/**
 * The subscript UI feature. It introduces the Subscript button.
 *
 * @extends module:core/plugin~Plugin
 */
class SubscriptUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SubscriptUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		// Add subscript button to feature components.
		editor.ui.componentFactory.add( SUBSCRIPT, locale => {
			const command = editor.commands.get( SUBSCRIPT );
			const view = new ButtonView( locale );

			view.set( {
				label: t( 'Subscript' ),
				icon: subscriptIcon,
				tooltip: true,
				isToggleable: true
			} );

			view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

			// Execute command.
			this.listenTo( view, 'execute', () => {
				editor.execute( SUBSCRIPT );
				editor.editing.view.focus();
			} );

			return view;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The subscript feature.
 *
 * It loads the {@link module:basic-styles/subscript/subscriptediting~SubscriptEditing} and
 * {@link module:basic-styles/subscript/subscriptui~SubscriptUI} plugins.
 *
 * @extends module:core/plugin~Plugin
 */
class Subscript extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ SubscriptEditing, SubscriptUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Subscript';
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const SUPERSCRIPT$1 = 'superscript';

/**
 * The superscript editing feature.
 *
 * It registers the `super` command and introduces the `super` attribute in the model which renders to the view
 * as a `<super>` element.
 *
 * @extends module:core/plugin~Plugin
 */
class SuperscriptEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SuperscriptEditing';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		// Allow super attribute on text nodes.
		editor.model.schema.extend( '$text', { allowAttributes: SUPERSCRIPT$1 } );
		editor.model.schema.setAttributeProperties( SUPERSCRIPT$1, {
			isFormatting: true,
			copyOnEnter: true
		} );

		// Build converter from model to view for data and editing pipelines.

		editor.conversion.attributeToElement( {
			model: SUPERSCRIPT$1,
			view: 'sup',
			upcastAlso: [
				{
					styles: {
						'vertical-align': 'super'
					}
				}
			]
		} );

		// Create super command.
		editor.commands.add( SUPERSCRIPT$1, new AttributeCommand( editor, SUPERSCRIPT$1 ) );
	}
}var superscriptIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M15.677 8.678h2.549c.254 0 .447.05.58.152a.49.49 0 0 1 .201.413.54.54 0 0 1-.159.393c-.105.108-.266.162-.48.162h-3.594c-.245 0-.435-.066-.572-.197a.621.621 0 0 1-.205-.463c0-.114.044-.265.132-.453a1.62 1.62 0 0 1 .288-.444c.433-.436.824-.81 1.172-1.122.348-.312.597-.517.747-.615.267-.183.49-.368.667-.553.177-.185.312-.375.405-.57.093-.194.139-.384.139-.57a1.008 1.008 0 0 0-.554-.917 1.197 1.197 0 0 0-.56-.133c-.426 0-.761.182-1.005.546a2.332 2.332 0 0 0-.164.39 1.609 1.609 0 0 1-.258.488c-.096.114-.237.17-.423.17a.558.558 0 0 1-.405-.156.568.568 0 0 1-.161-.427c0-.218.05-.446.151-.683.101-.238.252-.453.452-.646s.454-.349.762-.467a2.998 2.998 0 0 1 1.081-.178c.498 0 .923.076 1.274.228a1.916 1.916 0 0 1 1.004 1.032 1.984 1.984 0 0 1-.156 1.794c-.2.32-.405.572-.613.754-.208.182-.558.468-1.048.857-.49.39-.826.691-1.008.906a2.703 2.703 0 0 0-.24.309zM7.03 10.349l3.818-3.819a.8.8 0 1 1 1.132 1.132L8.16 11.48l3.819 3.818a.8.8 0 1 1-1.132 1.132L7.03 12.61l-3.818 3.82a.8.8 0 1 1-1.132-1.132L5.9 11.48 2.08 7.662A.8.8 0 1 1 3.212 6.53l3.818 3.82z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const SUPERSCRIPT = 'superscript';

/**
 * The superscript UI feature. It introduces the Superscript button.
 *
 * @extends module:core/plugin~Plugin
 */
class SuperscriptUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SuperscriptUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		// Add superscript button to feature components.
		editor.ui.componentFactory.add( SUPERSCRIPT, locale => {
			const command = editor.commands.get( SUPERSCRIPT );
			const view = new ButtonView( locale );

			view.set( {
				label: t( 'Superscript' ),
				icon: superscriptIcon,
				tooltip: true,
				isToggleable: true
			} );

			view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

			// Execute command.
			this.listenTo( view, 'execute', () => {
				editor.execute( SUPERSCRIPT );
				editor.editing.view.focus();
			} );

			return view;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The superscript feature.
 *
 * It loads the {@link module:basic-styles/superscript/superscriptediting~SuperscriptEditing} and
 * {@link module:basic-styles/superscript/superscriptui~SuperscriptUI} plugins.
 *
 * @extends module:core/plugin~Plugin
 */
class Superscript extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ SuperscriptEditing, SuperscriptUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Superscript';
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const UNDERLINE$1 = 'underline';

/**
 * The underline editing feature.
 *
 * It registers the `'underline'` command, the <kbd>Ctrl+U</kbd> keystroke
 * and introduces the `underline` attribute in the model which renders to the view as an `<u>` element.
 *
 * @extends module:core/plugin~Plugin
 */
class UnderlineEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'UnderlineEditing';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		// Allow strikethrough attribute on text nodes.
		editor.model.schema.extend( '$text', { allowAttributes: UNDERLINE$1 } );
		editor.model.schema.setAttributeProperties( UNDERLINE$1, {
			isFormatting: true,
			copyOnEnter: true
		} );

		editor.conversion.attributeToElement( {
			model: UNDERLINE$1,
			view: 'u',
			upcastAlso: {
				styles: {
					'text-decoration': 'underline'
				}
			}
		} );

		// Create underline command.
		editor.commands.add( UNDERLINE$1, new AttributeCommand( editor, UNDERLINE$1 ) );

		// Set the Ctrl+U keystroke.
		editor.keystrokes.set( 'CTRL+U', 'underline' );
	}
}var underlineIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M3 18v-1.5h14V18zm2.2-8V3.6c0-.4.4-.6.8-.6.3 0 .7.2.7.6v6.2c0 2 1.3 2.8 3.2 2.8 1.9 0 3.4-.9 3.4-2.9V3.6c0-.3.4-.5.8-.5.3 0 .7.2.7.5V10c0 2.7-2.2 4-4.9 4-2.6 0-4.7-1.2-4.7-4z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const UNDERLINE = 'underline';

/**
 * The underline UI feature. It introduces the Underline button.
 *
 * @extends module:core/plugin~Plugin
 */
class UnderlineUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'UnderlineUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		// Add bold button to feature components.
		editor.ui.componentFactory.add( UNDERLINE, locale => {
			const command = editor.commands.get( UNDERLINE );
			const view = new ButtonView( locale );

			view.set( {
				label: t( 'Underline' ),
				icon: underlineIcon,
				keystroke: 'CTRL+U',
				tooltip: true,
				isToggleable: true
			} );

			view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

			// Execute command.
			this.listenTo( view, 'execute', () => {
				editor.execute( UNDERLINE );
				editor.editing.view.focus();
			} );

			return view;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The underline feature.
 *
 * For a detailed overview check the {@glink features/basic-styles Basic styles feature documentation}
 * and the {@glink api/basic-styles package page}.
 *
 * This is a "glue" plugin which loads the {@link module:basic-styles/underline/underlineediting~UnderlineEditing} and
 * {@link module:basic-styles/underline/underlineui~UnderlineUI} plugins.
 *
 * @extends module:core/plugin~Plugin
 */
class Underline extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ UnderlineEditing, UnderlineUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'Underline';
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A class representing the navigation part of the special characters UI. It is responsible
 * for describing the feature and allowing the user to select a particular character group.
 *
 * @extends module:ui/formheader/formheaderview~FormHeaderView
 */
class SpecialCharactersNavigationView extends FormHeaderView {
	/**
	 * Creates an instance of the {@link module:special-characters/ui/specialcharactersnavigationview~SpecialCharactersNavigationView}
	 * class.
	 *
	 * @param {module:utils/locale~Locale} locale The localization services instance.
	 * @param {Iterable.<String>} groupNames The names of the character groups.
	 */
	constructor( locale, groupNames ) {
		super( locale );

		const t = locale.t;

		this.set( 'class', 'ck-special-characters-navigation' );

		/**
		 * A dropdown that allows selecting a group of special characters to be displayed.
		 *
		 * @member {module:ui/dropdown/dropdownview~DropdownView}
		 */
		this.groupDropdownView = this._createGroupDropdown( groupNames );
		this.groupDropdownView.panelPosition = locale.uiLanguageDirection === 'rtl' ? 'se' : 'sw';

		/**
		 * @inheritDoc
		 */
		this.label = t( 'Special characters' );

		/**
		 * @inheritDoc
		 */
		this.children.add( this.groupDropdownView );
	}

	/**
	 * Returns the name of the character group currently selected in the {@link #groupDropdownView}.
	 *
	 * @type {String}
	 */
	get currentGroupName() {
		return this.groupDropdownView.value;
	}

	/**
	 * Focuses the character categories dropdown.
	 */
	focus() {
		this.groupDropdownView.focus();
	}

	/**
	 * Returns a dropdown that allows selecting character groups.
	 *
	 * @private
	 * @param {Iterable.<String>} groupNames The names of the character groups.
	 * @returns {module:ui/dropdown/dropdownview~DropdownView}
	 */
	_createGroupDropdown( groupNames ) {
		const locale = this.locale;
		const t = locale.t;
		const dropdown = createDropdown( locale );
		const groupDefinitions = this._getCharacterGroupListItemDefinitions( dropdown, groupNames );

		dropdown.set( 'value', groupDefinitions.first.model.label );

		dropdown.buttonView.bind( 'label' ).to( dropdown, 'value' );

		dropdown.buttonView.set( {
			isOn: false,
			withText: true,
			tooltip: t( 'Character categories' ),
			class: [ 'ck-dropdown__button_label-width_auto' ]
		} );

		dropdown.on( 'execute', evt => {
			dropdown.value = evt.source.label;
		} );

		dropdown.delegate( 'execute' ).to( this );

		addListToDropdown( dropdown, groupDefinitions );

		return dropdown;
	}

	/**
	 * Returns list item definitions to be used in the character group dropdown
	 * representing specific character groups.
	 *
	 * @private
	 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdown
	 * @param {Iterable.<String>} groupNames The names of the character groups.
	 * @returns {Iterable.<module:ui/dropdown/utils~ListDropdownItemDefinition>}
	 */
	_getCharacterGroupListItemDefinitions( dropdown, groupNames ) {
		const groupDefs = new Collection();

		for ( const name of groupNames ) {
			const definition = {
				type: 'button',
				model: new Model( {
					label: name,
					withText: true
				} )
			};

			definition.model.bind( 'isOn' ).to( dropdown, 'value', value => {
				return value === definition.model.label;
			} );

			groupDefs.add( definition );
		}

		return groupDefs;
	}
}var css_248z$4 = ".ck.ck-character-grid{max-width:100%}.ck.ck-character-grid .ck-character-grid__tiles{display:grid}:root{--ck-character-grid-tile-size:24px}.ck.ck-character-grid{max-height:200px;overflow-x:hidden;overflow-y:auto;width:350px}@media screen and (max-width:600px){.ck.ck-character-grid{width:190px}}.ck.ck-character-grid .ck-character-grid__tiles{grid-gap:var(--ck-spacing-standard);grid-template-columns:repeat(10,1fr);margin:var(--ck-spacing-standard) var(--ck-spacing-large)}@media screen and (max-width:600px){.ck.ck-character-grid .ck-character-grid__tiles{grid-template-columns:repeat(5,1fr)}}.ck.ck-character-grid .ck-character-grid__tile{border:0;font-size:1.2em;height:var(--ck-character-grid-tile-size);min-height:var(--ck-character-grid-tile-size);min-width:var(--ck-character-grid-tile-size);padding:0;transition:box-shadow .2s ease;width:var(--ck-character-grid-tile-size)}.ck.ck-character-grid .ck-character-grid__tile:focus:not(.ck-disabled),.ck.ck-character-grid .ck-character-grid__tile:hover:not(.ck-disabled){border:0;box-shadow:inset 0 0 0 1px var(--ck-color-base-background),0 0 0 2px var(--ck-color-focus-border)}.ck.ck-character-grid .ck-character-grid__tile .ck-button__label{line-height:var(--ck-character-grid-tile-size);text-align:center;width:100%}";
styleInject(css_248z$4);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A grid of character tiles. It allows browsing special characters and selecting the character to
 * be inserted into the content.
 *
 * @extends module:ui/view~View
 */
class CharacterGridView extends View {
	/**
	 * Creates an instance of a character grid containing tiles representing special characters.
	 *
	 * @param {module:utils/locale~Locale} locale The localization services instance.
	 */
	constructor( locale ) {
		super( locale );

		/**
		 * A collection of the child tile views. Each tile represents a particular character.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.tiles = this.createCollection();

		this.setTemplate( {
			tag: 'div',
			children: [
				{
					tag: 'div',
					attributes: {
						class: [
							'ck',
							'ck-character-grid__tiles'
						]
					},
					children: this.tiles
				}
			],
			attributes: {
				class: [
					'ck',
					'ck-character-grid'
				]
			}
		} );

		/**
		 * Tracks information about the DOM focus in the grid.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
		 *
		 * @readonly
		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
		 */
		this.keystrokes = new KeystrokeHandler();

		addKeyboardHandlingForGrid( {
			keystrokeHandler: this.keystrokes,
			focusTracker: this.focusTracker,
			gridItems: this.tiles,
			numberOfColumns: () => global$2.window
				.getComputedStyle( this.element.firstChild ) // Responsive .ck-character-grid__tiles
				.getPropertyValue( 'grid-template-columns' )
				.split( ' ' )
				.length
		} );

		/**
		 * Fired when any of {@link #tiles grid tiles} is clicked.
		 *
		 * @event execute
		 * @param {Object} data Additional information about the event.
		 * @param {String} data.name The name of the tile that caused the event (e.g. "greek small letter epsilon").
		 * @param {String} data.character A human-readable character displayed as the label (e.g. "ε").
		 */

		/**
		 * Fired when a mouse or another pointing device caused the cursor to move onto any {@link #tiles grid tile}
		 * (similar to the native `mouseover` DOM event).
		 *
		 * @event tileHover
		 * @param {Object} data Additional information about the event.
		 * @param {String} data.name The name of the tile that caused the event (e.g. "greek small letter epsilon").
		 * @param {String} data.character A human-readable character displayed as the label (e.g. "ε").
		 */
	}

	/**
	 * Creates a new tile for the grid.
	 *
	 * @param {String} character A human-readable character displayed as the label (e.g. "ε").
	 * @param {String} name The name of the character (e.g. "greek small letter epsilon").
	 * @returns {module:ui/button/buttonview~ButtonView}
	 */
	createTile( character, name ) {
		const tile = new ButtonView( this.locale );

		tile.set( {
			label: character,
			withText: true,
			class: 'ck-character-grid__tile'
		} );

		// Labels are vital for the users to understand what character they're looking at.
		// For now we're using native title attribute for that, see #5817.
		tile.extendTemplate( {
			attributes: {
				title: name
			},
			on: {
				mouseover: tile.bindTemplate.to( 'mouseover' )
			}
		} );

		tile.on( 'mouseover', () => {
			this.fire( 'tileHover', { name, character } );
		} );

		tile.on( 'execute', () => {
			this.fire( 'execute', { name, character } );
		} );

		return tile;
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		for ( const item of this.tiles ) {
			this.focusTracker.add( item.element );
		}

		this.tiles.on( 'change', ( eventInfo, { added, removed } ) => {
			if ( added.length > 0 ) {
				for ( const item of added ) {
					this.focusTracker.add( item.element );
				}
			}
			if ( removed.length > 0 ) {
				for ( const item of removed ) {
					this.focusTracker.remove( item.element );
				}
			}
		} );

		this.keystrokes.listenTo( this.element );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this.keystrokes.destroy();
	}

	/**
	 * Focuses the first focusable in {@link #tiles}.
	 */
	focus() {
		this.tiles.get( 0 ).focus();
	}
}var css_248z$3 = ".ck.ck-character-info{border-top:1px solid var(--ck-color-base-border);display:flex;justify-content:space-between;padding:var(--ck-spacing-small) var(--ck-spacing-large)}.ck.ck-character-info>*{font-size:var(--ck-font-size-small);text-transform:uppercase}.ck.ck-character-info .ck-character-info__name{max-width:280px;overflow:hidden;text-overflow:ellipsis}.ck.ck-character-info .ck-character-info__code{opacity:.6}@media screen and (max-width:600px){.ck.ck-character-info{max-width:190px}}";
styleInject(css_248z$3);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The view displaying detailed information about a special character glyph, e.g. upon
 * hovering it with a mouse.
 *
 * @extends module:ui/view~View
 */
class CharacterInfoView extends View {
	constructor( locale ) {
		super( locale );

		const bind = this.bindTemplate;

		/**
		 * The character whose information is displayed by the view. For instance,
		 * "∑" or "¿".
		 *
		 * @observable
		 * @member {String|null} #character
		 */
		this.set( 'character', null );

		/**
		 * The name of the {@link #character}. For instance,
		 * "N-ary summation" or "Inverted question mark".
		 *
		 * @observable
		 * @member {String|null} #name
		 */
		this.set( 'name', null );

		/**
		 * The "Unicode string" of the {@link #character}. For instance,
		 * "U+0061".
		 *
		 * @observable
		 * @readonly
		 * @member {String} #code
		 */
		this.bind( 'code' ).to( this, 'character', characterToUnicodeString );

		this.setTemplate( {
			tag: 'div',
			children: [
				{
					tag: 'span',
					attributes: {
						class: [
							'ck-character-info__name'
						]
					},
					children: [
						{
							// Note: ZWSP to prevent vertical collapsing.
							text: bind.to( 'name', name => name ? name : '\u200B' )
						}
					]
				},
				{
					tag: 'span',
					attributes: {
						class: [
							'ck-character-info__code'
						]
					},
					children: [
						{
							text: bind.to( 'code' )
						}
					]
				}
			],
			attributes: {
				class: [
					'ck',
					'ck-character-info'
				]
			}
		} );
	}
}

// Converts a character into a "Unicode string", for instance:
//
//	"$" -> "U+0024"
//
// Returns an empty string when the character is `null`.
//
// @param {String} character
// @returns {String}
function characterToUnicodeString( character ) {
	if ( character === null ) {
		return '';
	}

	const hexCode = character.codePointAt( 0 ).toString( 16 );

	return 'U+' + ( '0000' + hexCode ).slice( -4 );
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A view that glues pieces of the special characters dropdown panel together:
 *
 * * the navigation view (allows selecting the category),
 * * the grid view (displays characters as a grid),
 * * and the info view (displays detailed info about a specific character).
 *
 * @extends module:ui/view~View
 */
class SpecialCharactersView extends View {
	/**
	 * Creates an instance of the `SpecialCharactersView`.
	 *
	 * @param {module:utils/locale~Locale} locale The localization services instance.
	 * @param {module:special-characters/ui/specialcharactersnavigationview~SpecialCharactersNavigationView} navigationView
	 * @param {module:special-characters/ui/charactergridview~CharacterGridView} gridView
	 * @param {module:special-characters/ui/characterinfoview~CharacterInfoView} infoView
	 */
	constructor( locale, navigationView, gridView, infoView ) {
		super( locale );

		/**
		 * A collection of the focusable children of the view.
		 *
		 * @readonly
		 * @member {module:ui/viewcollection~ViewCollection}
		 */
		this.items = this.createCollection();

		/**
		 * Tracks information about the DOM focus in the view.
		 *
		 * @readonly
		 * @member {module:utils/focustracker~FocusTracker}
		 */
		this.focusTracker = new FocusTracker();

		/**
		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
		 *
		 * @readonly
		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
		 */
		this.keystrokes = new KeystrokeHandler();

		/**
		 * Helps cycling over focusable {@link #items} in the view.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/focuscycler~FocusCycler}
		 */
		this._focusCycler = new FocusCycler( {
			focusables: this.items,
			focusTracker: this.focusTracker,
			keystrokeHandler: this.keystrokes,
			actions: {
				focusPrevious: 'shift + tab',
				focusNext: 'tab'
			}
		} );

		/**
		 * An instance of the `SpecialCharactersNavigationView`.
		 *
		 * @member {module:special-characters/ui/specialcharactersnavigationview~SpecialCharactersNavigationView}
		 */
		this.navigationView = navigationView;

		/**
		 * An instance of the `CharacterGridView`.
		 *
		 * @member {module:special-characters/ui/charactergridview~CharacterGridView}
		 */
		this.gridView = gridView;

		/**
		 * An instance of the `CharacterInfoView`.
		 *
		 * @member {module:special-characters/ui/characterinfoview~CharacterInfoView}
		 */
		this.infoView = infoView;

		this.setTemplate( {
			tag: 'div',
			children: [
				this.navigationView,
				this.gridView,
				this.infoView
			]
		} );

		this.items.add( this.navigationView.groupDropdownView.buttonView );
		this.items.add( this.gridView );
	}

	/**
	 * @inheritDoc
	 */
	render() {
		super.render();

		this.focusTracker.add( this.navigationView.groupDropdownView.buttonView.element );
		this.focusTracker.add( this.gridView.element );

		// Start listening for the keystrokes coming from #element.
		this.keystrokes.listenTo( this.element );
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		super.destroy();

		this.focusTracker.destroy();
		this.keystrokes.destroy();
	}

	/**
	 * Focuses the first focusable in {@link #items}.
	 */
	focus() {
		this.navigationView.focus();
	}
}var specialCharactersIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M10 2.5a7.47 7.47 0 0 1 4.231 1.31 7.268 7.268 0 0 1 2.703 3.454 7.128 7.128 0 0 1 .199 4.353c-.39 1.436-1.475 2.72-2.633 3.677h2.013c0-.226.092-.443.254-.603a.876.876 0 0 1 1.229 0c.163.16.254.377.254.603v.853c0 .209-.078.41-.22.567a.873.873 0 0 1-.547.28l-.101.006h-4.695a.517.517 0 0 1-.516-.518v-1.265c0-.21.128-.398.317-.489a5.601 5.601 0 0 0 2.492-2.371 5.459 5.459 0 0 0 .552-3.693 5.53 5.53 0 0 0-1.955-3.2A5.71 5.71 0 0 0 10 4.206 5.708 5.708 0 0 0 6.419 5.46 5.527 5.527 0 0 0 4.46 8.663a5.457 5.457 0 0 0 .554 3.695 5.6 5.6 0 0 0 2.497 2.37.55.55 0 0 1 .317.49v1.264c0 .286-.23.518-.516.518H2.618a.877.877 0 0 1-.614-.25.845.845 0 0 1-.254-.603v-.853c0-.226.091-.443.254-.603a.876.876 0 0 1 1.228 0c.163.16.255.377.255.603h1.925c-1.158-.958-2.155-2.241-2.545-3.678a7.128 7.128 0 0 1 .199-4.352 7.268 7.268 0 0 1 2.703-3.455A7.475 7.475 0 0 1 10 2.5z\"/></svg>";
var css_248z$2 = ".ck.ck-special-characters-navigation>.ck-label{max-width:160px;overflow:hidden;text-overflow:ellipsis}.ck.ck-special-characters-navigation>.ck-dropdown .ck-dropdown__panel{max-height:250px;overflow-x:hidden;overflow-y:auto}@media screen and (max-width:600px){.ck.ck-special-characters-navigation{max-width:190px}.ck.ck-special-characters-navigation>.ck-form__header__label{overflow:hidden;text-overflow:ellipsis}}";
styleInject(css_248z$2);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

const ALL_SPECIAL_CHARACTERS_GROUP = 'All';

/**
 * The special characters feature.
 *
 * Introduces the `'specialCharacters'` dropdown.
 *
 * @extends module:core/plugin~Plugin
 */
class SpecialCharacters extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ Typing ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SpecialCharacters';
	}

	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		/**
		 * Registered characters. A pair of a character name and its symbol.
		 *
		 * @private
		 * @member {Map.<String, String>} #_characters
		 */
		this._characters = new Map();

		/**
		 * Registered groups. Each group contains a collection with symbol names.
		 *
		 * @private
		 * @member {Map.<String, Set.<String>>} #_groups
		 */
		this._groups = new Map();
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		const inputCommand = editor.commands.get( 'input' );

		// Add the `specialCharacters` dropdown button to feature components.
		editor.ui.componentFactory.add( 'specialCharacters', locale => {
			const dropdownView = createDropdown( locale );
			let dropdownPanelContent;

			dropdownView.buttonView.set( {
				label: t( 'Special characters' ),
				icon: specialCharactersIcon,
				tooltip: true
			} );

			dropdownView.bind( 'isEnabled' ).to( inputCommand );

			// Insert a special character when a tile was clicked.
			dropdownView.on( 'execute', ( evt, data ) => {
				editor.execute( 'input', { text: data.character } );
				editor.editing.view.focus();
			} );

			dropdownView.on( 'change:isOpen', () => {
				if ( !dropdownPanelContent ) {
					dropdownPanelContent = this._createDropdownPanelContent( locale, dropdownView );

					const specialCharactersView = new SpecialCharactersView(
						locale,
						dropdownPanelContent.navigationView,
						dropdownPanelContent.gridView,
						dropdownPanelContent.infoView
					);

					dropdownView.panelView.children.add( specialCharactersView );
				}

				dropdownPanelContent.infoView.set( {
					character: null,
					name: null
				} );
			} );

			return dropdownView;
		} );
	}

	/**
	 * Adds a collection of special characters to the specified group. The title of a special character must be unique.
	 *
	 * **Note:** The "All" category name is reserved by the plugin and cannot be used as a new name for a special
	 * characters category.
	 *
	 * @param {String} groupName
	 * @param {Array.<module:special-characters/specialcharacters~SpecialCharacterDefinition>} items
	 */
	addItems( groupName, items ) {
		if ( groupName === ALL_SPECIAL_CHARACTERS_GROUP ) {
			/**
			 * The name "All" for a special category group cannot be used because it is a special category that displays all
			 * available special characters.
			 *
			 * @error special-character-invalid-group-name
			 */
			throw new CKEditorError(
				`special-character-invalid-group-name: The name "${ ALL_SPECIAL_CHARACTERS_GROUP }" is reserved and cannot be used.`
			);
		}

		const group = this._getGroup( groupName );

		for ( const item of items ) {
			group.add( item.title );
			this._characters.set( item.title, item.character );
		}
	}

	/**
	 * Returns an iterator of special characters groups.
	 *
	 * @returns {Iterable.<String>}
	 */
	getGroups() {
		return this._groups.keys();
	}

	/**
	 * Returns a collection of special characters symbol names (titles).
	 *
	 * @param {String} groupName
	 * @returns {Set.<String>|undefined}
	 */
	getCharactersForGroup( groupName ) {
		if ( groupName === ALL_SPECIAL_CHARACTERS_GROUP ) {
			return new Set( this._characters.keys() );
		}

		return this._groups.get( groupName );
	}

	/**
	 * Returns the symbol of a special character for the specified name. If the special character could not be found, `undefined`
	 * is returned.
	 *
	 * @param {String} title The title of a special character.
	 * @returns {String|undefined}
	 */
	getCharacter( title ) {
		return this._characters.get( title );
	}

	/**
	 * Returns a group of special characters. If the group with the specified name does not exist, it will be created.
	 *
	 * @private
	 * @param {String} groupName The name of the group to create.
	 */
	_getGroup( groupName ) {
		if ( !this._groups.has( groupName ) ) {
			this._groups.set( groupName, new Set() );
		}

		return this._groups.get( groupName );
	}

	/**
	 * Updates the symbol grid depending on the currently selected character group.
	 *
	 * @private
	 * @param {String} currentGroupName
	 * @param {module:special-characters/ui/charactergridview~CharacterGridView} gridView
	 */
	_updateGrid( currentGroupName, gridView ) {
		// Updating the grid starts with removing all tiles belonging to the old group.
		gridView.tiles.clear();

		const characterTitles = this.getCharactersForGroup( currentGroupName );

		for ( const title of characterTitles ) {
			const character = this.getCharacter( title );

			gridView.tiles.add( gridView.createTile( character, title ) );
		}
	}

	/**
	 * Initializes the dropdown, used for lazy loading.
	 *
	 * @private
	 * @param {module:utils/locale~Locale} locale
	 * @param {module:ui/dropdown/dropdownview~DropdownView} dropdownView
	 * @returns {Object} Returns an object with `navigationView`, `gridView` and `infoView` properties, containing UI parts.
	 */
	_createDropdownPanelContent( locale, dropdownView ) {
		const specialCharsGroups = [ ...this.getGroups() ];

		// Add a special group that shows all available special characters.
		specialCharsGroups.unshift( ALL_SPECIAL_CHARACTERS_GROUP );

		const navigationView = new SpecialCharactersNavigationView( locale, specialCharsGroups );
		const gridView = new CharacterGridView( locale );
		const infoView = new CharacterInfoView( locale );

		gridView.delegate( 'execute' ).to( dropdownView );

		gridView.on( 'tileHover', ( evt, data ) => {
			infoView.set( data );
		} );

		// Update the grid of special characters when a user changed the character group.
		navigationView.on( 'execute', () => {
			this._updateGrid( navigationView.currentGroupName, gridView );
		} );

		// Set the initial content of the special characters grid.
		this._updateGrid( navigationView.currentGroupName, gridView );

		return { navigationView, gridView, infoView };
	}
}

/**
 * @typedef {Object} module:special-characters/specialcharacters~SpecialCharacterDefinition
 *
 * @property {String} title A unique name of the character (e.g. "greek small letter epsilon").
 * @property {String} character A human-readable character displayed as the label (e.g. "ε").
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A plugin that provides special characters for the "Arrows" category.
 *
 *		ClassicEditor
 *			.create( {
 *				plugins: [ ..., SpecialCharacters, SpecialCharactersArrows ],
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * @extends module:core/plugin~Plugin
 */
class SpecialCharactersArrows extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SpecialCharactersArrows';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		editor.plugins.get( 'SpecialCharacters' ).addItems( 'Arrows', [
			{ title: t( 'leftwards double arrow' ), character: '⇐' },
			{ title: t( 'rightwards double arrow' ), character: '⇒' },
			{ title: t( 'upwards double arrow' ), character: '⇑' },
			{ title: t( 'downwards double arrow' ), character: '⇓' },
			{ title: t( 'leftwards dashed arrow' ), character: '⇠' },
			{ title: t( 'rightwards dashed arrow' ), character: '⇢' },
			{ title: t( 'upwards dashed arrow' ), character: '⇡' },
			{ title: t( 'downwards dashed arrow' ), character: '⇣' },
			{ title: t( 'leftwards arrow to bar' ), character: '⇤' },
			{ title: t( 'rightwards arrow to bar' ), character: '⇥' },
			{ title: t( 'upwards arrow to bar' ), character: '⤒' },
			{ title: t( 'downwards arrow to bar' ), character: '⤓' },
			{ title: t( 'up down arrow with base' ), character: '↨' },
			{ title: t( 'back with leftwards arrow above' ), character: '🔙' },
			{ title: t( 'end with leftwards arrow above' ), character: '🔚' },
			{ title: t( 'on with exclamation mark with left right arrow above' ), character: '🔛' },
			{ title: t( 'soon with rightwards arrow above' ), character: '🔜' },
			{ title: t( 'top with upwards arrow above' ), character: '🔝' }
		] );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A plugin that provides special characters for the "Text" category.
 *
 *		ClassicEditor
 *			.create( {
 *				plugins: [ ..., SpecialCharacters, SpecialCharactersText ],
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * @extends module:core/plugin~Plugin
 */
class SpecialCharactersText extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SpecialCharactersText';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		editor.plugins.get( 'SpecialCharacters' ).addItems( 'Text', [
			{ character: '‹', title: t( 'Single left-pointing angle quotation mark' ) },
			{ character: '›', title: t( 'Single right-pointing angle quotation mark' ) },
			{ character: '«', title: t( 'Left-pointing double angle quotation mark' ) },
			{ character: '»', title: t( 'Right-pointing double angle quotation mark' ) },
			{ character: '‘', title: t( 'Left single quotation mark' ) },
			{ character: '’', title: t( 'Right single quotation mark' ) },
			{ character: '“', title: t( 'Left double quotation mark' ) },
			{ character: '”', title: t( 'Right double quotation mark' ) },
			{ character: '‚', title: t( 'Single low-9 quotation mark' ) },
			{ character: '„', title: t( 'Double low-9 quotation mark' ) },
			{ character: '¡', title: t( 'Inverted exclamation mark' ) },
			{ character: '¿', title: t( 'Inverted question mark' ) },
			{ character: '‥', title: t( 'Two dot leader' ) },
			{ character: '…', title: t( 'Horizontal ellipsis' ) },
			{ character: '‡', title: t( 'Double dagger' ) },
			{ character: '‰', title: t( 'Per mille sign' ) },
			{ character: '‱', title: t( 'Per ten thousand sign' ) },
			{ character: '‼', title: t( 'Double exclamation mark' ) },
			{ character: '⁈', title: t( 'Question exclamation mark' ) },
			{ character: '⁉', title: t( 'Exclamation question mark' ) },
			{ character: '⁇', title: t( 'Double question mark' ) },
			{ character: '©', title: t( 'Copyright sign' ) },
			{ character: '®', title: t( 'Registered sign' ) },
			{ character: '™', title: t( 'Trade mark sign' ) },
			{ character: '§', title: t( 'Section sign' ) },
			{ character: '¶', title: t( 'Paragraph sign' ) },
			{ character: '⁋', title: t( 'Reversed paragraph sign' ) }
		] );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A plugin that provides special characters for the "Mathematical" category.
 *
 *		ClassicEditor
 *			.create( {
 *				plugins: [ ..., SpecialCharacters, SpecialCharactersMathematical ],
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * @extends module:core/plugin~Plugin
 */
class SpecialCharactersMathematical extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SpecialCharactersMathematical';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		editor.plugins.get( 'SpecialCharacters' ).addItems( 'Mathematical', [
			{ character: '<', title: t( 'Less-than sign' ) },
			{ character: '>', title: t( 'Greater-than sign' ) },
			{ character: '≤', title: t( 'Less-than or equal to' ) },
			{ character: '≥', title: t( 'Greater-than or equal to' ) },
			{ character: '–', title: t( 'En dash' ) },
			{ character: '—', title: t( 'Em dash' ) },
			{ character: '¯', title: t( 'Macron' ) },
			{ character: '‾', title: t( 'Overline' ) },
			{ character: '°', title: t( 'Degree sign' ) },
			{ character: '−', title: t( 'Minus sign' ) },
			{ character: '±', title: t( 'Plus-minus sign' ) },
			{ character: '÷', title: t( 'Division sign' ) },
			{ character: '⁄', title: t( 'Fraction slash' ) },
			{ character: '×', title: t( 'Multiplication sign' ) },
			{ character: 'ƒ', title: t( 'Latin small letter f with hook' ) },
			{ character: '∫', title: t( 'Integral' ) },
			{ character: '∑', title: t( 'N-ary summation' ) },
			{ character: '∞', title: t( 'Infinity' ) },
			{ character: '√', title: t( 'Square root' ) },
			{ character: '∼', title: t( 'Tilde operator' ) },
			{ character: '≅', title: t( 'Approximately equal to' ) },
			{ character: '≈', title: t( 'Almost equal to' ) },
			{ character: '≠', title: t( 'Not equal to' ) },
			{ character: '≡', title: t( 'Identical to' ) },
			{ character: '∈', title: t( 'Element of' ) },
			{ character: '∉', title: t( 'Not an element of' ) },
			{ character: '∋', title: t( 'Contains as member' ) },
			{ character: '∏', title: t( 'N-ary product' ) },
			{ character: '∧', title: t( 'Logical and' ) },
			{ character: '∨', title: t( 'Logical or' ) },
			{ character: '¬', title: t( 'Not sign' ) },
			{ character: '∩', title: t( 'Intersection' ) },
			{ character: '∪', title: t( 'Union' ) },
			{ character: '∂', title: t( 'Partial differential' ) },
			{ character: '∀', title: t( 'For all' ) },
			{ character: '∃', title: t( 'There exists' ) },
			{ character: '∅', title: t( 'Empty set' ) },
			{ character: '∇', title: t( 'Nabla' ) },
			{ character: '∗', title: t( 'Asterisk operator' ) },
			{ character: '∝', title: t( 'Proportional to' ) },
			{ character: '∠', title: t( 'Angle' ) },
			{ character: '¼', title: t( 'Vulgar fraction one quarter' ) },
			{ character: '½', title: t( 'Vulgar fraction one half' ) },
			{ character: '¾', title: t( 'Vulgar fraction three quarters' ) }
		] );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A plugin that provides special characters for the "Latin" category.
 *
 *		ClassicEditor
 *			.create( {
 *				plugins: [ ..., SpecialCharacters, SpecialCharactersLatin ],
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * @extends module:core/plugin~Plugin
 */
class SpecialCharactersLatin extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SpecialCharactersLatin';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		editor.plugins.get( 'SpecialCharacters' ).addItems( 'Latin', [
			{ character: 'Ā', title: t( 'Latin capital letter a with macron' ) },
			{ character: 'ā', title: t( 'Latin small letter a with macron' ) },
			{ character: 'Ă', title: t( 'Latin capital letter a with breve' ) },
			{ character: 'ă', title: t( 'Latin small letter a with breve' ) },
			{ character: 'Ą', title: t( 'Latin capital letter a with ogonek' ) },
			{ character: 'ą', title: t( 'Latin small letter a with ogonek' ) },
			{ character: 'Ć', title: t( 'Latin capital letter c with acute' ) },
			{ character: 'ć', title: t( 'Latin small letter c with acute' ) },
			{ character: 'Ĉ', title: t( 'Latin capital letter c with circumflex' ) },
			{ character: 'ĉ', title: t( 'Latin small letter c with circumflex' ) },
			{ character: 'Ċ', title: t( 'Latin capital letter c with dot above' ) },
			{ character: 'ċ', title: t( 'Latin small letter c with dot above' ) },
			{ character: 'Č', title: t( 'Latin capital letter c with caron' ) },
			{ character: 'č', title: t( 'Latin small letter c with caron' ) },
			{ character: 'Ď', title: t( 'Latin capital letter d with caron' ) },
			{ character: 'ď', title: t( 'Latin small letter d with caron' ) },
			{ character: 'Đ', title: t( 'Latin capital letter d with stroke' ) },
			{ character: 'đ', title: t( 'Latin small letter d with stroke' ) },
			{ character: 'Ē', title: t( 'Latin capital letter e with macron' ) },
			{ character: 'ē', title: t( 'Latin small letter e with macron' ) },
			{ character: 'Ĕ', title: t( 'Latin capital letter e with breve' ) },
			{ character: 'ĕ', title: t( 'Latin small letter e with breve' ) },
			{ character: 'Ė', title: t( 'Latin capital letter e with dot above' ) },
			{ character: 'ė', title: t( 'Latin small letter e with dot above' ) },
			{ character: 'Ę', title: t( 'Latin capital letter e with ogonek' ) },
			{ character: 'ę', title: t( 'Latin small letter e with ogonek' ) },
			{ character: 'Ě', title: t( 'Latin capital letter e with caron' ) },
			{ character: 'ě', title: t( 'Latin small letter e with caron' ) },
			{ character: 'Ĝ', title: t( 'Latin capital letter g with circumflex' ) },
			{ character: 'ĝ', title: t( 'Latin small letter g with circumflex' ) },
			{ character: 'Ğ', title: t( 'Latin capital letter g with breve' ) },
			{ character: 'ğ', title: t( 'Latin small letter g with breve' ) },
			{ character: 'Ġ', title: t( 'Latin capital letter g with dot above' ) },
			{ character: 'ġ', title: t( 'Latin small letter g with dot above' ) },
			{ character: 'Ģ', title: t( 'Latin capital letter g with cedilla' ) },
			{ character: 'ģ', title: t( 'Latin small letter g with cedilla' ) },
			{ character: 'Ĥ', title: t( 'Latin capital letter h with circumflex' ) },
			{ character: 'ĥ', title: t( 'Latin small letter h with circumflex' ) },
			{ character: 'Ħ', title: t( 'Latin capital letter h with stroke' ) },
			{ character: 'ħ', title: t( 'Latin small letter h with stroke' ) },
			{ character: 'Ĩ', title: t( 'Latin capital letter i with tilde' ) },
			{ character: 'ĩ', title: t( 'Latin small letter i with tilde' ) },
			{ character: 'Ī', title: t( 'Latin capital letter i with macron' ) },
			{ character: 'ī', title: t( 'Latin small letter i with macron' ) },
			{ character: 'Ĭ', title: t( 'Latin capital letter i with breve' ) },
			{ character: 'ĭ', title: t( 'Latin small letter i with breve' ) },
			{ character: 'Į', title: t( 'Latin capital letter i with ogonek' ) },
			{ character: 'į', title: t( 'Latin small letter i with ogonek' ) },
			{ character: 'İ', title: t( 'Latin capital letter i with dot above' ) },
			{ character: 'ı', title: t( 'Latin small letter dotless i' ) },
			{ character: 'Ĳ', title: t( 'Latin capital ligature ij' ) },
			{ character: 'ĳ', title: t( 'Latin small ligature ij' ) },
			{ character: 'Ĵ', title: t( 'Latin capital letter j with circumflex' ) },
			{ character: 'ĵ', title: t( 'Latin small letter j with circumflex' ) },
			{ character: 'Ķ', title: t( 'Latin capital letter k with cedilla' ) },
			{ character: 'ķ', title: t( 'Latin small letter k with cedilla' ) },
			{ character: 'ĸ', title: t( 'Latin small letter kra' ) },
			{ character: 'Ĺ', title: t( 'Latin capital letter l with acute' ) },
			{ character: 'ĺ', title: t( 'Latin small letter l with acute' ) },
			{ character: 'Ļ', title: t( 'Latin capital letter l with cedilla' ) },
			{ character: 'ļ', title: t( 'Latin small letter l with cedilla' ) },
			{ character: 'Ľ', title: t( 'Latin capital letter l with caron' ) },
			{ character: 'ľ', title: t( 'Latin small letter l with caron' ) },
			{ character: 'Ŀ', title: t( 'Latin capital letter l with middle dot' ) },
			{ character: 'ŀ', title: t( 'Latin small letter l with middle dot' ) },
			{ character: 'Ł', title: t( 'Latin capital letter l with stroke' ) },
			{ character: 'ł', title: t( 'Latin small letter l with stroke' ) },
			{ character: 'Ń', title: t( 'Latin capital letter n with acute' ) },
			{ character: 'ń', title: t( 'Latin small letter n with acute' ) },
			{ character: 'Ņ', title: t( 'Latin capital letter n with cedilla' ) },
			{ character: 'ņ', title: t( 'Latin small letter n with cedilla' ) },
			{ character: 'Ň', title: t( 'Latin capital letter n with caron' ) },
			{ character: 'ň', title: t( 'Latin small letter n with caron' ) },
			{ character: 'ŉ', title: t( 'Latin small letter n preceded by apostrophe' ) },
			{ character: 'Ŋ', title: t( 'Latin capital letter eng' ) },
			{ character: 'ŋ', title: t( 'Latin small letter eng' ) },
			{ character: 'Ō', title: t( 'Latin capital letter o with macron' ) },
			{ character: 'ō', title: t( 'Latin small letter o with macron' ) },
			{ character: 'Ŏ', title: t( 'Latin capital letter o with breve' ) },
			{ character: 'ŏ', title: t( 'Latin small letter o with breve' ) },
			{ character: 'Ő', title: t( 'Latin capital letter o with double acute' ) },
			{ character: 'ő', title: t( 'Latin small letter o with double acute' ) },
			{ character: 'Œ', title: t( 'Latin capital ligature oe' ) },
			{ character: 'œ', title: t( 'Latin small ligature oe' ) },
			{ character: 'Ŕ', title: t( 'Latin capital letter r with acute' ) },
			{ character: 'ŕ', title: t( 'Latin small letter r with acute' ) },
			{ character: 'Ŗ', title: t( 'Latin capital letter r with cedilla' ) },
			{ character: 'ŗ', title: t( 'Latin small letter r with cedilla' ) },
			{ character: 'Ř', title: t( 'Latin capital letter r with caron' ) },
			{ character: 'ř', title: t( 'Latin small letter r with caron' ) },
			{ character: 'Ś', title: t( 'Latin capital letter s with acute' ) },
			{ character: 'ś', title: t( 'Latin small letter s with acute' ) },
			{ character: 'Ŝ', title: t( 'Latin capital letter s with circumflex' ) },
			{ character: 'ŝ', title: t( 'Latin small letter s with circumflex' ) },
			{ character: 'Ş', title: t( 'Latin capital letter s with cedilla' ) },
			{ character: 'ş', title: t( 'Latin small letter s with cedilla' ) },
			{ character: 'Š', title: t( 'Latin capital letter s with caron' ) },
			{ character: 'š', title: t( 'Latin small letter s with caron' ) },
			{ character: 'Ţ', title: t( 'Latin capital letter t with cedilla' ) },
			{ character: 'ţ', title: t( 'Latin small letter t with cedilla' ) },
			{ character: 'Ť', title: t( 'Latin capital letter t with caron' ) },
			{ character: 'ť', title: t( 'Latin small letter t with caron' ) },
			{ character: 'Ŧ', title: t( 'Latin capital letter t with stroke' ) },
			{ character: 'ŧ', title: t( 'Latin small letter t with stroke' ) },
			{ character: 'Ũ', title: t( 'Latin capital letter u with tilde' ) },
			{ character: 'ũ', title: t( 'Latin small letter u with tilde' ) },
			{ character: 'Ū', title: t( 'Latin capital letter u with macron' ) },
			{ character: 'ū', title: t( 'Latin small letter u with macron' ) },
			{ character: 'Ŭ', title: t( 'Latin capital letter u with breve' ) },
			{ character: 'ŭ', title: t( 'Latin small letter u with breve' ) },
			{ character: 'Ů', title: t( 'Latin capital letter u with ring above' ) },
			{ character: 'ů', title: t( 'Latin small letter u with ring above' ) },
			{ character: 'Ű', title: t( 'Latin capital letter u with double acute' ) },
			{ character: 'ű', title: t( 'Latin small letter u with double acute' ) },
			{ character: 'Ų', title: t( 'Latin capital letter u with ogonek' ) },
			{ character: 'ų', title: t( 'Latin small letter u with ogonek' ) },
			{ character: 'Ŵ', title: t( 'Latin capital letter w with circumflex' ) },
			{ character: 'ŵ', title: t( 'Latin small letter w with circumflex' ) },
			{ character: 'Ŷ', title: t( 'Latin capital letter y with circumflex' ) },
			{ character: 'ŷ', title: t( 'Latin small letter y with circumflex' ) },
			{ character: 'Ÿ', title: t( 'Latin capital letter y with diaeresis' ) },
			{ character: 'Ź', title: t( 'Latin capital letter z with acute' ) },
			{ character: 'ź', title: t( 'Latin small letter z with acute' ) },
			{ character: 'Ż', title: t( 'Latin capital letter z with dot above' ) },
			{ character: 'ż', title: t( 'Latin small letter z with dot above' ) },
			{ character: 'Ž', title: t( 'Latin capital letter z with caron' ) },
			{ character: 'ž', title: t( 'Latin small letter z with caron' ) },
			{ character: 'ſ', title: t( 'Latin small letter long s' ) }
		] );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A plugin that provides special characters for the "Currency" category.
 *
 *		ClassicEditor
 *			.create( {
 *				plugins: [ ..., SpecialCharacters, SpecialCharactersCurrency ],
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * @extends module:core/plugin~Plugin
 */
class SpecialCharactersCurrency extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'SpecialCharactersCurrency';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		editor.plugins.get( 'SpecialCharacters' ).addItems( 'Currency', [
			{ character: '$', title: t( 'Dollar sign' ) },
			{ character: '€', title: t( 'Euro sign' ) },
			{ character: '¥', title: t( 'Yen sign' ) },
			{ character: '£', title: t( 'Pound sign' ) },
			{ character: '¢', title: t( 'Cent sign' ) },
			{ character: '₠', title: t( 'Euro-currency sign' ) },
			{ character: '₡', title: t( 'Colon sign' ) },
			{ character: '₢', title: t( 'Cruzeiro sign' ) },
			{ character: '₣', title: t( 'French franc sign' ) },
			{ character: '₤', title: t( 'Lira sign' ) },
			{ character: '¤', title: t( 'Currency sign' ) },
			{ character: '₿', title: t( 'Bitcoin sign' ) },
			{ character: '₥', title: t( 'Mill sign' ) },
			{ character: '₦', title: t( 'Naira sign' ) },
			{ character: '₧', title: t( 'Peseta sign' ) },
			{ character: '₨', title: t( 'Rupee sign' ) },
			{ character: '₩', title: t( 'Won sign' ) },
			{ character: '₪', title: t( 'New sheqel sign' ) },
			{ character: '₫', title: t( 'Dong sign' ) },
			{ character: '₭', title: t( 'Kip sign' ) },
			{ character: '₮', title: t( 'Tugrik sign' ) },
			{ character: '₯', title: t( 'Drachma sign' ) },
			{ character: '₰', title: t( 'German penny sign' ) },
			{ character: '₱', title: t( 'Peso sign' ) },
			{ character: '₲', title: t( 'Guarani sign' ) },
			{ character: '₳', title: t( 'Austral sign' ) },
			{ character: '₴', title: t( 'Hryvnia sign' ) },
			{ character: '₵', title: t( 'Cedi sign' ) },
			{ character: '₶', title: t( 'Livre tournois sign' ) },
			{ character: '₷', title: t( 'Spesmilo sign' ) },
			{ character: '₸', title: t( 'Tenge sign' ) },
			{ character: '₹', title: t( 'Indian rupee sign' ) },
			{ character: '₺', title: t( 'Turkish lira sign' ) },
			{ character: '₻', title: t( 'Nordic mark sign' ) },
			{ character: '₼', title: t( 'Manat sign' ) },
			{ character: '₽', title: t( 'Ruble sign' ) }
		] );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * A plugin combining a basic set of characters for the special characters plugin.
 *
 *		ClassicEditor
 *			.create( {
 *				plugins: [ ..., SpecialCharacters, SpecialCharactersEssentials ],
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * @extends module:core/plugin~Plugin
 */
class SpecialCharactersEssentials extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [
			SpecialCharactersCurrency,
			SpecialCharactersText,
			SpecialCharactersMathematical,
			SpecialCharactersArrows,
			SpecialCharactersLatin
		];
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The horizontal line command.
 *
 * The command is registered by {@link module:horizontal-line/horizontallineediting~HorizontalLineEditing} as `'horizontalLine'`.
 *
 * To insert a horizontal line at the current selection, execute the command:
 *
 *		editor.execute( 'horizontalLine' );
 *
 * @extends module:core/command~Command
 */
class HorizontalLineCommand extends Command {
	/**
	 * @inheritDoc
	 */
	refresh() {
		const model = this.editor.model;
		const schema = model.schema;
		const selection = model.document.selection;

		this.isEnabled = isHorizontalLineAllowedInParent( selection, schema, model );
	}

	/**
	 * Executes the command.
	 *
	 * @fires execute
	 */
	execute() {
		const model = this.editor.model;

		model.change( writer => {
			const horizontalElement = writer.createElement( 'horizontalLine' );

			model.insertObject( horizontalElement, null, null, { setSelection: 'after' } );
		} );
	}
}

// Checks if a horizontal line is allowed by the schema in the optimal insertion parent.
//
// @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
// @param {module:engine/model/schema~Schema} schema
// @param {module:engine/model/model~Model} model Model instance.
// @returns {Boolean}
function isHorizontalLineAllowedInParent( selection, schema, model ) {
	const parent = getInsertHorizontalLineParent( selection, model );

	return schema.checkChild( parent, 'horizontalLine' );
}

// Returns a node that will be used to insert a horizontal line with `model.insertContent` to check if the horizontal line can be
// placed there.
//
// @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
// @param {module:engine/model/model~Model} model Model instance.
// @returns {module:engine/model/element~Element}
function getInsertHorizontalLineParent( selection, model ) {
	const insertionRange = findOptimalInsertionRange( selection, model );
	const parent = insertionRange.start.parent;

	if ( parent.isEmpty && !parent.is( 'element', '$root' ) ) {
		return parent.parent;
	}

	return parent;
}var css_248z$1 = ".ck-editor__editable .ck-horizontal-line{display:flow-root}.ck-content hr{background:#dedede;border:0;height:4px;margin:15px 0}";
styleInject(css_248z$1);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The horizontal line editing feature.
 *
 * @extends module:core/plugin~Plugin
 */
class HorizontalLineEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'HorizontalLineEditing';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const schema = editor.model.schema;
		const t = editor.t;
		const conversion = editor.conversion;

		schema.register( 'horizontalLine', {
			inheritAllFrom: '$blockObject'
		} );

		conversion.for( 'dataDowncast' ).elementToElement( {
			model: 'horizontalLine',
			view: ( modelElement, { writer } ) => {
				return writer.createEmptyElement( 'hr' );
			}
		} );

		conversion.for( 'editingDowncast' ).elementToStructure( {
			model: 'horizontalLine',
			view: ( modelElement, { writer } ) => {
				const label = t( 'Horizontal line' );

				const viewWrapper = writer.createContainerElement( 'div', null,
					writer.createEmptyElement( 'hr' )
				);

				writer.addClass( 'ck-horizontal-line', viewWrapper );
				writer.setCustomProperty( 'hr', true, viewWrapper );

				return toHorizontalLineWidget( viewWrapper, writer, label );
			}
		} );

		conversion.for( 'upcast' ).elementToElement( { view: 'hr', model: 'horizontalLine' } );

		editor.commands.add( 'horizontalLine', new HorizontalLineCommand( editor ) );
	}
}

// Converts a given {@link module:engine/view/element~Element} to a horizontal line widget:
// * Adds a {@link module:engine/view/element~Element#_setCustomProperty custom property} allowing to
//   recognize the horizontal line widget element.
// * Calls the {@link module:widget/utils~toWidget} function with the proper element's label creator.
//
//  @param {module:engine/view/element~Element} viewElement
//  @param {module:engine/view/downcastwriter~DowncastWriter} writer An instance of the view writer.
//  @param {String} label The element's label.
//  @returns {module:engine/view/element~Element}
function toHorizontalLineWidget( viewElement, writer, label ) {
	writer.setCustomProperty( 'horizontalLine', true, viewElement );

	return toWidget( viewElement, writer, { label } );
}var horizontalLineIcon = "<svg viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M2 9h16v2H2z\"/></svg>";
/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The horizontal line UI plugin.
 *
 * @extends module:core/plugin~Plugin
 */
class HorizontalLineUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'HorizontalLineUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const t = editor.t;

		// Add the `horizontalLine` button to feature components.
		editor.ui.componentFactory.add( 'horizontalLine', locale => {
			const command = editor.commands.get( 'horizontalLine' );
			const view = new ButtonView( locale );

			view.set( {
				label: t( 'Horizontal line' ),
				icon: horizontalLineIcon,
				tooltip: true
			} );

			view.bind( 'isEnabled' ).to( command, 'isEnabled' );

			// Execute the command.
			this.listenTo( view, 'execute', () => {
				editor.execute( 'horizontalLine' );
				editor.editing.view.focus();
			} );

			return view;
		} );
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The horizontal line feature.
 *
 * It provides the possibility to insert a horizontal line into the rich-text editor.
 *
 * For a detailed overview, check the {@glink features/horizontal-line Horizontal line feature} documentation.
 *
 * @extends module:core/plugin~Plugin
 */
class HorizontalLine extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ HorizontalLineEditing, HorizontalLineUI, Widget ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'HorizontalLine';
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module word-count/utils
 */

/**
 * Returns a plain text representation of an element and its children.
 *
 * @param {module:engine/model/element~Element} element
 * @returns {String} Plain text representing the model's data.
 */
function modelElementToPlainText( element ) {
	if ( element.is( '$text' ) || element.is( '$textProxy' ) ) {
		return element.data;
	}

	let text = '';
	let prev = null;

	for ( const child of element.getChildren() ) {
		const childText = modelElementToPlainText( child );

		// If last block was finish, start from new line.
		if ( prev && prev.is( 'element' ) ) {
			text += '\n';
		}

		text += childText;

		prev = child;
	}

	return text;
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The word count plugin.
 *
 * This plugin calculates all words and characters in all {@link module:engine/model/text~Text text nodes} available in the model.
 * It also provides an HTML element that updates its state whenever the editor content is changed.
 *
 * The model's data is first converted to plain text using {@link module:word-count/utils~modelElementToPlainText}.
 * The number of words and characters in your text are determined based on the created plain text. Please keep in mind
 * that every block in the editor is separated with a newline character, which is included in the calculation.
 *
 * Here are some examples of how the word and character calculations are made:
 *
 * 		<paragraph>foo</paragraph>
 * 		<paragraph>bar</paragraph>
 * 		// Words: 2, Characters: 7
 *
 * 		<paragraph><$text bold="true">foo</$text>bar</paragraph>
 * 		// Words: 1, Characters: 6
 *
 * 		<paragraph>*&^%)</paragraph>
 * 		// Words: 0, Characters: 5
 *
 * 		<paragraph>foo(bar)</paragraph>
 * 		//Words: 1, Characters: 8
 *
 * 		<paragraph>12345</paragraph>
 * 		// Words: 1, Characters: 5
 *
 * @extends module:core/plugin~Plugin
 */
class WordCount extends Plugin {
	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		super( editor );

		/**
		 * The number of characters in the editor.
		 *
		 * @observable
		 * @readonly
		 * @member {Number} module:word-count/wordcount~WordCount#characters
		 */
		this.set( 'characters', 0 );

		/**
		 * The number of words in the editor.
		 *
		 * @observable
		 * @readonly
		 * @member {Number} module:word-count/wordcount~WordCount#words
		 */
		this.set( 'words', 0 );

		// Don't wait for the #update event to set the value of the properties but obtain it right away.
		// This way, accessing the properties directly returns precise numbers, e.g. for validation, etc.
		// If not accessed directly, the properties will be refreshed upon #update anyway.
		Object.defineProperties( this, {
			characters: {
				get() {
					return ( this.characters = this._getCharacters() );
				}
			},
			words: {
				get() {
					return ( this.words = this._getWords() );
				}
			}
		} );

		/**
		 * The label used to display the words value in the {@link #wordCountContainer output container}.
		 *
		 * @observable
		 * @private
		 * @readonly
		 * @member {String} module:word-count/wordcount~WordCount#_wordsLabel
		 */
		this.set( '_wordsLabel' );

		/**
		 * The label used to display the characters value in the {@link #wordCountContainer output container}.
		 *
		 * @observable
		 * @private
		 * @readonly
		 * @member {String} module:word-count/wordcount~WordCount#_charactersLabel
		 */
		this.set( '_charactersLabel' );

		/**
		 * The configuration of this plugin.
		 *
		 * @private
		 * @type {Object}
		 */
		this._config = editor.config.get( 'wordCount' ) || {};

		/**
		 * The reference to a {@link module:ui/view~View view object} that contains the self-updating HTML container.
		 *
		 * @private
		 * @readonly
		 * @type {module:ui/view~View}
		 */
		this._outputView = undefined;

		/**
		 * A regular expression used to recognize words in the editor's content.
		 *
		 * @readonly
		 * @private
		 * @type {RegExp}
		 */
		this._wordsMatchRegExp = env.features.isRegExpUnicodePropertySupported ?
			// Usage of regular expression literal cause error during build (ckeditor/ckeditor5-dev#534).
			// Groups:
			// {L} - Any kind of letter from any language.
			// {N} - Any kind of numeric character in any script.
			new RegExp( '([\\p{L}\\p{N}]+\\S?)+', 'gu' ) :
			/([a-zA-Z0-9À-ž]+\S?)+/gu;
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'WordCount';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;

		editor.model.document.on( 'change:data', throttle( this._refreshStats.bind( this ), 250 ) );

		if ( typeof this._config.onUpdate == 'function' ) {
			this.on( 'update', ( evt, data ) => {
				this._config.onUpdate( data );
			} );
		}

		if ( isElement( this._config.container ) ) {
			this._config.container.appendChild( this.wordCountContainer );
		}
	}

	/**
	 * @inheritDoc
	 */
	destroy() {
		if ( this._outputView ) {
			this._outputView.element.remove();
			this._outputView.destroy();
		}

		super.destroy();
	}

	/**
	 * Creates a self-updating HTML element. Repeated executions return the same element.
	 * The returned element has the following HTML structure:
	 *
	 * 		<div class="ck ck-word-count">
	 * 			<div class="ck-word-count__words">Words: 4</div>
	 * 			<div class="ck-word-count__characters">Characters: 28</div>
	 * 		</div>
	 *
	 * @type {HTMLElement}
	 */
	get wordCountContainer() {
		const editor = this.editor;
		const t = editor.t;
		const displayWords = editor.config.get( 'wordCount.displayWords' );
		const displayCharacters = editor.config.get( 'wordCount.displayCharacters' );
		const bind = Template.bind( this, this );
		const children = [];

		if ( !this._outputView ) {
			this._outputView = new View();

			if ( displayWords || displayWords === undefined ) {
				this.bind( '_wordsLabel' ).to( this, 'words', words => {
					return t( 'Words: %0', words );
				} );

				children.push( {
					tag: 'div',
					children: [
						{
							text: [ bind.to( '_wordsLabel' ) ]
						}
					],
					attributes: {
						class: 'ck-word-count__words'
					}
				} );
			}

			if ( displayCharacters || displayCharacters === undefined ) {
				this.bind( '_charactersLabel' ).to( this, 'characters', words => {
					return t( 'Characters: %0', words );
				} );

				children.push( {
					tag: 'div',
					children: [
						{
							text: [ bind.to( '_charactersLabel' ) ]
						}
					],
					attributes: {
						class: 'ck-word-count__characters'
					}
				} );
			}

			this._outputView.setTemplate( {
				tag: 'div',
				attributes: {
					class: [
						'ck',
						'ck-word-count'
					]
				},
				children
			} );

			this._outputView.render();
		}

		return this._outputView.element;
	}

	/**
	 * Determines the number of characters in the current editor's model.
	 *
	 * @private
	 * @returns {Number}
	 */
	_getCharacters() {
		const txt = modelElementToPlainText( this.editor.model.document.getRoot() );

		return txt.replace( /\n/g, '' ).length;
	}

	/**
	 * Determines the number of words in the current editor's model.
	 *
	 * @private
	 * @returns {Number}
	 */
	_getWords() {
		const txt = modelElementToPlainText( this.editor.model.document.getRoot() );
		const detectedWords = txt.match( this._wordsMatchRegExp ) || [];

		return detectedWords.length;
	}

	/**
	 * Determines the number of words and characters in the current editor's model and assigns it to {@link #characters} and {@link #words}.
	 * It also fires the {@link #event:update}.
	 *
	 * @private
	 * @fires update
	 */
	_refreshStats() {
		const words = this.words = this._getWords();
		const characters = this.characters = this._getCharacters();

		this.fire( 'update', {
			words,
			characters
		} );
	}
}

/**
 * An event fired after {@link #words} and {@link #characters} are updated.
 *
 * @event update
 * @param {Object} data
 * @param {Number} data.words The number of words in the current model.
 * @param {Number} data.characters The number of characters in the current model.
 */

/**
 * The configuration of the word count feature.
 *
 *		ClassicEditor
 *			.create( {
 *				wordCount: ... // Word count feature configuration.
 *			} )
 *			.then( ... )
 *			.catch( ... );
 *
 * See {@link module:core/editor/editorconfig~EditorConfig all editor options}.
 *
 * @interface module:word-count/wordcount~WordCountConfig
 */

/**
 * The configuration of the word count feature.
 * It is introduced by the {@link module:word-count/wordcount~WordCount} feature.
 *
 * Read more in {@link module:word-count/wordcount~WordCountConfig}.
 *
 * @member {module:word-count/wordcount~WordCountConfig} module:core/editor/editorconfig~EditorConfig#wordCount
 */

/**
 * This option allows for hiding the word counter. The element obtained through
 * {@link module:word-count/wordcount~WordCount#wordCountContainer} will only preserve
 * the characters part. Word counter is displayed by default when this configuration option is not defined.
 *
 *		const wordCountConfig = {
 *			displayWords: false
 *		};
 *
 * The configuration above will result in the following container:
 *
 *		<div class="ck ck-word-count">
 *			<div class="ck-word-count__characters">Characters: 28</div>
 *		</div>
 *
 * @member {Boolean} module:word-count/wordcount~WordCountConfig#displayWords
 */

/**
 * This option allows for hiding the character counter. The element obtained through
 * {@link module:word-count/wordcount~WordCount#wordCountContainer} will only preserve
 * the words part. Character counter is displayed by default when this configuration option is not defined.
 *
 *		const wordCountConfig = {
 *			displayCharacters: false
 *		};
 *
 * The configuration above will result in the following container:
 *
 *		<div class="ck ck-word-count">
 *			<div class="ck-word-count__words">Words: 4</div>
 *		</div>
 *
 * @member {Boolean} module:word-count/wordcount~WordCountConfig#displayCharacters
 */

/**
 * This configuration takes a function that is executed whenever the word count plugin updates its values.
 * This function is called with one argument, which is an object with the `words` and `characters` keys containing
 * the number of detected words and characters in the document.
 *
 *		const wordCountConfig = {
 *			onUpdate: function( stats ) {
 *				doSthWithWordNumber( stats.words );
 *				doSthWithCharacterNumber( stats.characters );
 *			}
 *		};
 *
 * @member {Function} module:word-count/wordcount~WordCountConfig#onUpdate
 */

/**
 * Allows for providing the HTML element that the
 * {@link module:word-count/wordcount~WordCount#wordCountContainer word count container} will be appended to automatically.
 *
 *		const wordCountConfig = {
 *			container: document.getElementById( 'container-for-word-count' );
 *		};
 *
 * @member {HTMLElement} module:word-count/wordcount~WordCountConfig#container
 *//**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */var index$1=/*#__PURE__*/Object.freeze({__proto__:null,Essentials:Essentials});/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The link image engine feature.
 *
 * It accepts the `linkHref="url"` attribute in the model for the {@link module:image/image~Image `<imageBlock>`} element
 * which allows linking images.
 *
 * @extends module:core/plugin~Plugin
 */
class LinkImageEditing extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ 'ImageEditing', 'ImageUtils', LinkEditing ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'LinkImageEditing';
	}

	init() {
		const editor = this.editor;
		const schema = editor.model.schema;

		if ( editor.plugins.has( 'ImageBlockEditing' ) ) {
			schema.extend( 'imageBlock', { allowAttributes: [ 'linkHref' ] } );
		}

		editor.conversion.for( 'upcast' ).add( upcastLink( editor ) );
		editor.conversion.for( 'downcast' ).add( downcastImageLink( editor ) );

		// Definitions for decorators are provided by the `link` command and the `LinkEditing` plugin.
		this._enableAutomaticDecorators();
		this._enableManualDecorators();
	}

	/**
	 * Processes {@link module:link/link~LinkDecoratorAutomaticDefinition automatic decorators} definitions and
	 * attaches proper converters that will work when linking an image.`
	 *
	 * @private
	 */
	_enableAutomaticDecorators() {
		const editor = this.editor;
		const command = editor.commands.get( 'link' );
		const automaticDecorators = command.automaticDecorators;

		if ( automaticDecorators.length ) {
			editor.conversion.for( 'downcast' ).add( automaticDecorators.getDispatcherForLinkedImage() );
		}
	}

	/**
	 * Processes transformed {@link module:link/utils~ManualDecorator} instances and attaches proper converters
	 * that will work when linking an image.
	 *
	 * @private
	 */
	_enableManualDecorators() {
		const editor = this.editor;
		const command = editor.commands.get( 'link' );

		for ( const decorator of command.manualDecorators ) {
			if ( editor.plugins.has( 'ImageBlockEditing' ) ) {
				editor.model.schema.extend( 'imageBlock', { allowAttributes: decorator.id } );
			}

			if ( editor.plugins.has( 'ImageInlineEditing' ) ) {
				editor.model.schema.extend( 'imageInline', { allowAttributes: decorator.id } );
			}

			editor.conversion.for( 'downcast' ).add( downcastImageLinkManualDecorator( decorator ) );
			editor.conversion.for( 'upcast' ).add( upcastImageLinkManualDecorator( editor, decorator ) );
		}
	}
}

// Returns a converter for linked block images that consumes the "href" attribute
// if a link contains an image.
//
// @private
// @param {module:core/editor/editor~Editor} editor The editor instance.
// @returns {Function}
function upcastLink( editor ) {
	const isImageInlinePluginLoaded = editor.plugins.has( 'ImageInlineEditing' );
	const imageUtils = editor.plugins.get( 'ImageUtils' );

	return dispatcher => {
		dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
			const viewLink = data.viewItem;
			const imageInLink = imageUtils.findViewImgElement( viewLink );

			if ( !imageInLink ) {
				return;
			}

			const blockImageView = imageInLink.findAncestor( element => imageUtils.isBlockImageView( element ) );

			// There are four possible cases to consider here
			//
			// 1. A "root > ... > figure.image > a > img" structure.
			// 2. A "root > ... > figure.image > a > picture > img" structure.
			// 3. A "root > ... > block > a > img" structure.
			// 4. A "root > ... > block > a > picture > img" structure.
			//
			// but the last 2 cases should only be considered by this converter when the inline image plugin
			// is NOT loaded in the editor (because otherwise, that would be a plain, linked inline image).
			if ( isImageInlinePluginLoaded && !blockImageView ) {
				return;
			}

			// There's an image inside an <a> element - we consume it so it won't be picked up by the Link plugin.
			const consumableAttributes = { attributes: [ 'href' ] };

			// Consume the `href` attribute so the default one will not convert it to $text attribute.
			if ( !conversionApi.consumable.consume( viewLink, consumableAttributes ) ) {
				// Might be consumed by something else - i.e. other converter with priority=highest - a standard check.
				return;
			}

			const linkHref = viewLink.getAttribute( 'href' );

			// Missing the 'href' attribute.
			if ( !linkHref ) {
				return;
			}

			// A full definition of the image feature.
			// figure > a > img: parent of the view link element is an image element (figure).
			let modelElement = data.modelCursor.parent;

			if ( !modelElement.is( 'element', 'imageBlock' ) ) {
				// a > img: parent of the view link is not the image (figure) element. We need to convert it manually.
				const conversionResult = conversionApi.convertItem( imageInLink, data.modelCursor );

				// Set image range as conversion result.
				data.modelRange = conversionResult.modelRange;

				// Continue conversion where image conversion ends.
				data.modelCursor = conversionResult.modelCursor;

				modelElement = data.modelCursor.nodeBefore;
			}

			if ( modelElement && modelElement.is( 'element', 'imageBlock' ) ) {
				// Set the linkHref attribute from link element on model image element.
				conversionApi.writer.setAttribute( 'linkHref', linkHref, modelElement );
			}
		}, { priority: 'high' } );
		// Using the same priority that `upcastImageLinkManualDecorator()` converter guarantees
		// that manual decorators will decorate the proper element.
	};
}

// Creates a converter that adds `<a>` to linked block image view elements.
//
// @private
function downcastImageLink( editor ) {
	const imageUtils = editor.plugins.get( 'ImageUtils' );

	return dispatcher => {
		dispatcher.on( 'attribute:linkHref:imageBlock', ( evt, data, conversionApi ) => {
			if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
				return;
			}

			// The image will be already converted - so it will be present in the view.
			const viewFigure = conversionApi.mapper.toViewElement( data.item );
			const writer = conversionApi.writer;

			// But we need to check whether the link element exists.
			const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );
			const viewImage = imageUtils.findViewImgElement( viewFigure );
			// <picture>...<img/></picture> or <img/>
			const viewImgOrPicture = viewImage.parent.is( 'element', 'picture' ) ? viewImage.parent : viewImage;

			// If so, update the attribute if it's defined or remove the entire link if the attribute is empty.
			if ( linkInImage ) {
				if ( data.attributeNewValue ) {
					writer.setAttribute( 'href', data.attributeNewValue, linkInImage );
				} else {
					writer.move( writer.createRangeOn( viewImgOrPicture ), writer.createPositionAt( viewFigure, 0 ) );
					writer.remove( linkInImage );
				}
			} else {
				// But if it does not exist. Let's wrap already converted image by newly created link element.
				// 1. Create an empty link element.
				const linkElement = writer.createContainerElement( 'a', { href: data.attributeNewValue } );

				// 2. Insert link inside the associated image.
				writer.insert( writer.createPositionAt( viewFigure, 0 ), linkElement );

				// 3. Move the image to the link.
				writer.move( writer.createRangeOn( viewImgOrPicture ), writer.createPositionAt( linkElement, 0 ) );
			}
		}, { priority: 'high' } );
	};
}

// Returns a converter that decorates the `<a>` element when the image is the link label.
//
// @private
// @returns {Function}
function downcastImageLinkManualDecorator( decorator ) {
	return dispatcher => {
		dispatcher.on( `attribute:${ decorator.id }:imageBlock`, ( evt, data, conversionApi ) => {
			const viewFigure = conversionApi.mapper.toViewElement( data.item );
			const linkInImage = Array.from( viewFigure.getChildren() ).find( child => child.name === 'a' );

			// The <a> element was removed by the time this converter is executed.
			// It may happen when the base `linkHref` and decorator attributes are removed
			// at the same time (see #8401).
			if ( !linkInImage ) {
				return;
			}

			for ( const [ key, val ] of toMap( decorator.attributes ) ) {
				conversionApi.writer.setAttribute( key, val, linkInImage );
			}

			if ( decorator.classes ) {
				conversionApi.writer.addClass( decorator.classes, linkInImage );
			}

			for ( const key in decorator.styles ) {
				conversionApi.writer.setStyle( key, decorator.styles[ key ], linkInImage );
			}
		} );
	};
}

// Returns a converter that checks whether manual decorators should be applied to the link.
//
// @private
// @returns {Function}
function upcastImageLinkManualDecorator( editor, decorator ) {
	const isImageInlinePluginLoaded = editor.plugins.has( 'ImageInlineEditing' );
	const imageUtils = editor.plugins.get( 'ImageUtils' );

	return dispatcher => {
		dispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
			const viewLink = data.viewItem;
			const imageInLink = imageUtils.findViewImgElement( viewLink );

			// We need to check whether an image is inside a link because the converter handles
			// only manual decorators for linked images. See #7975.
			if ( !imageInLink ) {
				return;
			}

			const blockImageView = imageInLink.findAncestor( element => imageUtils.isBlockImageView( element ) );

			if ( isImageInlinePluginLoaded && !blockImageView ) {
				return;
			}

			const matcher = new Matcher( decorator._createPattern() );
			const result = matcher.match( viewLink );

			// The link element does not have required attributes or/and proper values.
			if ( !result ) {
				return;
			}

			// Check whether we can consume those attributes.
			if ( !conversionApi.consumable.consume( viewLink, result.match ) ) {
				return;
			}

			// At this stage we can assume that we have the `<imageBlock>` element.
			// `nodeBefore` comes after conversion: `<a><img></a>`.
			// `parent` comes with full image definition: `<figure><a><img></a></figure>.
			// See the body of the `upcastLink()` function.
			const modelElement = data.modelCursor.nodeBefore || data.modelCursor.parent;

			conversionApi.writer.setAttribute( decorator.id, true, modelElement );
		}, { priority: 'high' } );
		// Using the same priority that `upcastLink()` converter guarantees that the linked image was properly converted.
	};
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The link image UI plugin.
 *
 * This plugin provides the `'linkImage'` button that can be displayed in the {@link module:image/imagetoolbar~ImageToolbar}.
 * It can be used to wrap images in links.
 *
 * @extends module:core/plugin~Plugin
 */
class LinkImageUI extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ LinkEditing, LinkUI, 'ImageBlockEditing' ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'LinkImageUI';
	}

	/**
	 * @inheritDoc
	 */
	init() {
		const editor = this.editor;
		const viewDocument = editor.editing.view.document;

		this.listenTo( viewDocument, 'click', ( evt, data ) => {
			if ( this._isSelectedLinkedImage( editor.model.document.selection ) ) {
				// Prevent browser navigation when clicking a linked image.
				data.preventDefault();

				// Block the `LinkUI` plugin when an image was clicked.
				// In such a case, we'd like to display the image toolbar.
				evt.stop();
			}
		}, { priority: 'high' } );

		this._createToolbarLinkImageButton();
	}

	/**
	 * Creates a `LinkImageUI` button view.
	 *
	 * Clicking this button shows a {@link module:link/linkui~LinkUI#_balloon} attached to the selection.
	 * When an image is already linked, the view shows {@link module:link/linkui~LinkUI#actionsView} or
	 * {@link module:link/linkui~LinkUI#formView} if it is not.
	 *
	 * @private
	 */
	_createToolbarLinkImageButton() {
		const editor = this.editor;
		const t = editor.t;

		editor.ui.componentFactory.add( 'linkImage', locale => {
			const button = new ButtonView( locale );
			const plugin = editor.plugins.get( 'LinkUI' );
			const linkCommand = editor.commands.get( 'link' );

			button.set( {
				isEnabled: true,
				label: t( 'Link image' ),
				icon: linkIcon,
				keystroke: LINK_KEYSTROKE,
				tooltip: true,
				isToggleable: true
			} );

			// Bind button to the command.
			button.bind( 'isEnabled' ).to( linkCommand, 'isEnabled' );
			button.bind( 'isOn' ).to( linkCommand, 'value', value => !!value );

			// Show the actionsView or formView (both from LinkUI) on button click depending on whether the image is linked already.
			this.listenTo( button, 'execute', () => {
				if ( this._isSelectedLinkedImage( editor.model.document.selection ) ) {
					plugin._addActionsView();
				} else {
					plugin._showUI( true );
				}
			} );

			return button;
		} );
	}

	/**
	 * Returns true if a linked image (either block or inline) is the only selected element
	 * in the model document.
	 *
	 * @private
	 * @param {module:engine/model/selection~Selection} selection
	 * @returns {Boolean}
	 */
	_isSelectedLinkedImage( selection ) {
		const selectedModelElement = selection.getSelectedElement();
		const imageUtils = this.editor.plugins.get( 'ImageUtils' );

		return imageUtils.isImage( selectedModelElement ) && selectedModelElement.hasAttribute( 'linkHref' );
	}
}var css_248z = ".ck.ck-editor__editable a span.image-inline:after,.ck.ck-editor__editable figure.image>a:after{display:block;position:absolute}:root{--ck-link-image-indicator-icon-size:20;--ck-link-image-indicator-icon-is-visible:clamp(0px,100% - 50px,1px)}.ck.ck-editor__editable a span.image-inline:after,.ck.ck-editor__editable figure.image>a:after{background-color:rgba(0,0,0,.4);background-image:url(\"data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjAgMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbD0iI2ZmZiIgZD0ibTExLjA3NyAxNSAuOTkxLTEuNDE2YS43NS43NSAwIDEgMSAxLjIyOS44NmwtMS4xNDggMS42NGEuNzQ4Ljc0OCAwIDAgMS0uMjE3LjIwNiA1LjI1MSA1LjI1MSAwIDAgMS04LjUwMy01Ljk1NS43NDEuNzQxIDAgMCAxIC4xMi0uMjc0bDEuMTQ3LTEuNjM5YS43NS43NSAwIDEgMSAxLjIyOC44Nkw0LjkzMyAxMC43bC4wMDYuMDAzYTMuNzUgMy43NSAwIDAgMCA2LjEzMiA0LjI5NGwuMDA2LjAwNHptNS40OTQtNS4zMzVhLjc0OC43NDggMCAwIDEtLjEyLjI3NGwtMS4xNDcgMS42MzlhLjc1Ljc1IDAgMSAxLTEuMjI4LS44NmwuODYtMS4yM2EzLjc1IDMuNzUgMCAwIDAtNi4xNDQtNC4zMDFsLS44NiAxLjIyOWEuNzUuNzUgMCAwIDEtMS4yMjktLjg2bDEuMTQ4LTEuNjRhLjc0OC43NDggMCAwIDEgLjIxNy0uMjA2IDUuMjUxIDUuMjUxIDAgMCAxIDguNTAzIDUuOTU1em0tNC41NjMtMi41MzJhLjc1Ljc1IDAgMCAxIC4xODQgMS4wNDVsLTMuMTU1IDQuNTA1YS43NS43NSAwIDEgMS0xLjIyOS0uODZsMy4xNTUtNC41MDZhLjc1Ljc1IDAgMCAxIDEuMDQ1LS4xODR6Ii8+PC9zdmc+\");background-position:50%;background-repeat:no-repeat;background-size:14px;border-radius:100%;content:\"\";height:calc(var(--ck-link-image-indicator-icon-is-visible)*var(--ck-link-image-indicator-icon-size));overflow:hidden;right:min(var(--ck-spacing-medium),6%);top:min(var(--ck-spacing-medium),6%);width:calc(var(--ck-link-image-indicator-icon-is-visible)*var(--ck-link-image-indicator-icon-size))}";
styleInject(css_248z);/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * The `LinkImage` plugin.
 *
 * This is a "glue" plugin that loads the {@link module:link/linkimageediting~LinkImageEditing link image editing feature}
 * and {@link module:link/linkimageui~LinkImageUI link image UI feature}.
 *
 * @extends module:core/plugin~Plugin
 */
class LinkImage extends Plugin {
	/**
	 * @inheritDoc
	 */
	static get requires() {
		return [ LinkImageEditing, LinkImageUI ];
	}

	/**
	 * @inheritDoc
	 */
	static get pluginName() {
		return 'LinkImage';
	}
}/**
 * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */var index=/*#__PURE__*/Object.freeze({__proto__:null,Link:Link,LinkEditing:LinkEditing,LinkUI:LinkUI,LinkImage:LinkImage,LinkImageEditing:LinkImageEditing,LinkImageUI:LinkImageUI,AutoLink:AutoLink});const CKEditor5Plugins = {
  Alignment,
  BlockQuote,
  Bold,
  Essentials,
  FindAndReplace,
  GeneralHtmlSupport,
  Heading,
  HorizontalLine,
  Indent,
  Italic,
  Link,
  List,
  Paragraph,
  PastePlainText,
  PasteFromOffice,
  RemoveFormat,
  SpecialCharacters,
  SpecialCharactersEssentials,
  SourceEditing,
  Style,
  Subscript,
  Superscript,
  Strikethrough,
  Table,
  TableToolbar,
  TableProperties,
  TableCellProperties,
  TextTransformation,
  Underline,
  WordCount,
};

class CKEditor5 extends ClassicEditor {}
CKEditor5.builtinPlugins = Object.values(CKEditor5Plugins);export{CKEditor5,CKEditor5Plugins,index$3 as Clipboard,index$7 as Core,index$5 as Engine,index$1 as Essentials,index as Link,LinkActionsView,utils as LinkUtils,index$4 as Typing,index$6 as UI,index$8 as Utils,index$2 as Widget,WordCount};