@ckeditor/ckeditor5-engine 44.2.1 → 44.3.0-alpha.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.
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * @module engine/conversion/viewconsumable
7
7
  */
8
- import { CKEditorError, toArray } from '@ckeditor/ckeditor5-utils';
8
+ import { CKEditorError } from '@ckeditor/ckeditor5-utils';
9
9
  /**
10
10
  * Class used for handling consumption of view {@link module:engine/view/element~Element elements},
11
11
  * {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}.
@@ -41,6 +41,35 @@ export default class ViewConsumable {
41
41
  */
42
42
  this._consumables = new Map();
43
43
  }
44
+ /**
45
+ * Adds view {@link module:engine/view/element~Element element}, {@link module:engine/view/text~Text text node} or
46
+ * {@link module:engine/view/documentfragment~DocumentFragment document fragment} as ready to be consumed.
47
+ *
48
+ * ```ts
49
+ * viewConsumable.add( p, { name: true } ); // Adds element's name to consume.
50
+ * viewConsumable.add( p, { attributes: 'name' } ); // Adds element's attribute.
51
+ * viewConsumable.add( p, { classes: 'foobar' } ); // Adds element's class.
52
+ * viewConsumable.add( p, { styles: 'color' } ); // Adds element's style
53
+ * viewConsumable.add( p, { attributes: 'name', styles: 'color' } ); // Adds attribute and style.
54
+ * viewConsumable.add( p, { classes: [ 'baz', 'bar' ] } ); // Multiple consumables can be provided.
55
+ * viewConsumable.add( textNode ); // Adds text node to consume.
56
+ * viewConsumable.add( docFragment ); // Adds document fragment to consume.
57
+ * ```
58
+ *
59
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
60
+ * attribute is provided - it should be handled separately by providing actual style/class.
61
+ *
62
+ * ```ts
63
+ * viewConsumable.add( p, { attributes: 'style' } ); // This call will throw an exception.
64
+ * viewConsumable.add( p, { styles: 'color' } ); // This is properly handled style.
65
+ * ```
66
+ *
67
+ * @param consumables Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
68
+ * @param consumables.name If set to true element's name will be included.
69
+ * @param consumables.attributes Attribute name or array of attribute names.
70
+ * @param consumables.classes Class name or array of class names.
71
+ * @param consumables.styles Style name or array of style names.
72
+ */
44
73
  add(element, consumables) {
45
74
  let elementConsumables;
46
75
  // For text nodes and document fragments just mark them as consumable.
@@ -56,7 +85,7 @@ export default class ViewConsumable {
56
85
  else {
57
86
  elementConsumables = this._consumables.get(element);
58
87
  }
59
- elementConsumables.add(consumables);
88
+ elementConsumables.add(consumables ? normalizeConsumables(consumables) : element._getConsumables());
60
89
  }
61
90
  /**
62
91
  * Tests if {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
@@ -100,7 +129,7 @@ export default class ViewConsumable {
100
129
  return elementConsumables;
101
130
  }
102
131
  // For elements test consumables object.
103
- return elementConsumables.test(consumables);
132
+ return elementConsumables.test(normalizeConsumables(consumables));
104
133
  }
105
134
  /**
106
135
  * Consumes {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
@@ -134,18 +163,20 @@ export default class ViewConsumable {
134
163
  * otherwise returns `false`.
135
164
  */
136
165
  consume(element, consumables) {
137
- if (this.test(element, consumables)) {
138
- if (element.is('$text') || element.is('documentFragment')) {
139
- // For text nodes and document fragments set value to false.
140
- this._consumables.set(element, false);
141
- }
142
- else {
143
- // For elements - consume consumables object.
144
- this._consumables.get(element).consume(consumables);
166
+ if (element.is('$text') || element.is('documentFragment')) {
167
+ if (!this.test(element, consumables)) {
168
+ return false;
145
169
  }
170
+ // For text nodes and document fragments set value to false.
171
+ this._consumables.set(element, false);
146
172
  return true;
147
173
  }
148
- return false;
174
+ // For elements - consume consumables object.
175
+ const elementConsumables = this._consumables.get(element);
176
+ if (elementConsumables === undefined) {
177
+ return false;
178
+ }
179
+ return elementConsumables.consume(normalizeConsumables(consumables));
149
180
  }
150
181
  /**
151
182
  * Reverts {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
@@ -187,40 +218,10 @@ export default class ViewConsumable {
187
218
  }
188
219
  else {
189
220
  // For elements - revert items from consumables object.
190
- elementConsumables.revert(consumables);
221
+ elementConsumables.revert(normalizeConsumables(consumables));
191
222
  }
192
223
  }
193
224
  }
194
- /**
195
- * Creates consumable object from {@link module:engine/view/element~Element view element}. Consumable object will include
196
- * element's name and all its attributes, classes and styles.
197
- */
198
- static consumablesFromElement(element) {
199
- const consumables = {
200
- element,
201
- name: true,
202
- attributes: [],
203
- classes: [],
204
- styles: []
205
- };
206
- const attributes = element.getAttributeKeys();
207
- for (const attribute of attributes) {
208
- // Skip classes and styles - will be added separately.
209
- if (attribute == 'style' || attribute == 'class') {
210
- continue;
211
- }
212
- consumables.attributes.push(attribute);
213
- }
214
- const classes = element.getClassNames();
215
- for (const className of classes) {
216
- consumables.classes.push(className);
217
- }
218
- const styles = element.getStyleNames();
219
- for (const style of styles) {
220
- consumables.styles.push(style);
221
- }
222
- return consumables;
223
- }
224
225
  /**
225
226
  * Creates {@link module:engine/conversion/viewconsumable~ViewConsumable ViewConsumable} instance from
226
227
  * {@link module:engine/view/node~Node node} or {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
@@ -236,22 +237,16 @@ export default class ViewConsumable {
236
237
  }
237
238
  if (from.is('$text')) {
238
239
  instance.add(from);
239
- return instance;
240
- }
241
- // Add `from` itself, if it is an element.
242
- if (from.is('element')) {
243
- instance.add(from, ViewConsumable.consumablesFromElement(from));
244
240
  }
245
- if (from.is('documentFragment')) {
241
+ else if (from.is('element') || from.is('documentFragment')) {
246
242
  instance.add(from);
247
- }
248
- for (const child of from.getChildren()) {
249
- instance = ViewConsumable.createFrom(child, instance);
243
+ for (const child of from.getChildren()) {
244
+ ViewConsumable.createFrom(child, instance);
245
+ }
250
246
  }
251
247
  return instance;
252
248
  }
253
249
  }
254
- const CONSUMABLE_TYPES = ['attributes', 'classes', 'styles'];
255
250
  /**
256
251
  * This is a private helper-class for {@link module:engine/conversion/viewconsumable~ViewConsumable}.
257
252
  * It represents and manipulates consumable parts of a single {@link module:engine/view/element~Element}.
@@ -260,16 +255,21 @@ export class ViewElementConsumables {
260
255
  /**
261
256
  * Creates ViewElementConsumables instance.
262
257
  *
263
- * @param from View node or document fragment from which `ViewElementConsumables` is being created.
258
+ * @param from View element from which `ViewElementConsumables` is being created.
264
259
  */
265
260
  constructor(from) {
266
- this.element = from;
261
+ /**
262
+ * Flag indicating if name of the element can be consumed.
263
+ */
267
264
  this._canConsumeName = null;
268
- this._consumables = {
269
- attributes: new Map(),
270
- styles: new Map(),
271
- classes: new Map()
272
- };
265
+ /**
266
+ * A map of element's consumables.
267
+ * * For plain attributes the value is a boolean indicating whether the attribute is available to consume.
268
+ * * For token based attributes (like class list and style) the value is a map of tokens to booleans
269
+ * indicating whether the token is available to consume on the given attribute.
270
+ */
271
+ this._attributes = new Map();
272
+ this.element = from;
273
273
  }
274
274
  /**
275
275
  * Adds consumable parts of the {@link module:engine/view/element~Element view element}.
@@ -283,31 +283,60 @@ export class ViewElementConsumables {
283
283
  * Attributes classes and styles:
284
284
  *
285
285
  * ```ts
286
- * consumables.add( { attributes: 'title', classes: 'foo', styles: 'color' } );
287
- * consumables.add( { attributes: [ 'title', 'name' ], classes: [ 'foo', 'bar' ] );
286
+ * consumables.add( { attributes: [ [ 'title' ], [ 'class', 'foo' ], [ 'style', 'color'] ] } );
287
+ * consumables.add( { attributes: [ [ 'title' ], [ 'name' ], [ 'class', 'foo' ], [ 'class', 'bar' ] ] } );
288
288
  * ```
289
289
  *
290
+ * Note: This method accepts only {@link module:engine/view/element~NormalizedConsumables}.
291
+ * You can use {@link module:engine/conversion/viewconsumable~normalizeConsumables} helper to convert from
292
+ * {@link module:engine/conversion/viewconsumable~Consumables} to `NormalizedConsumables`.
293
+ *
290
294
  * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
291
295
  * attribute is provided - it should be handled separately by providing `style` and `class` in consumables object.
292
296
  *
293
297
  * @param consumables Object describing which parts of the element can be consumed.
294
- * @param consumables.name If set to `true` element's name will be added as consumable.
295
- * @param consumables.attributes Attribute name or array of attribute names to add as consumable.
296
- * @param consumables.classes Class name or array of class names to add as consumable.
297
- * @param consumables.styles Style name or array of style names to add as consumable.
298
298
  */
299
299
  add(consumables) {
300
300
  if (consumables.name) {
301
301
  this._canConsumeName = true;
302
302
  }
303
- for (const type of CONSUMABLE_TYPES) {
304
- if (type in consumables) {
305
- this._add(type, consumables[type]);
303
+ for (const [name, token] of consumables.attributes) {
304
+ if (token) {
305
+ let attributeTokens = this._attributes.get(name);
306
+ if (!attributeTokens || typeof attributeTokens == 'boolean') {
307
+ attributeTokens = new Map();
308
+ this._attributes.set(name, attributeTokens);
309
+ }
310
+ attributeTokens.set(token, true);
311
+ }
312
+ else if (name == 'style' || name == 'class') {
313
+ /**
314
+ * Class and style attributes should be handled separately in
315
+ * {@link module:engine/conversion/viewconsumable~ViewConsumable#add `ViewConsumable#add()`}.
316
+ *
317
+ * What you have done is trying to use:
318
+ *
319
+ * ```ts
320
+ * consumables.add( { attributes: [ 'class', 'style' ] } );
321
+ * ```
322
+ *
323
+ * While each class and style should be registered separately:
324
+ *
325
+ * ```ts
326
+ * consumables.add( { classes: 'some-class', styles: 'font-weight' } );
327
+ * ```
328
+ *
329
+ * @error viewconsumable-invalid-attribute
330
+ */
331
+ throw new CKEditorError('viewconsumable-invalid-attribute', this);
332
+ }
333
+ else {
334
+ this._attributes.set(name, true);
306
335
  }
307
336
  }
308
337
  }
309
338
  /**
310
- * Tests if parts of the {@link module:engine/view/node~Node view node} can be consumed.
339
+ * Tests if parts of the {@link module:engine/view/element~Element view element} can be consumed.
311
340
  *
312
341
  * Element's name can be tested:
313
342
  *
@@ -318,15 +347,11 @@ export class ViewElementConsumables {
318
347
  * Attributes classes and styles:
319
348
  *
320
349
  * ```ts
321
- * consumables.test( { attributes: 'title', classes: 'foo', styles: 'color' } );
322
- * consumables.test( { attributes: [ 'title', 'name' ], classes: [ 'foo', 'bar' ] );
350
+ * consumables.test( { attributes: [ [ 'title' ], [ 'class', 'foo' ], [ 'style', 'color' ] ] } );
351
+ * consumables.test( { attributes: [ [ 'title' ], [ 'name' ], [ 'class', 'foo' ], [ 'class', 'bar' ] ] } );
323
352
  * ```
324
353
  *
325
354
  * @param consumables Object describing which parts of the element should be tested.
326
- * @param consumables.name If set to `true` element's name will be tested.
327
- * @param consumables.attributes Attribute name or array of attribute names to test.
328
- * @param consumables.classes Class name or array of class names to test.
329
- * @param consumables.styles Style name or array of style names to test.
330
355
  * @returns `true` when all tested items can be consumed, `null` when even one of the items
331
356
  * was never marked for consumption and `false` when even one of the items was already consumed.
332
357
  */
@@ -335,11 +360,38 @@ export class ViewElementConsumables {
335
360
  if (consumables.name && !this._canConsumeName) {
336
361
  return this._canConsumeName;
337
362
  }
338
- for (const type of CONSUMABLE_TYPES) {
339
- if (type in consumables) {
340
- const value = this._test(type, consumables[type]);
341
- if (value !== true) {
342
- return value;
363
+ for (const [name, token] of consumables.attributes) {
364
+ const value = this._attributes.get(name);
365
+ // Return null if attribute is not found.
366
+ if (value === undefined) {
367
+ return null;
368
+ }
369
+ // Already consumed.
370
+ if (value === false) {
371
+ return false;
372
+ }
373
+ // Simple attribute is not consumed so continue to next attribute.
374
+ if (value === true) {
375
+ continue;
376
+ }
377
+ if (!token) {
378
+ // Tokenized attribute but token is not specified so check if all tokens are not consumed.
379
+ for (const tokenValue of value.values()) {
380
+ // Already consumed token.
381
+ if (!tokenValue) {
382
+ return false;
383
+ }
384
+ }
385
+ }
386
+ else {
387
+ const tokenValue = value.get(token);
388
+ // Return null if token is not found.
389
+ if (tokenValue === undefined) {
390
+ return null;
391
+ }
392
+ // Already consumed.
393
+ if (!tokenValue) {
394
+ return false;
343
395
  }
344
396
  }
345
397
  }
@@ -347,8 +399,9 @@ export class ViewElementConsumables {
347
399
  return true;
348
400
  }
349
401
  /**
350
- * Consumes parts of {@link module:engine/view/element~Element view element}. This function does not check if consumable item
351
- * is already consumed - it consumes all consumable items provided.
402
+ * Tests if parts of the {@link module:engine/view/element~Element view element} can be consumed and consumes them if available.
403
+ * It returns `true` when all items included in method's call can be consumed, otherwise returns `false`.
404
+ *
352
405
  * Element's name can be consumed:
353
406
  *
354
407
  * ```ts
@@ -358,25 +411,44 @@ export class ViewElementConsumables {
358
411
  * Attributes classes and styles:
359
412
  *
360
413
  * ```ts
361
- * consumables.consume( { attributes: 'title', classes: 'foo', styles: 'color' } );
362
- * consumables.consume( { attributes: [ 'title', 'name' ], classes: [ 'foo', 'bar' ] );
414
+ * consumables.consume( { attributes: [ [ 'title' ], [ 'class', 'foo' ], [ 'style', 'color' ] ] } );
415
+ * consumables.consume( { attributes: [ [ 'title' ], [ 'name' ], [ 'class', 'foo' ], [ 'class', 'bar' ] ] } );
363
416
  * ```
364
417
  *
365
418
  * @param consumables Object describing which parts of the element should be consumed.
366
- * @param consumables.name If set to `true` element's name will be consumed.
367
- * @param consumables.attributes Attribute name or array of attribute names to consume.
368
- * @param consumables.classes Class name or array of class names to consume.
369
- * @param consumables.styles Style name or array of style names to consume.
419
+ * @returns `true` when all tested items can be consumed and `false` when even one of the items could not be consumed.
370
420
  */
371
421
  consume(consumables) {
422
+ if (!this.test(consumables)) {
423
+ return false;
424
+ }
372
425
  if (consumables.name) {
373
426
  this._canConsumeName = false;
374
427
  }
375
- for (const type of CONSUMABLE_TYPES) {
376
- if (type in consumables) {
377
- this._consume(type, consumables[type]);
428
+ for (const [name, token] of consumables.attributes) {
429
+ // `value` must be set, because `this.test()` returned `true`.
430
+ const value = this._attributes.get(name);
431
+ // Plain (not tokenized) not-consumed attribute.
432
+ if (typeof value == 'boolean') {
433
+ // Use Element API to collect related attributes.
434
+ for (const [toConsume] of this.element._getConsumables(name, token).attributes) {
435
+ this._attributes.set(toConsume, false);
436
+ }
437
+ }
438
+ else if (!token) {
439
+ // Tokenized attribute but token is not specified so consume all tokens.
440
+ for (const token of value.keys()) {
441
+ value.set(token, false);
442
+ }
443
+ }
444
+ else {
445
+ // Use Element API to collect related attribute tokens.
446
+ for (const [, toConsume] of this.element._getConsumables(name, token).attributes) {
447
+ value.set(toConsume, false);
448
+ }
378
449
  }
379
450
  }
451
+ return true;
380
452
  }
381
453
  /**
382
454
  * Revert already consumed parts of {@link module:engine/view/element~Element view Element}, so they can be consumed once again.
@@ -389,147 +461,77 @@ export class ViewElementConsumables {
389
461
  * Attributes classes and styles:
390
462
  *
391
463
  * ```ts
392
- * consumables.revert( { attributes: 'title', classes: 'foo', styles: 'color' } );
393
- * consumables.revert( { attributes: [ 'title', 'name' ], classes: [ 'foo', 'bar' ] );
464
+ * consumables.revert( { attributes: [ [ 'title' ], [ 'class', 'foo' ], [ 'style', 'color' ] ] } );
465
+ * consumables.revert( { attributes: [ [ 'title' ], [ 'name' ], [ 'class', 'foo' ], [ 'class', 'bar' ] ] } );
394
466
  * ```
395
467
  *
396
468
  * @param consumables Object describing which parts of the element should be reverted.
397
- * @param consumables.name If set to `true` element's name will be reverted.
398
- * @param consumables.attributes Attribute name or array of attribute names to revert.
399
- * @param consumables.classes Class name or array of class names to revert.
400
- * @param consumables.styles Style name or array of style names to revert.
401
469
  */
402
470
  revert(consumables) {
403
471
  if (consumables.name) {
404
472
  this._canConsumeName = true;
405
473
  }
406
- for (const type of CONSUMABLE_TYPES) {
407
- if (type in consumables) {
408
- this._revert(type, consumables[type]);
409
- }
410
- }
411
- }
412
- /**
413
- * Helper method that adds consumables of a given type: attribute, class or style.
414
- *
415
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
416
- * type is provided - it should be handled separately by providing actual style/class type.
417
- *
418
- * @param type Type of the consumable item: `attributes`, `classes` or `styles`.
419
- * @param item Consumable item or array of items.
420
- */
421
- _add(type, item) {
422
- const items = toArray(item);
423
- const consumables = this._consumables[type];
424
- for (const name of items) {
425
- if (type === 'attributes' && (name === 'class' || name === 'style')) {
426
- /**
427
- * Class and style attributes should be handled separately in
428
- * {@link module:engine/conversion/viewconsumable~ViewConsumable#add `ViewConsumable#add()`}.
429
- *
430
- * What you have done is trying to use:
431
- *
432
- * ```ts
433
- * consumables.add( { attributes: [ 'class', 'style' ] } );
434
- * ```
435
- *
436
- * While each class and style should be registered separately:
437
- *
438
- * ```ts
439
- * consumables.add( { classes: 'some-class', styles: 'font-weight' } );
440
- * ```
441
- *
442
- * @error viewconsumable-invalid-attribute
443
- */
444
- throw new CKEditorError('viewconsumable-invalid-attribute', this);
474
+ for (const [name, token] of consumables.attributes) {
475
+ const value = this._attributes.get(name);
476
+ // Plain consumed attribute.
477
+ if (value === false) {
478
+ this._attributes.set(name, true);
479
+ continue;
445
480
  }
446
- consumables.set(name, true);
447
- if (type === 'styles') {
448
- for (const alsoName of this.element.document.stylesProcessor.getRelatedStyles(name)) {
449
- consumables.set(alsoName, true);
450
- }
481
+ // Unknown attribute or not consumed.
482
+ if (value === undefined || value === true) {
483
+ continue;
451
484
  }
452
- }
453
- }
454
- /**
455
- * Helper method that tests consumables of a given type: attribute, class or style.
456
- *
457
- * @param type Type of the consumable item: `attributes`, `classes` or `styles`.
458
- * @param item Consumable item or array of items.
459
- * @returns Returns `true` if all items can be consumed, `null` when one of the items cannot be
460
- * consumed and `false` when one of the items is already consumed.
461
- */
462
- _test(type, item) {
463
- const items = toArray(item);
464
- const consumables = this._consumables[type];
465
- for (const name of items) {
466
- if (type === 'attributes' && (name === 'class' || name === 'style')) {
467
- const consumableName = name == 'class' ? 'classes' : 'styles';
468
- // Check all classes/styles if class/style attribute is tested.
469
- const value = this._test(consumableName, [...this._consumables[consumableName].keys()]);
470
- if (value !== true) {
471
- return value;
485
+ if (!token) {
486
+ // Tokenized attribute but token is not specified so revert all tokens.
487
+ for (const token of value.keys()) {
488
+ value.set(token, true);
472
489
  }
473
490
  }
474
491
  else {
475
- const value = consumables.get(name);
476
- // Return null if attribute is not found.
477
- if (value === undefined) {
478
- return null;
479
- }
480
- if (!value) {
481
- return false;
492
+ const tokenValue = value.get(token);
493
+ if (tokenValue === false) {
494
+ value.set(token, true);
482
495
  }
496
+ // Note that revert of consumed related styles is not handled.
483
497
  }
484
498
  }
485
- return true;
486
499
  }
487
- /**
488
- * Helper method that consumes items of a given type: attribute, class or style.
489
- *
490
- * @param type Type of the consumable item: `attributes`, `classes` or `styles`.
491
- * @param item Consumable item or array of items.
492
- */
493
- _consume(type, item) {
494
- const items = toArray(item);
495
- const consumables = this._consumables[type];
496
- for (const name of items) {
497
- if (type === 'attributes' && (name === 'class' || name === 'style')) {
498
- const consumableName = name == 'class' ? 'classes' : 'styles';
499
- // If class or style is provided for consumption - consume them all.
500
- this._consume(consumableName, [...this._consumables[consumableName].keys()]);
501
- }
502
- else {
503
- consumables.set(name, false);
504
- if (type == 'styles') {
505
- for (const toConsume of this.element.document.stylesProcessor.getRelatedStyles(name)) {
506
- consumables.set(toConsume, false);
507
- }
508
- }
509
- }
510
- }
500
+ }
501
+ /**
502
+ * Normalizes a {@link module:engine/conversion/viewconsumable~Consumables} or {@link module:engine/view/matcher~Match}
503
+ * to a {@link module:engine/view/element~NormalizedConsumables}.
504
+ */
505
+ export function normalizeConsumables(consumables) {
506
+ const attributes = [];
507
+ if ('attributes' in consumables && consumables.attributes) {
508
+ normalizeConsumablePart(attributes, consumables.attributes);
511
509
  }
512
- /**
513
- * Helper method that reverts items of a given type: attribute, class or style.
514
- *
515
- * @param type Type of the consumable item: `attributes`, `classes` or , `styles`.
516
- * @param item Consumable item or array of items.
517
- */
518
- _revert(type, item) {
519
- const items = toArray(item);
520
- const consumables = this._consumables[type];
521
- for (const name of items) {
522
- if (type === 'attributes' && (name === 'class' || name === 'style')) {
523
- const consumableName = name == 'class' ? 'classes' : 'styles';
524
- // If class or style is provided for reverting - revert them all.
525
- this._revert(consumableName, [...this._consumables[consumableName].keys()]);
526
- }
527
- else {
528
- const value = consumables.get(name);
529
- if (value === false) {
530
- consumables.set(name, true);
531
- }
532
- }
510
+ if ('classes' in consumables && consumables.classes) {
511
+ normalizeConsumablePart(attributes, consumables.classes, 'class');
512
+ }
513
+ if ('styles' in consumables && consumables.styles) {
514
+ normalizeConsumablePart(attributes, consumables.styles, 'style');
515
+ }
516
+ return {
517
+ name: consumables.name || false,
518
+ attributes
519
+ };
520
+ }
521
+ /**
522
+ * Normalizes a list of consumable attributes to a common tuple format.
523
+ */
524
+ function normalizeConsumablePart(attributes, items, prefix) {
525
+ if (typeof items == 'string') {
526
+ attributes.push(prefix ? [prefix, items] : [items]);
527
+ return;
528
+ }
529
+ for (const item of items) {
530
+ if (Array.isArray(item)) {
531
+ attributes.push(item);
532
+ }
533
+ else {
534
+ attributes.push(prefix ? [prefix, item] : [item]);
533
535
  }
534
536
  }
535
537
  }
@@ -105,4 +105,18 @@ export default class AttributeElement extends Element {
105
105
  * @returns Clone of this element.
106
106
  */
107
107
  _clone(deep?: boolean): this;
108
+ /**
109
+ * Used by {@link module:engine/view/element~Element#_mergeAttributesFrom} to verify if the given element can be merged without
110
+ * conflicts into this element.
111
+ *
112
+ * @internal
113
+ */
114
+ _canMergeAttributesFrom(otherElement: AttributeElement): boolean;
115
+ /**
116
+ * Used by {@link module:engine/view/element~Element#_subtractAttributesOf} to verify if the given element attributes
117
+ * can be fully subtracted from this element.
118
+ *
119
+ * @internal
120
+ */
121
+ _canSubtractAttributesOf(otherElement: AttributeElement): boolean;
108
122
  }
@@ -135,6 +135,32 @@ class AttributeElement extends Element {
135
135
  cloned._id = this._id;
136
136
  return cloned;
137
137
  }
138
+ /**
139
+ * Used by {@link module:engine/view/element~Element#_mergeAttributesFrom} to verify if the given element can be merged without
140
+ * conflicts into this element.
141
+ *
142
+ * @internal
143
+ */
144
+ _canMergeAttributesFrom(otherElement) {
145
+ // Can't merge if any of elements have an id or a difference of priority.
146
+ if (this.id !== null || otherElement.id !== null || this.priority !== otherElement.priority) {
147
+ return false;
148
+ }
149
+ return super._canMergeAttributesFrom(otherElement);
150
+ }
151
+ /**
152
+ * Used by {@link module:engine/view/element~Element#_subtractAttributesOf} to verify if the given element attributes
153
+ * can be fully subtracted from this element.
154
+ *
155
+ * @internal
156
+ */
157
+ _canSubtractAttributesOf(otherElement) {
158
+ // Can't subtract if any of elements have an id or a difference of priority.
159
+ if (this.id !== null || otherElement.id !== null || this.priority !== otherElement.priority) {
160
+ return false;
161
+ }
162
+ return super._canSubtractAttributesOf(otherElement);
163
+ }
138
164
  }
139
165
  AttributeElement.DEFAULT_PRIORITY = DEFAULT_PRIORITY;
140
166
  export default AttributeElement;