@ckeditor/ckeditor5-engine 32.0.0 → 33.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,14 +9,13 @@
9
9
 
10
10
  import Consumable from './modelconsumable';
11
11
  import Range from '../model/range';
12
- import Position, { getNodeAfterPosition, getTextNodeAtPosition } from '../model/position';
13
12
 
14
13
  import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
15
14
  import mix from '@ckeditor/ckeditor5-utils/src/mix';
16
15
 
17
16
  /**
18
17
  * The downcast dispatcher is a central point of downcasting (conversion from the model to the view), which is a process of reacting
19
- * to changes in the model and firing a set of events. Callbacks listening to these events are called converters. The
18
+ * to changes in the model and firing a set of events. The callbacks listening to these events are called converters. The
20
19
  * converters' role is to convert the model changes to changes in view (for example, adding view nodes or
21
20
  * changing attributes on view elements).
22
21
  *
@@ -25,7 +24,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
25
24
  * for example: "a node has been inserted" or "an attribute has changed". This is in contrary to upcasting (a view-to-model conversion)
26
25
  * where you convert the view state (view nodes) to a model tree.
27
26
  *
28
- * The events are prepared basing on a diff created by {@link module:engine/model/differ~Differ Differ}, which buffers them
27
+ * The events are prepared basing on a diff created by the {@link module:engine/model/differ~Differ Differ}, which buffers them
29
28
  * and then passes to the downcast dispatcher as a diff between the old model state and the new model state.
30
29
  *
31
30
  * Note that because the changes are converted, there is a need to have a mapping between the model structure and the view structure.
@@ -42,27 +41,28 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
42
41
  *
43
42
  * For {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert`}
44
43
  * and {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute`},
45
- * downcast dispatcher generates {@link module:engine/conversion/modelconsumable~ModelConsumable consumables}.
44
+ * the downcast dispatcher generates {@link module:engine/conversion/modelconsumable~ModelConsumable consumables}.
46
45
  * These are used to have control over which changes have already been consumed. It is useful when some converters
47
46
  * overwrite others or convert multiple changes (for example, it converts an insertion of an element and also converts that
48
47
  * element's attributes during the insertion).
49
48
  *
50
49
  * Additionally, downcast dispatcher fires events for {@link module:engine/model/markercollection~Marker marker} changes:
51
50
  *
52
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} – If a marker was added.
53
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} – If a marker was removed.
51
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker `addMarker`} – If a marker was added.
52
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker `removeMarker`} – If a marker was
53
+ * removed.
54
54
  *
55
55
  * Note that changing a marker is done through removing the marker from the old range and adding it to the new range,
56
- * so both events are fired.
56
+ * so both of these events are fired.
57
57
  *
58
- * Finally, downcast dispatcher also handles firing events for the {@link module:engine/model/selection model selection}
58
+ * Finally, a downcast dispatcher also handles firing events for the {@link module:engine/model/selection model selection}
59
59
  * conversion:
60
60
  *
61
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:selection}
61
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:selection `selection`}
62
62
  * – Converts the selection from the model to the view.
63
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute}
63
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute`}
64
64
  * – Fired for every selection attribute.
65
- * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}
65
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker `addMarker`}
66
66
  * – Fired for every marker that contains a selection.
67
67
  *
68
68
  * Unlike the model tree and the markers, the events for selection are not fired for changes but for a selection state.
@@ -70,18 +70,15 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
70
70
  * When providing custom listeners for a downcast dispatcher, remember to check whether a given change has not been
71
71
  * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} yet.
72
72
  *
73
- * When providing custom listeners for downcast dispatcher, keep in mind that any callback that has
74
- * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} a value from a consumable and
75
- * converted the change should also stop the event (for efficiency purposes).
73
+ * When providing custom listeners for a downcast dispatcher, keep in mind that you **should not** stop the event. If you stop it,
74
+ * then the default converter at the `lowest` priority will not trigger the conversion of this node's attributes and child nodes.
76
75
  *
77
- * When providing custom listeners for downcast dispatcher, remember to use the provided
76
+ * When providing custom listeners for a downcast dispatcher, remember to use the provided
78
77
  * {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} to apply changes to the view document.
79
78
  *
80
- * You can read more about conversion in the following guides:
79
+ * You can read more about conversion in the following guide:
81
80
  *
82
- * * {@glink framework/guides/deep-dive/conversion/conversion-introduction Advanced conversion concepts — attributes}
83
- * * {@glink framework/guides/deep-dive/conversion/conversion-extending-output Extending the editor output }
84
- * * {@glink framework/guides/deep-dive/conversion/custom-element-conversion Custom element conversion}
81
+ * * {@glink framework/guides/deep-dive/conversion/downcast Downcast conversion}
85
82
  *
86
83
  * An example of a custom converter for the downcast dispatcher:
87
84
  *
@@ -103,9 +100,6 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
103
100
  *
104
101
  * // Add the newly created view element to the view.
105
102
  * conversionApi.writer.insert( viewPosition, viewElement );
106
- *
107
- * // Remember to stop the event propagation.
108
- * evt.stop();
109
103
  * } );
110
104
  */
