| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518 |
- /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module engine/conversion/upcastdispatcher
- */
- import ViewConsumable from './viewconsumable';
- import ModelRange from '../model/range';
- import ModelPosition from '../model/position';
- import { SchemaContext } from '../model/schema';
- import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
- import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
- import mix from '@ckeditor/ckeditor5-utils/src/mix';
- /**
- * `UpcastDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of
- * converting given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
- * {@link module:engine/view/element~Element} into another structure.
- * In default application, {@link module:engine/view/view view} is converted to {@link module:engine/model/model}.
- *
- * During conversion process, for all {@link module:engine/view/node~Node view nodes} from the converted view document fragment,
- * `UpcastDispatcher` fires corresponding events. Special callbacks called "converters" should listen to
- * `UpcastDispatcher` for those events.
- *
- * Each callback, as the second argument, is passed a special object `data` that has `viewItem`, `modelCursor` and
- * `modelRange` properties. `viewItem` property contains {@link module:engine/view/node~Node view node} or
- * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
- * that is converted at the moment and might be handled by the callback. `modelRange` property should be used to save the result
- * of conversion and is always a {@link module:engine/model/range~Range} when conversion result is correct.
- * `modelCursor` property is a {@link module:engine/model/position~Position position} on which conversion result will be inserted
- * and is a context according to {@link module:engine/model/schema~Schema schema} will be checked before the conversion.
- * See also {@link ~UpcastDispatcher#convert}. It is also shared by reference by all callbacks listening to given event.
- *
- * The third parameter passed to a callback is an instance of {@link ~UpcastDispatcher}
- * which provides additional tools for converters.
- *
- * Examples of providing callbacks for `UpcastDispatcher`:
- *
- * // Converter for links (<a>).
- * upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
- * if ( conversionApi.consumable.consume( data.viewItem, { name: true, attributes: [ 'href' ] } ) ) {
- * // <a> element is inline and is represented by an attribute in the model.
- * // This is why we 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 all elements which have no custom converter into paragraph (autoparagraphing).
- * data.viewToModel.on( 'element', ( evt, data, conversionApi ) => {
- * // When element is already consumed by higher priority converters then do nothing.
- * if ( conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
- * const paragraph = conversionApi.writer.createElement( 'paragraph' );
- *
- * // Find allowed parent for paragraph that we are going to insert. If current parent does not allow
- * // to insert paragraph but one of the ancestors does then split nodes to allowed parent.
- * const splitResult = conversionApi.splitToAllowedParent( paragraph, data.modelCursor );
- *
- * // When there is no split result it means that we can't insert paragraph in this position.
- * if ( splitResult ) {
- * // Insert paragraph in allowed position.
- * conversionApi.writer.insert( paragraph, splitResult.position );
- *
- * // Convert children to paragraph.
- * const { modelRange } = conversionApi.convertChildren( data.viewItem, Position.createAt( paragraph ) );
- *
- * // Set as conversion result, attribute converters may use this property.
- * data.modelRange = new Range( Position.createBefore( paragraph ), modelRange.end );
- *
- * // Continue conversion inside paragraph.
- * data.modelCursor = data.modelRange.end;
- * }
- * }
- * }
- * }, { priority: 'low' } );
- *
- * Before each conversion process, `UpcastDispatcher` fires {@link ~UpcastDispatcher#event:viewCleanup}
- * event which can be used to prepare tree view for conversion.
- *
- * @mixes module:utils/emittermixin~EmitterMixin
- * @fires viewCleanup
- * @fires element
- * @fires text
- * @fires documentFragment
- */
- export default class UpcastDispatcher {
- /**
- * Creates a `UpcastDispatcher` that operates using passed API.
- *
- * @see module:engine/conversion/upcastdispatcher~ViewConversionApi
- * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
- * by `UpcastDispatcher`.
- */
- constructor( conversionApi = {} ) {
- /**
- * List of elements that will be checked after conversion process and if element in the list will be empty it
- * will be removed from conversion result.
- *
- * After conversion process list is cleared.
- *
- * @protected
- * @type {Set<module:engine/model/element~Element>}
- */
- this._removeIfEmpty = new Set();
- /**
- * Position in the temporary structure where the converted content is inserted. The structure reflect the context of
- * the target position where the content will be inserted. This property is build based on the context parameter of the
- * convert method.
- *
- * @private
- * @type {module:engine/model/position~Position|null}
- */
- this._modelCursor = null;
- /**
- * Interface passed by dispatcher to the events callbacks.
- *
- * @member {module:engine/conversion/upcastdispatcher~ViewConversionApi}
- */
- this.conversionApi = Object.assign( {}, conversionApi );
- // `convertItem`, `convertChildren` and `splitToAllowedParent` are bound to this `UpcastDispatcher`
- // instance and set on `conversionApi`. This way only a part of `UpcastDispatcher` API is exposed.
- this.conversionApi.convertItem = this._convertItem.bind( this );
- this.conversionApi.convertChildren = this._convertChildren.bind( this );
- this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.bind( this );
- }
- /**
- * Starts the conversion process. The entry point for the conversion.
- *
- * @fires element
- * @fires text
- * @fires documentFragment
- * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
- * Part of the view to be converted.
- * @param {module:engine/model/writer~Writer} writer Instance of 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 a result of the conversion process
- * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's
- * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
- */
- convert( viewItem, writer, context = [ '$root' ] ) {
- this.fire( 'viewCleanup', viewItem );
- // 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( viewItem );
- // Custom data stored by converter for conversion process.
- this.conversionApi.store = {};
- // Do the conversion.
- const { modelRange } = this._convertItem( viewItem, this._modelCursor );
- // Conversion result is always a document fragment so let's create this fragment.
- const documentFragment = writer.createDocumentFragment();
- // When there is a conversion result.
- if ( modelRange ) {
- // Remove all empty elements that was added to #_removeIfEmpty list.
- this._removeEmptyElements();
- // Move all items that was converted to context tree to 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.
- this._removeIfEmpty.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~ViewConversionApi#convertItem
- */
- _convertItem( viewItem, modelCursor ) {
- const data = Object.assign( { 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 ModelRange ) ) {
- /**
- * 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: Incorrect conversion result was dropped.' );
- }
- return { modelRange: data.modelRange, modelCursor: data.modelCursor };
- }
- /**
- * @private
- * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#convertChildren
- */
- _convertChildren( viewItem, modelCursor ) {
- const modelRange = new ModelRange( modelCursor );
- let nextModelCursor = modelCursor;
- for ( const viewChild of Array.from( viewItem.getChildren() ) ) {
- const result = this._convertItem( viewChild, nextModelCursor );
- if ( result.modelRange instanceof ModelRange ) {
- modelRange.end = result.modelRange.end;
- nextModelCursor = result.modelCursor;
- }
- }
- return { modelRange, modelCursor: nextModelCursor };
- }
- /**
- * @private
- * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#splitToAllowedParent
- */
- _splitToAllowedParent( node, modelCursor ) {
- // Try to find allowed parent.
- const allowedParent = this.conversionApi.schema.findAllowedParent( node, modelCursor );
- // When there is no parent that allows to insert node then return `null`.
- if ( !allowedParent ) {
- return null;
- }
- // 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.
- if ( this._modelCursor.parent.getAncestors().includes( allowedParent ) ) {
- return null;
- }
- // Split element to allowed parent.
- const splitResult = this.conversionApi.writer.split( modelCursor, allowedParent );
- // Remember all elements that are created as a result of split.
- // This is important because at the end of conversion we want to remove all empty split elements.
- //
- // Loop through positions between elements in range (except split result position) and collect parents.
- // <notSplit><split1><split2>[pos]</split2>[pos]</split1>[omit]<split1>[pos]<split2>[pos]</split2></split1></notSplit>
- for ( const position of splitResult.range.getPositions() ) {
- if ( !position.isEqual( splitResult.position ) ) {
- this._removeIfEmpty.add( position.parent );
- }
- }
- return {
- position: splitResult.position,
- cursorParent: splitResult.range.end.parent
- };
- }
- /**
- * Checks if {@link #_removeIfEmpty} contains empty elements and remove them.
- * We need to do it smart because there could be elements that are not empty because contains
- * other empty elements and after removing its children they become available to remove.
- * We need to continue iterating over split elements as long as any element will be removed.
- *
- * @private
- */
- _removeEmptyElements() {
- let removed = false;
- for ( const element of this._removeIfEmpty ) {
- if ( element.isEmpty ) {
- this.conversionApi.writer.remove( element );
- this._removeIfEmpty.delete( element );
- removed = true;
- }
- }
- if ( removed ) {
- this._removeEmptyElements();
- }
- }
- /**
- * Fired before the first conversion event, at the beginning of upcast (view to model conversion) process.
- *
- * @event viewCleanup
- * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
- * viewItem Part of the view to be converted.
- */
- /**
- * Fired when {@link module:engine/view/element~Element} is converted.
- *
- * `element` is a namespace event for a class of events. Names of actually called events follow this pattern:
- * `element:<elementName>` where `elementName` is the name of converted element. This way listeners may listen to
- * all elements conversion or to conversion of specific elements.
- *
- * @event element
- * @param {Object} data 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 those values will
- * be available in other callbacks.
- * @param {module:engine/view/item~Item} data.viewItem Converted item.
- * @param {module:engine/model/position~Position} data.modelCursor Position where a converter should start changes.
- * Change this value for the next converter to tell where the conversion should continue.
- * @param {module:engine/model/range~Range} data.modelRange The current state of conversion result. Every change to
- * converted element should be reflected by setting or modifying this property.
- * @param {ViewConversionApi} conversionApi Conversion utilities to be used by callback.
- */
- /**
- * Fired when {@link module:engine/view/text~Text} is converted.
- *
- * @event text
- * @see #event:element
- */
- /**
- * Fired when {@link module:engine/view/documentfragment~DocumentFragment} is converted.
- *
- * @event documentFragment
- * @see #event:element
- */
- }
- mix( UpcastDispatcher, EmitterMixin );
- // Traverses given model item and searches elements which marks marker range. Found element is removed from
- // DocumentFragment but path of this element is stored in a Map which is then returned.
- //
- // @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
- // @returns {Map<String, module:engine/model/range~Range>} List of static markers.
- function extractMarkersFromModelFragment( modelItem, writer ) {
- const markerElements = new Set();
- const markers = new Map();
- // Create ModelTreeWalker.
- const range = ModelRange.createIn( modelItem ).getItems();
- // Walk through DocumentFragment and collect marker elements.
- for ( const item of range ) {
- // Check if current element is a marker.
- if ( item.name == '$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 = ModelPosition.createBefore( markerElement );
- // When marker of given name is not stored it means that we have found the beginning of the range.
- if ( !markers.has( markerName ) ) {
- markers.set( markerName, new ModelRange( ModelPosition.createFromPosition( currentPosition ) ) );
- // Otherwise is means that we have found end of the marker range.
- } else {
- markers.get( markerName ).end = ModelPosition.createFromPosition( currentPosition );
- }
- // Remove marker element from DocumentFragment.
- writer.remove( markerElement );
- }
- return markers;
- }
- // Creates model fragment according to given context and returns position in top 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.append( current, position );
- }
- position = ModelPosition.createAt( current );
- }
- return position;
- }
- /**
- * Conversion interface that is registered for given {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}
- * and is passed as one of parameters when {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher dispatcher}
- * fires it's events.
- *
- * @interface ViewConversionApi
- */
- /**
- * Starts conversion of given item by firing an appropriate event.
- *
- * Every fired event is passed (as first parameter) an object with `modelRange` property. Every event may set and/or
- * modify that property. When all callbacks are done, the final value of `modelRange` property is returned by this method.
- * The `modelRange` must be {@link module:engine/model/range~Range model range} or `null` (as set by default).
- *
- * @method #convertItem
- * @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 {module:engine/view/item~Item} viewItem Item to convert.
- * @param {module:engine/model/position~Position} modelCursor Position of conversion.
- * @returns {Object} result Conversion result.
- * @returns {module:engine/model/range~Range|null} result.modelRange Model range containing result of item conversion,
- * created and modified by callbacks attached to fired event, or `null` if the conversion result was incorrect.
- * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
- */
- /**
- * Starts conversion of all children of given item by firing appropriate events for all those children.
- *
- * @method #convertChildren
- * @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 {module:engine/view/item~Item} viewItem Item to convert.
- * @param {module:engine/model/position~Position} modelCursor Position of conversion.
- * @returns {Object} result Conversion result.
- * @returns {module:engine/model/range~Range} result.modelRange Model range containing results of conversion of all children of given item.
- * When no children was converted then range is collapsed.
- * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
- */
- /**
- * Checks {@link module:engine/model/schema~Schema schema} to find allowed parent for element that we are going to insert
- * starting from given position. If current parent does not allow to insert element but one of the ancestors does then
- * split nodes to allowed parent.
- *
- * If schema allows to insert node in given position, nothing is split and object with that position is returned.
- *
- * If it was not possible to find allowed parent, `null` is returned, nothing is split.
- *
- * Otherwise, ancestors are split and object with position and the copy of the split element is returned.
- *
- * For instance, if `<image>` is not allowed in `<paragraph>` but is allowed in `$root`:
- *
- * <paragraph>foo[]bar</paragraph>
- *
- * -> split for `<image>` ->
- *
- * <paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
- *
- * In the sample above position between `<paragraph>` elements will be returned as `position` and the second `paragraph`
- * as `cursorParent`.
- *
- * @method #splitToAllowedParent
- * @param {module:engine/model/position~Position} position Position on which element is going to be inserted.
- * @param {module:engine/model/node~Node} node Node to insert.
- * @returns {Object|null} Split result. If it was not possible to find allowed position `null` is returned.
- * @returns {module:engine/model/position~Position} position between split elements.
- * @returns {module:engine/model/element~Element} [cursorParent] Element inside which cursor should be placed to
- * continue conversion. When element is not defined it means that there was no split.
- */
- /**
- * Instance of {@link module:engine/conversion/viewconsumable~ViewConsumable}. It stores
- * information about what parts of processed view item are still waiting to be handled. After a piece of view item
- * was converted, appropriate consumable value should be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
- *
- * @param {Object} #consumable
- */
- /**
- * Custom data stored by converters for conversion process. Custom properties of this object can be defined and use to
- * pass parameters between converters.
- *
- * The difference between this property and `data` parameter of
- * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element} is that `data` parameters allows you
- * to pass parameters within a single event and `store` within the whole conversion.
- *
- * @param {Object} #store
- */
|