@ckeditor/ckeditor5-html-support 44.2.1 → 44.3.0-alpha.1

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/dist/index.js CHANGED
@@ -433,6 +433,10 @@ var defaultConfig = {
433
433
  model: 'imageInline',
434
434
  view: 'img'
435
435
  },
436
+ {
437
+ model: 'horizontalLine',
438
+ view: 'hr'
439
+ },
436
440
  // Compatibility features.
437
441
  {
438
442
  model: 'htmlP',
@@ -856,6 +860,14 @@ var defaultConfig = {
856
860
  inheritAllFrom: '$container',
857
861
  isBlock: false
858
862
  }
863
+ },
864
+ {
865
+ model: 'htmlHr',
866
+ view: 'hr',
867
+ isEmpty: true,
868
+ modelSchema: {
869
+ inheritAllFrom: '$blockObject'
870
+ }
859
871
  }
860
872
  ],
861
873
  inline: [
@@ -2021,6 +2033,11 @@ var defaultConfig = {
2021
2033
  const conversion = editor.conversion;
2022
2034
  const { view: viewName, model: modelName } = definition;
2023
2035
  if (!schema.isRegistered(definition.model)) {
2036
+ // Do not register converters and empty schema for editor existing feature
2037
+ // as empty schema won't allow element anywhere in the model.
2038
+ if (!definition.modelSchema) {
2039
+ return;
2040
+ }
2024
2041
  schema.register(definition.model, definition.modelSchema);
2025
2042
  if (!viewName) {
2026
2043
  return;
@@ -2035,7 +2052,7 @@ var defaultConfig = {
2035
2052
  });
2036
2053
  conversion.for('downcast').elementToElement({
2037
2054
  model: modelName,
2038
- view: viewName
2055
+ view: (modelElement, { writer })=>definition.isEmpty ? writer.createEmptyElement(viewName) : writer.createContainerElement(viewName)
2039
2056
  });
2040
2057
  }
2041
2058
  if (!viewName) {
@@ -2112,49 +2129,50 @@ var defaultConfig = {
2112
2129
  const matches = matcher.matchAll(viewElement) || [];
2113
2130
  const stylesProcessor = viewElement.document.stylesProcessor;
2114
2131
  return matches.reduce((result, { match })=>{
2115
- // Verify and consume styles.
2116
- for (const style of match.styles || []){
2117
- // Check longer forms of the same style as those could be matched
2118
- // but not present in the element directly.
2119
- // Consider only longhand (or longer than current notation) so that
2120
- // we do not include all sides of the box if only one side is allowed.
2121
- const sortedRelatedStyles = stylesProcessor.getRelatedStyles(style).filter((relatedStyle)=>relatedStyle.split('-').length > style.split('-').length).sort((a, b)=>b.split('-').length - a.split('-').length);
2122
- for (const relatedStyle of sortedRelatedStyles){
2132
+ for (const [key, token] of match.attributes || []){
2133
+ // Verify and consume styles.
2134
+ if (key == 'style') {
2135
+ const style = token;
2136
+ // Check longer forms of the same style as those could be matched
2137
+ // but not present in the element directly.
2138
+ // Consider only longhand (or longer than current notation) so that
2139
+ // we do not include all sides of the box if only one side is allowed.
2140
+ const sortedRelatedStyles = stylesProcessor.getRelatedStyles(style).filter((relatedStyle)=>relatedStyle.split('-').length > style.split('-').length).sort((a, b)=>b.split('-').length - a.split('-').length);
2141
+ for (const relatedStyle of sortedRelatedStyles){
2142
+ if (consumable.consume(viewElement, {
2143
+ styles: [
2144
+ relatedStyle
2145
+ ]
2146
+ })) {
2147
+ result.styles.push(relatedStyle);
2148
+ }
2149
+ }
2150
+ // Verify and consume style as specified in the matcher.
2123
2151
  if (consumable.consume(viewElement, {
2124
2152
  styles: [
2125
- relatedStyle
2153
+ style
2126
2154
  ]
2127
2155
  })) {
2128
- result.styles.push(relatedStyle);
2156
+ result.styles.push(style);
2157
+ }
2158
+ } else if (key == 'class') {
2159
+ const className = token;
2160
+ if (consumable.consume(viewElement, {
2161
+ classes: [
2162
+ className
2163
+ ]
2164
+ })) {
2165
+ result.classes.push(className);
2166
+ }
2167
+ } else {
2168
+ // Verify and consume other attributes.
2169
+ if (consumable.consume(viewElement, {
2170
+ attributes: [
2171
+ key
2172
+ ]
2173
+ })) {
2174
+ result.attributes.push(key);
2129
2175
  }
2130
- }
2131
- // Verify and consume style as specified in the matcher.
2132
- if (consumable.consume(viewElement, {
2133
- styles: [
2134
- style
2135
- ]
2136
- })) {
2137
- result.styles.push(style);
2138
- }
2139
- }
2140
- // Verify and consume class names.
2141
- for (const className of match.classes || []){
2142
- if (consumable.consume(viewElement, {
2143
- classes: [
2144
- className
2145
- ]
2146
- })) {
2147
- result.classes.push(className);
2148
- }
2149
- }
2150
- // Verify and consume other attributes.
2151
- for (const attributeName of match.attributes || []){
2152
- if (consumable.consume(viewElement, {
2153
- attributes: [
2154
- attributeName
2155
- ]
2156
- })) {
2157
- result.attributes.push(attributeName);
2158
2176
  }
2159
2177
  }
2160
2178
  return result;
@@ -3310,6 +3328,75 @@ function modelToViewMediaAttributeConverter(mediaElementName) {
3310
3328
  return listType === 'numbered' || listType == 'customNumbered' ? 'htmlOlAttributes' : 'htmlUlAttributes';
3311
3329
  }
3312
3330
 
3331
+ /**
3332
+ * Provides the General HTML Support integration with the {@link module:horizontal-line/horizontalline~HorizontalLine} feature.
3333
+ */ class HorizontalLineElementSupport extends Plugin {
3334
+ /**
3335
+ * @inheritDoc
3336
+ */ static get requires() {
3337
+ return [
3338
+ DataFilter
3339
+ ];
3340
+ }
3341
+ /**
3342
+ * @inheritDoc
3343
+ */ static get pluginName() {
3344
+ return 'HorizontalLineElementSupport';
3345
+ }
3346
+ /**
3347
+ * @inheritDoc
3348
+ */ static get isOfficialPlugin() {
3349
+ return true;
3350
+ }
3351
+ /**
3352
+ * @inheritDoc
3353
+ */ init() {
3354
+ const editor = this.editor;
3355
+ if (!editor.plugins.has('HorizontalLineEditing')) {
3356
+ return;
3357
+ }
3358
+ const schema = editor.model.schema;
3359
+ const conversion = editor.conversion;
3360
+ const dataFilter = editor.plugins.get(DataFilter);
3361
+ dataFilter.on('register:hr', (evt, definition)=>{
3362
+ if (definition.model !== 'horizontalLine') {
3363
+ return;
3364
+ }
3365
+ schema.extend('horizontalLine', {
3366
+ allowAttributes: [
3367
+ 'htmlHrAttributes'
3368
+ ]
3369
+ });
3370
+ conversion.for('upcast').add(viewToModelBlockAttributeConverter(definition, dataFilter));
3371
+ conversion.for('downcast').add(modelToViewHorizontalLineAttributeConverter());
3372
+ evt.stop();
3373
+ });
3374
+ }
3375
+ }
3376
+ /**
3377
+ * A model-to-view conversion helper applying attributes from the
3378
+ * {@link module:horizontal-line/horizontalline~HorizontalLine HorizontalLine} feature.
3379
+ *
3380
+ * @returns Returns a conversion callback.
3381
+ */ function modelToViewHorizontalLineAttributeConverter() {
3382
+ return (dispatcher)=>{
3383
+ dispatcher.on('attribute:htmlHrAttributes:horizontalLine', (evt, data, conversionApi)=>{
3384
+ if (!conversionApi.consumable.test(data.item, evt.name)) {
3385
+ return;
3386
+ }
3387
+ const { attributeOldValue, attributeNewValue } = data;
3388
+ const containerElement = conversionApi.mapper.toViewElement(data.item);
3389
+ const viewElement = getDescendantElement(conversionApi.writer, containerElement, 'hr');
3390
+ if (viewElement) {
3391
+ updateViewAttributes(conversionApi.writer, attributeOldValue, attributeNewValue, viewElement);
3392
+ conversionApi.consumable.consume(data.item, evt.name);
3393
+ }
3394
+ }, {
3395
+ priority: 'low'
3396
+ });
3397
+ };
3398
+ }
3399
+
3313
3400
  /**
3314
3401
  * Provides the General HTML Support for custom elements (not registered in the {@link module:html-support/dataschema~DataSchema}).
3315
3402
  */ class CustomElementSupport extends Plugin {
@@ -3501,6 +3588,7 @@ function modelToViewMediaAttributeConverter(mediaElementName) {
3501
3588
  TableElementSupport,
3502
3589
  StyleElementSupport,
3503
3590
  ListElementSupport,
3591
+ HorizontalLineElementSupport,
3504
3592
  CustomElementSupport
3505
3593
  ];
3506
3594
  }
@@ -4036,5 +4124,154 @@ function modelToViewMediaAttributeConverter(mediaElementName) {
4036
4124
  }
4037
4125
  }
4038
4126
 
4039
- export { DataFilter, DataSchema, FullPage, GeneralHtmlSupport, HtmlComment, HtmlPageDataProcessor };
4127
+ const EMPTY_BLOCK_MODEL_ATTRIBUTE = 'htmlEmptyBlock';
4128
+ /**
4129
+ * This is experimental plugin that allows for preserving empty block elements
4130
+ * in the editor content instead of automatically filling them with block fillers (` `).
4131
+ *
4132
+ * This is useful when you want to:
4133
+ *
4134
+ * * Preserve empty block elements exactly as they were in the source HTML.
4135
+ * * Allow for styling empty blocks with CSS (block fillers can interfere with height/margin).
4136
+ * * Maintain compatibility with external systems that expect empty blocks to remain empty.
4137
+ *
4138
+ * Known limitations:
4139
+ *
4140
+ * * Empty blocks may not work correctly with revision history features.
4141
+ * * Keyboard navigation through the document might behave unexpectedly, especially when
4142
+ * navigating through structures like lists and tables.
4143
+ *
4144
+ * For example, this allows for HTML like:
4145
+ *
4146
+ * ```html
4147
+ * <p></p>
4148
+ * <p class="spacer"></p>
4149
+ * <td></td>
4150
+ * ```
4151
+ * to remain empty instead of being converted to:
4152
+ *
4153
+ * ```html
4154
+ * <p>&nbsp;</p>
4155
+ * <p class="spacer">&nbsp;</p>
4156
+ * <td>&nbsp;</td>
4157
+ * ```
4158
+ *
4159
+ * **Warning**: This is an experimental plugin. It may have bugs and breaking changes may be introduced without prior notice.
4160
+ */ class EmptyBlock extends Plugin {
4161
+ /**
4162
+ * @inheritDoc
4163
+ */ static get pluginName() {
4164
+ return 'EmptyBlock';
4165
+ }
4166
+ /**
4167
+ * @inheritDoc
4168
+ */ static get isOfficialPlugin() {
4169
+ return true;
4170
+ }
4171
+ /**
4172
+ * @inheritDoc
4173
+ */ afterInit() {
4174
+ const { model, conversion, plugins, config } = this.editor;
4175
+ const schema = model.schema;
4176
+ const preserveEmptyBlocksInEditingView = config.get('htmlSupport.preserveEmptyBlocksInEditingView');
4177
+ schema.extend('$block', {
4178
+ allowAttributes: [
4179
+ EMPTY_BLOCK_MODEL_ATTRIBUTE
4180
+ ]
4181
+ });
4182
+ schema.extend('$container', {
4183
+ allowAttributes: [
4184
+ EMPTY_BLOCK_MODEL_ATTRIBUTE
4185
+ ]
4186
+ });
4187
+ if (schema.isRegistered('tableCell')) {
4188
+ schema.extend('tableCell', {
4189
+ allowAttributes: [
4190
+ EMPTY_BLOCK_MODEL_ATTRIBUTE
4191
+ ]
4192
+ });
4193
+ }
4194
+ if (preserveEmptyBlocksInEditingView) {
4195
+ conversion.for('downcast').add(createEmptyBlockDowncastConverter());
4196
+ } else {
4197
+ conversion.for('dataDowncast').add(createEmptyBlockDowncastConverter());
4198
+ }
4199
+ conversion.for('upcast').add(createEmptyBlockUpcastConverter(schema));
4200
+ if (plugins.has('ClipboardPipeline')) {
4201
+ this._registerClipboardPastingHandler();
4202
+ }
4203
+ }
4204
+ /**
4205
+ * Handle clipboard paste events:
4206
+ *
4207
+ * * It does not affect *copying* content from the editor, only *pasting*.
4208
+ * * When content is pasted from another editor instance with `<p></p>`,
4209
+ * the `&nbsp;` filler is added, so the getData result is `<p>&nbsp;</p>`.
4210
+ * * When content is pasted from the same editor instance with `<p></p>`,
4211
+ * the `&nbsp;` filler is not added, so the getData result is `<p></p>`.
4212
+ */ _registerClipboardPastingHandler() {
4213
+ const clipboardPipeline = this.editor.plugins.get('ClipboardPipeline');
4214
+ this.listenTo(clipboardPipeline, 'contentInsertion', (evt, data)=>{
4215
+ if (data.sourceEditorId === this.editor.id) {
4216
+ return;
4217
+ }
4218
+ this.editor.model.change((writer)=>{
4219
+ for (const { item } of writer.createRangeIn(data.content)){
4220
+ if (item.is('element') && item.hasAttribute(EMPTY_BLOCK_MODEL_ATTRIBUTE)) {
4221
+ writer.removeAttribute(EMPTY_BLOCK_MODEL_ATTRIBUTE, item);
4222
+ }
4223
+ }
4224
+ });
4225
+ });
4226
+ }
4227
+ }
4228
+ /**
4229
+ * Creates a downcast converter for handling empty blocks.
4230
+ * This converter prevents filler elements from being added to elements marked as empty blocks.
4231
+ */ function createEmptyBlockDowncastConverter() {
4232
+ return (dispatcher)=>{
4233
+ dispatcher.on(`attribute:${EMPTY_BLOCK_MODEL_ATTRIBUTE}`, (evt, data, conversionApi)=>{
4234
+ const { mapper, consumable } = conversionApi;
4235
+ const { item } = data;
4236
+ if (!consumable.consume(item, evt.name)) {
4237
+ return;
4238
+ }
4239
+ const viewElement = mapper.toViewElement(item);
4240
+ if (viewElement && data.attributeNewValue) {
4241
+ viewElement.getFillerOffset = ()=>null;
4242
+ }
4243
+ });
4244
+ };
4245
+ }
4246
+ /**
4247
+ * Creates an upcast converter for handling empty blocks.
4248
+ * The converter detects empty elements and marks them with the empty block attribute.
4249
+ */ function createEmptyBlockUpcastConverter(schema) {
4250
+ return (dispatcher)=>{
4251
+ dispatcher.on('element', (evt, data, conversionApi)=>{
4252
+ const { viewItem, modelRange } = data;
4253
+ if (!viewItem.is('element') || !viewItem.isEmpty || viewItem.getCustomProperty('$hasBlockFiller')) {
4254
+ return;
4255
+ }
4256
+ // Handle element itself.
4257
+ const modelElement = modelRange && modelRange.start.nodeAfter;
4258
+ if (!modelElement || !schema.checkAttribute(modelElement, EMPTY_BLOCK_MODEL_ATTRIBUTE)) {
4259
+ return;
4260
+ }
4261
+ conversionApi.writer.setAttribute(EMPTY_BLOCK_MODEL_ATTRIBUTE, true, modelElement);
4262
+ // Handle an auto-paragraphed bogus paragraph inside empty element.
4263
+ if (modelElement.childCount != 1) {
4264
+ return;
4265
+ }
4266
+ const firstModelChild = modelElement.getChild(0);
4267
+ if (firstModelChild.is('element', 'paragraph') && schema.checkAttribute(firstModelChild, EMPTY_BLOCK_MODEL_ATTRIBUTE)) {
4268
+ conversionApi.writer.setAttribute(EMPTY_BLOCK_MODEL_ATTRIBUTE, true, firstModelChild);
4269
+ }
4270
+ }, {
4271
+ priority: 'lowest'
4272
+ });
4273
+ };
4274
+ }
4275
+
4276
+ export { DataFilter, DataSchema, EmptyBlock, FullPage, GeneralHtmlSupport, HtmlComment, HtmlPageDataProcessor };
4040
4277
  //# sourceMappingURL=index.js.map