@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.
@@ -15,11 +15,12 @@ import ViewRange from '../view/range';
15
15
  import ViewText from '../view/text';
16
16
 
17
17
  import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
18
+ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
18
19
  import mix from '@ckeditor/ckeditor5-utils/src/mix';
19
20
 
20
21
  /**
21
- * Maps elements, positions and markers between {@link module:engine/view/document~Document the view} and
22
- * {@link module:engine/model/model the model}.
22
+ * Maps elements, positions and markers between the {@link module:engine/view/document~Document view} and
23
+ * the {@link module:engine/model/model model}.
23
24
  *
24
25
  * The instance of the Mapper used for the editing pipeline is available in
25
26
  * {@link module:engine/controller/editingcontroller~EditingController#mapper `editor.editing.mapper`}.
@@ -27,12 +28,12 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
27
28
  * Mapper uses bound elements to find corresponding elements and positions, so, to get proper results,
28
29
  * all model elements should be {@link module:engine/conversion/mapper~Mapper#bindElements bound}.
29
30
  *
30
- * To map complex model to/from view relations, you may provide custom callbacks for
31
+ * To map the complex model to/from view relations, you may provide custom callbacks for the
31
32
  * {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition modelToViewPosition event} and
32
33
  * {@link module:engine/conversion/mapper~Mapper#event:viewToModelPosition viewToModelPosition event} that are fired whenever
33
34
  * a position mapping request occurs.
34
- * Those events are fired by {@link module:engine/conversion/mapper~Mapper#toViewPosition toViewPosition}
35
- * and {@link module:engine/conversion/mapper~Mapper#toModelPosition toModelPosition} methods. `Mapper` adds it's own default callbacks
35
+ * Those events are fired by the {@link module:engine/conversion/mapper~Mapper#toViewPosition toViewPosition}
36
+ * and {@link module:engine/conversion/mapper~Mapper#toModelPosition toModelPosition} methods. `Mapper` adds its own default callbacks
36
37
  * with `'lowest'` priority. To override default `Mapper` mapping, add custom callback with higher priority and
37
38
  * stop the event.
38
39
  * @mixes module:utils/emittermixin~EmitterMixin
@@ -89,7 +90,15 @@ export default class Mapper {
89
90
  this._elementToMarkerNames = new Map();
90
91
 
91
92
  /**
92
- * Stores marker names of markers which has changed due to unbinding a view element (so it is assumed that the view element
93
+ * The map of removed view elements with their current root (used for deferred unbinding).
94
+ *
95
+ * @private
96
+ * @member {Map.<module:engine/view/element~Element,module:engine/view/documentfragment~DocumentFragment>}
97
+ */
98
+ this._deferredBindingRemovals = new Map();
99
+
100
+ /**
101
+ * Stores marker names of markers which have changed due to unbinding a view element (so it is assumed that the view element
93
102
  * has been removed, moved or renamed).
94
103
  *
95
104
  * @private
@@ -105,6 +114,18 @@ export default class Mapper {
105
114
 
106
115
  const viewContainer = this._modelToViewMapping.get( data.modelPosition.parent );
107
116
 
117
+ if ( !viewContainer ) {
118
+ /**
119
+ * A model position could not be mapped to the view because the parent of the model position
120
+ * does not have a mapped view element (might have not been converted yet or it has no converter).
121
+ *
122
+ * Make sure that the model element is correctly converted to the view.
123
+ *
124
+ * @error mapping-view-position-parent-not-found
125
+ */
126
+ throw new CKEditorError( 'mapping-view-position-parent-not-found', this, { modelPosition: data.modelPosition } );
127
+ }
128
+
108
129
  data.viewPosition = this.findPositionIn( viewContainer, data.modelPosition.offset );
109
130
  }, { priority: 'low' } );
110
131
 
@@ -137,37 +158,44 @@ export default class Mapper {
137
158
  }
138
159
 