111
105
  export default class DowncastDispatcher {
@@ -118,206 +112,102 @@ export default class DowncastDispatcher {
118
112
  */
119
113
  constructor( conversionApi ) {
120
114
  /**
121
- * An interface passed by the dispatcher to the event callbacks.
115
+ * A template for an interface passed by the dispatcher to the event callbacks.
122
116
  *
117
+ * @protected
123
118
  * @member {module:engine/conversion/downcastdispatcher~DowncastConversionApi}
124
119
  */
125
- this.conversionApi = Object.assign( { dispatcher: this }, conversionApi );
120
+ this._conversionApi = { dispatcher: this, ...conversionApi };
126
121
 
127
122
  /**
128
- * Maps conversion event names that will trigger element reconversion for a given element name.
123
+ * A map of already fired events for a given `ModelConsumable`.
129
124
  *
130
- * @type {Map<String, String>}
131
125
  * @private
126
+ * @member {WeakMap.<module:engine/conversion/downcastdispatcher~DowncastConversionApi,Map>}
132
127
  */
133
- this._reconversionEventsMapping = new Map();
128
+ this._firedEventsMap = new WeakMap();
134
129
  }
135
130
 
136
131
  /**
137
- * Takes a {@link module:engine/model/differ~Differ model differ} object with buffered changes and fires conversion basing on it.
132
+ * Converts changes buffered in the given {@link module:engine/model/differ~Differ model differ}
133
+ * and fires conversion events based on it.
138
134
  *
135
+ * @fires insert
136
+ * @fires remove
137
+ * @fires attribute
138
+ * @fires addMarker
139
+ * @fires removeMarker
140
+ * @fires reduceChanges
139
141
  * @param {module:engine/model/differ~Differ} differ The differ object with buffered changes.
140
- * @param {module:engine/model/markercollection~MarkerCollection} markers Markers connected with the converted model.
142
+ * @param {module:engine/model/markercollection~MarkerCollection} markers Markers related to the model fragment to convert.
141
143
  * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
142
144
  */
143
145
  convertChanges( differ, markers, writer ) {
146
+ const conversionApi = this._createConversionApi( writer, differ.getRefreshedItems() );
147
+
144
148
  // Before the view is updated, remove markers which have changed.
145
149
  for ( const change of differ.getMarkersToRemove() ) {
146
- this.convertMarkerRemove( change.name, change.range, writer );
150
+ this._convertMarkerRemove( change.name, change.range, conversionApi );
147
151
  }
148
152
 
149
- const changes = this._mapChangesWithAutomaticReconversion( differ );
153
+ // Let features modify the change list (for example to allow reconversion).
154
+ const changes = this._reduceChanges( differ.getChanges() );
150
155
 
151
156
  // Convert changes that happened on model tree.
152
157
  for ( const entry of changes ) {
153
158
  if ( entry.type === 'insert' ) {
154
- this.convertInsert( Range._createFromPositionAndShift( entry.position, entry.length ), writer );
159
+ this._convertInsert( Range._createFromPositionAndShift( entry.position, entry.length ), conversionApi );
160
+ } else if ( entry.type === 'reinsert' ) {
161
+ this._convertReinsert( Range._createFromPositionAndShift( entry.position, entry.length ), conversionApi );
155
162
  } else if ( entry.type === 'remove' ) {
156
- this.convertRemove( entry.position, entry.length, entry.name, writer );
157
- } else if ( entry.type === 'reconvert' ) {
158
- this.reconvertElement( entry.element, writer );
163
+ this._convertRemove( entry.position, entry.length, entry.name, conversionApi );
159
164
  } else {
160
165
  // Defaults to 'attribute' change.
161
- this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue, writer );
166
+ this._convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue, conversionApi );
162
167
  }
163
168
  }
164
169
 
165
- for ( const markerName of this.conversionApi.mapper.flushUnboundMarkerNames() ) {
170
+ for ( const markerName of conversionApi.mapper.flushUnboundMarkerNames() ) {
166
171
  const markerRange = markers.get( markerName ).getRange();
167
172
 
168
- this.convertMarkerRemove( markerName, markerRange, writer );
169
- this.convertMarkerAdd( markerName, markerRange, writer );
173
+ this._convertMarkerRemove( markerName, markerRange, conversionApi );
174
+ this._convertMarkerAdd( markerName, markerRange, conversionApi );
170
175
  }
171
176
 
172
177
  // After the view is updated, convert markers which have changed.
173
178
  for ( const change of differ.getMarkersToAdd() ) {
174
- this.convertMarkerAdd( change.name, change.range, writer );
175
- }
176
- }
177
-
178
- /**
179
- * Starts a conversion of a range insertion.
180
- *
181
- * For each node in the range, {@link #event:insert `insert` event is fired}. For each attribute on each node,
182
- * {@link #event:attribute `attribute` event is fired}.
183
- *
184
- * @fires insert
185
- * @fires attribute
186
- * @param {module:engine/model/range~Range} range The inserted range.
187
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
188
- */
189
- convertInsert( range, writer ) {
190
- this.conversionApi.writer = writer;
191
-
192
- // Create a list of things that can be consumed, consisting of nodes and their attributes.
193
- this.conversionApi.consumable = this._createInsertConsumable( range );
194
-
195
- // Fire a separate insert event for each node and text fragment contained in the range.
196
- for ( const data of Array.from( range ).map( walkerValueToEventData ) ) {
197
- this._convertInsertWithAttributes( data );
179
+ this._convertMarkerAdd( change.name, change.range, conversionApi );
198
180
  }
199
181
 
200
- this._clearConversionApi();
201
- }
202
-
203
- /**
204
- * Fires conversion of a single node removal. Fires {@link #event:remove remove event} with provided data.
205
- *
206
- * @param {module:engine/model/position~Position} position Position from which node was removed.
207
- * @param {Number} length Offset size of removed node.
208
- * @param {String} name Name of removed node.
209
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
210
- */
211
- convertRemove( position, length, name, writer ) {
212
- this.conversionApi.writer = writer;
213
-
214
- this.fire( 'remove:' + name, { position, length }, this.conversionApi );
182
+ // Remove mappings for all removed view elements.
183
+ conversionApi.mapper.flushDeferredBindings();
215
184
 
216
- this._clearConversionApi();
185
+ // Verify if all insert consumables were consumed.
186
+ conversionApi.consumable.verifyAllConsumed( 'insert' );
217
187
  }
218
188
 
219
189
  /**
220
- * Starts a conversion of an attribute change on a given `range`.
221
- *
222
- * For each node in the given `range`, {@link #event:attribute attribute event} is fired with the passed data.
223
- *
224
- * @fires attribute
225
- * @param {module:engine/model/range~Range} range Changed range.
226
- * @param {String} key Key of the attribute that has changed.
227
- * @param {*} oldValue Attribute value before the change or `null` if the attribute has not been set before.
228
- * @param {*} newValue New attribute value or `null` if the attribute has been removed.
229
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
230
- */
231
- convertAttribute( range, key, oldValue, newValue, writer ) {
232
- this.conversionApi.writer = writer;
233
-
234
- // Create a list with attributes to consume.
235
- this.conversionApi.consumable = this._createConsumableForRange( range, `attribute:${ key }` );
236
-
237
- // Create a separate attribute event for each node in the range.
238
- for ( const value of range ) {
239
- const item = value.item;
240
- const itemRange = Range._createFromPositionAndShift( value.previousPosition, value.length );
241
- const data = {
242
- item,
243
- range: itemRange,
244
- attributeKey: key,
245
- attributeOldValue: oldValue,
246
- attributeNewValue: newValue
247
- };
248
-
249
- this._testAndFire( `attribute:${ key }`, data );
250
- }
251
-
252
- this._clearConversionApi();
253
- }
254
-
255
- /**
256
- * Starts the reconversion of an element. It will:
257
- *
258
- * * Fire an {@link #event:insert `insert` event} for the element to reconvert.
259
- * * Fire an {@link #event:attribute `attribute` event} for element attributes.
260
- *
261
- * This will not reconvert children of the element if they have existing (already converted) views. For newly inserted child elements
262
- * it will behave the same as {@link #convertInsert}.
263
- *
264
- * Element reconversion is defined by the `triggerBy` configuration for the
265
- * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} conversion helper.
190
+ * Starts a conversion of a model range and the provided markers.
266
191
  *
267
192
  * @fires insert
268
193
  * @fires attribute
269
- * @param {module:engine/model/element~Element} element The element to be reconverted.
194
+ * @fires addMarker
195
+ * @param {module:engine/model/range~Range} range The inserted range.
196
+ * @param {Map<String,module:engine/model/range~Range>} markers The map of markers that should be down-casted.
270
197
  * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
198
+ * @param {Object} [options] Optional options object passed to `convertionApi.options`.
271
199
  */
272
- reconvertElement( element, writer ) {
273
- const elementRange = Range._createOn( element );
274
-
275
- this.conversionApi.writer = writer;
276
-
277
- // Create a list of things that can be consumed, consisting of nodes and their attributes.
278
- this.conversionApi.consumable = this._createInsertConsumable( elementRange );
279
-
280
- const mapper = this.conversionApi.mapper;
281
- const currentView = mapper.toViewElement( element );
282
-
283
- // Remove the old view but do not remove mapper mappings - those will be used to revive existing elements.
284
- writer.remove( currentView );
285
-
286
- // Convert the element - without converting children.
287
- this._convertInsertWithAttributes( {
288
- item: element,
289
- range: elementRange
290
- } );
291
-
292
- const convertedViewElement = mapper.toViewElement( element );
200
+ convert( range, markers, writer, options = {} ) {
201
+ const conversionApi = this._createConversionApi( writer, undefined, options );
293
202
 
294
- // Iterate over children of reconverted element in order to...
295
- for ( const value of Range._createIn( element ) ) {
296
- const { item } = value;
203
+ this._convertInsert( range, conversionApi );
297
204
 
298
- const view = elementOrTextProxyToView( item, mapper );
299
-
300
- // ...either bring back previously converted view...
301
- if ( view ) {
302
- // Do not move views that are already in converted element - those might be created by the main element converter in case
303
- // when main element converts also its direct children.
304
- if ( view.root !== convertedViewElement.root ) {
305
- writer.move(
306
- writer.createRangeOn( view ),
307
- mapper.toViewPosition( Position._createBefore( item ) )
308
- );
309
- }
310
- }
311
- // ... or by converting newly inserted elements.
312
- else {
313
- this._convertInsertWithAttributes( walkerValueToEventData( value ) );
314
- }
205
+ for ( const [ name, range ] of markers ) {
206
+ this._convertMarkerAdd( name, range, conversionApi );
315
207
  }
316
208
 
317
- // After reconversion is done we can unbind the old view.
318
- mapper.unbindViewElement( currentView );
319
-
320
- this._clearConversionApi();
209
+ // Verify if all insert consumables were consumed.
210
+ conversionApi.consumable.verifyAllConsumed( 'insert' );
321
211
  }
322
212
 
323
213
  /**
@@ -335,21 +225,20 @@ export default class DowncastDispatcher {
335
225
  convertSelection( selection, markers, writer ) {
336
226
  const markersAtSelection = Array.from( markers.getMarkersAtPosition( selection.getFirstPosition() ) );
337
227
 
338
- this.conversionApi.writer = writer;
339
- this.conversionApi.consumable = this._createSelectionConsumable( selection, markersAtSelection );
228
+ const conversionApi = this._createConversionApi( writer );
340
229
 
341
- this.fire( 'selection', { selection }, this.conversionApi );
230
+ this._addConsumablesForSelection( conversionApi.consumable, selection, markersAtSelection );
342
231
 
343
- if ( !selection.isCollapsed ) {
344
- this._clearConversionApi();
232
+ this.fire( 'selection', { selection }, conversionApi );
345
233
 
234
+ if ( !selection.isCollapsed ) {
346
235
  return;
347
236
  }
348
237
 
349
238
  for ( const marker of markersAtSelection ) {
350
239
  const markerRange = marker.getRange();
351
240
 
352
- if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
241
+ if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, conversionApi.mapper ) ) {
353
242
  continue;
354
243
  }
355
244
 
@@ -359,8 +248,8 @@ export default class DowncastDispatcher {
359
248
  markerRange
360
249
  };
361
250
 
362
- if ( this.conversionApi.consumable.test( selection, 'addMarker:' + marker.name ) ) {
363
- this.fire( 'addMarker:' + marker.name, data, this.conversionApi );
251
+ if ( conversionApi.consumable.test( selection, 'addMarker:' + marker.name ) ) {
252
+ this.fire( 'addMarker:' + marker.name, data, conversionApi );
364
253
  }
365
254
  }
366
255
 
@@ -374,130 +263,218 @@ export default class DowncastDispatcher {
374
263
  };
375
264
 
376
265
  // Do not fire event if the attribute has been consumed.
377
- if ( this.conversionApi.consumable.test( selection, 'attribute:' + data.attributeKey ) ) {
378
- this.fire( 'attribute:' + data.attributeKey + ':$text', data, this.conversionApi );
266
+ if ( conversionApi.consumable.test( selection, 'attribute:' + data.attributeKey ) ) {
267
+ this.fire( 'attribute:' + data.attributeKey + ':$text', data, conversionApi );
379
268
  }
380
269
  }
270
+ }
381
271
 
382
- this._clearConversionApi();
272
+ /**
273
+ * Fires insertion conversion of a range of nodes.
274
+ *
275
+ * For each node in the range, {@link #event:insert `insert` event is fired}. For each attribute on each node,
276
+ * {@link #event:attribute `attribute` event is fired}.
277
+ *
278
+ * @protected
279
+ * @fires insert
280
+ * @fires attribute
281
+ * @param {module:engine/model/range~Range} range The inserted range.
282
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
283
+ * @param {Object} [options]
284
+ * @param {Boolean} [options.doNotAddConsumables=false] Whether the ModelConsumable should not get populated
285
+ * for items in the provided range.
286
+ */
287
+ _convertInsert( range, conversionApi, options = {} ) {
288
+ if ( !options.doNotAddConsumables ) {
289
+ // Collect a list of things that can be consumed, consisting of nodes and their attributes.
290
+ this._addConsumablesForInsert( conversionApi.consumable, Array.from( range ) );
291
+ }
292
+
293
+ // Fire a separate insert event for each node and text fragment contained in the range.
294
+ for ( const data of Array.from( range.getWalker( { shallow: true } ) ).map( walkerValueToEventData ) ) {
295
+ this._testAndFire( 'insert', data, conversionApi );
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Fires conversion of a single node removal. Fires {@link #event:remove remove event} with provided data.
301
+ *
302
+ * @protected
303
+ * @param {module:engine/model/position~Position} position Position from which node was removed.
304
+ * @param {Number} length Offset size of removed node.
305
+ * @param {String} name Name of removed node.
306
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
307
+ */
308
+ _convertRemove( position, length, name, conversionApi ) {
309
+ this.fire( 'remove:' + name, { position, length }, conversionApi );
310
+ }
311
+
312
+ /**
313
+ * Starts a conversion of an attribute change on a given `range`.
314
+ *
315
+ * For each node in the given `range`, {@link #event:attribute attribute event} is fired with the passed data.
316
+ *
317
+ * @protected
318
+ * @fires attribute
319
+ * @param {module:engine/model/range~Range} range Changed range.
320
+ * @param {String} key Key of the attribute that has changed.
321
+ * @param {*} oldValue Attribute value before the change or `null` if the attribute has not been set before.
322
+ * @param {*} newValue New attribute value or `null` if the attribute has been removed.
323
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
324
+ */
325
+ _convertAttribute( range, key, oldValue, newValue, conversionApi ) {
326
+ // Create a list with attributes to consume.
327
+ this._addConsumablesForRange( conversionApi.consumable, range, `attribute:${ key }` );
328
+
329
+ // Create a separate attribute event for each node in the range.
330
+ for ( const value of range ) {
331
+ const data = {
332
+ item: value.item,
333
+ range: Range._createFromPositionAndShift( value.previousPosition, value.length ),
334
+ attributeKey: key,
335
+ attributeOldValue: oldValue,
336
+ attributeNewValue: newValue
337
+ };
338
+
339
+ this._testAndFire( `attribute:${ key }`, data, conversionApi );
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Fires re-insertion conversion (with a `reconversion` flag passed to `insert` events)
345
+ * of a range of elements (only elements on the range depth, without children).
346
+ *
347
+ * For each node in the range on its depth (without children), {@link #event:insert `insert` event} is fired.
348
+ * For each attribute on each node, {@link #event:attribute `attribute` event} is fired.
349
+ *
350
+ * @protected
351
+ * @fires insert
352
+ * @fires attribute
353
+ * @param {module:engine/model/range~Range} range The range to reinsert.
354
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
355
+ */
356
+ _convertReinsert( range, conversionApi ) {
357
+ // Convert the elements - without converting children.
358
+ const walkerValues = Array.from( range.getWalker( { shallow: true } ) );
359
+
360
+ // Collect a list of things that can be consumed, consisting of nodes and their attributes.
361
+ this._addConsumablesForInsert( conversionApi.consumable, walkerValues );
362
+
363
+ // Fire a separate insert event for each node and text fragment contained shallowly in the range.
364
+ for ( const data of walkerValues.map( walkerValueToEventData ) ) {
365
+ this._testAndFire( 'insert', { ...data, reconversion: true }, conversionApi );
366
+ }
383
367
  }
384
368
 
385
369
  /**
386
370
  * Converts the added marker. Fires the {@link #event:addMarker `addMarker`} event for each item
387
371
  * in the marker's range. If the range is collapsed, a single event is dispatched. See the event description for more details.
388
372
  *
373
+ * @protected
389
374
  * @fires addMarker
390
375
  * @param {String} markerName Marker name.
391
376
  * @param {module:engine/model/range~Range} markerRange The marker range.
392
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify the view document.
377
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
393
378
  */
394
- convertMarkerAdd( markerName, markerRange, writer ) {
379
+ _convertMarkerAdd( markerName, markerRange, conversionApi ) {
395
380
  // Do not convert if range is in graveyard.
396
381
  if ( markerRange.root.rootName == '$graveyard' ) {
397
382
  return;
398
383
  }
399
384
 
400
- this.conversionApi.writer = writer;
401
-
402
385
  // In markers' case, event name == consumable name.
403
386
  const eventName = 'addMarker:' + markerName;
404
387
 
405
388
  //
406
389
  // First, fire an event for the whole marker.
407
390
  //
408
- const consumable = new Consumable();
409
- consumable.add( markerRange, eventName );
410
-
411
- this.conversionApi.consumable = consumable;
391
+ conversionApi.consumable.add( markerRange, eventName );
412
392
 
413
- this.fire( eventName, { markerName, markerRange }, this.conversionApi );
393
+ this.fire( eventName, { markerName, markerRange }, conversionApi );
414
394
 
415
395
  //
416
396
  // Do not fire events for each item inside the range if the range got consumed.
397
+ // Also consume the whole marker consumable if it wasn't consumed.
417
398
  //
418
- if ( !consumable.test( markerRange, eventName ) ) {
419
- this._clearConversionApi();
420
-
399
+ if ( !conversionApi.consumable.consume( markerRange, eventName ) ) {
421
400
  return;
422
401
  }
423
402
 
424
403
  //
425
404
  // Then, fire an event for each item inside the marker range.
426
405
  //
427
- this.conversionApi.consumable = this._createConsumableForRange( markerRange, eventName );
406
+ this._addConsumablesForRange( conversionApi.consumable, markerRange, eventName );
428
407
 
429
408
  for ( const item of markerRange.getItems() ) {
430
409
  // Do not fire event for already consumed items.
431
- if ( !this.conversionApi.consumable.test( item, eventName ) ) {
410
+ if ( !conversionApi.consumable.test( item, eventName ) ) {
432
411
  continue;
433
412
  }
434
413
 
435
414
  const data = { item, range: Range._createOn( item ), markerName, markerRange };
436
415
 
437
- this.fire( eventName, data, this.conversionApi );
416
+ this.fire( eventName, data, conversionApi );
438
417
  }
439
-
440
- this._clearConversionApi();
441
418
  }
442
419
 
443
420
  /**
444
421
  * Fires the conversion of the marker removal. Fires the {@link #event:removeMarker `removeMarker`} event with the provided data.
445
422
  *
423
+ * @protected
446
424
  * @fires removeMarker
447
425
  * @param {String} markerName Marker name.
448
426
  * @param {module:engine/model/range~Range} markerRange The marker range.
449
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify the view document.
427
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
450
428
  */
451
- convertMarkerRemove( markerName, markerRange, writer ) {
429
+ _convertMarkerRemove( markerName, markerRange, conversionApi ) {
452
430
  // Do not convert if range is in graveyard.
453
431
  if ( markerRange.root.rootName == '$graveyard' ) {
454
432
  return;
455
433
  }
456
434
 
457
- this.conversionApi.writer = writer;
458
-
459
- this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi );
460
-
461
- this._clearConversionApi();
435
+ this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, conversionApi );
462
436
  }
463
437
 
464
438
  /**
465
- * Maps the model element "insert" reconversion for given event names. The event names must be fully specified:
466
- *
467
- * * For "attribute" change event, it should include the main element name, i.e: `'attribute:attributeName:elementName'`.
468
- * * For child node change events, these should use the child event name as well, i.e:
469
- * * For adding a node: `'insert:childElementName'`.
470
- * * For removing a node: `'remove:childElementName'`.
439
+ * Fires the reduction of changes buffered in the {@link module:engine/model/differ~Differ `Differ`}.
471
440
  *
472
- * **Note**: This method should not be used directly. The reconversion is defined by the `triggerBy()` configuration of the
473
- * `elementToElement()` conversion helper.
441
+ * Features can replace selected {@link module:engine/model/differ~DiffItem `DiffItem`}s with `reinsert` entries to trigger
442
+ * reconversion. The {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
443
+ * `DowncastHelpers.elementToStructure()`} is using this event to trigger reconversion.
474
444
  *
475
- * @protected
476
- * @param {String} modelName The name of the main model element for which the events will trigger the reconversion.
477
- * @param {String} eventName The name of an event that would trigger conversion for a given model element.
445
+ * @private
446
+ * @fires reduceChanges
447
+ * @param {Iterable.<module:engine/model/differ~DiffItem>} changes
448
+ * @returns {Iterable.<module:engine/model/differ~DiffItem>}
478
449
  */
479
- _mapReconversionTriggerEvent( modelName, eventName ) {
480
- this._reconversionEventsMapping.set( eventName, modelName );
450
+ _reduceChanges( changes ) {
451
+ const data = { changes };
452
+
453
+ this.fire( 'reduceChanges', data );
454
+
455
+ return data.changes;
481
456
  }
482
457
 
483
458
  /**
484
- * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from a given range,
459
+ * Populates provided {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from a given range,
485
460
  * assuming that the range has just been inserted to the model.
486
461
  *
487
462
  * @private
488
- * @param {module:engine/model/range~Range} range The inserted range.
463
+ * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable The consumable.
464
+ * @param {Iterable.<module:engine/model/treewalker~TreeWalkerValue>} walkerValues The walker values for the inserted range.
489
465
  * @returns {module:engine/conversion/modelconsumable~ModelConsumable} The values to consume.
490
466
  */
491
- _createInsertConsumable( range ) {
492
- const consumable = new Consumable();
493
-
494
- for ( const value of range ) {
467
+ _addConsumablesForInsert( consumable, walkerValues ) {
468
+ for ( const value of walkerValues ) {
495
469
  const item = value.item;
496
470
 
497
- consumable.add( item, 'insert' );
471
+ // Add consumable if it wasn't there yet.
472
+ if ( consumable.test( item, 'insert' ) === null ) {
473
+ consumable.add( item, 'insert' );
498
474
 
499
- for ( const key of item.getAttributeKeys() ) {
500
- consumable.add( item, 'attribute:' + key );
475
+ for ( const key of item.getAttributeKeys() ) {
476
+ consumable.add( item, 'attribute:' + key );
477
+ }
501
478
  }
502
479
  }
503
480
 
@@ -505,16 +482,15 @@ export default class DowncastDispatcher {
505
482
  }
506
483
 
507
484
  /**
508
- * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume for a given range.
485
+ * Populates provided {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume for a given range.
509
486
  *
510
487
  * @private
488
+ * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable The consumable.
511
489
  * @param {module:engine/model/range~Range} range The affected range.
512
490
  * @param {String} type Consumable type.
513
491
  * @returns {module:engine/conversion/modelconsumable~ModelConsumable} The values to consume.
514
492
  */
515
- _createConsumableForRange( range, type ) {
516
- const consumable = new Consumable();
517
-
493
+ _addConsumablesForRange( consumable, range, type ) {
518
494
  for ( const item of range.getItems() ) {
519
495
  consumable.add( item, type );
520
496
  }
@@ -523,16 +499,15 @@ export default class DowncastDispatcher {
523
499
  }
524
500
 
525
501
  /**
526
- * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values.
502
+ * Populates provided {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values.
527
503
  *
528
504
  * @private
505
+ * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable The consumable.
529
506
  * @param {module:engine/model/selection~Selection} selection The selection to create the consumable from.
530
507
  * @param {Iterable.<module:engine/model/markercollection~Marker>} markers Markers that contain the selection.
531
508
  * @returns {module:engine/conversion/modelconsumable~ModelConsumable} The values to consume.
532
509
  */
533
- _createSelectionConsumable( selection, markers ) {
534
- const consumable = new Consumable();
535
-
510
+ _addConsumablesForSelection( consumable, selection, markers ) {
536
511
  consumable.add( selection, 'selection' );
537
512
 
538
513
  for ( const marker of markers ) {
@@ -547,152 +522,97 @@ export default class DowncastDispatcher {
547
522
  }
548
523
 
549
524
  /**
550
- * Tests passed `consumable` to check whether given event can be fired and if so, fires it.
525
+ * Tests whether given event wasn't already fired and if so, fires it.
551
526
  *
552
527
  * @private
553
528
  * @fires insert
554
529
  * @fires attribute
555
530
  * @param {String} type Event type.
556
531
  * @param {Object} data Event data.
532
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
557
533
  */
558
- _testAndFire( type, data ) {
559
- if ( !this.conversionApi.consumable.test( data.item, type ) ) {
560
- // Do not fire event if the item was consumed.
534
+ _testAndFire( type, data, conversionApi ) {
535
+ const eventName = getEventName( type, data );
536
+ const itemKey = data.item.is( '$textProxy' ) ? conversionApi.consumable._getSymbolForTextProxy( data.item ) : data.item;
537
+
538
+ const eventsFiredForConversion = this._firedEventsMap.get( conversionApi );
539
+ const eventsFiredForItem = eventsFiredForConversion.get( itemKey );
540
+
541
+ if ( !eventsFiredForItem ) {
542
+ eventsFiredForConversion.set( itemKey, new Set( [ eventName ] ) );
543
+ } else if ( !eventsFiredForItem.has( eventName ) ) {
544
+ eventsFiredForItem.add( eventName );
545
+ } else {
561
546
  return;
562
547
  }
563
548
 
564
- this.fire( getEventName( type, data ), data, this.conversionApi );
565
- }
566
-
567
- /**
568
- * Clears the conversion API object.
569
- *
570
- * @private
571
- */
572
- _clearConversionApi() {
573
- delete this.conversionApi.writer;
574
- delete this.conversionApi.consumable;
549
+ this.fire( eventName, data, conversionApi );
575
550
  }
576
551
 
577
552
  /**
578
- * Internal method for converting element insertion. It will fire events for the inserted element and events for its attributes.
553
+ * Fires not already fired events for setting attributes on just inserted item.
579
554
  *
580
555
  * @private
581
- * @fires insert
582
- * @fires attribute
583
- * @param {Object} data Event data.
556
+ * @param {module:engine/model/item~Item} item The model item to convert attributes for.
557
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
584
558
  */
585
- _convertInsertWithAttributes( data ) {
586
- this._testAndFire( 'insert', data );
559
+ _testAndFireAddAttributes( item, conversionApi ) {
560
+ const data = {
561
+ item,
562
+ range: Range._createOn( item )
563
+ };
587
564
 
588
- // Fire a separate addAttribute event for each attribute that was set on inserted items.
589
- // This is important because most attributes converters will listen only to add/change/removeAttribute events.
590
- // If we would not add this part, attributes on inserted nodes would not be converted.
591
565
  for ( const key of data.item.getAttributeKeys() ) {
592
566
  data.attributeKey = key;
593
567
  data.attributeOldValue = null;
594
568
  data.attributeNewValue = data.item.getAttribute( key );
595
569
 
596
- this._testAndFire( `attribute:${ key }`, data );
570
+ this._testAndFire( `attribute:${ key }`, data, conversionApi );
597
571
  }
598
572
  }
599
573
 
600
574
  /**
601
- * Returns differ changes together with added "reconvert" type changes for {@link #reconvertElement}. These are defined by
602
- * a the `triggerBy()` configuration for the
603
- * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} conversion helper.
604
- *
605
- * This method will remove every mapped insert or remove change with a single "reconvert" change.
606
- *
607
- * For instance: Having a `triggerBy()` configuration defined for the `<complex>` element that issues this element reconversion on
608
- * `foo` and `bar` attributes change, and a set of changes for this element:
609
- *
610
- * const differChanges = [
611
- * { type: 'attribute', attributeKey: 'foo', ... },
612
- * { type: 'attribute', attributeKey: 'bar', ... },
613
- * { type: 'attribute', attributeKey: 'baz', ... }
614
- * ];
615
- *
616
- * This method will return:
575
+ * Builds an instance of the {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi} from a template and a given
576
+ * {@link module:engine/view/downcastwriter~DowncastWriter `DowncastWriter`} and options object.
617
577
  *
618
- * const updatedChanges = [
619
- * { type: 'reconvert', element: complexElementInstance },
620
- * { type: 'attribute', attributeKey: 'baz', ... }
621
- * ];
622
- *
623
- * In the example above, the `'baz'` attribute change will fire an {@link #event:attribute attribute event}
624
- *
625
- * @param {module:engine/model/differ~Differ} differ The differ object with buffered changes.
626
- * @returns {Array.<Object>} Updated set of changes.
627
578
  * @private
579
+ * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify the view document.
580
+ * @param {Set.<module:engine/model/element~Element>} [refreshedItems] A set of model elements that should not reuse their
581
+ * previous view representations.
582
+ * @param {Object} [options] Optional options passed to `convertionApi.options`.
583
+ * @return {module:engine/conversion/downcastdispatcher~DowncastConversionApi} The conversion API object.
628
584
  */
629
- _mapChangesWithAutomaticReconversion( differ ) {
630
- const itemsToReconvert = new Set();
631
- const updated = [];
632
-
633
- for ( const entry of differ.getChanges() ) {
634
- const position = entry.position || entry.range.start;
635
- // Cached parent - just in case. See https://github.com/ckeditor/ckeditor5/issues/6579.
636
- const positionParent = position.parent;
637
- const textNode = getTextNodeAtPosition( position, positionParent );
638
-
639
- // Reconversion is done only on elements so skip text changes.
640
- if ( textNode ) {
641
- updated.push( entry );
642
-
643
- continue;
644
- }
645
-
646
- const element = entry.type === 'attribute' ? getNodeAfterPosition( position, positionParent, null ) : positionParent;
647
-
648
- // Case of text node set directly in root. For now used only in tests but can be possible when enabled in paragraph-like roots.
649
- // See: https://github.com/ckeditor/ckeditor5/issues/762.
650
- if ( element.is( '$text' ) ) {
651
- updated.push( entry );
652
-
653
- continue;
654
- }
655
-
656
- let eventName;
657
-
658
- if ( entry.type === 'attribute' ) {
659
- eventName = `attribute:${ entry.attributeKey }:${ element.name }`;
660
- } else {
661
- eventName = `${ entry.type }:${ entry.name }`;
662
- }
663
-
664
- if ( this._isReconvertTriggerEvent( eventName, element.name ) ) {
665
- if ( itemsToReconvert.has( element ) ) {
666
- // Element is already reconverted, so skip this change.
667
- continue;
668
- }
669
-
670
- itemsToReconvert.add( element );
671
-
672
- // Add special "reconvert" change.
673
- updated.push( { type: 'reconvert', element } );
674
- } else {
675
- updated.push( entry );
676
- }
677
- }
678
-
679
- return updated;
585
+ _createConversionApi( writer, refreshedItems = new Set(), options = {} ) {
586
+ const conversionApi = {
587
+ ...this._conversionApi,
588
+ consumable: new Consumable(),
589
+ writer,
590
+ options,
591
+ convertItem: item => this._convertInsert( Range._createOn( item ), conversionApi ),
592
+ convertChildren: element => this._convertInsert( Range._createIn( element ), conversionApi, { doNotAddConsumables: true } ),
593
+ convertAttributes: item => this._testAndFireAddAttributes( item, conversionApi ),
594
+ canReuseView: viewElement => !refreshedItems.has( conversionApi.mapper.toModelElement( viewElement ) )
595
+ };
596
+
597
+ this._firedEventsMap.set( conversionApi, new Map() );
598
+
599
+ return conversionApi;
680
600
  }
681
601
 
682
602
  /**
683
- * Checks if the resulting change should trigger element reconversion.
684
- *
685
- * These are defined by a `triggerBy()` configuration for the
686
- * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} conversion helper.
687
- *
688
- * @private
689
- * @param {String} eventName The event name to check.
690
- * @param {String} elementName The element name to check.
691
- * @returns {Boolean}
603
+ * Fired to enable reducing (transforming) changes buffered in the {@link module:engine/model/differ~Differ `Differ`} before
604
+ * {@link #convertChanges `convertChanges()`} will fire any conversion events.
605
+ *
606
+ * For instance, a feature can replace selected {@link module:engine/model/differ~DiffItem `DiffItem`}s with a `reinsert` entry
607
+ * to trigger reconversion of an element when e.g. its attribute has changes.
608
+ * The {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
609
+ * `DowncastHelpers.elementToStructure()`} helper is using this event to trigger reconversion of an element when the element,
610
+ * its attributes or direct children changed.
611
+ *
612
+ * @param {Object} data
613
+ * @param {Iterable.<module:engine/model/differ~DiffItem>} data.changes A buffered changes to get reduced.
614
+ * @event reduceChanges
692
615
  */
693
- _isReconvertTriggerEvent( eventName, elementName ) {
694
- return this._reconversionEventsMapping.get( eventName ) === elementName;
695
- }
696
616
 
697
617
  /**
698
618
  * Fired for inserted nodes.
@@ -701,24 +621,24 @@ export default class DowncastDispatcher {
701
621
  * `insert:name`. `name` is either `'$text'`, when {@link module:engine/model/text~Text a text node} has been inserted,
702
622
  * or {@link module:engine/model/element~Element#name name} of inserted element.
703
623
  *
704
- * This way listeners can either listen to a general `insert` event or specific event (for example `insert:paragraph`).
624
+ * This way, the listeners can either listen to a general `insert` event or specific event (for example `insert:paragraph`).
705
625
  *
706
626
  * @event insert
707
627
  * @param {Object} data Additional information about the change.
708
- * @param {module:engine/model/item~Item} data.item Inserted item.
628
+ * @param {module:engine/model/item~Item} data.item The inserted item.
709
629
  * @param {module:engine/model/range~Range} data.range Range spanning over inserted item.
710
630
  * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
711
- * to be used by callback, passed in `DowncastDispatcher` constructor.
631
+ * to be used by callback, passed in the `DowncastDispatcher` constructor.
712
632
  */
713
633
 
714
634
  /**
715
635
  * Fired for removed nodes.
716
636
  *
717
637
  * `remove` is a namespace for a class of events. Names of actually called events follow this pattern:
718
- * `remove:name`. `name` is either `'$text'`, when {@link module:engine/model/text~Text a text node} has been removed,
638
+ * `remove:name`. `name` is either `'$text'`, when a {@link module:engine/model/text~Text a text node} has been removed,
719
639
  * or the {@link module:engine/model/element~Element#name name} of removed element.
720
640
  *
721
- * This way listeners can either listen to a general `remove` event or specific event (for example `remove:paragraph`).
641
+ * This way, listeners can either listen to a general `remove` event or specific event (for example `remove:paragraph`).
722
642
  *
723
643
  * @event remove
724
644
  * @param {Object} data Additional information about the change.
@@ -733,7 +653,7 @@ export default class DowncastDispatcher {
733
653
  *
734
654
  * * when an attribute has been added, changed, or removed from a node,
735
655
  * * when a node with an attribute is inserted,
736
- * * when collapsed model selection attribute is converted.
656
+ * * when a collapsed model selection attribute is converted.
737
657
  *
738
658
  * `attribute` is a namespace for a class of events. Names of actually called events follow this pattern:
739
659
  * `attribute:attributeKey:name`. `attributeKey` is the key of added/changed/removed attribute.
@@ -857,17 +777,6 @@ function walkerValueToEventData( value ) {
857
777
  };
858
778
  }
859
779
 
860
- function elementOrTextProxyToView( item, mapper ) {
861
- if ( item.is( 'textProxy' ) ) {
862
- const mappedPosition = mapper.toViewPosition( Position._createBefore( item ) );
863
- const positionParent = mappedPosition.parent;
864
-
865
- return positionParent.is( '$text' ) ? positionParent : null;
866
- }
867
-
868
- return mapper.toViewElement( item );
869
- }
870
-
871
780
  /**
872
781
  * Conversion interface that is registered for given {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}
873
782
  * and is passed as one of parameters when {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher dispatcher}
@@ -907,6 +816,28 @@ function elementOrTextProxyToView( item, mapper ) {
907
816
  * @member {module:engine/view/downcastwriter~DowncastWriter} #writer
908
817
  */
909
818
 
819
+ /**
820
+ * Triggers conversion of a specified item.
821
+ * This conversion is triggered within (as a separate process of) the parent conversion.
822
+ *
823
+ * @method #convertItem
824
+ * @param {module:engine/model/item~Item} item The model item to trigger nested insert conversion on.
825
+ */
826
+
827
+ /**
828
+ * Triggers conversion of children of a specified element.
829
+ *
830
+ * @method #convertChildren
831
+ * @param {module:engine/model/element~Element} element The model element to trigger children insert conversion on.
832
+ */
833
+
834
+ /**
835
+ * Triggers conversion of attributes of a specified item.
836
+ *
837
+ * @method #convertAttributes
838
+ * @param {module:engine/model/item~Item} item The model item to trigger attribute conversion on.
839
+ */
840
+
910
841
  /**
911
842
  * An object with an additional configuration which can be used during the conversion process. Available only for data downcast conversion.
912
843
  *