@ckeditor/ckeditor5-engine
Version:
The editing engine of CKEditor 5 – the best browser-based rich text editor.
494 lines (493 loc) • 23.3 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
*/
/**
* @module engine/conversion/upcastdispatcher
*/
import { ViewConsumable } from './viewconsumable.js';
import { ModelRange } from '../model/range.js';
import { ModelPosition } from '../model/position.js';
import { type ModelElement } from '../model/element.js';
import { type ModelNode } from '../model/node.js';
import { type ViewElement } from '../view/element.js';
import { type ViewText } from '../view/text.js';
import { type ViewDocumentFragment } from '../view/documentfragment.js';
import { type ModelDocumentFragment } from '../model/documentfragment.js';
import type { ModelSchema, ModelSchemaContextDefinition } from '../model/schema.js';
import { type ModelWriter } from '../model/writer.js';
import { type ViewItem } from '../view/item.js';
declare const UpcastDispatcher_base: {
new (): import("@ckeditor/ckeditor5-utils").Emitter;
prototype: import("@ckeditor/ckeditor5-utils").Emitter;
};
/**
* 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~ViewDocumentFragment view document fragment} or
* {@link module:engine/view/element~ViewElement view element} into a correct model structure.
*
* During the conversion process, the dispatcher fires events for all {@link module:engine/view/node~ViewNode 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~ViewNode view node} or a
* {@link module:engine/view/documentfragment~ViewDocumentFragment 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~ModelRange} when the conversion succeeds.
* * `data.modelCursor` is a {@link module:engine/model/position~ModelPosition 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/deep-dive/conversion/upcast Upcast conversion} guide.
*
* Examples of event-based converters:
*
* ```ts
* // 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' } );
* ```
*
* @fires viewCleanup
* @fires element
* @fires text
* @fires documentFragment
*/
export declare class UpcastDispatcher extends /* #__PURE__ */ UpcastDispatcher_base {
/**
* An interface passed by the dispatcher to the event callbacks.
*/
conversionApi: UpcastConversionApi;
/**
* The list of elements that were created during splitting.
*
* After the conversion process, the list is cleared.
*/
private _splitParts;
/**
* The list of cursor parent elements that were created during splitting.
*
* After the conversion process the list is cleared.
*/
private _cursorParents;
/**
* 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 _modelCursor;
/**
* 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 _emptyElementsToKeep;
/**
* Creates an upcast dispatcher that operates using the passed API.
*
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi
* @param conversionApi Additional properties for an interface that will be passed to events fired
* by the upcast dispatcher.
*/
constructor(conversionApi: Pick<UpcastConversionApi, 'schema'>);
/**
* Starts the conversion process. The entry point for the conversion.
*
* @fires element
* @fires text
* @fires documentFragment
* @param viewElement The part of the view to be converted.
* @param writer An instance of the model writer.
* @param context Elements will be converted according to this context.
* @returns 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~ModelDocumentFragment#markers static markers map}.
*/
convert(viewElement: ViewElement | ViewDocumentFragment, writer: ModelWriter, context?: ModelSchemaContextDefinition): ModelDocumentFragment;
/**
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertItem
*/
private _convertItem;
/**
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertChildren
*/
private _convertChildren;
/**
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#safeInsert
*/
private _safeInsert;
/**
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#updateConversionResult
*/
private _updateConversionResult;
/**
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#splitToAllowedParent
*/
private _splitToAllowedParent;
/**
* 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 _registerSplitPair;
/**
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#getSplitParts
*/
private _getSplitParts;
/**
* Mark an element that were created during the splitting to not get removed on conversion end even if it is empty.
*/
private _keepEmptyElement;
/**
* 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;
}
/**
* Fired before the first conversion event, at the beginning of the upcast (view-to-model conversion) process.
*
* @eventName ~UpcastDispatcher#viewCleanup
* @param viewItem A part of the view to be converted.
*/
export type UpcastViewCleanupEvent = {
name: 'viewCleanup';
args: [ViewElement | ViewDocumentFragment];
};
export type UpcastEvent<TName extends string, TItem extends ViewItem | ViewDocumentFragment> = {
name: TName | `${TName}:${string}`;
args: [data: UpcastConversionData<TItem>, conversionApi: UpcastConversionApi];
};
/**
* Conversion data.
*
* **Note:** Keep in mind that this object is shared by reference between all conversion callbacks that will be called.
* This means that callbacks can override values if needed, and these values will be available in other callbacks.
*/
export interface UpcastConversionData<TItem extends ViewItem | ViewDocumentFragment = ViewItem | ViewDocumentFragment> {
/**
* The converted item.
*/
viewItem: TItem;
/**
* The position where the converter should start changes.
* Change this value for the next converter to tell where the conversion should continue.
*/
modelCursor: ModelPosition;
/**
* The current state of conversion result. Every change to
* the converted element should be reflected by setting or modifying this property.
*/
modelRange: ModelRange | null;
}
/**
* Fired when an {@link module:engine/view/element~ViewElement} is converted.
*
* `element` is a namespace event for a class of events. Names of actually called events follow the pattern of
* `element:<elementName>` where `elementName` is the name of the converted element. This way listeners may listen to
* a conversion of all or just specific elements.
*
* @eventName ~UpcastDispatcher#element
* @param data The conversion data. Keep in mind that this object is shared by reference between all callbacks
* that will be called. This means that callbacks can override values if needed, and these values
* will be available in other callbacks.
* @param conversionApi Conversion utilities to be used by the callback.
*/
export type UpcastElementEvent = UpcastEvent<'element', ViewElement>;
/**
* Fired when a {@link module:engine/view/text~ViewText} is converted.
*
* @eventName ~UpcastDispatcher#text
* @see ~UpcastDispatcher#event:element
*/
export type UpcastTextEvent = UpcastEvent<'text', ViewText>;
/**
* Fired when a {@link module:engine/view/documentfragment~ViewDocumentFragment} is converted.
*
* @eventName ~UpcastDispatcher#documentFragment
* @see ~UpcastDispatcher#event:element
*/
export type UpcastDocumentFragmentEvent = UpcastEvent<'documentFragment', ViewDocumentFragment>;
/**
* A set of conversion utilities available as the third parameter of the
* {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher upcast dispatcher}'s events.
*/
export interface UpcastConversionApi {
/**
* Stores information about what parts of the processed view item are still waiting to be handled. After a piece of view item
* was converted, an appropriate consumable value should be
* {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
*/
consumable: ViewConsumable;
/**
* The model's schema instance.
*/
schema: ModelSchema;
/**
* The {@link module:engine/model/writer~ModelWriter} instance used to manipulate the data during conversion.
*/
writer: ModelWriter;
/**
* Custom data stored by converters for the conversion process. Custom properties of this object can be defined and use to
* pass parameters between converters.
*
* The difference between this property and the `data` parameter of
* {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element} is that the `data` parameters allow you
* to pass parameters within a single event and `store` within the whole conversion.
*/
store: unknown;
/**
* Starts the conversion of a given item by firing an appropriate event.
*
* Every fired event is passed (as the first parameter) an object with the `modelRange` property. Every event may set and/or
* modify that property. When all callbacks are done, the final value of the `modelRange` property is returned by this method.
* The `modelRange` must be a {@link module:engine/model/range~ModelRange model range} or `null` (as set by default).
*
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
* @param viewItem Item to convert.
* @param modelCursor The conversion position.
* @returns The conversion result:
* * `result.modelRange` The model range containing the result of the item conversion,
* created and modified by callbacks attached to the fired event, or `null` if the conversion result was incorrect.
* * `result.modelCursor` The position where the conversion should be continued.
*/
convertItem(viewItem: ViewItem, modelCursor: ModelPosition): {
modelRange: ModelRange | null;
modelCursor: ModelPosition;
};
/**
* Starts the conversion of all children of a given item by firing appropriate events for all the children.
*
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
* @param viewElement An element whose children should be converted.
* @param positionOrElement A position or an element of
* the conversion.
* @returns The conversion result:
* * `result.modelRange` The model range containing the results of the conversion of all children
* of the given item. When no child was converted, the range is collapsed.
* * `result.modelCursor` The position where the conversion should be continued.
*/
convertChildren(viewElement: ViewElement | ViewDocumentFragment, positionOrElement: ModelPosition | ModelElement): {
modelRange: ModelRange | null;
modelCursor: ModelPosition;
};
/**
* Safely inserts an element to the document, checking the
* {@link module:engine/model/schema~ModelSchema schema} to find an allowed parent
* for an element that you are going to insert, starting from the given position. If the current parent does not allow to insert
* the element but one of the ancestors does, then splits the nodes to allowed parent.
*
* If the schema allows to insert the node in a given position, nothing is split.
*
* If it was not possible to find an allowed parent, `false` is returned and nothing is split.
*
* Otherwise, ancestors are split.
*
* For instance, if `<imageBlock>` is not allowed in `<paragraph>` but is allowed in `$root`:
*
* ```
* <paragraph>foo[]bar</paragraph>
*
* -> safe insert for `<imageBlock>` will split ->
*
* <paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
*```
*
* Example usage:
*
* ```
* const myElement = conversionApi.writer.createElement( 'myElement' );
*
* if ( !conversionApi.safeInsert( myElement, data.modelCursor ) ) {
* return;
* }
*```
*
* The split result is saved and {@link #updateConversionResult} should be used to update the
* {@link module:engine/conversion/upcastdispatcher~UpcastConversionData conversion data}.
*
* @param modelNode The node to insert.
* @param position The position where an element is going to be inserted.
* @returns The split result. If it was not possible to find an allowed position, `false` is returned.
*/
safeInsert(modelNode: ModelNode, position: ModelPosition): boolean;
/**
* Updates the conversion result and sets a proper {@link module:engine/conversion/upcastdispatcher~UpcastConversionData#modelRange} and
* the next {@link module:engine/conversion/upcastdispatcher~UpcastConversionData#modelCursor} after the conversion.
* Used together with {@link #safeInsert}, it enables you to easily convert elements without worrying if the node was split
* during the conversion of its children.
*
* A usage example in converter code:
*
* ```ts
* const myElement = conversionApi.writer.createElement( 'myElement' );
*
* if ( !conversionApi.safeInsert( myElement, data.modelCursor ) ) {
* return;
* }
*
* // Children conversion may split `myElement`.
* conversionApi.convertChildren( data.viewItem, myElement );
*
* conversionApi.updateConversionResult( myElement, data );
* ```
*/
updateConversionResult(modelElement: ModelElement, data: UpcastConversionData): void;
/**
* Checks the {@link module:engine/model/schema~ModelSchema schema} to find an allowed parent for an element
* that is going to be inserted starting from the given position. If the current parent does not allow
* inserting an element but one of the ancestors does, the method splits nodes to allowed parent.
*
* If the schema allows inserting the node in the given position, nothing is split and an object with that position is returned.
*
* If it was not possible to find an allowed parent, `null` is returned and nothing is split.
*
* Otherwise, ancestors are split and an object with a position and the copy of the split element is returned.
*
* For instance, if `<imageBlock>` is not allowed in `<paragraph>` but is allowed in `$root`:
*
* ```
* <paragraph>foo[]bar</paragraph>
*
* -> split for `<imageBlock>` ->
*
* <paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
* ```
*
* In the example above, the position between `<paragraph>` elements will be returned as `position` and the second `paragraph`
* as `cursorParent`.
*
* **Note:** This is an advanced method. For most cases {@link #safeInsert} and {@link #updateConversionResult} should be used.
*
* @param modelNode The node to insert.
* @param modelCursor The position where the element is going to be inserted.
* @returns The split result. If it was not possible to find an allowed position, `null` is returned.
* * `position` The position between split elements.
* * `cursorParent` The element inside which the cursor should be placed to
* continue the conversion. When the element is not defined it means that there was no split.
*/
splitToAllowedParent(modelNode: ModelNode, modelCursor: ModelPosition): {
position: ModelPosition;
cursorParent?: ModelElement | ModelDocumentFragment;
} | null;
/**
* Returns all the split parts of the given `element` that were created during upcasting through using {@link #splitToAllowedParent}.
* It enables you to easily track these elements and continue processing them after they are split during the conversion of their
* children.
*
* ```
* <paragraph>Foo<imageBlock />bar<imageBlock />baz</paragraph> ->
* <paragraph>Foo</paragraph><imageBlock /><paragraph>bar</paragraph><imageBlock /><paragraph>baz</paragraph>
* ```
*
* For a reference to any of above paragraphs, the function will return all three paragraphs (the original element included),
* sorted in the order of their creation (the original element is the first one).
*
* If the given `element` was not split, an array with a single element is returned.
*
* A usage example in the converter code:
*
* ```ts
* const myElement = conversionApi.writer.createElement( 'myElement' );
*
* // Children conversion may split `myElement`.
* conversionApi.convertChildren( data.viewItem, data.modelCursor );
*
* const splitParts = conversionApi.getSplitParts( myElement );
* const lastSplitPart = splitParts[ splitParts.length - 1 ];
*
* // Setting `data.modelRange` basing on split parts:
* data.modelRange = conversionApi.writer.createRange(
* conversionApi.writer.createPositionBefore( myElement ),
* conversionApi.writer.createPositionAfter( lastSplitPart )
* );
*
* // Setting `data.modelCursor` to continue after the last split element:
* data.modelCursor = conversionApi.writer.createPositionAfter( lastSplitPart );
* ```
*
* **Tip:** If you are unable to get a reference to the original element (for example because the code is split into multiple converters
* or even classes) but it has already been converted, you may want to check the first element in `data.modelRange`. This is a common
* situation if an attribute converter is separated from an element converter.
*
* **Note:** This is an advanced method. For most cases {@link #safeInsert} and {@link #updateConversionResult} should be used.
*/
getSplitParts(modelElement: ModelElement): Array<ModelElement>;
/**
* Mark an element that was created during splitting to not get removed on conversion end even if it is empty.
*
* **Note:** This is an advanced method. For most cases you will not need to keep the split empty element.
*/
keepEmptyElement(modelElement: ModelElement): void;
}
export {};