@ckeditor/ckeditor5-html-support 34.0.0 → 35.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/LICENSE.md +6 -2
  3. package/build/html-support.js +1 -1
  4. package/build/translations/ar.js +1 -0
  5. package/build/translations/bg.js +1 -0
  6. package/build/translations/bn.js +1 -0
  7. package/build/translations/ca.js +1 -0
  8. package/build/translations/da.js +1 -0
  9. package/build/translations/et.js +1 -0
  10. package/build/translations/fi.js +1 -0
  11. package/build/translations/fr.js +1 -0
  12. package/build/translations/he.js +1 -0
  13. package/build/translations/hi.js +1 -0
  14. package/build/translations/ja.js +1 -0
  15. package/build/translations/ko.js +1 -0
  16. package/build/translations/lt.js +1 -0
  17. package/build/translations/lv.js +1 -0
  18. package/build/translations/ms.js +1 -0
  19. package/build/translations/no.js +1 -0
  20. package/build/translations/pt.js +1 -0
  21. package/build/translations/ro.js +1 -0
  22. package/build/translations/sv.js +1 -0
  23. package/build/translations/th.js +1 -0
  24. package/build/translations/tr.js +1 -0
  25. package/build/translations/uk.js +1 -0
  26. package/build/translations/ur.js +1 -0
  27. package/build/translations/vi.js +1 -0
  28. package/build/translations/zh-cn.js +1 -0
  29. package/lang/translations/ar.po +21 -0
  30. package/lang/translations/bg.po +21 -0
  31. package/lang/translations/bn.po +21 -0
  32. package/lang/translations/ca.po +21 -0
  33. package/lang/translations/da.po +21 -0
  34. package/lang/translations/es.po +1 -1
  35. package/lang/translations/et.po +21 -0
  36. package/lang/translations/fi.po +21 -0
  37. package/lang/translations/fr.po +21 -0
  38. package/lang/translations/he.po +21 -0
  39. package/lang/translations/hi.po +21 -0
  40. package/lang/translations/it.po +1 -1
  41. package/lang/translations/ja.po +21 -0
  42. package/lang/translations/ko.po +21 -0
  43. package/lang/translations/lt.po +21 -0
  44. package/lang/translations/lv.po +21 -0
  45. package/lang/translations/ms.po +21 -0
  46. package/lang/translations/no.po +21 -0
  47. package/lang/translations/pt-br.po +1 -1
  48. package/lang/translations/pt.po +21 -0
  49. package/lang/translations/ro.po +21 -0
  50. package/lang/translations/sv.po +21 -0
  51. package/lang/translations/th.po +21 -0
  52. package/lang/translations/tr.po +21 -0
  53. package/lang/translations/uk.po +21 -0
  54. package/lang/translations/ur.po +21 -0
  55. package/lang/translations/vi.po +21 -0
  56. package/lang/translations/zh-cn.po +21 -0
  57. package/package.json +32 -31
  58. package/src/converters.js +16 -5
  59. package/src/datafilter.js +149 -13
  60. package/src/dataschema.js +3 -0
  61. package/src/generalhtmlsupport.js +7 -5
  62. package/src/integrations/codeblock.js +8 -1
  63. package/src/integrations/customelement.js +179 -0
  64. package/src/integrations/documentlist.js +10 -3
  65. package/src/integrations/dualcontent.js +19 -3
  66. package/src/integrations/heading.js +7 -0
  67. package/src/integrations/image.js +36 -10
  68. package/src/integrations/mediaembed.js +42 -6
  69. package/src/integrations/script.js +7 -0
  70. package/src/integrations/style.js +7 -0
  71. package/src/integrations/table.js +36 -7
  72. package/src/schemadefinitions.js +19 -0
  73. package/build/html-support.js.map +0 -1
package/src/datafilter.js CHANGED
@@ -7,6 +7,8 @@
7
7
  * @module html-support/datafilter
8
8
  */
9
9
 
10
+ /* globals document */
11
+
10
12
  import DataSchema from './dataschema';
11
13
 
12
14
  import { Plugin } from 'ckeditor5/src/core';
@@ -53,6 +55,9 @@ import '../theme/datafilter.css';
53
55
  * }
