@ckeditor/ckeditor5-html-support 29.0.0 → 31.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.
package/src/datafilter.js CHANGED
@@ -14,8 +14,6 @@ import { Matcher } from 'ckeditor5/src/engine';
14
14
  import { priorities, CKEditorError } from 'ckeditor5/src/utils';
15
15
  import { Widget } from 'ckeditor5/src/widget';
16
16
  import {
17
- disallowedAttributesConverter,
18
-
19
17
  viewToModelObjectConverter,
20
18
  toObjectWidgetConverter,
21
19
  createObjectView,
@@ -210,7 +208,7 @@ export default class DataFilter extends Plugin {
210
208
  }
211
209
 
212
210
  /**
213
- * Matches and consumes allowed view attributes.
211
+ * Matches and consumes allowed and disallowed view attributes and returns the allowed ones.
214
212
  *
215
213
  * @protected
216
214
  * @param {module:engine/view/element~Element} viewElement
@@ -221,22 +219,11 @@ export default class DataFilter extends Plugin {
221
219
  * @returns {Array.<String>} result.classes Set with matched class names.
222
220
  */
223
221
  _consumeAllowedAttributes( viewElement, conversionApi ) {
224
- return consumeAttributes( viewElement, conversionApi, this._allowedAttributes );
225
- }
222
+ // Make sure that the disabled attributes are handled before the allowed attributes are called.
223
+ // For example, for block images the <figure> converter triggers conversion for <img> first and then for other elements, i.e. <a>.
224
+ consumeAttributes( viewElement, conversionApi, this._disallowedAttributes );
226
225
 
227
- /**
228
- * Matches and consumes disallowed view attributes.
229
- *
230
- * @protected
231
- * @param {module:engine/view/element~Element} viewElement
232
- * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
233
- * @returns {Object} [result]
234
- * @returns {Object} result.attributes Set with matched attribute names.
235
- * @returns {Object} result.styles Set with matched style names.
236
- * @returns {Array.<String>} result.classes Set with matched class names.
237
- */
238
- _consumeDisallowedAttributes( viewElement, conversionApi ) {
239
- return consumeAttributes( viewElement, conversionApi, this._disallowedAttributes );
226
+ return consumeAttributes( viewElement, conversionApi, this._allowedAttributes );
240
227
  }
241
228
 
242
229
  /**
@@ -335,7 +322,6 @@ export default class DataFilter extends Plugin {
335
322
  name: viewName
336
323
  } );
337
324
 
338
- conversion.for( 'upcast' ).add( disallowedAttributesConverter( definition, this ) );
339
325
  conversion.for( 'upcast' ).elementToElement( {
340
326
  view: viewName,
341
327
  model: viewToModelObjectConverter( definition ),
@@ -400,7 +386,6 @@ export default class DataFilter extends Plugin {
400
386
  allowAttributes: 'htmlAttributes'
401
387
  } );
402
388
 
403
- conversion.for( 'upcast' ).add( disallowedAttributesConverter( definition, this ) );
404
389
  conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, this ) );
405
390
  conversion.for( 'downcast' ).add( modelToViewBlockAttributeConverter( definition ) );
406
391
  }
@@ -427,7 +412,6 @@ export default class DataFilter extends Plugin {
427
412
  schema.setAttributeProperties( attributeKey, definition.attributeProperties );
428
413
  }
429
414
 
430
- conversion.for( 'upcast' ).add( disallowedAttributesConverter( definition, this ) );
431
415
  conversion.for( 'upcast' ).add( viewToAttributeInlineConverter( definition, this ) );
432
416
 
433
417
  conversion.for( 'downcast' ).attributeToElement( {
package/src/dataschema.js CHANGED
@@ -10,6 +10,7 @@
10
10
  import { Plugin } from 'ckeditor5/src/core';
11
11
  import { toArray } from 'ckeditor5/src/utils';
12
12
  import defaultConfig from './schemadefinitions';
13
+ import { mergeWith } from 'lodash-es';
13
14
 
14
15
  /**
15
16
  * Holds representation of the extended HTML document type definitions to be used by the
@@ -93,6 +94,30 @@ export default class DataSchema extends Plugin {
93
94
  this._definitions.set( definition.model, { ...definition, isInline: true } );
94
95
  }
95
96
 
97
+ /**
98
+ * Updates schema definition describing block element with new properties.
99
+ *
100
+ * Creates new scheme if it doesn't exist.
101
+ * Array properties are concatenated with original values.
102
+ *
103
+ * @param {module:html-support/dataschema~DataSchemaBlockElementDefinition} definition Definition update.
104
+ */
105
+ extendBlockElement( definition ) {
106
+ this._extendDefinition( { ...definition, isBlock: true } );
107
+ }
108
+
109
+ /**
110
+ * Updates schema definition describing inline element with new properties.
111
+ *
112
+ * Creates new scheme if it doesn't exist.
113
+ * Array properties are concatenated with original values.
114
+ *
115
+ * @param {module:html-support/dataschema~DataSchemaInlineElementDefinition} definition Definition update.
116
+ */
117
+ extendInlineElement( definition ) {
118
+ this._extendDefinition( { ...definition, isInline: true } );
119
+ }
120
+
96
121
  /**
97
122
  * Returns all definitions matching the given view name.
98
123
  *
@@ -155,6 +180,25 @@ export default class DataSchema extends Plugin {
155
180
  }
156
181
  }
157
182
  }
183
+
184
+ /**
185
+ * Updates schema definition with new properties.
186
+ *
187
+ * Creates new scheme if it doesn't exist.
188
+ * Array properties are concatenated with original values.
189
+ *
190
+ * @private
191
+ * @param {module:html-support/dataschema~DataSchemaDefinition} definition Definition update.
192
+ */
193
+ _extendDefinition( definition ) {
194
+ const currentDefinition = this._definitions.get( definition.model );
195
+
196
+ const mergedDefinition = mergeWith( {}, currentDefinition, definition, ( target, source ) => {
197
+ return Array.isArray( target ) ? target.concat( source ) : undefined;
198
+ } );
199
+
200
+ this._definitions.set( definition.model, mergedDefinition );
201
+ }
158
202
  }