139
160
  /**
140
- * Unbinds given {@link module:engine/view/element~Element view element} from the map.
161
+ * Unbinds the given {@link module:engine/view/element~Element view element} from the map.
141
162
  *
142
163
  * **Note:** view-to-model binding will be removed, if it existed. However, corresponding model-to-view binding
143
- * will be removed only if model element is still bound to passed `viewElement`.
164
+ * will be removed only if model element is still bound to the passed `viewElement`.
144
165
  *
145
- * This behavior lets for re-binding model element to another view element without fear of losing the new binding
166
+ * This behavior allows for re-binding model element to another view element without fear of losing the new binding
146
167
  * when the previously bound view element is unbound.
147
168
  *
148
169
  * @param {module:engine/view/element~Element} viewElement View element to unbind.
170
+ * @param {Object} [options={}] The options object.
171
+ * @param {Boolean} [options.defer=false] Controls whether the binding should be removed immediately or deferred until a
172
+ * {@link #flushDeferredBindings `flushDeferredBindings()`} call.
149
173
  */
150
- unbindViewElement( viewElement ) {
174
+ unbindViewElement( viewElement, options = {} ) {
151
175
  const modelElement = this.toModelElement( viewElement );
152
176
 
153
- this._viewToModelMapping.delete( viewElement );
154
-
155
177
  if ( this._elementToMarkerNames.has( viewElement ) ) {
156
178
  for ( const markerName of this._elementToMarkerNames.get( viewElement ) ) {
157
179
  this._unboundMarkerNames.add( markerName );
158
180
  }
159
181
  }
160
182
 
161
- if ( this._modelToViewMapping.get( modelElement ) == viewElement ) {
162
- this._modelToViewMapping.delete( modelElement );
183
+ if ( options.defer ) {
184
+ this._deferredBindingRemovals.set( viewElement, viewElement.root );
185
+ } else {
186
+ this._viewToModelMapping.delete( viewElement );
187
+
188
+ if ( this._modelToViewMapping.get( modelElement ) == viewElement ) {
189
+ this._modelToViewMapping.delete( modelElement );
190
+ }
163
191
  }
164
192
  }
165
193
 
166
194
  /**
167
- * Unbinds given {@link module:engine/model/element~Element model element} from the map.
195
+ * Unbinds the given {@link module:engine/model/element~Element model element} from the map.
168
196
  *
169
- * **Note:** model-to-view binding will be removed, if it existed. However, corresponding view-to-model binding
170
- * will be removed only if view element is still bound to passed `modelElement`.
197
+ * **Note:** the model-to-view binding will be removed, if it existed. However, the corresponding view-to-model binding
198
+ * will be removed only if the view element is still bound to the passed `modelElement`.
171
199
  *
172
200
  * This behavior lets for re-binding view element to another model element without fear of losing the new binding
173
201
  * when the previously bound model element is unbound.
@@ -185,8 +213,8 @@ export default class Mapper {
185
213
  }
186
214
 
187
215
  /**
188
- * Binds given marker name with given {@link module:engine/view/element~Element view element}. The element
189
- * will be added to the current set of elements bound with given marker name.
216
+ * Binds the given marker name with the given {@link module:engine/view/element~Element view element}. The element
217
+ * will be added to the current set of elements bound with the given marker name.
190
218
  *
191
219
  * @param {module:engine/view/element~Element} element Element to bind.
192
220
  * @param {String} name Marker name.
@@ -231,7 +259,7 @@ export default class Mapper {
231
259
  }
232
260
 
233
261
  /**
234
- * Returns all marker names of markers which has changed due to unbinding a view element (so it is assumed that the view element
262
+ * Returns all marker names of markers which have changed due to unbinding a view element (so it is assumed that the view element
235
263
  * has been removed, moved or renamed) since the last flush. After returning, the marker names list is cleared.
236
264
  *
237
265
  * @returns {Array.<String>}
@@ -244,6 +272,22 @@ export default class Mapper {
244
272
  return markerNames;
245
273
  }
246
274
 
275
+ /**
276
+ * Unbinds all deferred binding removals of view elements that in the meantime were not re-attached to some root or document fragment.
277
+ *
278
+ * See: {@link #unbindViewElement `unbindViewElement()`}.
279
+ */
280
+ flushDeferredBindings() {
281
+ for ( const [ viewElement, root ] of this._deferredBindingRemovals ) {
282
+ // Unbind it only if it wasn't re-attached to some root or document fragment.
283
+ if ( viewElement.root == root ) {
284
+ this.unbindViewElement( viewElement );
285
+ }
286
+ }
287
+
288
+ this._deferredBindingRemovals = new Map();
289
+ }
290
+
247
291
  /**
248
292
  * Removes all model to view and view to model bindings.
249
293
  */
@@ -253,6 +297,7 @@ export default class Mapper {
253
297
  this._markerNameToElements = new Map();
254
298
  this._elementToMarkerNames = new Map();
255
299
  this._unboundMarkerNames = new Set();
300
+ this._deferredBindingRemovals = new Map();
256
301
  }
257
302
 
258
303
  /**
@@ -341,8 +386,8 @@ export default class Mapper {
341
386
  * Gets all view elements bound to the given marker name.
342
387
  *
343
388
  * @param {String} name Marker name.
344
- * @returns {Set.<module:engine/view/element~Element>|null} View elements bound with given marker name or `null`
345
- * if no elements are bound to given marker name.
389
+ * @returns {Set.<module:engine/view/element~Element>|null} View elements bound with the given marker name or `null`
390
+ * if no elements are bound to the given marker name.
346
391
  */
347
392
  markerNameToElements( name ) {
348
393
  const boundElements = this._markerNameToElements.get( name );
@@ -367,10 +412,10 @@ export default class Mapper {
367
412
  }
368
413
 
369
414
  /**
370
- * Registers a callback that evaluates the length in the model of a view element with given name.
415
+ * Registers a callback that evaluates the length in the model of a view element with the given name.
371
416
  *
372
417
  * The callback is fired with one argument, which is a view element instance. The callback is expected to return
373
- * a number representing the length of view element in model.
418
+ * a number representing the length of the view element in the model.
374
419
  *
375
420
  * // List item in view may contain nested list, which have other list items. In model though,
376
421
  * // the lists are represented by flat structure. Because of those differences, length of list view element
@@ -400,10 +445,10 @@ export default class Mapper {
400
445
  }
401
446
 
402
447
  /**
403
- * For given `viewPosition`, finds and returns the closest ancestor of this position that has a mapping to
448
+ * For the given `viewPosition`, finds and returns the closest ancestor of this position that has a mapping to
404
449
  * the model.
405
450
  *
406
- * @param {module:engine/view/position~Position} viewPosition Position for which mapped ancestor should be found.
451
+ * @param {module:engine/view/position~Position} viewPosition Position for which a mapped ancestor should be found.
407
452
  * @returns {module:engine/view/element~Element}
408
453
  */
409
454
  findMappedViewAncestor( viewPosition ) {
@@ -464,17 +509,17 @@ export default class Mapper {
464
509
  * Gets the length of the view element in the model.
465
510
  *
466
511
  * The length is calculated as follows:
467
- * * if {@link #registerViewToModelLength length mapping callback} is provided for given `viewNode` it is used to
468
- * evaluate model length (`viewNode` is used as first and only parameter passed to the callback),
469
- * * length of a {@link module:engine/view/text~Text text node} is equal to the length of it's
512
+ * * if a {@link #registerViewToModelLength length mapping callback} is provided for the given `viewNode`, it is used to
513
+ * evaluate the model length (`viewNode` is used as first and only parameter passed to the callback),
514
+ * * length of a {@link module:engine/view/text~Text text node} is equal to the length of its
470
515
  * {@link module:engine/view/text~Text#data data},
471
516
  * * length of a {@link module:engine/view/uielement~UIElement ui element} is equal to 0,
472
517
  * * length of a mapped {@link module:engine/view/element~Element element} is equal to 1,
473
- * * length of a not-mapped {@link module:engine/view/element~Element element} is equal to the length of it's children.
518
+ * * length of a non-mapped {@link module:engine/view/element~Element element} is equal to the length of its children.
474
519
  *
475
520
  * Examples:
476
521
  *
477
- * foo -> 3 // Text length is equal to it's data length.
522
+ * foo -> 3 // Text length is equal to its data length.
478
523
  * <p>foo</p> -> 1 // Length of an element which is mapped is by default equal to 1.
479
524
  * <b>foo</b> -> 3 // Length of an element which is not mapped is a length of its children.
480
525
  * <div><p>x</p><p>y</p></div> -> 2 // Assuming that <div> is not mapped and <p> are mapped.
@@ -505,7 +550,7 @@ export default class Mapper {
505
550
  }
506
551
 
507
552
  /**
508
- * Finds the position in the view node (or its children) with the expected model offset.
553
+ * Finds the position in the view node (or in its children) with the expected model offset.
509
554
  *
510
555
  * Example:
511
556
  *
@@ -537,7 +582,7 @@ export default class Mapper {
537
582
  let modelOffset = 0;
538
583
  let viewOffset = 0;
539
584
 
540
- // In the text node it is simple: offset in the model equals offset in the text.
585
+ // In the text node it is simple: the offset in the model equals the offset in the text.
541
586
  if ( viewParent.is( '$text' ) ) {
542
587
  return new ViewPosition( viewParent, expectedOffset );
543
588
  }
@@ -565,20 +610,20 @@ export default class Mapper {
565
610
  }
566
611
 
567
612
  /**
568
- * Because we prefer positions in text nodes over positions next to text node moves view position to the text node
569
- * if it was next to it.
613
+ * Because we prefer positions in the text nodes over positions next to text nodes, if the view position was next to a text node,
614
+ * it moves it into the text node instead.
570
615
  *
571
616
  * <p>[]<b>foo</b></p> -> <p>[]<b>foo</b></p> // do not touch if position is not directly next to text
572
617
  * <p>foo[]<b>foo</b></p> -> <p>foo{}<b>foo</b></p> // move to text node
573
618
  * <p><b>[]foo</b></p> -> <p><b>{}foo</b></p> // move to text node
574
619
  *
575
620
  * @private
576
- * @param {module:engine/view/position~Position} viewPosition Position potentially next to text node.
577
- * @returns {module:engine/view/position~Position} Position in text node if possible.
621
+ * @param {module:engine/view/position~Position} viewPosition Position potentially next to the text node.
622
+ * @returns {module:engine/view/position~Position} Position in the text node if possible.
578
623
  */
579
624
  _moveViewPositionToTextNode( viewPosition ) {
580
- // If the position is just after text node, put it at the end of that text node.
581
- // If the position is just before text node, put it at the beginning of that text node.
625
+ // If the position is just after a text node, put it at the end of that text node.
626
+ // If the position is just before a text node, put it at the beginning of that text node.
582
627
  const nodeBefore = viewPosition.nodeBefore;
583
628
  const nodeAfter = viewPosition.nodeAfter;
584
629
 
@@ -595,8 +640,8 @@ export default class Mapper {
595
640
  /**
596
641
  * Fired for each model-to-view position mapping request. The purpose of this event is to enable custom model-to-view position
597
642
  * mapping. Callbacks added to this event take {@link module:engine/model/position~Position model position} and are expected to
598
- * calculate {@link module:engine/view/position~Position view position}. Calculated view position should be added as `viewPosition`
599
- * value in `data` object that is passed as one of parameters to the event callback.
643
+ * calculate the {@link module:engine/view/position~Position view position}. The calculated view position should be added as
644
+ * a `viewPosition` value in the `data` object that is passed as one of parameters to the event callback.
600
645
  *
601
646
  * // Assume that "captionedImage" model element is converted to <img> and following <span> elements in view,
602
647
  * // and the model element is bound to <img> element. Force mapping model positions inside "captionedImage" to that
@@ -615,14 +660,14 @@ export default class Mapper {
615
660
  * }
616
661
  * } );
617
662
  *
618
- * **Note:** keep in mind that sometimes a "phantom" model position is being converted. "Phantom" model position is
619
- * a position that points to a non-existing place in model. Such position might still be valid for conversion, though
620
- * (it would point to a correct place in view when converted). One example of such situation is when a range is
621
- * removed from model, there may be a need to map the range's end (which is no longer valid model position). To
622
- * handle such situation, check `data.isPhantom` flag:
663
+ * **Note:** keep in mind that sometimes a "phantom" model position is being converted. A "phantom" model position is
664
+ * a position that points to a nonexistent place in model. Such a position might still be valid for conversion, though
665
+ * (it would point to a correct place in the view when converted). One example of such a situation is when a range is
666
+ * removed from the model, there may be a need to map the range's end (which is no longer a valid model position). To
667
+ * handle such situations, check the `data.isPhantom` flag:
623
668
  *
624
- * // Assume that there is "customElement" model element and whenever position is before it, we want to move it
625
- * // to the inside of the view element bound to "customElement".
669
+ * // Assume that there is a "customElement" model element and whenever the position is before it,
670
+ * // we want to move it to the inside of the view element bound to "customElement".
626
671
  * mapper.on( 'modelToViewPosition', ( evt, data ) => {
627
672
  * if ( data.isPhantom ) {
628
673
  * return;
@@ -643,19 +688,19 @@ export default class Mapper {
643
688
  * evt.stop();
644
689
  * } );
645
690
  *
646
- * **Note:** default mapping callback is provided with `low` priority setting and does not cancel the event, so it is possible to
647
- * attach a custom callback after default callback and also use `data.viewPosition` calculated by default callback
691
+ * **Note:** the default mapping callback is provided with a `low` priority setting and does not cancel the event, so it is possible to
692
+ * attach a custom callback after a default callback and also use `data.viewPosition` calculated by the default callback
648
693
  * (for example to fix it).
649
694
  *
650
- * **Note:** default mapping callback will not fire if `data.viewPosition` is already set.
695
+ * **Note:** the default mapping callback will not fire if `data.viewPosition` is already set.
651
696
  *
652
697
  * **Note:** these callbacks are called **very often**. For efficiency reasons, it is advised to use them only when position
653
- * mapping between given model and view elements is unsolvable using just elements mapping and default algorithm. Also,
654
- * the condition that checks if special case scenario happened should be as simple as possible.
698
+ * mapping between the given model and view elements is unsolvable by using just elements mapping and default algorithm.
699
+ * Also, the condition that checks if a special case scenario happened should be as simple as possible.
655
700
  *
656
701
  * @event modelToViewPosition
657
702
  * @param {Object} data Data pipeline object that can store and pass data between callbacks. The callback should add
658
- * `viewPosition` value to that object with calculated {@link module:engine/view/position~Position view position}.
703
+ * the `viewPosition` value to that object with calculated the {@link module:engine/view/position~Position view position}.
659
704
  * @param {module:engine/conversion/mapper~Mapper} data.mapper Mapper instance that fired the event.
660
705
  */
661
706
 
@@ -676,15 +721,15 @@ export default class Mapper {
676
721
  * }
677
722
  * } );
678
723
  *
679
- * **Note:** default mapping callback is provided with `low` priority setting and does not cancel the event, so it is possible to
680
- * attach a custom callback after default callback and also use `data.modelPosition` calculated by default callback
724
+ * **Note:** the default mapping callback is provided with a `low` priority setting and does not cancel the event, so it is possible to
725
+ * attach a custom callback after the default callback and also use `data.modelPosition` calculated by the default callback
681
726
  * (for example to fix it).
682
727
  *
683
- * **Note:** default mapping callback will not fire if `data.modelPosition` is already set.
728
+ * **Note:** the default mapping callback will not fire if `data.modelPosition` is already set.
684
729
  *
685
730
  * **Note:** these callbacks are called **very often**. For efficiency reasons, it is advised to use them only when position
686
- * mapping between given model and view elements is unsolvable using just elements mapping and default algorithm. Also,
687
- * the condition that checks if special case scenario happened should be as simple as possible.
731
+ * mapping between the given model and view elements is unsolvable by using just elements mapping and default algorithm.
732
+ * Also, the condition that checks if special case scenario happened should be as simple as possible.
688
733
  *
689
734
  * @event viewToModelPosition
690
735
  * @param {Object} data Data pipeline object that can store and pass data between callbacks. The callback should add
@@ -8,33 +8,34 @@
8
8
  */
9
9
 
10
10
  import TextProxy from '../model/textproxy';
11
+ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
11
12
 
12
13
  /**
13
- * Manages a list of consumable values for {@link module:engine/model/item~Item model items}.
14
+ * Manages a list of consumable values for the {@link module:engine/model/item~Item model items}.
14
15
  *
15
- * Consumables are various aspects of the model. A model item can be broken down into singular properties that might be
16
+ * Consumables are various aspects of the model. A model item can be broken down into separate, single properties that might be
16
17
  * taken into consideration when converting that item.
17
18
  *
18
- * `ModelConsumable` is used by {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} while analyzing changed
19
+ * `ModelConsumable` is used by {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} while analyzing the changed
19
20
  * parts of {@link module:engine/model/document~Document the document}. The added / changed / removed model items are broken down
20
- * into singular properties (the item itself and it's attributes). All those parts are saved in `ModelConsumable`. Then,
21
- * during conversion, when given part of model item is converted (i.e. the view element has been inserted into the view,
22
- * but without attributes), consumable value is removed from `ModelConsumable`.
21
+ * into singular properties (the item itself and its attributes). All those parts are saved in `ModelConsumable`. Then,
22
+ * during conversion, when the given part of a model item is converted (i.e. the view element has been inserted into the view,
23
+ * but without attributes), the consumable value is removed from `ModelConsumable`.
23
24
  *
24
25
  * For model items, `ModelConsumable` stores consumable values of one of following types: `insert`, `addattribute:<attributeKey>`,
25
26
  * `changeattributes:<attributeKey>`, `removeattributes:<attributeKey>`.
26
27
  *
27
- * In most cases, it is enough to let {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}
28
+ * In most cases, it is enough to let th {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}
28
29
  * gather consumable values, so there is no need to use
29
- * {@link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly.
30
+ * the {@link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly.
30
31
  * However, it is important to understand how consumable values can be
31
32
  * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}.
32
33
  * See {@link module:engine/conversion/downcasthelpers default downcast converters} for more information.
33
34
  *
34
- * Keep in mind, that one conversion event may have multiple callbacks (converters) attached to it. Each of those is
35
+ * Keep in mind that one conversion event may have multiple callbacks (converters) attached to it. Each of those is
35
36
  * able to convert one or more parts of the model. However, when one of those callbacks actually converts
36
- * something, other should not, because they would duplicate the results. Using `ModelConsumable` helps avoiding
37
- * this situation, because callbacks should only convert those values, which were not yet consumed from `ModelConsumable`.
37
+ * something, the others should not, because they would duplicate the results. Using `ModelConsumable` helps to avoid
38
+ * this situation, because callbacks should only convert these values that were not yet consumed from `ModelConsumable`.
38
39
  *
39
40
  * Consuming multiple values in a single callback:
40
41
  *
@@ -101,12 +102,12 @@ export default class ModelConsumable {
101
102
  this._consumable = new Map();
102
103
 
103
104
  /**
104
- * For each {@link module:engine/model/textproxy~TextProxy} added to `ModelConsumable`, this registry holds parent
105
- * of that `TextProxy` and start and end indices of that `TextProxy`. This allows identification of `TextProxy`
106
- * instances that points to the same part of the model but are different instances. Each distinct `TextProxy`
107
- * is given unique `Symbol` which is then registered as consumable. This process is transparent for `ModelConsumable`
108
- * API user because whenever `TextProxy` is added, tested, consumed or reverted, internal mechanisms of
109
- * `ModelConsumable` translates `TextProxy` to that unique `Symbol`.
105
+ * For each {@link module:engine/model/textproxy~TextProxy} added to `ModelConsumable`, this registry holds a parent
106
+ * of that `TextProxy` and the start and end indices of that `TextProxy`. This allows identification of the `TextProxy`
107
+ * instances that point to the same part of the model but are different instances. Each distinct `TextProxy`
108
+ * is given a unique `Symbol` which is then registered as consumable. This process is transparent for the `ModelConsumable`
109
+ * API user because whenever `TextProxy` is added, tested, consumed or reverted, the internal mechanisms of
110
+ * `ModelConsumable` translate `TextProxy` to that unique `Symbol`.
110
111
  *
111
112
  * @private
112
113
  * @member {Map} module:engine/conversion/modelconsumable~ModelConsumable#_textProxyRegistry
@@ -115,7 +116,7 @@ export default class ModelConsumable {
115
116
  }
116
117
 
117
118
  /**
118
- * Adds a consumable value to the consumables list and links it with given model item.
119
+ * Adds a consumable value to the consumables list and links it with a given model item.
119
120
  *
120
121
  * modelConsumable.add( modelElement, 'insert' ); // Add `modelElement` insertion change to consumable values.
121
122
  * modelConsumable.add( modelElement, 'addAttribute:bold' ); // Add `bold` attribute insertion on `modelElement` change.
@@ -143,7 +144,7 @@ export default class ModelConsumable {
143
144
  }
144
145
 
145
146
  /**
146
- * Removes given consumable value from given model item.
147
+ * Removes a given consumable value from a given model item.
147
148
  *
148
149
  * modelConsumable.consume( modelElement, 'insert' ); // Remove `modelElement` insertion change from consumable values.
149
150
  * modelConsumable.consume( modelElement, 'addAttribute:bold' ); // Remove `bold` attribute insertion on `modelElement` change.
@@ -174,7 +175,7 @@ export default class ModelConsumable {
174
175
  }
175
176
 
176
177
  /**
177
- * Tests whether there is a consumable value of given type connected with given model item.
178
+ * Tests whether there is a consumable value of a given type connected with a given model item.
178
179
  *
179
180
  * modelConsumable.test( modelElement, 'insert' ); // Check for `modelElement` insertion change.
180
181
  * modelConsumable.test( modelElement, 'addAttribute:bold' ); // Check for `bold` attribute insertion on `modelElement` change.
@@ -212,7 +213,7 @@ export default class ModelConsumable {
212
213
  }
213
214
 
214
215
  /**
215
- * Reverts consuming of consumable value.
216
+ * Reverts consuming of a consumable value.
216
217
  *
217
218
  * modelConsumable.revert( modelElement, 'insert' ); // Revert consuming `modelElement` insertion change.
218
219
  * modelConsumable.revert( modelElement, 'addAttribute:bold' ); // Revert consuming `bold` attribute insert from `modelElement`.
@@ -247,12 +248,54 @@ export default class ModelConsumable {
247
248
  }
248
249
 
249
250
  /**
250
- * Gets a unique symbol for passed {@link module:engine/model/textproxy~TextProxy} instance. All `TextProxy` instances that
251
+ * Verifies if all events from the specified group were consumed.
252
+ *
253
+ * @param {String} eventGroup The events group to verify.
254
+ */
255
+ verifyAllConsumed( eventGroup ) {
256
+ const items = [];
257
+
258
+ for ( const [ item, consumables ] of this._consumable ) {
259
+ for ( const [ event, canConsume ] of consumables ) {
260
+ const eventPrefix = event.split( ':' )[ 0 ];
261
+
262
+ if ( canConsume && eventGroup == eventPrefix ) {
263
+ items.push( {
264
+ event,
265
+ item: item.name || item.description
266
+ } );
267
+ }
268
+ }
269
+ }
270
+
271
+ if ( items.length ) {
272
+ /**
273
+ * Some of the {@link module:engine/model/item~Item model items} were not consumed while downcasting the model to view.
274
+ *
275
+ * This might be the effect of:
276
+ *
277
+ * * A missing converter for some model elements. Make sure that you registered downcast converters for all model elements.
278
+ * * A custom converter that does not consume converted items. Make sure that you
279
+ * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} all model elements that you converted
280
+ * from the model to the view.
281
+ * * A custom converter that called `event.stop()`. When providing a custom converter, keep in mind that you should not stop
282
+ * the event. If you stop it then the default converter at the `lowest` priority will not trigger the conversion of this node's
283
+ * attributes and child nodes.
284
+ *
285
+ * @error conversion-model-consumable-not-consumed
286
+ * @param {Array.<module:engine/model/item~Item>} items Items that were not consumed.
287
+ */
288
+ throw new CKEditorError( 'conversion-model-consumable-not-consumed', null, { items } );
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Gets a unique symbol for the passed {@link module:engine/model/textproxy~TextProxy} instance. All `TextProxy` instances that
251
294
  * have same parent, same start index and same end index will get the same symbol.
252
295
  *
253
296
  * Used internally to correctly consume `TextProxy` instances.
254
297
  *
255
- * @private
298
+ * @protected
256
299
  * @param {module:engine/model/textproxy~TextProxy} textProxy `TextProxy` instance to get a symbol for.
257
300
  * @returns {Symbol} Symbol representing all equal instances of `TextProxy`.
258
301
  */
@@ -270,25 +313,27 @@ export default class ModelConsumable {
270
313
  }
271
314
 
272
315
  if ( !symbol ) {
273
- symbol = this._addSymbolForTextProxy( textProxy.startOffset, textProxy.endOffset, textProxy.parent );
316
+ symbol = this._addSymbolForTextProxy( textProxy );
274
317
  }
275
318
 
276
319
  return symbol;
277
320
  }
278
321
 
279
322
  /**
280
- * Adds a symbol for given properties that characterizes a {@link module:engine/model/textproxy~TextProxy} instance.
323
+ * Adds a symbol for the given {@link module:engine/model/textproxy~TextProxy} instance.
281
324
  *
282
325
  * Used internally to correctly consume `TextProxy` instances.
283
326
  *
284
327
  * @private
285
- * @param {Number} startIndex Text proxy start index in it's parent.
286
- * @param {Number} endIndex Text proxy end index in it's parent.
287
- * @param {module:engine/model/element~Element} parent Text proxy parent.
288
- * @returns {Symbol} Symbol generated for given properties.
328
+ * @param {module:engine/model/textproxy~TextProxy} textProxy Text proxy instance.
329
+ * @returns {Symbol} Symbol generated for given `TextProxy`.
289
330
  */
290
- _addSymbolForTextProxy( start, end, parent ) {
291
- const symbol = Symbol( 'textProxySymbol' );
331
+ _addSymbolForTextProxy( textProxy ) {
332
+ const start = textProxy.startOffset;
333
+ const end = textProxy.endOffset;
334
+ const parent = textProxy.parent;
335
+
336
+ const symbol = Symbol( '$textProxy:' + textProxy.data );
292
337
  let startMap, endMap;
293
338
 
294
339
  startMap = this._textProxyRegistry.get( start );
@@ -311,15 +356,20 @@ export default class ModelConsumable {
311
356
  }
312
357
  }
313
358
 
314
- // Returns a normalized consumable type name from given string. A normalized consumable type name is a string that has
315
- // at most one colon, for example: `insert` or `addMarker:highlight`. If string to normalize has more "parts" (more colons),
316
- // the other parts are dropped, for example: `addattribute:bold:$text` -> `addattributes:bold`.
359
+ // Returns a normalized consumable type name from the given string. A normalized consumable type name is a string that has
360
+ // at most one colon, for example: `insert` or `addMarker:highlight`. If a string to normalize has more "parts" (more colons),
361
+ // the further parts are dropped, for example: `addattribute:bold:$text` -> `addattributes:bold`.
317
362
  //
318
363
  // @param {String} type Consumable type.
319
364
  // @returns {String} Normalized consumable type.
320
365
  function _normalizeConsumableType( type ) {
321
366
  const parts = type.split( ':' );
322
367
 
368
+ // For inserts allow passing event name, it's stored in the context of a specified element so the element name is not needed.
369
+ if ( parts[ 0 ] == 'insert' ) {
370
+ return parts[ 0 ];
371
+ }
372
+
323
373
  // Markers are identified by the whole name (otherwise we would consume the whole markers group).
324
374
  if ( parts[ 0 ] == 'addMarker' || parts[ 0 ] == 'removeMarker' ) {
325
375
  return type;
@@ -40,10 +40,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
40
40
  * The third parameter of the callback is an instance of {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi}
41
41
  * which provides additional tools for converters.
42
42
  *
43
- * You can read more about conversion in the following guides:
44
- *
45
- * * {@glink framework/guides/deep-dive/conversion/conversion-introduction Advanced conversion concepts &mdash; attributes}
46
- * * {@glink framework/guides/deep-dive/conversion/custom-element-conversion Custom element conversion}
43
+ * You can read more about conversion in the {@glink framework/guides/deep-dive/conversion/upcast Upcast conversion} guide.
47
44
  *
48
45
  * Examples of event-based converters:
49
46
  *
@@ -127,7 +124,7 @@ export default class UpcastDispatcher {
127
124
  /**
128
125
  * The list of elements that were created during splitting.
129
126
  *
130
- * After the conversion process the list is cleared.
127
+ * After the conversion process, the list is cleared.
131
128
  *
132
129
  * @private
133
130
  * @type {Map.<module:engine/model/element~Element,Array.<module:engine/model/element~Element>>}
@@ -12,7 +12,7 @@ import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
12
12
  import { isParagraphable, wrapInParagraph } from '../model/utils/autoparagraphing';
13
13
 
14
14
  /**
15
- * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
15
+ * Contains the {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
16
16
  * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}.
17
17
  *
18
18
  * @module engine/conversion/upcasthelpers
@@ -21,6 +21,8 @@ import { isParagraphable, wrapInParagraph } from '../model/utils/autoparagraphin
21
21
  /**
22
22
  * Upcast conversion helper functions.
23
23
  *
24
+ * Learn more about {@glink framework/guides/deep-dive/conversion/upcast upcast helpers}.
25
+ *
24
26
  * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
25
27
  */
26
28
  export default class UpcastHelpers extends ConversionHelpers {
@@ -114,7 +114,7 @@ export default class HtmlDataProcessor {
114
114
  * @returns {DocumentFragment}
115
115
  */
116
116
  _toDom( data ) {
117
- // Wrap data with a <body> so leading non-layout nodes (like <script>, <style>, HTML comment)
117
+ // Wrap data with a <body> tag so leading non-layout nodes (like <script>, <style>, HTML comment)
118
118
  // will be preserved in the body collection.
119
119
  // Do it only for data that is not a full HTML document.
120
120
  if ( !data.match( /<(?:html|body|head|meta)(?:\s[^>]*)?>/i ) ) {