@ckeditor/ckeditor5-ai
Version:
AI features for CKEditor 5.
292 lines (291 loc) • 10.4 kB
TypeScript
/**
* @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
*/
import { type Element, type Document } from '../utils/htmlparser.js';
declare const AIResponseApplier_base: {
new (): import("ckeditor5/src/utils.js").Observable;
prototype: import("ckeditor5/src/utils.js").Observable;
};
/**
* AIResponseApplier is a part of the AI API response processing pipeline.
*
* The main purpose of this class is to merge HTML suggestions (modified content chunks) returned
* from the AI API into the existing editor content. This is "black box" processing, which means that
* the class does not know anything about the editor content structure, it just merges the content
* based on provided HTML data (part/suggestion and content) and 'data-id' attributes.
*/
export declare class AIResponseApplier extends /* #__PURE__ -- @preserve */ AIResponseApplier_base {
constructor(generateUid?: () => string);
/**
* Merges the given content part (AI suggestion) with the provided editor content.
*
* The main purpose of this function is to merge a piece of AI modified content (`contentPart`)
* into the given content (editor content, `content`).
*
* This is done based on a few rules:
*
* 1. Any removed element is not present in the `contentPart` itself but is represented by a special
* "removed comment" node: `<!-- removed data-id="some_id" -->`. This comment node is located
* in the same position (relative position to other present nodes) as the element in the original
* content.
* 2. Any newly added element is presented in the `contentPart` with a special `data-id="new-element"`
* attribute.
* 3. Any existing (from the original `content`) and modified element is presented in the `contentPart`
* with the same `data-id` value as in the original `content`. Its contents may be different (tag,
* attributes, entire subtree may change). Also modified elements don't have to be a top level elements
* from the original `content`, which means the `contentPart` may not keep the original structure
* or nesting order of the elements.
*
* Each type of modification is handled differently when merged into the `content`:
*
* 1. Modified elements (with `data-id` attribute with a value which exists in the `content`) replaces
* the existing element with the same `data-id`, without changing its position in the `content`.
* 2. Removed elements (marked with a special "removed comment") are removed from the `content` based
* on the `data-id` comment value.
* 3. New elements (marked with `data-id="new-element"`) are inserted into the `content` based on their
* relative position to other nodes in the `contentPart`. The detailed rules for inserting new elements are
* described in the next section.
* 4. Invalid elements (elements with `data-id` attribute which value is not present in the `content`,
* or without `data-id` attribute at all) are ignored and not inserted into the `content`.
*
* The position on which new elements from the `contentPart` are inserted into the `content`
* is determined based on the reference nodes. The reference node is:
*
* - A node in the `contentPart` which also exists in the `content` with the same `data-id` attribute value.
* - A special "removed comment" node which points to the existing element in the `content`.
* - A special "existing document" comment node which implicates that there should be any content
* before/after the new element position on which it is inserted into the `content`.
*
* The algorithm operates on the top level nodes of the `contentPart` which means only direct root children
* of the `contentPart` are processed. Nested elements are not processed directly, but as a children
* of the top level nodes they are included in the merge process.
*
* The algorithm iterates over the `contentPart` top level nodes and tries to find a position (directly or via reference node)
* for any element which should be merged into the `content`.
*
* Example 1: Modified element.
*
* ```html
* // content:
* <p data-id="1">Foo</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
*
* // contentPart:
* <p data-id="1">New content</p>
*
* // merge result:
* <p data-id="1">New content</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
* ```
*
* Example 2: Multiple modified element.
*
* ```html
* // content:
* <p data-id="1">Foo</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
*
* // contentPart:
* <p data-id="1">New content</p>
* <!-- existing document !-->
* <p data-id="3">Another <strong>change</strong></p>
*
* // merge result:
* <p data-id="1">New content</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Another <strong>change</strong></p>
* ```
*
* Example 3: Removed element.
*
* ```html
* // content:
* <p data-id="1">Foo</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
*
* // contentPart:
* <!-- removed data-id="1" !-->
*
* // merge result:
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
* ```
*
* Example 4: New element with reference node before.
*
* ```html
* // content:
* <p data-id="1">Foo</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
*
* // contentPart:
* <p data-id="1">Foo</p>
* <p data-id="new-element">New element</p>
* <!-- existing document !-->
*
* // merge result:
* <p data-id="1">Foo</p>
* <p data-id="new-id">New element</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
* ```
*
* Example 5: New element with reference node after.
*
* ```html
* // content:
* <p data-id="1">Foo</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
*
* // contentPart:
* <!-- existing document !-->
* <p data-id="new-element">New element</p>
* <p data-id="3">Cup</p>
*
* // merge result:
* <p data-id="1">Foo</p>
* <p data-id="2">Bar</p>
* <p data-id="new-id">New element</p>
* <p data-id="3">Cup</p>
* ```
*
* Example 6: New element at the start of the content.
*
* ```html
* // content:
* <p data-id="1">Foo</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
*
* // contentPart:
* <p data-id="new-element">New element</p>
* <!-- existing document !-->
*
* // merge result:
* <p data-id="new-id">New element</p>
* <p data-id="1">Foo</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
* ```
*
* Example 7: New element at the end of the content.
*
* ```html
* // content:
* <p data-id="1">Foo</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
*
* // contentPart:
* <!-- existing document !-->
* <p data-id="new-element">New element</p>
*
* // merge result:
* <p data-id="1">Foo</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
* <p data-id="new-id">New element</p>
* ```
*
* Example 8: New element after removed element.
*
* ```html
* // content:
* <p data-id="1">Foo</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
*
* // contentPart:
* <!-- removed data-id="2" !-->
* <p data-id="new-element">New element</p>
*
* // merge result:
* <p data-id="1">Foo</p>
* <p data-id="new-id">New element</p>
* <p data-id="3">Cup</p>
* ```
*
* Example 9: Wrapped element.
*
* ```html
* // content:
* <p data-id="1">Foo</p>
* <p data-id="2">Bar</p>
* <p data-id="3">Cup</p>
*
* // contentPart:
* <p data-id="1">Foo</p>
* <div data-id="new-element">
* <p data-id="2">Bar</p>
* </div>
*
* // merge result:
* <p data-id="1">Foo</p>
* <div data-id="new-id">
* <p data-id="2">Bar</p>
* </div>
* <p data-id="3">Cup</p>
* ```
*
* Example 10: Modification of nested elements.
*
* ```html
* // content:
* <table data-id="1">
* <tr data-id="11">
* <td data-id="111">Foo</td>
* </tr>
* <tr data-id="12">
* <td data-id="121">Bar</td>
* <td data-id="122">Cup</td>
* </tr>
* </table>
*
* // contentPart:
* <tr data-id="12">
* <td data-id="new-element">New content</td>
* </tr>
*
* // merge result:
* <table data-id="1">
* <tr data-id="11">
* <td data-id="111">Foo</td>
* </tr>
* <tr data-id="12">
* <td data-id="new-id">New content</td>
* </tr>
* </table>
* ```
*
* Since existing elements are either modifications or only reference nodes in the `contentPart`, there is an additional logic
* for checking if such elements are identical to the existing ones in the `content`. This way the algorithm can correctly detect
* if an existing element was modified or not.
*
* Additionally, this method supports the following options:
* - `cutAfterLastChange`: if set to `true`, the content will be cut after the last modified element.
* - `markUnstableElements`: if set to `true`, the function will mark all elements starting from the last tag element
* as unstable by adding a special 'data-unstable' attribute to them.
* - `markUnstableElementsDepth`: if set, it will determine how deep the unstable marking should go.
*/
merge(contentPart: Document, parsedContent: Document, options?: MergeOptions): MergeResult;
}
export type MergeResult = {
parsedContent: Document;
newNodeIds: Array<string>;
modifiedNodeIds: Array<string>;
removedNodeIds: Array<string>;
};
export type MergeOptions = {
cutAfterLastChange?: boolean;
markUnstableElements?: boolean;
markUnstableElementsDepth?: number;
replaceRemovedWith?: Element;
};
export {};