159
203
 
160
204
  // Test view name against the given pattern.
@@ -191,6 +235,10 @@ function testViewName( pattern, viewName ) {
191
235
  * @typedef {Object} module:html-support/dataschema~DataSchemaBlockElementDefinition
192
236
  * @property {Boolean} isBlock Indicates that the definition describes block element.
193
237
  * Set by {@link module:html-support/dataschema~DataSchema#registerBlockElement} method.
238
+ * @property {String} [paragraphLikeModel] Should be used when an element can behave both as a sectioning element (e.g. article) and
239
+ * element accepting only inline content (e.g. paragraph).
240
+ * If an element contains only inline content, this option will be used as a model
241
+ * name.
194
242
  * @extends module:html-support/dataschema~DataSchemaDefinition
195
243
  */
196
244
 
@@ -199,7 +247,7 @@ function testViewName( pattern, viewName ) {
199
247
  *
200
248
  * @typedef {Object} module:html-support/dataschema~DataSchemaInlineElementDefinition
201
249
  * @property {module:engine/model/schema~AttributeProperties} [attributeProperties] Additional metadata describing the model attribute.
202
- * @property {Boolean} isInline Indicates that the definition descibes inline element.
250
+ * @property {Boolean} isInline Indicates that the definition describes inline element.
203
251
  * @property {Number} [priority] Element priority. Decides in what order elements are wrapped by
204
252
  * {@link module:engine/view/downcastwriter~DowncastWriter}.
205
253
  * Set by {@link module:html-support/dataschema~DataSchema#registerInlineElement} method.
@@ -8,8 +8,14 @@
8
8
  */
9
9
 
10
10
  import { Plugin } from 'ckeditor5/src/core';
11
+
11
12
  import DataFilter from './datafilter';
12
- import CodeBlockHtmlSupport from './integrations/codeblock';
13
+ import CodeBlockElementSupport from './integrations/codeblock';
14
+ import DualContentModelElementSupport from './integrations/dualcontent';
15
+ import HeadingElementSupport from './integrations/heading';
16
+ import ImageElementSupport from './integrations/image';
17
+ import MediaEmbedElementSupport from './integrations/mediaembed';
18
+ import TableElementSupport from './integrations/table';
13
19
 
14
20
  /**
15
21
  * The General HTML Support feature.
@@ -27,24 +33,32 @@ export default class GeneralHtmlSupport extends Plugin {
27
33
  return 'GeneralHtmlSupport';
28
34
  }
29
35
 
30
- init() {
31
- const editor = this.editor;
32
- const dataFilter = editor.plugins.get( DataFilter );
33
-
34
- // Load the filtering configuration.
35
- dataFilter.loadAllowedConfig( editor.config.get( 'htmlSupport.allow' ) || [] );
36
- dataFilter.loadDisallowedConfig( editor.config.get( 'htmlSupport.disallow' ) || [] );
37
- }
38
-
39
36
  /**
40
37
  * @inheritDoc
41
38
  */
42
39
  static get requires() {
43
40
  return [
44
41
  DataFilter,
45
- CodeBlockHtmlSupport
42
+ CodeBlockElementSupport,
43
+ DualContentModelElementSupport,
44
+ HeadingElementSupport,
45
+ ImageElementSupport,
46
+ MediaEmbedElementSupport,
47
+ TableElementSupport
46
48
  ];
47
49
  }
50
+
51
+ /**
52
+ * @inheritDoc
53
+ */
54
+ init() {
55
+ const editor = this.editor;
56
+ const dataFilter = editor.plugins.get( DataFilter );
57
+
58
+ // Load the filtering configuration.
59
+ dataFilter.loadAllowedConfig( editor.config.get( 'htmlSupport.allow' ) || [] );
60
+ dataFilter.loadDisallowedConfig( editor.config.get( 'htmlSupport.disallow' ) || [] );
61
+ }
48
62
  }
49
63
 
50
64
  /**
@@ -0,0 +1,250 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+
6
+ /**
7
+ * @module html-support/htmlcomment
8
+ */
9
+
10
+ import { Plugin } from 'ckeditor5/src/core';
11
+ import { uid } from 'ckeditor5/src/utils';
12
+
13
+ /**
14
+ * The HTML comment feature. It preserves the HTML comments (`<!-- -->`) in the editor data.
15
+ *
16
+ * For a detailed overview, check the {@glink features/general-html-support#html-comments HTML comment feature documentation}.
17
+ *
18
+ * @extends module:core/plugin~Plugin
19
+ */
20
+ export default class HtmlComment extends Plugin {
21
+ /**
22
+ * @inheritDoc
23
+ */
24
+ static get pluginName() {
25
+ return 'HtmlComment';
26
+ }
27
+
28
+ /**
29
+ * @inheritDoc
30
+ */
31
+ init() {
32
+ const editor = this.editor;
33
+
34
+ // Allow storing comment's content as the $root attribute with the name `$comment:<unique id>`.
35
+ editor.model.schema.addAttributeCheck( ( context, attributeName ) => {
36
+ if ( context.endsWith( '$root' ) && attributeName.startsWith( '$comment' ) ) {
37
+ return true;
38
+ }
39
+ } );
40
+
41
+ // Convert the `$comment` view element to `$comment:<unique id>` marker and store its content (the comment itself) as a $root
42
+ // attribute. The comment content is needed in the `dataDowncast` pipeline to re-create the comment node.
43
+ editor.conversion.for( 'upcast' ).elementToMarker( {
44
+ view: '$comment',
45
+ model: ( viewElement, { writer } ) => {
46
+ const root = this.editor.model.document.getRoot();
47
+ const commentContent = viewElement.getCustomProperty( '$rawContent' );
48
+ const markerName = `$comment:${ uid() }`;
49
+
50
+ writer.setAttribute( markerName, commentContent, root );
51
+
52
+ return markerName;
53
+ }
54
+ } );
55
+
56
+ // Convert the `$comment` marker to `$comment` UI element with `$rawContent` custom property containing the comment content.
57
+ editor.conversion.for( 'dataDowncast' ).markerToElement( {
58
+ model: '$comment',
59
+ view: ( modelElement, { writer } ) => {
60
+ const root = this.editor.model.document.getRoot();
61
+ const markerName = modelElement.markerName;
62
+ const commentContent = root.getAttribute( markerName );
63
+ const comment = writer.createUIElement( '$comment' );
64
+
65
+ writer.setCustomProperty( '$rawContent', commentContent, comment );
66
+
67
+ return comment;
68
+ }
69
+ } );
70
+
71
+ // Remove comments' markers and their corresponding $root attributes, which are no longer present.
72
+ editor.model.document.registerPostFixer( writer => {
73
+ const root = editor.model.document.getRoot();
74
+
75
+ const changedMarkers = editor.model.document.differ.getChangedMarkers();
76
+
77
+ const changedCommentMarkers = changedMarkers.filter( marker => {
78
+ return marker.name.startsWith( '$comment' );
79
+ } );
80
+
81
+ const removedCommentMarkers = changedCommentMarkers.filter( marker => {
82
+ const newRange = marker.data.newRange;
83
+
84
+ return newRange && newRange.root.rootName === '$graveyard';
85
+ } );
86
+
87
+ if ( removedCommentMarkers.length === 0 ) {
88
+ return false;
89
+ }
90
+
91
+ for ( const marker of removedCommentMarkers ) {
92
+ writer.removeMarker( marker.name );
93
+ writer.removeAttribute( marker.name, root );
94
+ }
95
+
96
+ return true;
97
+ } );
98
+
99
+ // Delete all comment markers from the document before setting new data.
100
+ editor.data.on( 'set', () => {
101
+ for ( const commentMarker of editor.model.markers.getMarkersGroup( '$comment' ) ) {
102
+ this.removeHtmlComment( commentMarker.name );
103
+ }
104
+ }, { priority: 'high' } );
105
+
106
+ // Delete all comment markers that are within a removed range.
107
+ // Delete all comment markers at the limit element boundaries if the whole content of the limit element is removed.
108
+ editor.model.on( 'deleteContent', ( evt, [ selection ] ) => {
109
+ for ( const range of selection.getRanges() ) {
110
+ const limitElement = editor.model.schema.getLimitElement( range );
111
+ const firstPosition = editor.model.createPositionAt( limitElement, 0 );
112
+ const lastPosition = editor.model.createPositionAt( limitElement, 'end' );
113
+
114
+ let affectedCommentIDs;
115
+
116
+ if ( firstPosition.isTouching( range.start ) && lastPosition.isTouching( range.end ) ) {
117
+ affectedCommentIDs = this.getHtmlCommentsInRange( editor.model.createRange( firstPosition, lastPosition ) );
118
+ } else {
119
+ affectedCommentIDs = this.getHtmlCommentsInRange( range, { skipBoundaries: true } );
120
+ }
121
+
122
+ for ( const commentMarkerID of affectedCommentIDs ) {
123
+ this.removeHtmlComment( commentMarkerID );
124
+ }
125
+ }
126
+ }, { priority: 'high' } );
127
+ }
128
+
129
+ /**
130
+ * Creates an HTML comment on the specified position and returns its ID.
131
+ *
132
+ * *Note*: If two comments are created at the same position, the second comment will be inserted before the first one.
133
+ *
134
+ * @param {module:engine/model/position~Position} position
135
+ * @param {String} content
136
+ * @returns {String} Comment ID. This ID can be later used to e.g. remove the comment from the content.
137
+ */
138
+ createHtmlComment( position, content ) {
139
+ const id = uid();
140
+ const editor = this.editor;
141
+ const model = editor.model;
142
+ const root = model.document.getRoot();
143
+ const markerName = `$comment:${ id }`;
144
+
145
+ return model.change( writer => {
146
+ const range = writer.createRange( position );
147
+
148
+ writer.addMarker( markerName, {
149
+ usingOperation: true,
150
+ affectsData: true,
151
+ range
152
+ } );
153
+
154
+ writer.setAttribute( markerName, content, root );
155
+
156
+ return markerName;
157
+ } );
158
+ }
159
+
160
+ /**
161
+ * Removes an HTML comment with the given comment ID.
162
+ *
163
+ * It does nothing and returns `false` if the comment with the given ID does not exist.
164
+ * Otherwise it removes the comment and returns `true`.
165
+ *
166
+ * Note that a comment can be removed also by removing the content around the comment.
167
+ *
168
+ * @param {String} commentID The ID of the comment to be removed.
169
+ * @returns {Boolean} `true` when the comment with the given ID was removed, `false` otherwise.
170
+ */
171
+ removeHtmlComment( commentID ) {
172
+ const editor = this.editor;
173
+ const root = editor.model.document.getRoot();
174
+
175
+ const marker = editor.model.markers.get( commentID );
176
+
177
+ if ( !marker ) {
178
+ return false;
179
+ }
180
+
181
+ editor.model.change( writer => {
182
+ writer.removeMarker( marker );
183
+ writer.removeAttribute( commentID, root );
184
+ } );
185
+
186
+ return true;
187
+ }
188
+
189
+ /**
190
+ * Gets the HTML comment data for the comment with a given ID.
191
+ *
192
+ * Returns `null` if the comment does not exist.
193
+ *
194
+ * @param {String} commentID
195
+ * @returns {module:html-support/htmlcomment~HtmlCommentData}
196
+ */
197
+ getHtmlCommentData( commentID ) {
198
+ const editor = this.editor;
199
+ const marker = editor.model.markers.get( commentID );
200
+ const root = editor.model.document.getRoot();
201
+
202
+ if ( !marker ) {
203
+ return null;
204
+ }
205
+
206
+ return {
207
+ content: root.getAttribute( commentID ),
208
+ position: marker.getStart()
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Gets all HTML comments in the given range.
214
+ *
215
+ * By default it includes comments at the range boundaries.
216
+ *
217
+ * @param {module:engine/model/range~Range} range
218
+ * @param {Object} [options]
219
+ * @param {Boolean} [options.skipBoundaries=false] When set to `true` the range boundaries will be skipped.
220
+ * @returns {Array.<String>} HTML comment IDs
221
+ */
222
+ getHtmlCommentsInRange( range, { skipBoundaries = false } = {} ) {
223
+ const includeBoundaries = !skipBoundaries;
224
+
225
+ // Unfortunately, MarkerCollection#getMarkersAtPosition() filters out collapsed markers.
226
+ return Array.from( this.editor.model.markers.getMarkersGroup( '$comment' ) )
227
+ .filter( marker => isCommentMarkerInRange( marker, range ) )
228
+ .map( marker => marker.name );
229
+
230
+ function isCommentMarkerInRange( commentMarker, range ) {
231
+ const position = commentMarker.getRange().start;
232
+
233
+ return (
234
+ ( position.isAfter( range.start ) || ( includeBoundaries && position.isEqual( range.start ) ) ) &&
235
+ ( position.isBefore( range.end ) || ( includeBoundaries && position.isEqual( range.end ) ) )
236
+ );
237
+ }
238
+ }
239
+ }
240
+
241
+ /**
242
+ * An interface for the HTML comments data.
243
+ *
244
+ * It consists of the {@link module:engine/model/position~Position `position`} and `content`.
245
+ *
246
+ * @typedef {Object} module:html-support/htmlcomment~HtmlCommentData
247
+ *
248
+ * @property {module:engine/model/position~Position} position
249
+ * @property {String} content
250
+ */
package/src/index.js CHANGED
@@ -10,3 +10,4 @@
10
10
  export { default as GeneralHtmlSupport } from './generalhtmlsupport';
11
11
  export { default as DataFilter } from './datafilter';
12
12
  export { default as DataSchema } from './dataschema';
13
+ export { default as HtmlComment } from './htmlcomment';
@@ -8,7 +8,6 @@
8
8
  */
9
9
 
10
10
  import { Plugin } from 'ckeditor5/src/core';
11
- import { disallowedAttributesConverter } from '../converters';
12
11
  import { setViewAttributes } from '../conversionutils.js';
13
12
 
14
13
  import DataFilter from '../datafilter';
@@ -18,11 +17,17 @@ import DataFilter from '../datafilter';
18
17
  *
19
18
  * @extends module:core/plugin~Plugin
20
19
  */
21
- export default class CodeBlockHtmlSupport extends Plugin {
20
+ export default class CodeBlockElementSupport extends Plugin {
21
+ /**
22
+ * @inheritDoc
23
+ */
22
24
  static get requires() {
23
25
  return [ DataFilter ];
24
26
  }
25
27
 
28
+ /**
29
+ * @inheritDoc
30
+ */
26
31
  init() {
27
32
  if ( !this.editor.plugins.has( 'CodeBlockEditing' ) ) {
28
33
  return;
@@ -44,7 +49,6 @@ export default class CodeBlockHtmlSupport extends Plugin {
44
49
  allowAttributes: [ 'htmlAttributes', 'htmlContentAttributes' ]
45
50
  } );
46
51
 
47
- conversion.for( 'upcast' ).add( disallowedAttributesConverter( definition, dataFilter ) );
48
52
  conversion.for( 'upcast' ).add( viewToModelCodeBlockAttributeConverter( dataFilter ) );
49
53
  conversion.for( 'downcast' ).add( modelToViewCodeBlockAttributeConverter() );
50
54
 
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. 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/dualcontent
8
+ */
9
+
10
+ import { Plugin } from 'ckeditor5/src/core';
11
+ import { priorities } from 'ckeditor5/src/utils';
12
+ import {
13
+ modelToViewBlockAttributeConverter,
14
+ viewToModelBlockAttributeConverter
15
+ } from '../converters';
16
+
17
+ import DataFilter from '../datafilter';
18
+
19
+ /**
20
+ * Provides the General HTML Support integration for elements which can behave like sectioning element (e.g. article) or
21
+ * element accepting only inline content (e.g. paragraph).
22
+ *
23
+ * The distinction between this two content models is important for choosing correct schema model and proper content conversion.
24
+ * As an example, it ensures that:
25
+ *
26
+ * * children elements paragraphing is enabled for sectioning elements only,
27
+ * * element and its content can be correctly handled by editing view (splitting and merging elements),
28
+ * * model element HTML is semantically correct and easier to work with.
29
+ *
30
+ * If element contains any block element, it will be treated as a sectioning element and registered using
31
+ * {@link module:html-support/dataschema~DataSchemaDefinition#model} and
32
+ * {@link module:html-support/dataschema~DataSchemaDefinition#modelSchema} in editor schema.
33
+ * Otherwise, it will be registered under {@link module:html-support/dataschema~DataSchemaBlockElementDefinition#paragraphLikeModel} model
34
+ * name with model schema accepting only inline content (inheriting from `$block`).
35
+ *
36
+ * @extends module:core/plugin~Plugin
37
+ */
38
+ export default class DualContentModelElementSupport extends Plugin {
39
+ /**
40
+ * @inheritDoc
41
+ */
42
+ static get requires() {
43
+ return [ DataFilter ];
44
+ }
45
+
46
+ /**
47
+ * @inheritDoc
48
+ */
49
+ init() {
50
+ const dataFilter = this.editor.plugins.get( DataFilter );
51
+
52
+ dataFilter.on( 'register', ( evt, definition ) => {
53
+ const editor = this.editor;
54
+ const schema = editor.model.schema;
55
+ const conversion = editor.conversion;
56
+
57
+ if ( !definition.paragraphLikeModel ) {
58
+ return;
59
+ }
60
+
61
+ // Can only apply to newly registered features.
62
+ if ( schema.isRegistered( definition.model ) || schema.isRegistered( definition.paragraphLikeModel ) ) {
63
+ return;
64
+ }
65
+
66
+ const paragraphLikeModelDefinition = {
67
+ model: definition.paragraphLikeModel,
68
+ view: definition.view
69
+ };
70
+
71
+ schema.register( definition.model, definition.modelSchema );
72
+ schema.register( paragraphLikeModelDefinition.model, {
73
+ inheritAllFrom: '$block'
74
+ } );
75
+
76
+ conversion.for( 'upcast' ).elementToElement( {
77
+ view: definition.view,
78
+ model: ( viewElement, { writer } ) => {
79
+ if ( this._hasBlockContent( viewElement ) ) {
80
+ return writer.createElement( definition.model );
81
+ }
82
+
83
+ return writer.createElement( paragraphLikeModelDefinition.model );
84
+ },
85
+ // With a `low` priority, `paragraph` plugin auto-paragraphing mechanism is executed. Make sure
86
+ // this listener is called before it. If not, some elements will be transformed into a paragraph.
87
+ converterPriority: priorities.get( 'low' ) + 1
88
+ } );
89
+
90
+ conversion.for( 'downcast' ).elementToElement( {
91
+ view: definition.view,
92
+ model: definition.model
93
+ } );
94
+ this._addAttributeConversion( definition );
95
+
96
+ conversion.for( 'downcast' ).elementToElement( {
97
+ view: paragraphLikeModelDefinition.view,
98
+ model: paragraphLikeModelDefinition.model
99
+ } );
100
+ this._addAttributeConversion( paragraphLikeModelDefinition );
101
+
102
+ evt.stop();
103
+ } );
104
+ }
105
+
106
+ /**
107
+ * Checks whether the given view element includes any other block element.
108
+ *
109
+ * @private
110
+ * @param {module:engine/view/element~Element} viewElement
111
+ * @returns {Boolean}
112
+ */
113
+ _hasBlockContent( viewElement ) {
114
+ const blockElements = this.editor.editing.view.domConverter.blockElements;
115
+
116
+ return Array.from( viewElement.getChildren() )
117
+ .some( node => blockElements.includes( node.name ) );
118
+ }
119
+
120
+ /**
121
+ * Adds attribute filtering conversion for the given data schema.
122
+ *
123
+ * @private
124
+ * @param {module:html-support/dataschema~DataSchemaBlockElementDefinition} definition
125
+ */
126
+ _addAttributeConversion( definition ) {
127
+ const editor = this.editor;
128
+ const conversion = editor.conversion;
129
+ const dataFilter = editor.plugins.get( DataFilter );
130
+
131
+ editor.model.schema.extend( definition.model, {
132
+ allowAttributes: 'htmlAttributes'
133
+ } );
134
+
135
+ conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, dataFilter ) );
136
+ conversion.for( 'downcast' ).add( modelToViewBlockAttributeConverter( definition ) );
137
+ }
138
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. 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/heading
8
+ */
9
+
10
+ import { Plugin } from 'ckeditor5/src/core';
11
+
12
+ import DataSchema from '../dataschema';
13
+
14
+ /**
15
+ * Provides the General HTML Support integration with {@link module:heading/heading~Heading Heading} feature.
16
+ *
17
+ * @extends module:core/plugin~Plugin
18
+ */
19
+ export default class HeadingElementSupport extends Plugin {
20
+ /**
21
+ * @inheritDoc
22
+ */
23
+ static get requires() {
24
+ return [ DataSchema ];
25
+ }
26
+
27
+ /**
28
+ * @inheritDoc
29
+ */
30
+ init() {
31
+ const editor = this.editor;
32
+
33
+ if ( !editor.plugins.has( 'HeadingEditing' ) ) {
34
+ return;
35
+ }
36
+
37
+ const dataSchema = editor.plugins.get( DataSchema );
38
+ const options = editor.config.get( 'heading.options' );
39
+ const headerModels = [];
40
+
41
+ // We are registering all elements supported by HeadingEditing
42
+ // to enable custom attributes for those elements.
43
+ for ( const option of options ) {
44
+ if ( 'model' in option && 'view' in option ) {
45
+ dataSchema.registerBlockElement( {
46
+ view: option.view,
47
+ model: option.model
48
+ } );
49
+
50
+ headerModels.push( option.model );
51
+ }
52
+ }
53
+
54
+ dataSchema.extendBlockElement( {
55
+ model: 'htmlHgroup',
56
+ modelSchema: {
57
+ allowChildren: headerModels
58
+ }
59
+ } );
60
+ }
61
+ }