@ckeditor/ckeditor5-html-support 34.0.0 → 34.1.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.
package/src/datafilter.js CHANGED
@@ -53,6 +53,9 @@ import '../theme/datafilter.css';
53
53
  * }
54
54
  * } );
55
55
  *
56
+ * To apply the information about allowed and disallowed attributes in custom integration plugin,
57
+ * use the {@link module:html-support/datafilter~DataFilter#processViewAttributes `processViewAttributes()`} method.
58
+ *
56
59
  * @extends module:core/plugin~Plugin
57
60
  */
58
61
  export default class DataFilter extends Plugin {
@@ -106,8 +109,18 @@ export default class DataFilter extends Plugin {
106
109
  */
107
110
  this._dataInitialized = false;
108
111
 
112
+ /**
113
+ * Cached map of coupled attributes. Keys are the feature attributes names
114
+ * and values are arrays with coupled GHS attributes names.
115
+ *
116
+ * @private
117
+ * @member {Map.<String,Array>}
118
+ */
119
+ this._coupledAttributes = null;
120
+
109
121
  this._registerElementsAfterInit();
110
122
  this._registerElementHandlers();
123
+ this._registerModelPostFixer();
111
124
  }
112
125
 
113
126
  /**
@@ -167,6 +180,9 @@ export default class DataFilter extends Plugin {
167
180
  if ( this._dataInitialized ) {
168
181
  this._fireRegisterEvent( definition );
169
182
  }
183
+
184
+ // Reset cached map to recalculate it on the next usage.
185
+ this._coupledAttributes = null;
170
186
  }
171
187
  }
172
188
 
@@ -208,17 +224,30 @@ export default class DataFilter extends Plugin {
208
224
  }
209
225
 
210
226
  /**
211
- * Matches and consumes allowed and disallowed view attributes and returns the allowed ones.
227
+ * Processes all allowed and disallowed attributes on the view element by consuming them and returning the allowed ones.
228
+ *
229
+ * This method applies the configuration set up by {@link #allowAttributes `allowAttributes()`}
230
+ * and {@link #disallowAttributes `disallowAttributes()`} over the given view element by consuming relevant attributes.
231
+ * It returns the allowed attributes that were found on the given view element for further processing by integration code.
232
+ *
233
+ * dispatcher.on( 'element:myElement', ( evt, data, conversionApi ) => {
234
+ * // Get rid of disallowed and extract all allowed attributes from a viewElement.
235
+ * const viewAttributes = dataFilter.processViewAttributes( data.viewItem, conversionApi );
236
+ * // Do something with them, i.e. store inside a model as a dictionary.
237
+ * if ( viewAttributes ) {
238
+ * conversionApi.writer.setAttribute( 'htmlAttributesOfMyElement', viewAttributes, data.modelRange );
239
+ * }
240
+ * } );
212
241
  *
213
- * @protected
242
+ * @see module:engine/conversion/viewconsumable~ViewConsumable#consume
214
243
  * @param {module:engine/view/element~Element} viewElement
215
- * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
244
+ * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi
216
245
  * @returns {Object} [result]
217
246
  * @returns {Object} result.attributes Set with matched attribute names.
218
247
  * @returns {Object} result.styles Set with matched style names.
219
248
  * @returns {Array.<String>} result.classes Set with matched class names.
220
249
  */
221
- _consumeAllowedAttributes( viewElement, conversionApi ) {
250
+ processViewAttributes( viewElement, conversionApi ) {
222
251
  // Make sure that the disabled attributes are handled before the allowed attributes are called.
223
252
  // For example, for block images the <figure> converter triggers conversion for <img> first and then for other elements, i.e. <a>.
224
253
  consumeAttributes( viewElement, conversionApi, this._disallowedAttributes );
@@ -288,6 +317,91 @@ export default class DataFilter extends Plugin {
288
317
  }, { priority: 'lowest' } );
289
318
  }
290
319
 
320
+ /**
321
+ * Registers a model post-fixer that is removing coupled GHS attributes of inline elements. Those attributes
322
+ * are removed if a coupled feature attribute is removed.
323
+ *
324
+ * For example, consider following HTML:
325
+ *
326
+ * <a href="foo.html" id="myId">bar</a>
327
+ *
328
+ * Which would be upcasted to following text node in the model:
329
+ *
330
+ * <$text linkHref="foo.html" htmlA="{ attributes: { id: 'myId' } }">bar</$text>
331
+ *
332
+ * When the user removes the link from that text (using UI), only `linkHref` attribute would be removed:
333
+ *
334
+ * <$text htmlA="{ attributes: { id: 'myId' } }">bar</$text>
335
+ *
336
+ * The `htmlA` attribute would stay in the model and would cause GHS to generate an `<a>` element.
337
+ * This is incorrect from UX point of view, as the user wanted to remove the whole link (not only `href`).
338
+ *
339
+ * @private
340
+ */
341
+ _registerModelPostFixer() {
342
+ const model = this.editor.model;
343
+
344
+ model.document.registerPostFixer( writer => {
345
+ const changes = model.document.differ.getChanges();
346
+ let changed = false;
347
+
348
+ const coupledAttributes = this._getCoupledAttributesMap();
349
+
350
+ for ( const change of changes ) {
351
+ // Handle only attribute removals.
352
+ if ( change.type != 'attribute' || change.attributeNewValue !== null ) {
353
+ continue;
354
+ }
355
+
356
+ // Find a list of coupled GHS attributes.
357
+ const attributeKeys = coupledAttributes.get( change.attributeKey );
358
+
359
+ if ( !attributeKeys ) {
360
+ continue;
361
+ }
362
+
363
+ // Remove the coupled GHS attributes on the same range as the feature attribute was removed.
364
+ for ( const { item } of change.range.getWalker( { shallow: true } ) ) {
365
+ for ( const attributeKey of attributeKeys ) {
366
+ if ( item.hasAttribute( attributeKey ) ) {
367
+ writer.removeAttribute( attributeKey, item );
368
+ changed = true;
369
+ }
370
+ }
371
+ }
372
+ }
373
+
374
+ return changed;
375
+ } );
376
+ }
377
+
378
+ /**
379
+ * Collects the map of coupled attributes. The returned map is keyed by the feature attribute name
380
+ * and coupled GHS attribute names are stored in the value array .
381
+ *
382
+ * @private
383
+ * @returns {Map.<String,Array>}
384
+ */
385
+ _getCoupledAttributesMap() {
386
+ if ( this._coupledAttributes ) {
387
+ return this._coupledAttributes;
388
+ }
389
+
390
+ this._coupledAttributes = new Map();
391
+
392
+ for ( const definition of this._allowedElements ) {
393
+ if ( definition.coupledAttribute && definition.model ) {
394
+ const attributeNames = this._coupledAttributes.get( definition.coupledAttribute );
395
+
396
+ if ( attributeNames ) {
397
+ attributeNames.push( definition.model );
398
+ } else {
399
+ this._coupledAttributes.set( definition.coupledAttribute, [ definition.model ] );
400
+ }
401
+ }
402
+ }
403
+ }
404
+
291
405
  /**
292
406
  * Fires `register` event for the given element definition.
293
407
  *
@@ -462,7 +576,7 @@ export default class DataFilter extends Plugin {
462
576
  //
463
577
  // @private
464
578
  // @param {module:engine/view/element~Element} viewElement
465
- // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
579
+ // @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi
466
580
  // @param {module:engine/view/matcher~Matcher Matcher} matcher
467
581
  // @returns {Object} [result]
468
582
  // @returns {Object} result.attributes
@@ -496,7 +610,7 @@ function consumeAttributes( viewElement, conversionApi, matcher ) {
496
610
  //
497
611
  // @private
498
612
  // @param {module:engine/view/element~Element} viewElement
499
- // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
613
+ // @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi
500
614
  // @param {module:engine/view/matcher~Matcher Matcher} matcher
501
615
  // @returns {Array.<Object>} Array with match information about found attributes.
502
616
  function consumeAttributeMatches( viewElement, { consumable }, matcher ) {
@@ -509,9 +623,8 @@ function consumeAttributeMatches( viewElement, { consumable }, matcher ) {
509
623
  // We only want to consume attributes, so element can be still processed by other converters.
510
624
  delete match.match.name;
511
625
 
512
- if ( consumable.consume( viewElement, match.match ) ) {
513
- consumedMatches.push( match );
514
- }
626
+ consumable.consume( viewElement, match.match );
627
+ consumedMatches.push( match );
515
628
  }
516
629
 
517
630
  return consumedMatches;
@@ -521,7 +634,7 @@ function consumeAttributeMatches( viewElement, { consumable }, matcher ) {
521
634
  //
522
635
  // @private
523
636
  // @param {module:engine/view/element~Element} viewElement
524
- // @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable
637
+ // @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable
525
638
  // @param {Object} match
526
639
  function removeConsumedAttributes( consumable, viewElement, match ) {
527
640
  for ( const key of [ 'attributes', 'classes', 'styles' ] ) {
@@ -531,7 +644,8 @@ function removeConsumedAttributes( consumable, viewElement, match ) {
531
644
  continue;
532
645
  }
533
646
 
534
- for ( const value of attributes ) {
647
+ // Iterating over a copy of an array so removing items doesn't influence iteration.
648
+ for ( const value of Array.from( attributes ) ) {
535
649
  if ( !consumable.test( viewElement, ( { [ key ]: [ value ] } ) ) ) {
536
650
  removeItemFromArray( attributes, value );
537
651
  }
package/src/dataschema.js CHANGED
@@ -251,5 +251,8 @@ function testViewName( pattern, viewName ) {
251
251
  * @property {Number} [priority] Element priority. Decides in what order elements are wrapped by
252
252
  * {@link module:engine/view/downcastwriter~DowncastWriter}.
253
253
  * Set by {@link module:html-support/dataschema~DataSchema#registerInlineElement} method.
254
+ * @property {String} [coupledAttribute] The name of the model attribute that generates the same view element. GHS inline attribute
255
+ * will be removed from the model tree as soon as the coupled attribute is removed. See
256
+ * {@link module:html-support/datafilter~DataFilter#_registerModelPostFixer GHS post-fixer} for more details.
254
257
  * @extends module:html-support/dataschema~DataSchemaDefinition
255
258
  */
@@ -86,7 +86,7 @@ export default class GeneralHtmlSupport extends Plugin {
86
86
  }
87
87
 
88
88
  /**
89
- * Updates GHS model attribute for a specified view element name, so it includes a given class name.
89
+ * Updates GHS model attribute for a specified view element name, so it includes the given class name.
90
90
  *
91
91
  * @protected
92
92
  * @param {String} viewElementName A view element name.
@@ -109,7 +109,7 @@ export default class GeneralHtmlSupport extends Plugin {
109
109
  }
110
110
 
111
111
  /**
112
- * Updates GHS model attribute for a specified view element name, so it does not include a given class name.
112
+ * Updates GHS model attribute for a specified view element name, so it does not include the given class name.
113
113
  *
114
114
  * @protected
115
115
  * @param {String} viewElementName A view element name.
@@ -132,7 +132,7 @@ export default class GeneralHtmlSupport extends Plugin {
132
132
  }
133
133
 
134
134
  /**
135
- * Updates GHS model attribute for a specified view element name, so it includes a given attribute.
135
+ * Updates GHS model attribute for a specified view element name, so it includes the given attribute.
136
136
  *
137
137
  * @protected
138
138
  * @param {String} viewElementName A view element name.
@@ -155,7 +155,7 @@ export default class GeneralHtmlSupport extends Plugin {
155
155
  }
156
156
 
157
157
  /**
158
- * Updates GHS model attribute for a specified view element name, so it does not include a given attribute.
158
+ * Updates GHS model attribute for a specified view element name, so it does not include the given attribute.
159
159
  *
160
160
  * @protected
161
161
  * @param {String} viewElementName A view element name.
@@ -79,7 +79,7 @@ function viewToModelCodeBlockAttributeConverter( dataFilter ) {
79
79
  preserveElementAttributes( viewCodeElement, 'htmlContentAttributes' );
80
80
 
81
81
  function preserveElementAttributes( viewElement, attributeName ) {
82
- const viewAttributes = dataFilter._consumeAllowedAttributes( viewElement, conversionApi );
82
+ const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
83
83
 
84
84
  if ( viewAttributes ) {
85
85
  conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange );
@@ -14,7 +14,7 @@ import { setViewAttributes } from '../conversionutils.js';
14
14
  import DataFilter from '../datafilter';
15
15
 
16
16
  /**
17
- * Provides the General HTML Support integration with {@link module:list/documentlist~DocumentList Document List} feature.
17
+ * Provides the General HTML Support integration with the {@link module:list/documentlist~DocumentList Document List} feature.
18
18
  *
19
19
  * @extends module:core/plugin~Plugin
20
20
  */
@@ -85,7 +85,7 @@ export default class DocumentListElementSupport extends Plugin {
85
85
  } );
86
86
 
87
87
  // Make sure that all items in a single list (items at the same level & listType) have the same properties.
88
- // Note: This is almost exact copy from DocumentListPropertiesEditing.
88
+ // Note: This is almost an exact copy from DocumentListPropertiesEditing.
89
89
  documentListEditing.on( 'postFixer', ( evt, { listNodes, writer } ) => {
90
90
  const previousNodesByIndent = []; // Last seen nodes of lower indented lists.
91
91
 
@@ -180,7 +180,7 @@ function viewToModelListAttributeConverter( attributeName, dataFilter ) {
180
180
  Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) );
181
181
  }
182
182
 
183
- const viewAttributes = dataFilter._consumeAllowedAttributes( viewElement, conversionApi );
183
+ const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
184
184
 
185
185
  for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
186
186
  // Apply only to list item blocks.
@@ -111,10 +111,19 @@ export default class DualContentModelElementSupport extends Plugin {
111
111
  * @returns {Boolean}
112
112
  */
113
113
  _hasBlockContent( viewElement ) {
114
- const blockElements = this.editor.editing.view.domConverter.blockElements;
114
+ const view = this.editor.editing.view;
115
+ const blockElements = view.domConverter.blockElements;
116
+
117
+ // Traversing the viewElement subtree looking for block elements.
118
+ // Especially for the cases like <div><a href="#"><p>foo</p></a></div>.
119
+ // https://github.com/ckeditor/ckeditor5/issues/11513
120
+ for ( const viewItem of view.createRangeIn( viewElement ).getItems() ) {
121
+ if ( viewItem.is( 'element' ) && blockElements.includes( viewItem.name ) ) {
122
+ return true;
123
+ }
124
+ }
115
125
 
116
- return Array.from( viewElement.getChildren() )
117
- .some( node => blockElements.includes( node.name ) );
126
+ return false;
118
127
  }
119
128
 
120
129
  /**
@@ -43,6 +43,10 @@ export default class ImageElementSupport extends Plugin {
43
43
  const conversion = editor.conversion;
44
44
  const dataFilter = editor.plugins.get( DataFilter );
45
45
 
46
+ dataFilter.on( 'register:figure', () => {
47
+ conversion.for( 'upcast' ).add( viewToModelFigureAttributeConverter( dataFilter ) );
48
+ } );
49
+
46
50
  dataFilter.on( 'register:img', ( evt, definition ) => {
47
51
  if ( definition.model !== 'imageBlock' && definition.model !== 'imageInline' ) {
48
52
  return;
@@ -96,31 +100,46 @@ function viewToModelImageAttributeConverter( dataFilter ) {
96
100
 
97
101
  preserveElementAttributes( viewImageElement, 'htmlAttributes' );
98
102
 
99
- if ( viewContainerElement.is( 'element', 'figure' ) ) {
100
- preserveElementAttributes( viewContainerElement, 'htmlFigureAttributes' );
101
- } else if ( viewContainerElement.is( 'element', 'a' ) ) {
103
+ if ( viewContainerElement.is( 'element', 'a' ) ) {
102
104
  preserveLinkAttributes( viewContainerElement );
103
105
  }
104
106
 
105
107
  function preserveElementAttributes( viewElement, attributeName ) {
106
- const viewAttributes = dataFilter._consumeAllowedAttributes( viewElement, conversionApi );
108
+ const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
107
109
 
108
110
  if ( viewAttributes ) {
109
111
  conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange );
110
112
  }
111
113
  }
112
114
 
113
- // For a block image, we want to preserve the attributes on our own.
114
- // The inline image attributes will be handled by the GHS automatically.
115
115
  function preserveLinkAttributes( viewContainerElement ) {
116
116
  if ( data.modelRange && data.modelRange.getContainedElement().is( 'element', 'imageBlock' ) ) {
117
117
  preserveElementAttributes( viewContainerElement, 'htmlLinkAttributes' );
118
118
  }
119
+ }
120
+ }, { priority: 'low' } );
121
+ };
122
+ }
119
123
 
120
- // If we're in a link, then the `<figure>` element should be one level higher.
121
- if ( viewContainerElement.parent.is( 'element', 'figure' ) ) {
122
- preserveElementAttributes( viewContainerElement.parent, 'htmlFigureAttributes' );
123
- }
124
+ // View-to-model conversion helper preserving allowed attributes on {@link module:image/image~Image Image}
125
+ // feature model element from figure view element.
126
+ //
127
+ // @private
128
+ // @param {module:html-support/datafilter~DataFilter} dataFilter
129
+ // @returns {Function} Returns a conversion callback.
130
+ function viewToModelFigureAttributeConverter( dataFilter ) {
131
+ return dispatcher => {
132
+ dispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
133
+ const viewFigureElement = data.viewItem;
134
+
135
+ if ( !data.modelRange || !viewFigureElement.hasClass( 'image' ) ) {
136
+ return;
137
+ }
138
+
139
+ const viewAttributes = dataFilter.processViewAttributes( viewFigureElement, conversionApi );
140
+
141
+ if ( viewAttributes ) {
142
+ conversionApi.writer.setAttribute( 'htmlFigureAttributes', viewAttributes, data.modelRange );
124
143
  }
125
144
  }, { priority: 'low' } );
126
145
  };
@@ -44,6 +44,10 @@ export default class MediaEmbedElementSupport extends Plugin {
44
44
  view: mediaElementName
45
45
  } );
46
46
 
47
+ dataFilter.on( 'register:figure', ( ) => {
48
+ conversion.for( 'upcast' ).add( viewToModelFigureAttributesConverter( dataFilter ) );
49
+ } );
50
+
47
51
  dataFilter.on( `register:${ mediaElementName }`, ( evt, definition ) => {
48
52
  if ( definition.model !== 'media' ) {
49
53
  return;
@@ -71,16 +75,11 @@ function viewToModelMediaAttributesConverter( dataFilter, mediaElementName ) {
71
75
 
72
76
  function upcastMedia( evt, data, conversionApi ) {
73
77
  const viewMediaElement = data.viewItem;
74
- const viewParent = viewMediaElement.parent;
75
78
 
76
79
  preserveElementAttributes( viewMediaElement, 'htmlAttributes' );
77
80
 
78
- if ( viewParent.is( 'element', 'figure' ) && viewParent.hasClass( 'media' ) ) {
79
- preserveElementAttributes( viewParent, 'htmlFigureAttributes' );
80
- }
81
-
82
81
  function preserveElementAttributes( viewElement, attributeName ) {
83
- const viewAttributes = dataFilter._consumeAllowedAttributes( viewElement, conversionApi );
82
+ const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
84
83
 
85
84
  if ( viewAttributes ) {
86
85
  conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange );
@@ -89,6 +88,30 @@ function viewToModelMediaAttributesConverter( dataFilter, mediaElementName ) {
89
88
  }
90
89
  }
91
90
 
91
+ // View-to-model conversion helper preserving allowed attributes on {@link module:media-embed/mediaembed~MediaEmbed MediaEmbed}
92
+ // feature model element from figure view element.
93
+ //
94
+ // @private
95
+ // @param {module:html-support/datafilter~DataFilter} dataFilter
96
+ // @returns {Function} Returns a conversion callback.
97
+ function viewToModelFigureAttributesConverter( dataFilter ) {
98
+ return dispatcher => {
99
+ dispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
100
+ const viewFigureElement = data.viewItem;
101
+
102
+ if ( !data.modelRange || !viewFigureElement.hasClass( 'media' ) ) {
103
+ return;
104
+ }
105
+
106
+ const viewAttributes = dataFilter.processViewAttributes( viewFigureElement, conversionApi );
107
+
108
+ if ( viewAttributes ) {
109
+ conversionApi.writer.setAttribute( 'htmlFigureAttributes', viewAttributes, data.modelRange );
110
+ }
111
+ }, { priority: 'low' } );
112
+ };
113
+ }
114
+
92
115
  function modelToViewMediaAttributeConverter( mediaElementName ) {
93
116
  return dispatcher => {
94
117
  addAttributeConversionDispatcherHandler( mediaElementName, 'htmlAttributes' );
@@ -9,7 +9,6 @@
9
9
 
10
10
  import { Plugin } from 'ckeditor5/src/core';
11
11
  import { setViewAttributes } from '../conversionutils.js';
12
-
13
12
  import DataFilter from '../datafilter';
14
13
 
15
14
  /**
@@ -39,6 +38,10 @@ export default class TableElementSupport extends Plugin {
39
38
  const conversion = editor.conversion;
40
39
  const dataFilter = editor.plugins.get( DataFilter );
41
40
 
41
+ dataFilter.on( 'register:figure', ( ) => {
42
+ conversion.for( 'upcast' ).add( viewToModelFigureAttributeConverter( dataFilter ) );
43
+ } );
44
+
42
45
  dataFilter.on( 'register:table', ( evt, definition ) => {
43
46
  if ( definition.model !== 'table' ) {
44
47
  return;
@@ -74,11 +77,6 @@ function viewToModelTableAttributeConverter( dataFilter ) {
74
77
 
75
78
  preserveElementAttributes( viewTableElement, 'htmlAttributes' );
76
79
 
77
- const viewFigureElement = viewTableElement.parent;
78
- if ( viewFigureElement.is( 'element', 'figure' ) ) {
79
- preserveElementAttributes( viewFigureElement, 'htmlFigureAttributes' );
80
- }
81
-
82
80
  for ( const childNode of viewTableElement.getChildren() ) {
83
81
  if ( childNode.is( 'element', 'thead' ) ) {
84
82
  preserveElementAttributes( childNode, 'htmlTheadAttributes' );
@@ -90,12 +88,36 @@ function viewToModelTableAttributeConverter( dataFilter ) {
90
88
  }
91
89
 
92
90
  function preserveElementAttributes( viewElement, attributeName ) {
93
- const viewAttributes = dataFilter._consumeAllowedAttributes( viewElement, conversionApi );
91
+ const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
94
92
 
95
93
  if ( viewAttributes ) {
96
94
  conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange );
97
95
  }
98
96
  }
97
+ } );
98
+ };
99
+ }
100
+
101
+ // View-to-model conversion helper preserving allowed attributes on {@link module:table/table~Table Table}
102
+ // feature model element from figure view element.
103
+ //
104
+ // @private
105
+ // @param {module:html-support/datafilter~DataFilter} dataFilter
106
+ // @returns {Function} Returns a conversion callback.
107
+ function viewToModelFigureAttributeConverter( dataFilter ) {
108
+ return dispatcher => {
109
+ dispatcher.on( 'element:figure', ( evt, data, conversionApi ) => {
110
+ const viewFigureElement = data.viewItem;
111
+
112
+ if ( !data.modelRange || !viewFigureElement.hasClass( 'table' ) ) {
113
+ return;
114
+ }
115
+
116
+ const viewAttributes = dataFilter.processViewAttributes( viewFigureElement, conversionApi );
117
+
118
+ if ( viewAttributes ) {
119
+ conversionApi.writer.setAttribute( 'htmlFigureAttributes', viewAttributes, data.modelRange );
120
+ }
99
121
  }, { priority: 'low' } );
100
122
  };
101
123
  }
@@ -577,6 +577,7 @@ export default {
577
577
  model: 'htmlA',
578
578
  view: 'a',
579
579
  priority: 5,
580
+ coupledAttribute: 'linkHref',
580
581
  attributeProperties: {
581
582
  copyOnEnter: true
582
583
  }
@@ -584,6 +585,7 @@ export default {
584
585
  {
585
586
  model: 'htmlStrong',
586
587
  view: 'strong',
588
+ coupledAttribute: 'bold',
587
589
  attributeProperties: {
588
590
  copyOnEnter: true
589
591
  }
@@ -591,6 +593,7 @@ export default {
591
593
  {
592
594
  model: 'htmlB',
593
595
  view: 'b',
596
+ coupledAttribute: 'bold',
594
597
  attributeProperties: {
595
598
  copyOnEnter: true
596
599
  }
@@ -598,6 +601,7 @@ export default {
598
601
  {
599
602
  model: 'htmlI',
600
603
  view: 'i',
604
+ coupledAttribute: 'italic',
601
605
  attributeProperties: {
602
606
  copyOnEnter: true
603
607
  }
@@ -605,6 +609,7 @@ export default {
605
609
  {
606
610
  model: 'htmlEm',
607
611
  view: 'em',
612
+ coupledAttribute: 'italic',
608
613
  attributeProperties: {
609
614
  copyOnEnter: true
610
615
  }
@@ -612,6 +617,7 @@ export default {
612
617
  {
613
618
  model: 'htmlS',
614
619
  view: 's',
620
+ coupledAttribute: 'strikethrough',
615
621
  attributeProperties: {
616
622
  copyOnEnter: true
617
623
  }
@@ -620,6 +626,7 @@ export default {
620
626
  {
621
627
  model: 'htmlDel',
622
628
  view: 'del',
629
+ coupledAttribute: 'strikethrough',
623
630
  attributeProperties: {
624
631
  copyOnEnter: true
625
632
  }
@@ -635,6 +642,7 @@ export default {
635
642
  {
636
643
  model: 'htmlU',
637
644
  view: 'u',
645
+ coupledAttribute: 'underline',
638
646
  attributeProperties: {
639
647
  copyOnEnter: true
640
648
  }
@@ -642,6 +650,7 @@ export default {
642
650
  {
643
651
  model: 'htmlSub',
644
652
  view: 'sub',
653
+ coupledAttribute: 'subscript',
645
654
  attributeProperties: {
646
655
  copyOnEnter: true
647
656
  }
@@ -649,6 +658,7 @@ export default {
649
658
  {
650
659
  model: 'htmlSup',
651
660
  view: 'sup',
661
+ coupledAttribute: 'superscript',
652
662
  attributeProperties: {
653
663
  copyOnEnter: true
654
664
  }
@@ -656,6 +666,7 @@ export default {
656
666
  {
657
667
  model: 'htmlCode',
658
668
  view: 'code',
669
+ coupledAttribute: 'code',
659
670
  attributeProperties: {
660
671
  copyOnEnter: true
661
672
  }