UNPKG

@ckeditor/ckeditor5-engine

Version:

The editing engine of CKEditor 5 – the best browser-based rich text editor.

531 lines (530 loc) • 20.3 kB
/** * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options */ /** * @module engine/model/differ */ import { ModelPosition } from './position.js'; import { ModelRange } from './range.js'; import type { MarkerCollection, MarkerData } from './markercollection.js'; import { type ModelItem } from './item.js'; import { type ModelNode } from './node.js'; import { type ModelRootElement } from './rootelement.js'; import { type Operation } from './operation/operation.js'; /** * 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. */ export declare class Differ { /** * Priority of the {@link ~Differ#_elementState element states}. States on higher indexes of the array can overwrite states on the lower * indexes. */ private static readonly _statesPriority; /** * Reference to the model's marker collection. */ private readonly _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 readonly _changesInElement; /** * Stores a snapshot for these model nodes that might have changed. * * This complements {@link ~Differ#_elementChildrenSnapshots `_elementChildrenSnapshots`}. * * See also {@link ~DifferSnapshot}. */ private readonly _elementsSnapshots; /** * For each element or document fragment inside which there was a change, it stores a snapshot of the child nodes list (an array * of children snapshots that represent the state in the element / fragment before any change has happened). * * This complements {@link ~Differ#_elementsSnapshots `_elementsSnapshots`}. * * See also {@link ~DifferSnapshot}. */ private readonly _elementChildrenSnapshots; /** * Keeps the state for a given element, describing how the element was changed so far. It is used to evaluate the `action` property * of diff items returned by {@link ~Differ#getChanges}. * * Possible values, in the order from the lowest priority to the highest priority: * * * `'refresh'` - element was refreshed, * * `'rename'` - element was renamed, * * `'move'` - element was moved (or, usually, removed, that is moved to the graveyard). * * Element that was refreshed, may change its state to `'rename'` if it was later renamed, or to `'move'` if it was removed. * But the element cannot change its state from `'move'` to `'rename'`, or from `'rename'` to `'refresh'`. * * Only already existing elements are registered in `_elementState`. If a new element was inserted as a result of a buffered operation, * it is not be registered in `_elementState`. */ private readonly _elementState; /** * 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 readonly _changedMarkers; /** * A map that stores all roots that have been changed. * * The keys are the names of the roots while value represents the changes. */ private readonly _changedRoots; /** * Stores the number of changes that were processed. Used to order the changes chronologically. It is important * when changes are sorted. */ private _changeCount; /** * 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 _cachedChanges; /** * 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 _cachedChangesWithGraveyard; /** * Set of model items that were marked to get refreshed in {@link #_refreshItem}. */ private _refreshedItems; /** * Creates a `Differ` instance. * * @param markerCollection Model's marker collection. */ constructor(markerCollection: MarkerCollection); /** * Informs whether there are any changes buffered in `Differ`. */ get isEmpty(): boolean; /** * Buffers the given operation. **An operation has to be buffered before it is executed.** * * @param operationToBuffer An operation to buffer. */ bufferOperation(operationToBuffer: Operation): void; /** * Buffers a marker change. * * @param markerName The name of the marker that changed. * @param oldMarkerData Marker data before the change. * @param newMarkerData Marker data after the change. */ bufferMarkerChange(markerName: string, oldMarkerData: MarkerData, newMarkerData: MarkerData): void; /** * Returns all markers that should be removed as a result of buffered changes. * * @returns Markers to remove. Each array item is an object containing the `name` and `range` properties. */ getMarkersToRemove(): Array<{ name: string; range: ModelRange; }>; /** * Returns all markers which should be added as a result of buffered changes. * * @returns Markers to add. Each array item is an object containing the `name` and `range` properties. */ getMarkersToAdd(): Array<{ name: string; range: ModelRange; }>; /** * Returns all markers which changed. */ getChangedMarkers(): Array<{ name: string; data: { oldRange: ModelRange | null; newRange: ModelRange | null; }; }>; /** * Checks whether some of the buffered changes affect the editor data. * * Types of changes which affect the editor data: * * * model structure changes, * * attribute changes, * * a root is added or detached, * * changes of markers which were defined as `affectsData`, * * changes of markers' `affectsData` property. */ hasDataChanges(): boolean; /** * 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~DifferItem 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~ModelPosition#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 options Additional options. * @param options.includeChangesInGraveyard 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 Diff between the old and the new model tree state. */ getChanges(options?: { includeChangesInGraveyard?: boolean; }): Array<DifferItem>; /** * Returns all roots that have changed (either were attached, or detached, or their attributes changed). * * @returns Diff between the old and the new roots state. */ getChangedRoots(): Array<DifferItemRoot>; /** * Returns a set of model items that were marked to get refreshed. */ getRefreshedItems(): Set<ModelItem>; /** * Resets `Differ`. Removes all buffered changes. */ reset(): void; /** * 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. * * @internal * @param item Item to refresh. */ _refreshItem(item: ModelItem): void; /** * Buffers all the data related to given root like it was all just added to the editor. * * Following changes are buffered: * * * root is attached, * * all root content is inserted, * * all root attributes are added, * * all markers inside the root are added. * * @internal */ _bufferRootLoad(root: ModelRootElement): void; /** * Buffers the root state change after the root was attached or detached */ private _bufferRootStateChange; /** * Buffers a root attribute change. */ private _bufferRootAttributeChange; /** * Saves and handles an insert change. */ private _markInsert; /** * Saves and handles a remove change. */ private _markRemove; /** * Saves and handles an attribute change. */ private _markAttribute; /** * Saves and handles a model change. */ private _markChange; /** * Tries to set given state for given item. * * This method does simple validation (it sets the state only for model elements, not for text proxy nodes). It also follows state * setting rules, that is, `'refresh'` cannot overwrite `'rename'`, and `'rename'` cannot overwrite `'move'`. */ private _setElementState; /** * Returns a value for {@link ~DifferItemAction `action`} property for diff items returned by {@link ~Differ#getChanges}. * This method aims to return `'rename'` or `'refresh'` when it should, and `diffItemType` ("default action") in all other cases. * * It bases on a few factors: * * * for text nodes, the method always returns `diffItemType`, * * for newly inserted element, the method returns `diffItemType`, * * if {@link ~Differ#_elementState element state} was not recorded, the method returns `diffItemType`, * * if state was recorded, and it was `'move'` (default action), the method returns `diffItemType`, * * finally, if state was `'refresh'` or `'rename'`, the method returns the state value. */ private _getDiffActionForNode; /** * Gets an array of changes that have already been saved for a given element. */ private _getChangesForElement; /** * Creates and saves a snapshot for all children of the given element. */ private _makeSnapshots; /** * 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. * * @param inc Incoming (new) change. * @param changes An array containing all the changes done on that element. */ private _handleChange; /** * Returns an object with a single insert change description. * * @param parent The element in which the change happened. * @param offset The offset at which change happened. * @param action Further specifies what kind of action led to generating this change. * @param elementSnapshot Snapshot of the inserted node after changes. * @param elementSnapshotBefore Snapshot of the inserted node before changes. * @returns The diff item. */ private _getInsertDiff; /** * Returns an object with a single remove change description. * * @param parent The element in which change happened. * @param offset The offset at which change happened. * @param action Further specifies what kind of action led to generating this change. * @param elementSnapshot The snapshot of the removed node before changes. * @returns The diff item. */ private _getRemoveDiff; /** * Returns an array of objects where each one is a single attribute change description. * * @param range The range where the change happened. * @param oldAttributes A map, map iterator or compatible object that contains attributes before the change. * @param newAttributes A map, map iterator or compatible object that contains attributes after the change. * @returns An array containing one or more diff items. */ private _getAttributesDiff; /** * Checks whether given element or any of its parents is an element that is buffered as an inserted element. */ private _isInInsertedElement; /** * Removes deeply all buffered changes that are registered in elements from range specified by `parent`, `offset` * and `howMany`. */ private _removeAllNestedChanges; } /** * Further specifies what kind of action led to generating a change: * * * `'insert'` if element was inserted, * * `'remove'` if element was removed, * * `'rename'` if element got renamed (e.g. `writer.rename()`), * * `'refresh'` if element got refreshed (e.g. `model.editing.reconvertItem()`). */ export type DifferItemAction = 'insert' | 'remove' | 'rename' | 'refresh'; /** * A snapshot is representing state of a given element before the first change was applied on that element. * * @internal */ export interface DifferSnapshot { /** * Node for which was snapshot was made. */ node: ModelNode; /** * Name of the element at the time when the snapshot was made. For text node snapshots, it is set to `'$text'`. */ name: string; /** * Attributes of the node at the time when the snapshot was made. */ attributes: Map<string, unknown>; } /** * The single diff item. * * Could be one of: * * * {@link module:engine/model/differ~DifferItemInsert `DifferItemInsert`}, * * {@link module:engine/model/differ~DifferItemRemove `DifferItemRemove`}, * * {@link module:engine/model/differ~DifferItemAttribute `DifferItemAttribute`}. */ export type DifferItem = DifferItemInsert | DifferItemRemove | DifferItemAttribute; /** * A single diff item for inserted nodes. */ export interface DifferItemInsert { /** * The type of diff item. */ type: 'insert'; /** * Further specifies what kind of action led to generating this change. * * The action is set in relation to the document state before any change. It means that, for example, if an element was added and * then renamed (during the same {@link module:engine/model/batch~Batch batch}), the action will be set to `'insert'`, because when * compared to the document state before changes, a new element was inserted, and the renaming does not matter from this point of view. */ action: DifferItemAction; /** * The name of the inserted elements or `'$text'` for a text node. */ name: string; /** * Map of attributes of the inserted element. */ attributes: Map<string, unknown>; /** * The position where the node was inserted. */ position: ModelPosition; /** * The length of an inserted text node. For elements, it is always 1 as each inserted element is counted as a one. */ length: number; /** * Holds information about the element state before all changes happened. * * For example, when `<paragraph textAlign="right">` was changed to `<codeBlock language="plaintext">`, * `before.name` will be equal to `'paragraph'` and `before.attributes` map will have one entry: `'textAlign' -> 'right'`. * * The property is available only if the insertion change was due to element rename or refresh (when `action` property is `'rename'` * or `'refresh'`). As such, `before` property is never available for text node changes. */ before?: { /** * The name of the inserted element before all changes. */ name: string; /** * Map of the attributes of the inserted element before all changes. */ attributes: Map<string, unknown>; }; } /** * A single diff item for re-inserted nodes. */ export interface DifferItemReinsert { /** * The type of diff item. */ type: 'reinsert'; /** * The name of the re-inserted elements or `'$text'` for a text node. */ name: string; /** * The position where the node was reinserted. */ position: ModelPosition; /** * The length of a re-inserted text node. For elements, it is always 1 as each re-inserted element is counted as a one. */ length: number; } /** * A single diff item for removed nodes. */ export interface DifferItemRemove { /** * The type of diff item. */ type: 'remove'; /** * Further specifies what kind of action led to generating this change. * * The action is set in relation to the document state before any change. It means that, for example, if an element was renamed and * then removed (during the same {@link module:engine/model/batch~Batch batch}), the action will be set to `'remove'`, because when * compared to the document state before changes, the element was removed, and it does not matter that it was also renamed at one point. */ action: DifferItemAction; /** * The name of the removed element or `'$text'` for a text node. */ name: string; /** * Map of attributes that were set on the item while it was removed. */ attributes: Map<string, unknown>; /** * The position where the node was removed. */ position: ModelPosition; /** * The length of a removed text node. For elements, it is always 1, as each removed element is counted as a one. */ length: number; } /** * A single diff item for attribute change. */ export interface DifferItemAttribute { /** * The type of diff item. */ type: 'attribute'; /** * The name of the changed attribute. */ attributeKey: string; /** * An attribute previous value (before change). */ attributeOldValue: unknown; /** * An attribute new value (after change). */ attributeNewValue: unknown; /** * The range where the change happened. */ range: ModelRange; } /** * A single diff item for a changed root. */ export interface DifferItemRoot { /** * Name of the changed root. */ name: string; /** * Set accordingly if the root got attached or detached. Otherwise, not set. */ state?: 'attached' | 'detached'; /** * Keeps all attribute changes that happened on the root. * * The keys are keys of the changed attributes. The values are objects containing the attribute value before the change * (`oldValue`) and after the change (`newValue`). * * Note, that if the root state changed (`state` is set), then `attributes` property will not be set. All attributes should be * handled together with the root being attached or detached. */ attributes?: Record<string, { oldValue: unknown; newValue: unknown; }>; }