54
56
  * } );
55
57
  *
58
+ * To apply the information about allowed and disallowed attributes in custom integration plugin,
59
+ * use the {@link module:html-support/datafilter~DataFilter#processViewAttributes `processViewAttributes()`} method.
60
+ *
56
61
  * @extends module:core/plugin~Plugin
57
62
  */
58
63
  export default class DataFilter extends Plugin {
@@ -106,8 +111,18 @@ export default class DataFilter extends Plugin {
106
111
  */
107
112
  this._dataInitialized = false;
108
113
 
114
+ /**
115
+ * Cached map of coupled attributes. Keys are the feature attributes names
116
+ * and values are arrays with coupled GHS attributes names.
117
+ *
118
+ * @private
119
+ * @member {Map.<String,Array>}
120
+ */
121
+ this._coupledAttributes = null;
122
+
109
123
  this._registerElementsAfterInit();
110
124
  this._registerElementHandlers();
125
+ this._registerModelPostFixer();
111
126
  }
112
127
 
113
128
  /**
@@ -167,6 +182,9 @@ export default class DataFilter extends Plugin {
167
182
  if ( this._dataInitialized ) {
168
183
  this._fireRegisterEvent( definition );
169
184
  }
185
+
186
+ // Reset cached map to recalculate it on the next usage.
187
+ this._coupledAttributes = null;
170
188
  }
171
189
  }
172
190
 
@@ -208,17 +226,30 @@ export default class DataFilter extends Plugin {
208
226
  }
209
227
 
210
228
  /**
211
- * Matches and consumes allowed and disallowed view attributes and returns the allowed ones.
229
+ * Processes all allowed and disallowed attributes on the view element by consuming them and returning the allowed ones.
212
230
  *
213
- * @protected
231
+ * This method applies the configuration set up by {@link #allowAttributes `allowAttributes()`}
232
+ * and {@link #disallowAttributes `disallowAttributes()`} over the given view element by consuming relevant attributes.
233
+ * It returns the allowed attributes that were found on the given view element for further processing by integration code.
234
+ *
235
+ * dispatcher.on( 'element:myElement', ( evt, data, conversionApi ) => {
236
+ * // Get rid of disallowed and extract all allowed attributes from a viewElement.
237
+ * const viewAttributes = dataFilter.processViewAttributes( data.viewItem, conversionApi );
238
+ * // Do something with them, i.e. store inside a model as a dictionary.
239
+ * if ( viewAttributes ) {
240
+ * conversionApi.writer.setAttribute( 'htmlAttributesOfMyElement', viewAttributes, data.modelRange );
241
+ * }
242
+ * } );
243
+ *
244
+ * @see module:engine/conversion/viewconsumable~ViewConsumable#consume
214
245
  * @param {module:engine/view/element~Element} viewElement
215
- * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
246
+ * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi
216
247
  * @returns {Object} [result]
217
248
  * @returns {Object} result.attributes Set with matched attribute names.
218
249
  * @returns {Object} result.styles Set with matched style names.
219
250
  * @returns {Array.<String>} result.classes Set with matched class names.
220
251
  */
221
- _consumeAllowedAttributes( viewElement, conversionApi ) {
252
+ processViewAttributes( viewElement, conversionApi ) {
222
253
  // Make sure that the disabled attributes are handled before the allowed attributes are called.
223
254
  // For example, for block images the <figure> converter triggers conversion for <img> first and then for other elements, i.e. <a>.
224
255
  consumeAttributes( viewElement, conversionApi, this._disallowedAttributes );
@@ -288,6 +319,91 @@ export default class DataFilter extends Plugin {
288
319
  }, { priority: 'lowest' } );
289
320
  }
290
321
 
322
+ /**
323
+ * Registers a model post-fixer that is removing coupled GHS attributes of inline elements. Those attributes
324
+ * are removed if a coupled feature attribute is removed.
325
+ *
326
+ * For example, consider following HTML:
327
+ *
328
+ * <a href="foo.html" id="myId">bar</a>
329
+ *
330
+ * Which would be upcasted to following text node in the model:
331
+ *
332
+ * <$text linkHref="foo.html" htmlA="{ attributes: { id: 'myId' } }">bar</$text>
333
+ *
334
+ * When the user removes the link from that text (using UI), only `linkHref` attribute would be removed:
335
+ *
336
+ * <$text htmlA="{ attributes: { id: 'myId' } }">bar</$text>
337
+ *
338
+ * The `htmlA` attribute would stay in the model and would cause GHS to generate an `<a>` element.
339
+ * This is incorrect from UX point of view, as the user wanted to remove the whole link (not only `href`).
340
+ *
341
+ * @private
342
+ */
343
+ _registerModelPostFixer() {
344
+ const model = this.editor.model;
345
+
346
+ model.document.registerPostFixer( writer => {
347
+ const changes = model.document.differ.getChanges();
348
+ let changed = false;
349
+
350
+ const coupledAttributes = this._getCoupledAttributesMap();
351
+
352
+ for ( const change of changes ) {
353
+ // Handle only attribute removals.
354
+ if ( change.type != 'attribute' || change.attributeNewValue !== null ) {
355
+ continue;
356
+ }
357
+
358
+ // Find a list of coupled GHS attributes.
359
+ const attributeKeys = coupledAttributes.get( change.attributeKey );
360
+
361
+ if ( !attributeKeys ) {
362
+ continue;
363
+ }
364
+
365
+ // Remove the coupled GHS attributes on the same range as the feature attribute was removed.
366
+ for ( const { item } of change.range.getWalker( { shallow: true } ) ) {
367
+ for ( const attributeKey of attributeKeys ) {
368
+ if ( item.hasAttribute( attributeKey ) ) {
369
+ writer.removeAttribute( attributeKey, item );
370
+ changed = true;
371
+ }
372
+ }
373
+ }
374
+ }
375
+
376
+ return changed;
377
+ } );
378
+ }
379
+
380
+ /**
381
+ * Collects the map of coupled attributes. The returned map is keyed by the feature attribute name
382
+ * and coupled GHS attribute names are stored in the value array .
383
+ *
384
+ * @private
385
+ * @returns {Map.<String,Array>}
386
+ */
387
+ _getCoupledAttributesMap() {
388
+ if ( this._coupledAttributes ) {
389
+ return this._coupledAttributes;
390
+ }
391
+
392
+ this._coupledAttributes = new Map();
393
+
394
+ for ( const definition of this._allowedElements ) {
395
+ if ( definition.coupledAttribute && definition.model ) {
396
+ const attributeNames = this._coupledAttributes.get( definition.coupledAttribute );
397
+
398
+ if ( attributeNames ) {
399
+ attributeNames.push( definition.model );
400
+ } else {
401
+ this._coupledAttributes.set( definition.coupledAttribute, [ definition.model ] );
402
+ }
403
+ }
404
+ }
405
+ }
406
+
291
407
  /**
292
408
  * Fires `register` event for the given element definition.
293
409
  *
@@ -438,14 +554,14 @@ export default class DataFilter extends Plugin {
438
554
  * as an event namespace, e.g. `register:span`.
439
555
  *
440
556
  * dataFilter.on( 'register', ( evt, definition ) => {
441
- * editor.schema.register( definition.model, definition.modelSchema );
557
+ * editor.model.schema.register( definition.model, definition.modelSchema );
442
558
  * editor.conversion.elementToElement( { model: definition.model, view: definition.view } );
443
559
  *
444
560
  * evt.stop();
445
561
  * } );
446
562
  *
447
563
  * dataFilter.on( 'register:span', ( evt, definition ) => {
448
- * editor.schema.extend( '$text', { allowAttributes: 'htmlSpan' } );
564
+ * editor.model.schema.extend( '$text', { allowAttributes: 'htmlSpan' } );
449
565
  *
450
566
  * editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'span', model: 'htmlSpan' } );
451
567
  * editor.conversion.for( 'downcast' ).attributeToElement( { view: 'span', model: 'htmlSpan' } );
@@ -462,7 +578,7 @@ export default class DataFilter extends Plugin {
462
578
  //
463
579
  // @private
464
580
  // @param {module:engine/view/element~Element} viewElement
465
- // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
581
+ // @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi
466
582
  // @param {module:engine/view/matcher~Matcher Matcher} matcher
467
583
  // @returns {Object} [result]
468
584
  // @returns {Object} result.attributes
@@ -473,6 +589,15 @@ function consumeAttributes( viewElement, conversionApi, matcher ) {
473
589
  const { attributes, styles, classes } = mergeMatchResults( matches );
474
590
  const viewAttributes = {};
475
591
 
592
+ // Remove invalid DOM element attributes.
593
+ if ( attributes.size ) {
594
+ for ( const key of attributes ) {
595
+ if ( !isValidAttributeName( key ) ) {
596
+ attributes.delete( key );
597
+ }
598
+ }
599
+ }
600
+
476
601
  if ( attributes.size ) {
477
602
  viewAttributes.attributes = iterableToObject( attributes, key => viewElement.getAttribute( key ) );
478
603
  }
@@ -496,7 +621,7 @@ function consumeAttributes( viewElement, conversionApi, matcher ) {
496
621
  //
497
622
  // @private
498
623
  // @param {module:engine/view/element~Element} viewElement
499
- // @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
624
+ // @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi
500
625
  // @param {module:engine/view/matcher~Matcher Matcher} matcher
501
626
  // @returns {Array.<Object>} Array with match information about found attributes.
502
627
  function consumeAttributeMatches( viewElement, { consumable }, matcher ) {
@@ -509,9 +634,8 @@ function consumeAttributeMatches( viewElement, { consumable }, matcher ) {
509
634
  // We only want to consume attributes, so element can be still processed by other converters.
510
635
  delete match.match.name;
511
636
 
512
- if ( consumable.consume( viewElement, match.match ) ) {
513
- consumedMatches.push( match );
514
- }
637
+ consumable.consume( viewElement, match.match );
638
+ consumedMatches.push( match );
515
639
  }
516
640
 
517
641
  return consumedMatches;
@@ -521,7 +645,7 @@ function consumeAttributeMatches( viewElement, { consumable }, matcher ) {
521
645
  //
522
646
  // @private
523
647
  // @param {module:engine/view/element~Element} viewElement
524
- // @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable
648
+ // @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable
525
649
  // @param {Object} match
526
650
  function removeConsumedAttributes( consumable, viewElement, match ) {
527
651
  for ( const key of [ 'attributes', 'classes', 'styles' ] ) {
@@ -531,7 +655,8 @@ function removeConsumedAttributes( consumable, viewElement, match ) {
531
655
  continue;
532
656
  }
533
657
 
534
- for ( const value of attributes ) {
658
+ // Iterating over a copy of an array so removing items doesn't influence iteration.
659
+ for ( const value of Array.from( attributes ) ) {
535
660
  if ( !consumable.test( viewElement, ( { [ key ]: [ value ] } ) ) ) {
536
661
  removeItemFromArray( attributes, value );
537
662
  }
@@ -638,3 +763,14 @@ function splitRules( rules ) {
638
763
 
639
764
  return splittedRules;
640
765
  }
766
+
767
+ // Returns true if name is valid for a DOM attribute name.
768
+ function isValidAttributeName( name ) {
769
+ try {
770
+ document.createAttribute( name );
771
+ } catch ( error ) {
772
+ return false;
773
+ }
774
+
775
+ return true;
776
+ }
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
  */
@@ -20,6 +20,7 @@ import ScriptElementSupport from './integrations/script';
20
20
  import TableElementSupport from './integrations/table';
21
21
  import StyleElementSupport from './integrations/style';
22
22
  import DocumentListElementSupport from './integrations/documentlist';
23
+ import CustomElementSupport from './integrations/customelement';
23
24
 
24
25
  /**
25
26
  * The General HTML Support feature.
@@ -51,7 +52,8 @@ export default class GeneralHtmlSupport extends Plugin {
51
52
  ScriptElementSupport,
52
53
  TableElementSupport,
53
54
  StyleElementSupport,
54
- DocumentListElementSupport
55
+ DocumentListElementSupport,
56
+ CustomElementSupport
55
57
  ];
56
58
  }
57
59
 
@@ -86,7 +88,7 @@ export default class GeneralHtmlSupport extends Plugin {
86
88
  }
87
89
 
88
90
  /**
89
- * Updates GHS model attribute for a specified view element name, so it includes a given class name.
91
+ * Updates GHS model attribute for a specified view element name, so it includes the given class name.
90
92
  *
91
93
  * @protected
92
94
  * @param {String} viewElementName A view element name.
@@ -109,7 +111,7 @@ export default class GeneralHtmlSupport extends Plugin {
109
111
  }
110
112
 
111
113
  /**
112
- * Updates GHS model attribute for a specified view element name, so it does not include a given class name.
114
+ * Updates GHS model attribute for a specified view element name, so it does not include the given class name.
113
115
  *
114
116
  * @protected
115
117
  * @param {String} viewElementName A view element name.
@@ -132,7 +134,7 @@ export default class GeneralHtmlSupport extends Plugin {
132
134
  }
133
135
 
134
136
  /**
135
- * Updates GHS model attribute for a specified view element name, so it includes a given attribute.
137
+ * Updates GHS model attribute for a specified view element name, so it includes the given attribute.
136
138
  *
137
139
  * @protected
138
140
  * @param {String} viewElementName A view element name.
@@ -155,7 +157,7 @@ export default class GeneralHtmlSupport extends Plugin {
155
157
  }
156
158
 
157
159
  /**
158
- * Updates GHS model attribute for a specified view element name, so it does not include a given attribute.
160
+ * Updates GHS model attribute for a specified view element name, so it does not include the given attribute.
159
161
  *
160
162
  * @protected
161
163
  * @param {String} viewElementName A view element name.
@@ -25,6 +25,13 @@ export default class CodeBlockElementSupport extends Plugin {
25
25
  return [ DataFilter ];
26
26
  }
27
27
 
28
+ /**
29
+ * @inheritDoc
30
+ */
31
+ static get pluginName() {
32
+ return 'CodeBlockElementSupport';
33
+ }
34
+
28
35
  /**
29
36
  * @inheritDoc
30
37
  */
@@ -79,7 +86,7 @@ function viewToModelCodeBlockAttributeConverter( dataFilter ) {
79
86
  preserveElementAttributes( viewCodeElement, 'htmlContentAttributes' );
80
87
 
81
88
  function preserveElementAttributes( viewElement, attributeName ) {
82
- const viewAttributes = dataFilter._consumeAllowedAttributes( viewElement, conversionApi );
89
+ const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
83
90
 
84
91
  if ( viewAttributes ) {
85
92
  conversionApi.writer.setAttribute( attributeName, viewAttributes, data.modelRange );
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+
6
+ /**
7
+ * @module html-support/integrations/customelement
8
+ */
9
+
10
+ /* globals document */
11
+
12
+ import { Plugin } from 'ckeditor5/src/core';
13
+ import { UpcastWriter } from 'ckeditor5/src/engine';
14
+
15
+ import DataSchema from '../dataschema';
16
+ import DataFilter from '../datafilter';
17
+ import { setViewAttributes } from '../conversionutils';
18
+
19
+ /**
20
+ * Provides the General HTML Support for custom elements (not registered in the {@link module:html-support/dataschema~DataSchema}).
21
+ *
22
+ * @extends module:core/plugin~Plugin
23
+ */
24
+ export default class CustomElementSupport extends Plugin {
25
+ /**
26
+ * @inheritDoc
27
+ */
28
+ static get requires() {
29
+ return [ DataFilter, DataSchema ];
30
+ }
31
+
32
+ /**
33
+ * @inheritDoc
34
+ */
35
+ static get pluginName() {
36
+ return 'CustomElementSupport';
37
+ }
38
+
39
+ /**
40
+ * @inheritDoc
41
+ */
42
+ init() {
43
+ const dataFilter = this.editor.plugins.get( DataFilter );
44
+ const dataSchema = this.editor.plugins.get( DataSchema );
45
+
46
+ dataFilter.on( 'register:$customElement', ( evt, definition ) => {
47
+ evt.stop();
48
+
49
+ const editor = this.editor;
50
+ const schema = editor.model.schema;
51
+ const conversion = editor.conversion;
52
+ const unsafeElements = editor.editing.view.domConverter.unsafeElements;
53
+ const preLikeElements = editor.data.htmlProcessor.domConverter.preElements;
54
+
55
+ schema.register( definition.model, definition.modelSchema );
56
+ schema.extend( definition.model, {
57
+ allowAttributes: [ 'htmlElementName', 'htmlAttributes', 'htmlContent' ],
58
+ isContent: true
59
+ } );
60
+
61
+ // Being executed on the low priority, it will catch all elements that were not caught by other converters.
62
+ conversion.for( 'upcast' ).elementToElement( {
63
+ view: /.*/,
64
+ model: ( viewElement, conversionApi ) => {
65
+ // Do not try to convert $comment fake element.
66
+ if ( viewElement.name == '$comment' ) {
67
+ return;
68
+ }
69
+
70
+ if ( !isValidElementName( viewElement.name ) ) {
71
+ return;
72
+ }
73
+
74
+ // Allow for fallback only if this element is not defined in data schema to make sure
75
+ // that this will handle only custom elements not registered in the data schema.
76
+ if ( dataSchema.getDefinitionsForView( viewElement.name ).size ) {
77
+ return;
78
+ }
79
+
80
+ // Make sure that this element will not render in the editing view.
81
+ if ( !unsafeElements.includes( viewElement.name ) ) {
82
+ unsafeElements.push( viewElement.name );
83
+ }
84
+
85
+ // Make sure that whitespaces will not be trimmed or replaced by nbsps while stringify content.
86
+ if ( !preLikeElements.includes( viewElement.name ) ) {
87
+ preLikeElements.push( viewElement.name );
88
+ }
89
+
90
+ const modelElement = conversionApi.writer.createElement( definition.model, {
91
+ htmlElementName: viewElement.name
92
+ } );
93
+
94
+ const htmlAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
95
+
96
+ if ( htmlAttributes ) {
97
+ conversionApi.writer.setAttribute( 'htmlAttributes', htmlAttributes, modelElement );
98
+ }
99
+
100
+ // Store the whole element in the attribute so that DomConverter will be able to use the pre like element context.
101
+ const viewWriter = new UpcastWriter( viewElement.document );
102
+ const documentFragment = viewWriter.createDocumentFragment( viewElement );
103
+ const htmlContent = editor.data.processor.toData( documentFragment );
104
+
105
+ conversionApi.writer.setAttribute( 'htmlContent', htmlContent, modelElement );
106
+
107
+ // Consume the content of the element.
108
+ for ( const { item } of editor.editing.view.createRangeIn( viewElement ) ) {
109
+ conversionApi.consumable.consume( item, { name: true } );
110
+ }
111
+
112
+ return modelElement;
113
+ },
114
+ converterPriority: 'low'
115
+ } );
116
+
117
+ // Because this element is unsafe (DomConverter#unsafeElements), it will render as a transparent <span> but it must
118
+ // be rendered anyway for the mapping between the model and the view to exist.
119
+ conversion.for( 'editingDowncast' ).elementToElement( {
120
+ model: {
121
+ name: definition.model,
122
+ attributes: [ 'htmlElementName', 'htmlAttributes', 'htmlContent' ]
123
+ },
124
+ view: ( modelElement, { writer } ) => {
125
+ const viewName = modelElement.getAttribute( 'htmlElementName' );
126
+ const viewElement = writer.createRawElement( viewName );
127
+
128
+ if ( modelElement.hasAttribute( 'htmlAttributes' ) ) {
129
+ setViewAttributes( writer, modelElement.getAttribute( 'htmlAttributes' ), viewElement );
130
+ }
131
+
132
+ return viewElement;
133
+ }
134
+ } );
135
+
136
+ conversion.for( 'dataDowncast' ).elementToElement( {
137
+ model: {
138
+ name: definition.model,
139
+ attributes: [ 'htmlElementName', 'htmlAttributes', 'htmlContent' ]
140
+ },
141
+ view: ( modelElement, { writer } ) => {
142
+ const viewName = modelElement.getAttribute( 'htmlElementName' );
143
+ const htmlContent = modelElement.getAttribute( 'htmlContent' );
144
+
145
+ const viewElement = writer.createRawElement( viewName, null, ( domElement, domConverter ) => {
146
+ domConverter.setContentOf( domElement, htmlContent );
147
+
148
+ // Unwrap the custom element content (it was stored in the attribute as the whole custom element).
149
+ // See the upcast conversion for the "htmlContent" attribute to learn more.
150
+ const customElement = domElement.firstChild;
151
+
152
+ customElement.remove();
153
+
154
+ while ( customElement.firstChild ) {
155
+ domElement.appendChild( customElement.firstChild );
156
+ }
157
+ } );
158
+
159
+ if ( modelElement.hasAttribute( 'htmlAttributes' ) ) {
160
+ setViewAttributes( writer, modelElement.getAttribute( 'htmlAttributes' ), viewElement );
161
+ }
162
+
163
+ return viewElement;
164
+ }
165
+ } );
166
+ } );
167
+ }
168
+ }
169
+
170
+ // Returns true if name is valid for a DOM element name.
171
+ function isValidElementName( name ) {
172
+ try {
173
+ document.createElement( name );
174
+ } catch ( error ) {
175
+ return false;
176
+ }
177
+
178
+ return true;
179
+ }
@@ -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
  */
@@ -26,6 +26,13 @@ export default class DocumentListElementSupport extends Plugin {
26
26
  return [ DataFilter ];
27
27
  }
28
28
 
29
+ /**
30
+ * @inheritDoc
31
+ */
32
+ static get pluginName() {
33
+ return 'DocumentListElementSupport';
34
+ }
35
+
29
36
  /**
30
37
  * @inheritDoc
31
38
  */
@@ -85,7 +92,7 @@ export default class DocumentListElementSupport extends Plugin {
85
92
  } );
86
93
 
87
94
  // 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.
95
+ // Note: This is almost an exact copy from DocumentListPropertiesEditing.
89
96
  documentListEditing.on( 'postFixer', ( evt, { listNodes, writer } ) => {
90
97
  const previousNodesByIndent = []; // Last seen nodes of lower indented lists.
91
98
 
@@ -180,7 +187,7 @@ function viewToModelListAttributeConverter( attributeName, dataFilter ) {
180
187
  Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) );
181
188
  }
182
189
 
183
- const viewAttributes = dataFilter._consumeAllowedAttributes( viewElement, conversionApi );
190
+ const viewAttributes = dataFilter.processViewAttributes( viewElement, conversionApi );
184
191
 
185
192
  for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
186
193
  // Apply only to list item blocks.
@@ -43,6 +43,13 @@ export default class DualContentModelElementSupport extends Plugin {
43
43
  return [ DataFilter ];
44
44
  }
45
45
 
46
+ /**
47
+ * @inheritDoc
48
+ */
49
+ static get pluginName() {
50
+ return 'DualContentModelElementSupport';
51
+ }
52
+
46
53
  /**
47
54
  * @inheritDoc
48
55
  */
@@ -111,10 +118,19 @@ export default class DualContentModelElementSupport extends Plugin {
111
118
  * @returns {Boolean}
112
119
  */
113
120
  _hasBlockContent( viewElement ) {
114
- const blockElements = this.editor.editing.view.domConverter.blockElements;
121
+ const view = this.editor.editing.view;
122
+ const blockElements = view.domConverter.blockElements;
123
+
124
+ // Traversing the viewElement subtree looking for block elements.
125
+ // Especially for the cases like <div><a href="#"><p>foo</p></a></div>.
126
+ // https://github.com/ckeditor/ckeditor5/issues/11513
127
+ for ( const viewItem of view.createRangeIn( viewElement ).getItems() ) {
128
+ if ( viewItem.is( 'element' ) && blockElements.includes( viewItem.name ) ) {
129
+ return true;
130
+ }
131
+ }
115
132
 
116
- return Array.from( viewElement.getChildren() )
117
- .some( node => blockElements.includes( node.name ) );
133
+ return false;
118
134
  }
119
135
 
120
136
  /**
@@ -24,6 +24,13 @@ export default class HeadingElementSupport extends Plugin {
24
24
  return [ DataSchema ];
25
25
  }
26
26
 
27
+ /**
28
+ * @inheritDoc
29
+ */
30
+ static get pluginName() {
31
+ return 'HeadingElementSupport';
32
+ }
33
+
27
34
  /**
28
35
  * @inheritDoc
29
36
  */