@ckeditor/ckeditor5-engine 30.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 (117) hide show
  1. package/LICENSE.md +17 -0
  2. package/README.md +30 -0
  3. package/package.json +70 -0
  4. package/src/controller/datacontroller.js +563 -0
  5. package/src/controller/editingcontroller.js +149 -0
  6. package/src/conversion/conversion.js +644 -0
  7. package/src/conversion/conversionhelpers.js +40 -0
  8. package/src/conversion/downcastdispatcher.js +914 -0
  9. package/src/conversion/downcasthelpers.js +1706 -0
  10. package/src/conversion/mapper.js +696 -0
  11. package/src/conversion/modelconsumable.js +329 -0
  12. package/src/conversion/upcastdispatcher.js +807 -0
  13. package/src/conversion/upcasthelpers.js +997 -0
  14. package/src/conversion/viewconsumable.js +623 -0
  15. package/src/dataprocessor/basichtmlwriter.js +32 -0
  16. package/src/dataprocessor/dataprocessor.jsdoc +64 -0
  17. package/src/dataprocessor/htmldataprocessor.js +159 -0
  18. package/src/dataprocessor/htmlwriter.js +22 -0
  19. package/src/dataprocessor/xmldataprocessor.js +161 -0
  20. package/src/dev-utils/model.js +482 -0
  21. package/src/dev-utils/operationreplayer.js +140 -0
  22. package/src/dev-utils/utils.js +103 -0
  23. package/src/dev-utils/view.js +1091 -0
  24. package/src/index.js +52 -0
  25. package/src/model/batch.js +82 -0
  26. package/src/model/differ.js +1282 -0
  27. package/src/model/document.js +483 -0
  28. package/src/model/documentfragment.js +390 -0
  29. package/src/model/documentselection.js +1261 -0
  30. package/src/model/element.js +438 -0
  31. package/src/model/history.js +138 -0
  32. package/src/model/item.jsdoc +14 -0
  33. package/src/model/liveposition.js +182 -0
  34. package/src/model/liverange.js +221 -0
  35. package/src/model/markercollection.js +553 -0
  36. package/src/model/model.js +934 -0
  37. package/src/model/node.js +507 -0
  38. package/src/model/nodelist.js +217 -0
  39. package/src/model/operation/attributeoperation.js +202 -0
  40. package/src/model/operation/detachoperation.js +103 -0
  41. package/src/model/operation/insertoperation.js +188 -0
  42. package/src/model/operation/markeroperation.js +154 -0
  43. package/src/model/operation/mergeoperation.js +216 -0
  44. package/src/model/operation/moveoperation.js +209 -0
  45. package/src/model/operation/nooperation.js +58 -0
  46. package/src/model/operation/operation.js +139 -0
  47. package/src/model/operation/operationfactory.js +49 -0
  48. package/src/model/operation/renameoperation.js +155 -0
  49. package/src/model/operation/rootattributeoperation.js +211 -0
  50. package/src/model/operation/splitoperation.js +254 -0
  51. package/src/model/operation/transform.js +2389 -0
  52. package/src/model/operation/utils.js +292 -0
  53. package/src/model/position.js +1164 -0
  54. package/src/model/range.js +1049 -0
  55. package/src/model/rootelement.js +111 -0
  56. package/src/model/schema.js +1851 -0
  57. package/src/model/selection.js +902 -0
  58. package/src/model/text.js +138 -0
  59. package/src/model/textproxy.js +279 -0
  60. package/src/model/treewalker.js +414 -0
  61. package/src/model/utils/autoparagraphing.js +77 -0
  62. package/src/model/utils/deletecontent.js +528 -0
  63. package/src/model/utils/getselectedcontent.js +150 -0
  64. package/src/model/utils/insertcontent.js +824 -0
  65. package/src/model/utils/modifyselection.js +229 -0
  66. package/src/model/utils/selection-post-fixer.js +297 -0
  67. package/src/model/writer.js +1574 -0
  68. package/src/view/attributeelement.js +274 -0
  69. package/src/view/containerelement.js +123 -0
  70. package/src/view/document.js +221 -0
  71. package/src/view/documentfragment.js +273 -0
  72. package/src/view/documentselection.js +387 -0
  73. package/src/view/domconverter.js +1437 -0
  74. package/src/view/downcastwriter.js +2121 -0
  75. package/src/view/editableelement.js +118 -0
  76. package/src/view/element.js +945 -0
  77. package/src/view/elementdefinition.jsdoc +59 -0
  78. package/src/view/emptyelement.js +119 -0
  79. package/src/view/filler.js +161 -0
  80. package/src/view/item.jsdoc +14 -0
  81. package/src/view/matcher.js +776 -0
  82. package/src/view/node.js +391 -0
  83. package/src/view/observer/arrowkeysobserver.js +58 -0
  84. package/src/view/observer/bubblingemittermixin.js +307 -0
  85. package/src/view/observer/bubblingeventinfo.js +71 -0
  86. package/src/view/observer/clickobserver.js +46 -0
  87. package/src/view/observer/compositionobserver.js +79 -0
  88. package/src/view/observer/domeventdata.js +82 -0
  89. package/src/view/observer/domeventobserver.js +99 -0
  90. package/src/view/observer/fakeselectionobserver.js +118 -0
  91. package/src/view/observer/focusobserver.js +106 -0
  92. package/src/view/observer/inputobserver.js +44 -0
  93. package/src/view/observer/keyobserver.js +83 -0
  94. package/src/view/observer/mouseobserver.js +56 -0
  95. package/src/view/observer/mutationobserver.js +345 -0
  96. package/src/view/observer/observer.js +118 -0
  97. package/src/view/observer/selectionobserver.js +242 -0
  98. package/src/view/placeholder.js +285 -0
  99. package/src/view/position.js +426 -0
  100. package/src/view/range.js +533 -0
  101. package/src/view/rawelement.js +148 -0
  102. package/src/view/renderer.js +1037 -0
  103. package/src/view/rooteditableelement.js +107 -0
  104. package/src/view/selection.js +718 -0
  105. package/src/view/styles/background.js +73 -0
  106. package/src/view/styles/border.js +362 -0
  107. package/src/view/styles/margin.js +41 -0
  108. package/src/view/styles/padding.js +40 -0
  109. package/src/view/styles/utils.js +277 -0
  110. package/src/view/stylesmap.js +938 -0
  111. package/src/view/text.js +147 -0
  112. package/src/view/textproxy.js +199 -0
  113. package/src/view/treewalker.js +496 -0
  114. package/src/view/uielement.js +238 -0
  115. package/src/view/upcastwriter.js +484 -0
  116. package/src/view/view.js +721 -0
  117. package/theme/placeholder.css +27 -0
@@ -0,0 +1,1706 @@
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
+ * Contains downcast (model-to-view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
8
+ *
9
+ * @module engine/conversion/downcasthelpers
10
+ */
11
+
12
+ import ModelRange from '../model/range';
13
+ import ModelSelection from '../model/selection';
14
+ import ModelElement from '../model/element';
15
+
16
+ import ViewAttributeElement from '../view/attributeelement';
17
+ import DocumentSelection from '../model/documentselection';
18
+ import ConversionHelpers from './conversionhelpers';
19
+
20
+ import { cloneDeep } from 'lodash-es';
21
+ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
22
+ import toArray from '@ckeditor/ckeditor5-utils/src/toarray';
23
+
24
+ /**
25
+ * Downcast conversion helper functions.
26
+ *
27
+ * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
28
+ */
29
+ export default class DowncastHelpers extends ConversionHelpers {
30
+ /**
31
+ * Model element to view element conversion helper.
32
+ *
33
+ * This conversion results in creating a view element. For example, model `<paragraph>Foo</paragraph>` becomes `<p>Foo</p>` in the view.
34
+ *
35
+ * editor.conversion.for( 'downcast' ).elementToElement( {
36
+ * model: 'paragraph',
37
+ * view: 'p'
38
+ * } );
39
+ *
40
+ * editor.conversion.for( 'downcast' ).elementToElement( {
41
+ * model: 'paragraph',
42
+ * view: 'div',
43
+ * converterPriority: 'high'
44
+ * } );
45
+ *
46
+ * editor.conversion.for( 'downcast' ).elementToElement( {
47
+ * model: 'fancyParagraph',
48
+ * view: {
49
+ * name: 'p',
50
+ * classes: 'fancy'
51
+ * }
52
+ * } );
53
+ *
54
+ * editor.conversion.for( 'downcast' ).elementToElement( {
55
+ * model: 'heading',
56
+ * view: ( modelElement, conversionApi ) => {
57
+ * const { writer } = conversionApi;
58
+ *
59
+ * return writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
60
+ * }
61
+ * } );
62
+ *
63
+ * The element-to-element conversion supports the reconversion mechanism. This is helpful in the conversion to complex view structures
64
+ * where multiple atomic element-to-element and attribute-to-attribute or attribute-to-element could be used. By specifying
65
+ * `triggerBy()` events you can trigger reconverting the model to full view tree structures at once.
66
+ *
67
+ * editor.conversion.for( 'downcast' ).elementToElement( {
68
+ * model: 'complex',
69
+ * view: ( modelElement, conversionApi ) => createComplexViewFromModel( modelElement, conversionApi ),
70
+ * triggerBy: {
71
+ * attributes: [ 'foo', 'bar' ],
72
+ * children: [ 'slot' ]
73
+ * }
74
+ * } );
75
+ *
76
+ * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
77
+ * to the conversion process.
78
+ *
79
+ * You can read more about element-to-element conversion in the
80
+ * {@glink framework/guides/deep-dive/conversion/custom-element-conversion Custom element conversion} guide.
81
+ *
82
+ * @method #elementToElement
83
+ * @param {Object} config Conversion configuration.
84
+ * @param {String} config.model The name of the model element to convert.
85
+ * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
86
+ * that takes the model element and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
87
+ * as parameters and returns a view container element.
88
+ * @param {Object} [config.triggerBy] Reconversion triggers. At least one trigger must be defined.
89
+ * @param {Array.<String>} config.triggerBy.attributes The name of the element's attributes whose change will trigger element
90
+ * reconversion.
91
+ * @param {Array.<String>} config.triggerBy.children The name of direct children whose adding or removing will trigger element
92
+ * reconversion.
93
+ * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
94
+ */
95
+ elementToElement( config ) {
96
+ return this.add( downcastElementToElement( config ) );
97
+ }
98
+
99
+ /**
100
+ * Model attribute to view element conversion helper.
101
+ *
102
+ * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with
103
+ * `"Foo"` as data and the `bold` attribute becomes `<strong>Foo</strong>` in the view.
104
+ *
105
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
106
+ * model: 'bold',
107
+ * view: 'strong'
108
+ * } );
109
+ *
110
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
111
+ * model: 'bold',
112
+ * view: 'b',
113
+ * converterPriority: 'high'
114
+ * } );
115
+ *
116
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
117
+ * model: 'invert',
118
+ * view: {
119
+ * name: 'span',
120
+ * classes: [ 'font-light', 'bg-dark' ]
121
+ * }
122
+ * } );
123
+ *
124
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
125
+ * model: {
126
+ * key: 'fontSize',
127
+ * values: [ 'big', 'small' ]
128
+ * },
129
+ * view: {
130
+ * big: {
131
+ * name: 'span',
132
+ * styles: {
133
+ * 'font-size': '1.2em'
134
+ * }
135
+ * },
136
+ * small: {
137
+ * name: 'span',
138
+ * styles: {
139
+ * 'font-size': '0.8em'
140
+ * }
141
+ * }
142
+ * }
143
+ * } );
144
+ *
145
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
146
+ * model: 'bold',
147
+ * view: ( modelAttributeValue, conversionApi ) => {
148
+ * const { writer } = conversionApi;
149
+ *
150
+ * return writer.createAttributeElement( 'span', {
151
+ * style: 'font-weight:' + modelAttributeValue
152
+ * } );
153
+ * }
154
+ * } );
155
+ *
156
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
157
+ * model: {
158
+ * key: 'color',
159
+ * name: '$text'
160
+ * },
161
+ * view: ( modelAttributeValue, conversionApi ) => {
162
+ * const { writer } = conversionApi;
163
+ *
164
+ * return writer.createAttributeElement( 'span', {
165
+ * style: 'color:' + modelAttributeValue
166
+ * } );
167
+ * }
168
+ * } );
169
+ *
170
+ * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
171
+ * to the conversion process.
172
+ *
173
+ * @method #attributeToElement
174
+ * @param {Object} config Conversion configuration.
175
+ * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
176
+ * of `String`s with possible values if the model attribute is an enumerable.
177
+ * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function
178
+ * that takes the model attribute value and
179
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as parameters and returns a view
180
+ * attribute element. If `config.model.values` is given, `config.view` should be an object assigning values from `config.model.values`
181
+ * to view element definitions or functions.
182
+ * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
183
+ * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
184
+ */
185
+ attributeToElement( config ) {
186
+ return this.add( downcastAttributeToElement( config ) );
187
+ }
188
+
189
+ /**
190
+ * Model attribute to view attribute conversion helper.
191
+ *
192
+ * This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example,
193
+ * `<imageInline src='foo.jpg'></imageInline>` is converted to `<img src='foo.jpg'></img>`.
194
+ *
195
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
196
+ * model: 'source',
197
+ * view: 'src'
198
+ * } );
199
+ *
200
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
201
+ * model: 'source',
202
+ * view: 'href',
203
+ * converterPriority: 'high'
204
+ * } );
205
+ *
206
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
207
+ * model: {
208
+ * name: 'imageInline',
209
+ * key: 'source'
210
+ * },
211
+ * view: 'src'
212
+ * } );
213
+ *
214
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
215
+ * model: {
216
+ * name: 'styled',
217
+ * values: [ 'dark', 'light' ]
218
+ * },
219
+ * view: {
220
+ * dark: {
221
+ * key: 'class',
222
+ * value: [ 'styled', 'styled-dark' ]
223
+ * },
224
+ * light: {
225
+ * key: 'class',
226
+ * value: [ 'styled', 'styled-light' ]
227
+ * }
228
+ * }
229
+ * } );
230
+ *
231
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
232
+ * model: 'styled',
233
+ * view: modelAttributeValue => ( {
234
+ * key: 'class',
235
+ * value: 'styled-' + modelAttributeValue
236
+ * } )
237
+ * } );
238
+ *
239
+ * **Note**: Downcasting to a style property requires providing `value` as an object:
240
+ *
241
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
242
+ * model: 'lineHeight',
243
+ * view: modelAttributeValue => ( {
244
+ * key: 'style',
245
+ * value: {
246
+ * 'line-height': modelAttributeValue,
247
+ * 'border-bottom': '1px dotted #ba2'
248
+ * }
249
+ * } )
250
+ * } );
251
+ *
252
+ * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
253
+ * to the conversion process.
254
+ *
255
+ * @method #attributeToAttribute
256
+ * @param {Object} config Conversion configuration.
257
+ * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
258
+ * the attribute key, possible values and, optionally, an element name to convert from.
259
+ * @param {String|Object|Function} config.view A view attribute key, or a `{ key, value }` object or a function that takes
260
+ * the model attribute value and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
261
+ * as parameters and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an
262
+ * array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`.
263
+ * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
264
+ * `{ key, value }` objects or a functions.
265
+ * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
266
+ * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
267
+ */
268
+ attributeToAttribute( config ) {
269
+ return this.add( downcastAttributeToAttribute( config ) );
270
+ }
271
+
272
+ /**
273
+ * Model marker to view element conversion helper.
274
+ *
275
+ * **Note**: This method should be used mainly for editing downcast and it is recommended
276
+ * to use {@link #markerToData `#markerToData()`} helper instead.
277
+ *
278
+ * This helper may produce invalid HTML code (e.g. a span between table cells).
279
+ * It should be used only when you are sure that the produced HTML will be semantically correct.
280
+ *
281
+ * This conversion results in creating a view element on the boundaries of the converted marker. If the converted marker
282
+ * is collapsed, only one element is created. For example, model marker set like this: `<paragraph>F[oo b]ar</paragraph>`
283
+ * becomes `<p>F<span data-marker="search"></span>oo b<span data-marker="search"></span>ar</p>` in the view.
284
+ *
285
+ * editor.conversion.for( 'editingDowncast' ).markerToElement( {
286
+ * model: 'search',
287
+ * view: 'marker-search'
288
+ * } );
289
+ *
290
+ * editor.conversion.for( 'editingDowncast' ).markerToElement( {
291
+ * model: 'search',
292
+ * view: 'search-result',
293
+ * converterPriority: 'high'
294
+ * } );
295
+ *
296
+ * editor.conversion.for( 'editingDowncast' ).markerToElement( {
297
+ * model: 'search',
298
+ * view: {
299
+ * name: 'span',
300
+ * attributes: {
301
+ * 'data-marker': 'search'
302
+ * }
303
+ * }
304
+ * } );
305
+ *
306
+ * editor.conversion.for( 'editingDowncast' ).markerToElement( {
307
+ * model: 'search',
308
+ * view: ( markerData, conversionApi ) => {
309
+ * const { writer } = conversionApi;
310
+ *
311
+ * return writer.createUIElement( 'span', {
312
+ * 'data-marker': 'search',
313
+ * 'data-start': markerData.isOpening
314
+ * } );
315
+ * }
316
+ * } );
317
+ *
318
+ * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function
319
+ * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
320
+ * as a parameters and should return an instance of the
321
+ * {@link module:engine/view/uielement~UIElement view UI element}. The `data` object and
322
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi `conversionApi`} are passed from
323
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
324
+ * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` to
325
+ * the marker end boundary element.
326
+ *
327
+ * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
328
+ * to the conversion process.
329
+ *
330
+ * @method #markerToElement
331
+ * @param {Object} config Conversion configuration.
332
+ * @param {String} config.model The name of the model marker (or model marker group) to convert.
333
+ * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function that
334
+ * takes the model marker data and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
335
+ * as a parameters and returns a view UI element.
336
+ * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
337
+ * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
338
+ */
339
+ markerToElement( config ) {
340
+ return this.add( downcastMarkerToElement( config ) );
341
+ }
342
+
343
+ /**
344
+ * Model marker to highlight conversion helper.
345
+ *
346
+ * This conversion results in creating a highlight on view nodes. For this kind of conversion,
347
+ * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor} should be provided.
348
+ *
349
+ * For text nodes, a `<span>` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes
350
+ * in the converted marker range. For example, a model marker set like this: `<paragraph>F[oo b]ar</paragraph>` becomes
351
+ * `<p>F<span class="comment">oo b</span>ar</p>` in the view.
352
+ *
353
+ * {@link module:engine/view/containerelement~ContainerElement} may provide a custom way of handling highlight. Most often,
354
+ * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in `<span>`).
355
+ * For example, a model marker set like this:
356
+ * `[<imageInline src="foo.jpg"></imageInline>]` becomes `<img src="foo.jpg" class="comment"></img>` in the view.
357
+ *
358
+ * For container elements, the conversion is two-step. While the converter processes the highlight descriptor and passes it
359
+ * to a container element, it is the container element instance itself that applies values from the highlight descriptor.
360
+ * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that.
361
+ *
362
+ * editor.conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
363
+ *
364
+ * editor.conversion.for( 'downcast' ).markerToHighlight( {
365
+ * model: 'comment',
366
+ * view: { classes: 'comment' },
367
+ * converterPriority: 'high'
368
+ * } );
369
+ *
370
+ * editor.conversion.for( 'downcast' ).markerToHighlight( {
371
+ * model: 'comment',
372
+ * view: ( data, conversionApi ) => {
373
+ * // Assuming that the marker name is in a form of comment:commentType:commentId.
374
+ * const [ , commentType, commentId ] = data.markerName.split( ':' );
375
+ *
376
+ * return {
377
+ * classes: [ 'comment', 'comment-' + commentType ],
378
+ * attributes: { 'data-comment-id': commentId }
379
+ * };
380
+ * }
381
+ * } );
382
+ *
383
+ * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function
384
+ * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
385
+ * as a parameters and should return a
386
+ * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor}.
387
+ * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
388
+ *
389
+ * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
390
+ * to the conversion process.
391
+ *
392
+ * @method #markerToHighlight
393
+ * @param {Object} config Conversion configuration.
394
+ * @param {String} config.model The name of the model marker (or model marker group) to convert.
395
+ * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} config.view A highlight descriptor
396
+ * that will be used for highlighting or a function that takes the model marker data and
397
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
398
+ * and returns a highlight descriptor.
399
+ * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
400
+ * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
401
+ */
402
+ markerToHighlight( config ) {
403
+ return this.add( downcastMarkerToHighlight( config ) );
404
+ }
405
+
406
+ /**
407
+ * Model marker converter for data downcast.
408
+ *
409
+ * This conversion creates a representation for model marker boundaries in the view:
410
+ *
411
+ * * If the marker boundary is before or after a model element, a view attribute is set on a corresponding view element.
412
+ * * In other cases, a view element with the specified tag name is inserted at the corresponding view position.
413
+ *
414
+ * Typically, the marker names use the `group:uniqueId:otherData` convention. For example: `comment:e34zfk9k2n459df53sjl34:zx32c`.
415
+ * The default configuration for this conversion is that the first part is the `group` part and the rest of
416
+ * the marker name becomes the `name` part.
417
+ *
418
+ * Tag and attribute names and values are generated from the marker name:
419
+ *
420
+ * * The templates for attributes are `data-[group]-start-before="[name]"`, `data-[group]-start-after="[name]"`,
421
+ * `data-[group]-end-before="[name]"` and `data-[group]-end-after="[name]"`.
422
+ * * The templates for view elements are `<[group]-start name="[name]">` and `<[group]-end name="[name]">`.
423
+ *
424
+ * Attributes mark whether the given marker's start or end boundary is before or after the given element.
425
+ * The `data-[group]-start-before` and `data-[group]-end-after` attributes are favored.
426
+ * The other two are used when the former two cannot be used.
427
+ *
428
+ * The conversion configuration can take a function that will generate different group and name parts.
429
+ * If such a function is set as the `config.view` parameter, it is passed a marker name and it is expected to return an object with two
430
+ * properties: `group` and `name`. If the function returns a falsy value, the conversion will not take place.
431
+ *
432
+ * Basic usage:
433
+ *
434
+ * // Using the default conversion.
435
+ * // In this case, all markers with names starting with 'comment:' will be converted.
436
+ * // The `group` parameter will be set to `comment`.
437
+ * // The `name` parameter will be the rest of the marker name (without the `:`).
438
+ * editor.conversion.for( 'dataDowncast' ).markerToData( {
439
+ * model: 'comment'
440
+ * } );
441
+ *
442
+ * An example of a view that may be generated by this conversion (assuming a marker with the name `comment:commentId:uid` marked
443
+ * by `[]`):
444
+ *
445
+ * // Model:
446
+ * <paragraph>Foo[bar</paragraph>
447
+ * <imageBlock src="abc.jpg"></imageBlock>]
448
+ *
449
+ * // View:
450
+ * <p>Foo<comment-start name="commentId:uid"></comment-start>bar</p>
451
+ * <figure data-comment-end-after="commentId:uid" class="image"><img src="abc.jpg" /></figure>
452
+ *
453
+ * In the example above, the comment starts before "bar" and ends after the image.
454
+ *
455
+ * If the `name` part is empty, the following view may be generated:
456
+ *
457
+ * <p>Foo <myMarker-start></myMarker-start>bar</p>
458
+ * <figure data-myMarker-end-after="" class="image"><img src="abc.jpg" /></figure>
459
+ *
460
+ * **Note:** A situation where some markers have the `name` part and some do not, is incorrect and should be avoided.
461
+ *
462
+ * Examples where `data-group-start-after` and `data-group-end-before` are used:
463
+ *
464
+ * // Model:
465
+ * <blockQuote>[]<paragraph>Foo</paragraph></blockQuote>
466
+ *
467
+ * // View:
468
+ * <blockquote><p data-group-end-before="name" data-group-start-before="name">Foo</p></blockquote>
469
+ *
470
+ * Similarly, when a marker is collapsed after the last element:
471
+ *
472
+ * // Model:
473
+ * <blockQuote><paragraph>Foo</paragraph>[]</blockQuote>
474
+ *
475
+ * // View:
476
+ * <blockquote><p data-group-end-after="name" data-group-start-after="name">Foo</p></blockquote>
477
+ *
478
+ * When there are multiple markers from the same group stored in the same attribute of the same element, their
479
+ * name parts are put together in the attribute value, for example: `data-group-start-before="name1,name2,name3"`.
480
+ *
481
+ * Other examples of usage:
482
+ *
483
+ * // Using a custom function which is the same as the default conversion:
484
+ * editor.conversion.for( 'dataDowncast' ).markerToData( {
485
+ * model: 'comment'
486
+ * view: markerName => ( {
487
+ * group: 'comment',
488
+ * name: markerName.substr( 8 ) // Removes 'comment:' part.
489
+ * } )
490
+ * } );
491
+ *
492
+ * // Using the converter priority:
493
+ * editor.conversion.for( 'dataDowncast' ).markerToData( {
494
+ * model: 'comment'
495
+ * view: markerName => ( {
496
+ * group: 'comment',
497
+ * name: markerName.substr( 8 ) // Removes 'comment:' part.
498
+ * } ),
499
+ * converterPriority: 'high'
500
+ * } );
501
+ *
502
+ * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline.
503
+ *
504
+ * See the {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} API guide to learn how to
505
+ * add a converter to the conversion process.
506
+ *
507
+ * @method #markerToData
508
+ * @param {Object} config Conversion configuration.
509
+ * @param {String} config.model The name of the model marker (or the model marker group) to convert.
510
+ * @param {Function} [config.view] A function that takes the model marker name and
511
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as the parameters
512
+ * and returns an object with the `group` and `name` properties.
513
+ * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
514
+ * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
515
+ */
516
+ markerToData( config ) {
517
+ return this.add( downcastMarkerToData( config ) );
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Function factory that creates a default downcast converter for text insertion changes.
523
+ *
524
+ * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
525
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
526
+ *
527
+ * modelDispatcher.on( 'insert:$text', insertText() );
528
+ *
529
+ * @returns {Function} Insert text event converter.
530
+ */
531
+ export function insertText() {
532
+ return ( evt, data, conversionApi ) => {
533
+ if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
534
+ return;
535
+ }
536
+
537
+ const viewWriter = conversionApi.writer;
538
+ const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
539
+ const viewText = viewWriter.createText( data.item.data );
540
+
541
+ viewWriter.insert( viewPosition, viewText );
542
+ };
543
+ }
544
+
545
+ /**
546
+ * Function factory that creates a default downcast converter for node remove changes.
547
+ *
548
+ * modelDispatcher.on( 'remove', remove() );
549
+ *
550
+ * @returns {Function} Remove event converter.
551
+ */
552
+ export function remove() {
553
+ return ( evt, data, conversionApi ) => {
554
+ // Find the view range start position by mapping the model position at which the remove happened.
555
+ const viewStart = conversionApi.mapper.toViewPosition( data.position );
556
+
557
+ const modelEnd = data.position.getShiftedBy( data.length );
558
+ const viewEnd = conversionApi.mapper.toViewPosition( modelEnd, { isPhantom: true } );
559
+
560
+ const viewRange = conversionApi.writer.createRange( viewStart, viewEnd );
561
+
562
+ // Trim the range to remove in case some UI elements are on the view range boundaries.
563
+ const removed = conversionApi.writer.remove( viewRange.getTrimmed() );
564
+
565
+ // After the range is removed, unbind all view elements from the model.
566
+ // Range inside view document fragment is used to unbind deeply.
567
+ for ( const child of conversionApi.writer.createRangeIn( removed ).getItems() ) {
568
+ conversionApi.mapper.unbindViewElement( child );
569
+ }
570
+ };
571
+ }
572
+
573
+ /**
574
+ * Creates a `<span>` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from the information
575
+ * provided by the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor} object. If the priority
576
+ * is not provided in the descriptor, the default priority will be used.
577
+ *
578
+ * @param {module:engine/view/downcastwriter~DowncastWriter} writer
579
+ * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor
580
+ * @returns {module:engine/view/attributeelement~AttributeElement}
581
+ */
582
+ export function createViewElementFromHighlightDescriptor( writer, descriptor ) {
583
+ const viewElement = writer.createAttributeElement( 'span', descriptor.attributes );
584
+
585
+ if ( descriptor.classes ) {
586
+ viewElement._addClass( descriptor.classes );
587
+ }
588
+
589
+ if ( typeof descriptor.priority === 'number' ) {
590
+ viewElement._priority = descriptor.priority;
591
+ }
592
+
593
+ viewElement._id = descriptor.id;
594
+
595
+ return viewElement;
596
+ }
597
+
598
+ /**
599
+ * Function factory that creates a converter which converts a non-collapsed {@link module:engine/model/selection~Selection model selection}
600
+ * to a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
601
+ * value from the `consumable` object and maps model positions from the selection to view positions.
602
+ *
603
+ * modelDispatcher.on( 'selection', convertRangeSelection() );
604
+ *
605
+ * @returns {Function} Selection converter.
606
+ */
607
+ export function convertRangeSelection() {
608
+ return ( evt, data, conversionApi ) => {
609
+ const selection = data.selection;
610
+
611
+ if ( selection.isCollapsed ) {
612
+ return;
613
+ }
614
+
615
+ if ( !conversionApi.consumable.consume( selection, 'selection' ) ) {
616
+ return;
617
+ }
618
+
619
+ const viewRanges = [];
620
+
621
+ for ( const range of selection.getRanges() ) {
622
+ const viewRange = conversionApi.mapper.toViewRange( range );
623
+ viewRanges.push( viewRange );
624
+ }
625
+
626
+ conversionApi.writer.setSelection( viewRanges, { backward: selection.isBackward } );
627
+ };
628
+ }
629
+
630
+ /**
631
+ * Function factory that creates a converter which converts a collapsed {@link module:engine/model/selection~Selection model selection} to
632
+ * a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
633
+ * value from the `consumable` object, maps the model selection position to the view position and breaks
634
+ * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position.
635
+ *
636
+ * modelDispatcher.on( 'selection', convertCollapsedSelection() );
637
+ *
638
+ * An example of the view state before and after converting the collapsed selection:
639
+ *
640
+ * <p><strong>f^oo<strong>bar</p>
641
+ * -> <p><strong>f</strong>^<strong>oo</strong>bar</p>
642
+ *
643
+ * By breaking attribute elements like `<strong>`, the selection is in a correct element. Then, when the selection attribute is
644
+ * converted, broken attributes might be merged again, or the position where the selection is may be wrapped
645
+ * with different, appropriate attribute elements.
646
+ *
647
+ * See also {@link module:engine/conversion/downcasthelpers~clearAttributes} which does a clean-up
648
+ * by merging attributes.
649
+ *
650
+ * @returns {Function} Selection converter.
651
+ */
652
+ export function convertCollapsedSelection() {
653
+ return ( evt, data, conversionApi ) => {
654
+ const selection = data.selection;
655
+
656
+ if ( !selection.isCollapsed ) {
657
+ return;
658
+ }
659
+
660
+ if ( !conversionApi.consumable.consume( selection, 'selection' ) ) {
661
+ return;
662
+ }
663
+
664
+ const viewWriter = conversionApi.writer;
665
+ const modelPosition = selection.getFirstPosition();
666
+ const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
667
+ const brokenPosition = viewWriter.breakAttributes( viewPosition );
668
+
669
+ viewWriter.setSelection( brokenPosition );
670
+ };
671
+ }
672
+
673
+ /**
674
+ * Function factory that creates a converter which clears artifacts after the previous
675
+ * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty
676
+ * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merges sibling attributes at all start and end
677
+ * positions of all ranges.
678
+ *
679
+ * <p><strong>^</strong></p>
680
+ * -> <p>^</p>
681
+ *
682
+ * <p><strong>foo</strong>^<strong>bar</strong>bar</p>
683
+ * -> <p><strong>foo^bar<strong>bar</p>
684
+ *
685
+ * <p><strong>foo</strong><em>^</em><strong>bar</strong>bar</p>
686
+ * -> <p><strong>foo^bar<strong>bar</p>
687
+ *
688
+ * This listener should be assigned before any converter for the new selection:
689
+ *
690
+ * modelDispatcher.on( 'selection', clearAttributes() );
691
+ *
692
+ * See {@link module:engine/conversion/downcasthelpers~convertCollapsedSelection}
693
+ * which does the opposite by breaking attributes in the selection position.
694
+ *
695
+ * @returns {Function} Selection converter.
696
+ */
697
+ export function clearAttributes() {
698
+ return ( evt, data, conversionApi ) => {
699
+ const viewWriter = conversionApi.writer;
700
+ const viewSelection = viewWriter.document.selection;
701
+
702
+ for ( const range of viewSelection.getRanges() ) {
703
+ // Not collapsed selection should not have artifacts.
704
+ if ( range.isCollapsed ) {
705
+ // Position might be in the node removed by the view writer.
706
+ if ( range.end.parent.isAttached() ) {
707
+ conversionApi.writer.mergeAttributes( range.start );
708
+ }
709
+ }
710
+ }
711
+ viewWriter.setSelection( null );
712
+ };
713
+ }
714
+
715
+ /**
716
+ * Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view.
717
+ * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the
718
+ * selection will be put inside it.
719
+ *
720
+ * Attributes from the model are converted to a view element that will be wrapping these view nodes that are bound to
721
+ * model elements having the given attribute. This is useful for attributes like `bold` that may be set on text nodes in the model
722
+ * but are represented as an element in the view:
723
+ *
724
+ * [paragraph] MODEL ====> VIEW <p>
725
+ * |- a {bold: true} |- <b>
726
+ * |- b {bold: true} | |- ab
727
+ * |- c |- c
728
+ *
729
+ * Passed `Function` will be provided with the attribute value and then all the parameters of the
730
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute` event}.
731
+ * It is expected that the function returns an {@link module:engine/view/element~Element}.
732
+ * The result of the function will be the wrapping element.
733
+ * When the provided `Function` does not return any element, no conversion will take place.
734
+ *
735
+ * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
736
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
737
+ *
738
+ * modelDispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, { writer } ) => {
739
+ * return writer.createAttributeElement( 'strong' );
740
+ * } );
741
+ *
742
+ * @protected
743
+ * @param {Function} elementCreator Function returning a view element that will be used for wrapping.
744
+ * @returns {Function} Set/change attribute converter.
745
+ */
746
+ export function wrap( elementCreator ) {
747
+ return ( evt, data, conversionApi ) => {
748
+ // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
749
+ // or the attribute was removed.
750
+ const oldViewElement = elementCreator( data.attributeOldValue, conversionApi );
751
+
752
+ // Create node to wrap with.
753
+ const newViewElement = elementCreator( data.attributeNewValue, conversionApi );
754
+
755
+ if ( !oldViewElement && !newViewElement ) {
756
+ return;
757
+ }
758
+
759
+ if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
760
+ return;
761
+ }
762
+
763
+ const viewWriter = conversionApi.writer;
764
+ const viewSelection = viewWriter.document.selection;
765
+
766
+ if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
767
+ // Selection attribute conversion.
768
+ viewWriter.wrap( viewSelection.getFirstRange(), newViewElement );
769
+ } else {
770
+ // Node attribute conversion.
771
+ let viewRange = conversionApi.mapper.toViewRange( data.range );
772
+
773
+ // First, unwrap the range from current wrapper.
774
+ if ( data.attributeOldValue !== null && oldViewElement ) {
775
+ viewRange = viewWriter.unwrap( viewRange, oldViewElement );
776
+ }
777
+
778
+ if ( data.attributeNewValue !== null && newViewElement ) {
779
+ viewWriter.wrap( viewRange, newViewElement );
780
+ }
781
+ }
782
+ };
783
+ }
784
+
785
+ /**
786
+ * Function factory that creates a converter which converts node insertion changes from the model to the view.
787
+ * The function passed will be provided with all the parameters of the dispatcher's
788
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}.
789
+ * It is expected that the function returns an {@link module:engine/view/element~Element}.
790
+ * The result of the function will be inserted into the view.
791
+ *
792
+ * The converter automatically consumes the corresponding value from the consumables list, stops the event (see
793
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and binds the model and view elements.
794
+ *
795
+ * downcastDispatcher.on(
796
+ * 'insert:myElem',
797
+ * insertElement( ( modelItem, { writer } ) => {
798
+ * const text = writer.createText( 'myText' );
799
+ * const myElem = writer.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text );
800
+ *
801
+ * // Do something fancy with `myElem` using `modelItem` or other parameters.
802
+ *
803
+ * return myElem;
804
+ * }
805
+ * ) );
806
+ *
807
+ * @protected
808
+ * @param {Function} elementCreator Function returning a view element, which will be inserted.
809
+ * @returns {Function} Insert element event converter.
810
+ */
811
+ export function insertElement( elementCreator ) {
812
+ return ( evt, data, conversionApi ) => {
813
+ const viewElement = elementCreator( data.item, conversionApi );
814
+
815
+ if ( !viewElement ) {
816
+ return;
817
+ }
818
+
819
+ if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
820
+ return;
821
+ }
822
+
823
+ const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
824
+
825
+ conversionApi.mapper.bindElements( data.item, viewElement );
826
+ conversionApi.writer.insert( viewPosition, viewElement );
827
+ };
828
+ }
829
+
830
+ /**
831
+ * Function factory that creates a converter which converts marker adding change to the
832
+ * {@link module:engine/view/uielement~UIElement view UI element}.
833
+ *
834
+ * The view UI element that will be added to the view depends on the passed parameter. See {@link ~insertElement}.
835
+ * In case of a non-collapsed range, the UI element will not wrap nodes but separate elements will be placed at the beginning
836
+ * and at the end of the range.
837
+ *
838
+ * This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
839
+ *
840
+ * @protected
841
+ * @param {module:engine/view/uielement~UIElement|Function} elementCreator A view UI element or a function returning the view element
842
+ * that will be inserted.
843
+ * @returns {Function} Insert element event converter.
844
+ */
845
+ export function insertUIElement( elementCreator ) {
846
+ return ( evt, data, conversionApi ) => {
847
+ // Create two view elements. One will be inserted at the beginning of marker, one at the end.
848
+ // If marker is collapsed, only "opening" element will be inserted.
849
+ data.isOpening = true;
850
+ const viewStartElement = elementCreator( data, conversionApi );
851
+
852
+ data.isOpening = false;
853
+ const viewEndElement = elementCreator( data, conversionApi );
854
+
855
+ if ( !viewStartElement || !viewEndElement ) {
856
+ return;
857
+ }
858
+
859
+ const markerRange = data.markerRange;
860
+
861
+ // Marker that is collapsed has consumable build differently that non-collapsed one.
862
+ // For more information see `addMarker` event description.
863
+ // If marker's range is collapsed - check if it can be consumed.
864
+ if ( markerRange.isCollapsed && !conversionApi.consumable.consume( markerRange, evt.name ) ) {
865
+ return;
866
+ }
867
+
868
+ // If marker's range is not collapsed - consume all items inside.
869
+ for ( const value of markerRange ) {
870
+ if ( !conversionApi.consumable.consume( value.item, evt.name ) ) {
871
+ return;
872
+ }
873
+ }
874
+
875
+ const mapper = conversionApi.mapper;
876
+ const viewWriter = conversionApi.writer;
877
+
878
+ // Add "opening" element.
879
+ viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
880
+ conversionApi.mapper.bindElementToMarker( viewStartElement, data.markerName );
881
+
882
+ // Add "closing" element only if range is not collapsed.
883
+ if ( !markerRange.isCollapsed ) {
884
+ viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
885
+ conversionApi.mapper.bindElementToMarker( viewEndElement, data.markerName );
886
+ }
887
+
888
+ evt.stop();
889
+ };
890
+ }
891
+
892
+ // Function factory that returns a default downcast converter for removing a {@link module:engine/view/uielement~UIElement UI element}
893
+ // based on marker remove change.
894
+ //
895
+ // This converter unbinds elements from the marker name.
896
+ //
897
+ // @returns {Function} Removed UI element converter.
898
+ function removeUIElement() {
899
+ return ( evt, data, conversionApi ) => {
900
+ const elements = conversionApi.mapper.markerNameToElements( data.markerName );
901
+
902
+ if ( !elements ) {
903
+ return;
904
+ }
905
+
906
+ for ( const element of elements ) {
907
+ conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
908
+ conversionApi.writer.clear( conversionApi.writer.createRangeOn( element ), element );
909
+ }
910
+
911
+ conversionApi.writer.clearClonedElementsGroup( data.markerName );
912
+
913
+ evt.stop();
914
+ };
915
+ }
916
+
917
+ // Function factory that creates a default converter for model markers.
918
+ //
919
+ // See {@link DowncastHelpers#markerToData} for more information what type of view is generated.
920
+ //
921
+ // This converter binds created UI elements and affected view elements with the marker name
922
+ // using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
923
+ //
924
+ // @returns {Function} Add marker converter.
925
+ function insertMarkerData( viewCreator ) {
926
+ return ( evt, data, conversionApi ) => {
927
+ const viewMarkerData = viewCreator( data.markerName, conversionApi );
928
+
929
+ if ( !viewMarkerData ) {
930
+ return;
931
+ }
932
+
933
+ const markerRange = data.markerRange;
934
+
935
+ if ( !conversionApi.consumable.consume( markerRange, evt.name ) ) {
936
+ return;
937
+ }
938
+
939
+ // Adding closing data first to keep the proper order in the view.
940
+ handleMarkerBoundary( markerRange, false, conversionApi, data, viewMarkerData );
941
+ handleMarkerBoundary( markerRange, true, conversionApi, data, viewMarkerData );
942
+
943
+ evt.stop();
944
+ };
945
+ }
946
+
947
+ // Helper function for `insertMarkerData()` that marks a marker boundary at the beginning or end of given `range`.
948
+ function handleMarkerBoundary( range, isStart, conversionApi, data, viewMarkerData ) {
949
+ const modelPosition = isStart ? range.start : range.end;
950
+ const elementAfter = modelPosition.nodeAfter && modelPosition.nodeAfter.is( 'element' ) ? modelPosition.nodeAfter : null;
951
+ const elementBefore = modelPosition.nodeBefore && modelPosition.nodeBefore.is( 'element' ) ? modelPosition.nodeBefore : null;
952
+
953
+ if ( elementAfter || elementBefore ) {
954
+ let modelElement;
955
+ let isBefore;
956
+
957
+ // If possible, we want to add `data-group-start-before` and `data-group-end-after` attributes.
958
+ if ( isStart && elementAfter || !isStart && !elementBefore ) {
959
+ // [<elementAfter>...</elementAfter> -> <elementAfter data-group-start-before="...">...</elementAfter>
960
+ // <parent>]<elementAfter> -> <parent><elementAfter data-group-end-before="...">
961
+ modelElement = elementAfter;
962
+ isBefore = true;
963
+ } else {
964
+ // <elementBefore>...</elementBefore>] -> <elementBefore data-group-end-after="...">...</elementBefore>
965
+ // </elementBefore>[</parent> -> </elementBefore data-group-start-after="..."></parent>
966
+ modelElement = elementBefore;
967
+ isBefore = false;
968
+ }
969
+
970
+ const viewElement = conversionApi.mapper.toViewElement( modelElement );
971
+
972
+ // In rare circumstances, the model element may be not mapped to any view element and that would cause an error.
973
+ // One of those situations is a soft break inside code block.
974
+ if ( viewElement ) {
975
+ insertMarkerAsAttribute( viewElement, isStart, isBefore, conversionApi, data, viewMarkerData );
976
+
977
+ return;
978
+ }
979
+ }
980
+
981
+ const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
982
+
983
+ insertMarkerAsElement( viewPosition, isStart, conversionApi, data, viewMarkerData );
984
+ }
985
+
986
+ // Helper function for `insertMarkerData()` that marks a marker boundary in the view as an attribute on a view element.
987
+ function insertMarkerAsAttribute( viewElement, isStart, isBefore, conversionApi, data, viewMarkerData ) {
988
+ const attributeName = `data-${ viewMarkerData.group }-${ isStart ? 'start' : 'end' }-${ isBefore ? 'before' : 'after' }`;
989
+
990
+ const markerNames = viewElement.hasAttribute( attributeName ) ? viewElement.getAttribute( attributeName ).split( ',' ) : [];
991
+
992
+ // Adding marker name at the beginning to have the same order in the attribute as there is with marker elements.
993
+ markerNames.unshift( viewMarkerData.name );
994
+
995
+ conversionApi.writer.setAttribute( attributeName, markerNames.join( ',' ), viewElement );
996
+ conversionApi.mapper.bindElementToMarker( viewElement, data.markerName );
997
+ }
998
+
999
+ // Helper function for `insertMarkerData()` that marks a marker boundary in the view as a separate view ui element.
1000
+ function insertMarkerAsElement( position, isStart, conversionApi, data, viewMarkerData ) {
1001
+ const viewElementName = `${ viewMarkerData.group }-${ isStart ? 'start' : 'end' }`;
1002
+
1003
+ const attrs = viewMarkerData.name ? { 'name': viewMarkerData.name } : null;
1004
+ const viewElement = conversionApi.writer.createUIElement( viewElementName, attrs );
1005
+
1006
+ conversionApi.writer.insert( position, viewElement );
1007
+ conversionApi.mapper.bindElementToMarker( viewElement, data.markerName );
1008
+ }
1009
+
1010
+ // Function factory that creates a converter for removing a model marker data added by the {@link #insertMarkerData} converter.
1011
+ //
1012
+ // @returns {Function} Remove marker converter.
1013
+ function removeMarkerData( viewCreator ) {
1014
+ return ( evt, data, conversionApi ) => {
1015
+ const viewData = viewCreator( data.markerName, conversionApi );
1016
+
1017
+ if ( !viewData ) {
1018
+ return;
1019
+ }
1020
+
1021
+ const elements = conversionApi.mapper.markerNameToElements( data.markerName );
1022
+
1023
+ if ( !elements ) {
1024
+ return;
1025
+ }
1026
+
1027
+ for ( const element of elements ) {
1028
+ conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
1029
+
1030
+ if ( element.is( 'containerElement' ) ) {
1031
+ removeMarkerFromAttribute( `data-${ viewData.group }-start-before`, element );
1032
+ removeMarkerFromAttribute( `data-${ viewData.group }-start-after`, element );
1033
+ removeMarkerFromAttribute( `data-${ viewData.group }-end-before`, element );
1034
+ removeMarkerFromAttribute( `data-${ viewData.group }-end-after`, element );
1035
+ } else {
1036
+ conversionApi.writer.clear( conversionApi.writer.createRangeOn( element ), element );
1037
+ }
1038
+ }
1039
+
1040
+ conversionApi.writer.clearClonedElementsGroup( data.markerName );
1041
+
1042
+ evt.stop();
1043
+
1044
+ function removeMarkerFromAttribute( attributeName, element ) {
1045
+ if ( element.hasAttribute( attributeName ) ) {
1046
+ const markerNames = new Set( element.getAttribute( attributeName ).split( ',' ) );
1047
+ markerNames.delete( viewData.name );
1048
+
1049
+ if ( markerNames.size == 0 ) {
1050
+ conversionApi.writer.removeAttribute( attributeName, element );
1051
+ } else {
1052
+ conversionApi.writer.setAttribute( attributeName, Array.from( markerNames ).join( ',' ), element );
1053
+ }
1054
+ }
1055
+ }
1056
+ };
1057
+ }
1058
+
1059
+ // Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view.
1060
+ //
1061
+ // Attributes from the model are converted to the view element attributes in the view. You may provide a custom function to generate
1062
+ // a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view element
1063
+ // attributes on a one-to-one basis.
1064
+ //
1065
+ // *Note:** The provided attribute creator should always return the same `key` for a given attribute from the model.
1066
+ //
1067
+ // The converter automatically consumes the corresponding value from the consumables list and stops the event (see
1068
+ // {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
1069
+ //
1070
+ // modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( value, data ) => {
1071
+ // // Change attribute key from `customAttr` to `class` in the view.
1072
+ // const key = 'class';
1073
+ // let value = data.attributeNewValue;
1074
+ //
1075
+ // // Force attribute value to 'empty' if the model element is empty.
1076
+ // if ( data.item.childCount === 0 ) {
1077
+ // value = 'empty';
1078
+ // }
1079
+ //
1080
+ // // Return the key-value pair.
1081
+ // return { key, value };
1082
+ // } ) );
1083
+ //
1084
+ // @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
1085
+ // represent the attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
1086
+ // The function is passed the model attribute value as the first parameter and additional data about the change as the second parameter.
1087
+ // @returns {Function} Set/change attribute converter.
1088
+ function changeAttribute( attributeCreator ) {
1089
+ return ( evt, data, conversionApi ) => {
1090
+ const oldAttribute = attributeCreator( data.attributeOldValue, conversionApi );
1091
+ const newAttribute = attributeCreator( data.attributeNewValue, conversionApi );
1092
+
1093
+ if ( !oldAttribute && !newAttribute ) {
1094
+ return;
1095
+ }
1096
+
1097
+ if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
1098
+ return;
1099
+ }
1100
+
1101
+ const viewElement = conversionApi.mapper.toViewElement( data.item );
1102
+ const viewWriter = conversionApi.writer;
1103
+
1104
+ // If model item cannot be mapped to a view element, it means item is not an `Element` instance but a `TextProxy` node.
1105
+ // Only elements can have attributes in a view so do not proceed for anything else (#1587).
1106
+ if ( !viewElement ) {
1107
+ /**
1108
+ * This error occurs when a {@link module:engine/model/textproxy~TextProxy text node's} attribute is to be downcasted
1109
+ * by {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `Attribute to Attribute converter`}.
1110
+ * In most cases it is caused by converters misconfiguration when only "generic" converter is defined:
1111
+ *
1112
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
1113
+ * model: 'attribute-name',
1114
+ * view: 'attribute-name'
1115
+ * } ) );
1116
+ *
1117
+ * and given attribute is used on text node, for example:
1118
+ *
1119
+ * model.change( writer => {
1120
+ * writer.insertText( 'Foo', { 'attribute-name': 'bar' }, parent, 0 );
1121
+ * } );
1122
+ *
1123
+ * In such cases, to convert the same attribute for both {@link module:engine/model/element~Element}
1124
+ * and {@link module:engine/model/textproxy~TextProxy `Text`} nodes, text specific
1125
+ * {@link module:engine/conversion/conversion~Conversion#attributeToElement `Attribute to Element converter`}
1126
+ * with higher {@link module:utils/priorities~PriorityString priority} must also be defined:
1127
+ *
1128
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
1129
+ * model: {
1130
+ * key: 'attribute-name',
1131
+ * name: '$text'
1132
+ * },
1133
+ * view: ( value, { writer } ) => {
1134
+ * return writer.createAttributeElement( 'span', { 'attribute-name': value } );
1135
+ * },
1136
+ * converterPriority: 'high'
1137
+ * } ) );
1138
+ *
1139
+ * @error conversion-attribute-to-attribute-on-text
1140
+ */
1141
+ throw new CKEditorError(
1142
+ 'conversion-attribute-to-attribute-on-text',
1143
+ [ data, conversionApi ]
1144
+ );
1145
+ }
1146
+
1147
+ // First remove the old attribute if there was one.
1148
+ if ( data.attributeOldValue !== null && oldAttribute ) {
1149
+ if ( oldAttribute.key == 'class' ) {
1150
+ const classes = toArray( oldAttribute.value );
1151
+
1152
+ for ( const className of classes ) {
1153
+ viewWriter.removeClass( className, viewElement );
1154
+ }
1155
+ } else if ( oldAttribute.key == 'style' ) {
1156
+ const keys = Object.keys( oldAttribute.value );
1157
+
1158
+ for ( const key of keys ) {
1159
+ viewWriter.removeStyle( key, viewElement );
1160
+ }
1161
+ } else {
1162
+ viewWriter.removeAttribute( oldAttribute.key, viewElement );
1163
+ }
1164
+ }
1165
+
1166
+ // Then set the new attribute.
1167
+ if ( data.attributeNewValue !== null && newAttribute ) {
1168
+ if ( newAttribute.key == 'class' ) {
1169
+ const classes = toArray( newAttribute.value );
1170
+
1171
+ for ( const className of classes ) {
1172
+ viewWriter.addClass( className, viewElement );
1173
+ }
1174
+ } else if ( newAttribute.key == 'style' ) {
1175
+ const keys = Object.keys( newAttribute.value );
1176
+
1177
+ for ( const key of keys ) {
1178
+ viewWriter.setStyle( key, newAttribute.value[ key ], viewElement );
1179
+ }
1180
+ } else {
1181
+ viewWriter.setAttribute( newAttribute.key, newAttribute.value, viewElement );
1182
+ }
1183
+ }
1184
+ };
1185
+ }
1186
+
1187
+ // Function factory that creates a converter which converts the text inside marker's range. The converter wraps the text with
1188
+ // {@link module:engine/view/attributeelement~AttributeElement} created from the provided descriptor.
1189
+ // See {link module:engine/conversion/downcasthelpers~createViewElementFromHighlightDescriptor}.
1190
+ //
1191
+ // It can also be used to convert the selection that is inside a marker. In that case, an empty attribute element will be
1192
+ // created and the selection will be put inside it.
1193
+ //
1194
+ // If the highlight descriptor does not provide the `priority` property, `10` will be used.
1195
+ //
1196
+ // If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
1197
+ //
1198
+ // This converter binds the created {@link module:engine/view/attributeelement~AttributeElement attribute elemens} with the marker name
1199
+ // using the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
1200
+ //
1201
+ // @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
1202
+ // @returns {Function}
1203
+ function highlightText( highlightDescriptor ) {
1204
+ return ( evt, data, conversionApi ) => {
1205
+ if ( !data.item ) {
1206
+ return;
1207
+ }
1208
+
1209
+ if ( !( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) && !data.item.is( '$textProxy' ) ) {
1210
+ return;
1211
+ }
1212
+
1213
+ const descriptor = prepareDescriptor( highlightDescriptor, data, conversionApi );
1214
+
1215
+ if ( !descriptor ) {
1216
+ return;
1217
+ }
1218
+
1219
+ if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
1220
+ return;
1221
+ }
1222
+
1223
+ const viewWriter = conversionApi.writer;
1224
+ const viewElement = createViewElementFromHighlightDescriptor( viewWriter, descriptor );
1225
+ const viewSelection = viewWriter.document.selection;
1226
+
1227
+ if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
1228
+ viewWriter.wrap( viewSelection.getFirstRange(), viewElement, viewSelection );
1229
+ } else {
1230
+ const viewRange = conversionApi.mapper.toViewRange( data.range );
1231
+ const rangeAfterWrap = viewWriter.wrap( viewRange, viewElement );
1232
+
1233
+ for ( const element of rangeAfterWrap.getItems() ) {
1234
+ if ( element.is( 'attributeElement' ) && element.isSimilar( viewElement ) ) {
1235
+ conversionApi.mapper.bindElementToMarker( element, data.markerName );
1236
+
1237
+ // One attribute element is enough, because all of them are bound together by the view writer.
1238
+ // Mapper uses this binding to get all the elements no matter how many of them are registered in the mapper.
1239
+ break;
1240
+ }
1241
+ }
1242
+ }
1243
+ };
1244
+ }
1245
+
1246
+ // Converter function factory. It creates a function which applies the marker's highlight to an element inside the marker's range.
1247
+ //
1248
+ // The converter checks if an element has the `addHighlight` function stored as a
1249
+ // {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight.
1250
+ // In such case the converter will consume all element's children, assuming that they were handled by the element itself.
1251
+ //
1252
+ // When the `addHighlight` custom property is not present, the element is not converted in any special way.
1253
+ // This means that converters will proceed to convert the element's child nodes.
1254
+ //
1255
+ // If the highlight descriptor does not provide the `priority` property, `10` will be used.
1256
+ //
1257
+ // If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
1258
+ //
1259
+ // This converter binds altered {@link module:engine/view/containerelement~ContainerElement container elements} with the marker name using
1260
+ // the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
1261
+ //
1262
+ // @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
1263
+ // @returns {Function}
1264
+ function highlightElement( highlightDescriptor ) {
1265
+ return ( evt, data, conversionApi ) => {
1266
+ if ( !data.item ) {
1267
+ return;
1268
+ }
1269
+
1270
+ if ( !( data.item instanceof ModelElement ) ) {
1271
+ return;
1272
+ }
1273
+
1274
+ const descriptor = prepareDescriptor( highlightDescriptor, data, conversionApi );
1275
+
1276
+ if ( !descriptor ) {
1277
+ return;
1278
+ }
1279
+
1280
+ if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
1281
+ return;
1282
+ }
1283
+
1284
+ const viewElement = conversionApi.mapper.toViewElement( data.item );
1285
+
1286
+ if ( viewElement && viewElement.getCustomProperty( 'addHighlight' ) ) {
1287
+ // Consume element itself.
1288
+ conversionApi.consumable.consume( data.item, evt.name );
1289
+
1290
+ // Consume all children nodes.
1291
+ for ( const value of ModelRange._createIn( data.item ) ) {
1292
+ conversionApi.consumable.consume( value.item, evt.name );
1293
+ }
1294
+
1295
+ viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor, conversionApi.writer );
1296
+
1297
+ conversionApi.mapper.bindElementToMarker( viewElement, data.markerName );
1298
+ }
1299
+ };
1300
+ }
1301
+
1302
+ // Function factory that creates a converter which converts the removing model marker to the view.
1303
+ //
1304
+ // Both text nodes and elements are handled by this converter but they are handled a bit differently.
1305
+ //
1306
+ // Text nodes are unwrapped using the {@link module:engine/view/attributeelement~AttributeElement attribute element} created from the
1307
+ // provided highlight descriptor. See {link module:engine/conversion/downcasthelpers~HighlightDescriptor}.
1308
+ //
1309
+ // For elements, the converter checks if an element has the `removeHighlight` function stored as a
1310
+ // {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight.
1311
+ // In such case, the children of that element will not be converted.
1312
+ //
1313
+ // When `removeHighlight` is not present, the element is not converted in any special way.
1314
+ // The converter will proceed to convert the element's child nodes instead.
1315
+ //
1316
+ // If the highlight descriptor does not provide the `priority` property, `10` will be used.
1317
+ //
1318
+ // If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
1319
+ //
1320
+ // This converter unbinds elements from the marker name.
1321
+ //
1322
+ // @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
1323
+ // @returns {Function}
1324
+ function removeHighlight( highlightDescriptor ) {
1325
+ return ( evt, data, conversionApi ) => {
1326
+ // This conversion makes sense only for non-collapsed range.
1327
+ if ( data.markerRange.isCollapsed ) {
1328
+ return;
1329
+ }
1330
+
1331
+ const descriptor = prepareDescriptor( highlightDescriptor, data, conversionApi );
1332
+
1333
+ if ( !descriptor ) {
1334
+ return;
1335
+ }
1336
+
1337
+ // View element that will be used to unwrap `AttributeElement`s.
1338
+ const viewHighlightElement = createViewElementFromHighlightDescriptor( conversionApi.writer, descriptor );
1339
+
1340
+ // Get all elements bound with given marker name.
1341
+ const elements = conversionApi.mapper.markerNameToElements( data.markerName );
1342
+
1343
+ if ( !elements ) {
1344
+ return;
1345
+ }
1346
+
1347
+ for ( const element of elements ) {
1348
+ conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
1349
+
1350
+ if ( element.is( 'attributeElement' ) ) {
1351
+ conversionApi.writer.unwrap( conversionApi.writer.createRangeOn( element ), viewHighlightElement );
1352
+ } else {
1353
+ // if element.is( 'containerElement' ).
1354
+ element.getCustomProperty( 'removeHighlight' )( element, descriptor.id, conversionApi.writer );
1355
+ }
1356
+ }
1357
+
1358
+ conversionApi.writer.clearClonedElementsGroup( data.markerName );
1359
+
1360
+ evt.stop();
1361
+ };
1362
+ }
1363
+
1364
+ // Model element to view element conversion helper.
1365
+ //
1366
+ // See {@link ~DowncastHelpers#elementToElement `.elementToElement()` downcast helper} for examples and config params description.
1367
+ //
1368
+ // @param {Object} config Conversion configuration.
1369
+ // @param {String} config.model
1370
+ // @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view
1371
+ // @param {Object} [config.triggerBy]
1372
+ // @param {Array.<String>} [config.triggerBy.attributes]
1373
+ // @param {Array.<String>} [config.triggerBy.children]
1374
+ // @returns {Function} Conversion helper.
1375
+ function downcastElementToElement( config ) {
1376
+ config = cloneDeep( config );
1377
+
1378
+ config.view = normalizeToElementConfig( config.view, 'container' );
1379
+
1380
+ return dispatcher => {
1381
+ dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.converterPriority || 'normal' } );
1382
+
1383
+ if ( config.triggerBy ) {
1384
+ if ( config.triggerBy.attributes ) {
1385
+ for ( const attributeKey of config.triggerBy.attributes ) {
1386
+ dispatcher._mapReconversionTriggerEvent( config.model, `attribute:${ attributeKey }:${ config.model }` );
1387
+ }
1388
+ }
1389
+
1390
+ if ( config.triggerBy.children ) {
1391
+ for ( const childName of config.triggerBy.children ) {
1392
+ dispatcher._mapReconversionTriggerEvent( config.model, `insert:${ childName }` );
1393
+ dispatcher._mapReconversionTriggerEvent( config.model, `remove:${ childName }` );
1394
+ }
1395
+ }
1396
+ }
1397
+ };
1398
+ }
1399
+
1400
+ // Model attribute to view element conversion helper.
1401
+ //
1402
+ // See {@link ~DowncastHelpers#attributeToElement `.attributeToElement()` downcast helper} for examples.
1403
+ //
1404
+ // @param {Object} config Conversion configuration.
1405
+ // @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
1406
+ // of `String`s with possible values if the model attribute is an enumerable.
1407
+ // @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function
1408
+ // that takes the model attribute value and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}
1409
+ // as parameters and returns a view attribute element. If `config.model.values` is
1410
+ // given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions.
1411
+ // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
1412
+ // @returns {Function} Conversion helper.
1413
+ function downcastAttributeToElement( config ) {
1414
+ config = cloneDeep( config );
1415
+
1416
+ const modelKey = config.model.key ? config.model.key : config.model;
1417
+ let eventName = 'attribute:' + modelKey;
1418
+
1419
+ if ( config.model.name ) {
1420
+ eventName += ':' + config.model.name;
1421
+ }
1422
+
1423
+ if ( config.model.values ) {
1424
+ for ( const modelValue of config.model.values ) {
1425
+ config.view[ modelValue ] = normalizeToElementConfig( config.view[ modelValue ], 'attribute' );
1426
+ }
1427
+ } else {
1428
+ config.view = normalizeToElementConfig( config.view, 'attribute' );
1429
+ }
1430
+
1431
+ const elementCreator = getFromAttributeCreator( config );
1432
+
1433
+ return dispatcher => {
1434
+ dispatcher.on( eventName, wrap( elementCreator ), { priority: config.converterPriority || 'normal' } );
1435
+ };
1436
+ }
1437
+
1438
+ // Model attribute to view attribute conversion helper.
1439
+ //
1440
+ // See {@link ~DowncastHelpers#attributeToAttribute `.attributeToAttribute()` downcast helper} for examples.
1441
+ //
1442
+ // @param {Object} config Conversion configuration.
1443
+ // @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
1444
+ // the attribute key, possible values and, optionally, an element name to convert from.
1445
+ // @param {String|Object|Function} config.view A view attribute key, or a `{ key, value }` object or a function that takes
1446
+ // the model attribute value and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an
1447
+ // array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`.
1448
+ // If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
1449
+ // `{ key, value }` objects or a functions.
1450
+ // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
1451
+ // @returns {Function} Conversion helper.
1452
+ function downcastAttributeToAttribute( config ) {
1453
+ config = cloneDeep( config );
1454
+
1455
+ const modelKey = config.model.key ? config.model.key : config.model;
1456
+ let eventName = 'attribute:' + modelKey;
1457
+
1458
+ if ( config.model.name ) {
1459
+ eventName += ':' + config.model.name;
1460
+ }
1461
+
1462
+ if ( config.model.values ) {
1463
+ for ( const modelValue of config.model.values ) {
1464
+ config.view[ modelValue ] = normalizeToAttributeConfig( config.view[ modelValue ] );
1465
+ }
1466
+ } else {
1467
+ config.view = normalizeToAttributeConfig( config.view );
1468
+ }
1469
+
1470
+ const elementCreator = getFromAttributeCreator( config );
1471
+
1472
+ return dispatcher => {
1473
+ dispatcher.on( eventName, changeAttribute( elementCreator ), { priority: config.converterPriority || 'normal' } );
1474
+ };
1475
+ }
1476
+
1477
+ // Model marker to view element conversion helper.
1478
+ //
1479
+ // See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
1480
+ //
1481
+ // @param {Object} config Conversion configuration.
1482
+ // @param {String} config.model The name of the model marker (or model marker group) to convert.
1483
+ // @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
1484
+ // that takes the model marker data as a parameter and returns a view UI element.
1485
+ // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
1486
+ // @returns {Function} Conversion helper.
1487
+ function downcastMarkerToElement( config ) {
1488
+ config = cloneDeep( config );
1489
+
1490
+ config.view = normalizeToElementConfig( config.view, 'ui' );
1491
+
1492
+ return dispatcher => {
1493
+ dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority: config.converterPriority || 'normal' } );
1494
+ dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority: config.converterPriority || 'normal' } );
1495
+ };
1496
+ }
1497
+
1498
+ // Model marker to view data conversion helper.
1499
+ //
1500
+ // See {@link ~DowncastHelpers#markerToData `markerToData()` downcast helper} to learn more.
1501
+ //
1502
+ // @param {Object} config
1503
+ // @param {String} config.model
1504
+ // @param {Function} [config.view]
1505
+ // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal']
1506
+ // @returns {Function} Conversion helper.
1507
+ function downcastMarkerToData( config ) {
1508
+ config = cloneDeep( config );
1509
+
1510
+ const group = config.model;
1511
+
1512
+ // Default conversion.
1513
+ if ( !config.view ) {
1514
+ config.view = markerName => ( {
1515
+ group,
1516
+ name: markerName.substr( config.model.length + 1 )
1517
+ } );
1518
+ }
1519
+
1520
+ return dispatcher => {
1521
+ dispatcher.on( 'addMarker:' + group, insertMarkerData( config.view ), { priority: config.converterPriority || 'normal' } );
1522
+ dispatcher.on( 'removeMarker:' + group, removeMarkerData( config.view ), { priority: config.converterPriority || 'normal' } );
1523
+ };
1524
+ }
1525
+
1526
+ // Model marker to highlight conversion helper.
1527
+ //
1528
+ // See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
1529
+ //
1530
+ // @param {Object} config Conversion configuration.
1531
+ // @param {String} config.model The name of the model marker (or model marker group) to convert.
1532
+ // @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} config.view A highlight descriptor
1533
+ // that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor.
1534
+ // @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
1535
+ // @returns {Function} Conversion helper.
1536
+ function downcastMarkerToHighlight( config ) {
1537
+ return dispatcher => {
1538
+ dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority: config.converterPriority || 'normal' } );
1539
+ dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority: config.converterPriority || 'normal' } );
1540
+ dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority: config.converterPriority || 'normal' } );
1541
+ };
1542
+ }
1543
+
1544
+ // Takes `config.view`, and if it is an {@link module:engine/view/elementdefinition~ElementDefinition}, converts it
1545
+ // to a function (because lower level converters accept only element creator functions).
1546
+ //
1547
+ // @param {module:engine/view/elementdefinition~ElementDefinition|Function} view View configuration.
1548
+ // @param {'container'|'attribute'|'ui'} viewElementType View element type to create.
1549
+ // @returns {Function} Element creator function to use in lower level converters.
1550
+ function normalizeToElementConfig( view, viewElementType ) {
1551
+ if ( typeof view == 'function' ) {
1552
+ // If `view` is already a function, don't do anything.
1553
+ return view;
1554
+ }
1555
+
1556
+ return ( modelData, conversionApi ) => createViewElementFromDefinition( view, conversionApi, viewElementType );
1557
+ }
1558
+
1559
+ // Creates a view element instance from the provided {@link module:engine/view/elementdefinition~ElementDefinition} and class.
1560
+ //
1561
+ // @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition
1562
+ // @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
1563
+ // @param {'container'|'attribute'|'ui'} viewElementType
1564
+ // @returns {module:engine/view/element~Element}
1565
+ function createViewElementFromDefinition( viewElementDefinition, conversionApi, viewElementType ) {
1566
+ if ( typeof viewElementDefinition == 'string' ) {
1567
+ // If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property.
1568
+ viewElementDefinition = { name: viewElementDefinition };
1569
+ }
1570
+
1571
+ let element;
1572
+ const viewWriter = conversionApi.writer;
1573
+ const attributes = Object.assign( {}, viewElementDefinition.attributes );
1574
+
1575
+ if ( viewElementType == 'container' ) {
1576
+ element = viewWriter.createContainerElement( viewElementDefinition.name, attributes );
1577
+ } else if ( viewElementType == 'attribute' ) {
1578
+ const options = {
1579
+ priority: viewElementDefinition.priority || ViewAttributeElement.DEFAULT_PRIORITY
1580
+ };
1581
+
1582
+ element = viewWriter.createAttributeElement( viewElementDefinition.name, attributes, options );
1583
+ } else {
1584
+ // 'ui'.
1585
+ element = viewWriter.createUIElement( viewElementDefinition.name, attributes );
1586
+ }
1587
+
1588
+ if ( viewElementDefinition.styles ) {
1589
+ const keys = Object.keys( viewElementDefinition.styles );
1590
+
1591
+ for ( const key of keys ) {
1592
+ viewWriter.setStyle( key, viewElementDefinition.styles[ key ], element );
1593
+ }
1594
+ }
1595
+
1596
+ if ( viewElementDefinition.classes ) {
1597
+ const classes = viewElementDefinition.classes;
1598
+
1599
+ if ( typeof classes == 'string' ) {
1600
+ viewWriter.addClass( classes, element );
1601
+ } else {
1602
+ for ( const className of classes ) {
1603
+ viewWriter.addClass( className, element );
1604
+ }
1605
+ }
1606
+ }
1607
+
1608
+ return element;
1609
+ }
1610
+
1611
+ function getFromAttributeCreator( config ) {
1612
+ if ( config.model.values ) {
1613
+ return ( modelAttributeValue, conversionApi ) => {
1614
+ const view = config.view[ modelAttributeValue ];
1615
+
1616
+ if ( view ) {
1617
+ return view( modelAttributeValue, conversionApi );
1618
+ }
1619
+
1620
+ return null;
1621
+ };
1622
+ } else {
1623
+ return config.view;
1624
+ }
1625
+ }
1626
+
1627
+ // Takes the configuration, adds default parameters if they do not exist and normalizes other parameters to be used in downcast converters
1628
+ // for generating a view attribute.
1629
+ //
1630
+ // @param {Object} view View configuration.
1631
+ function normalizeToAttributeConfig( view ) {
1632
+ if ( typeof view == 'string' ) {
1633
+ return modelAttributeValue => ( { key: view, value: modelAttributeValue } );
1634
+ } else if ( typeof view == 'object' ) {
1635
+ // { key, value, ... }
1636
+ if ( view.value ) {
1637
+ return () => view;
1638
+ }
1639
+ // { key, ... }
1640
+ else {
1641
+ return modelAttributeValue => ( { key: view.key, value: modelAttributeValue } );
1642
+ }
1643
+ } else {
1644
+ // function.
1645
+ return view;
1646
+ }
1647
+ }
1648
+
1649
+ // Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter.
1650
+ function prepareDescriptor( highlightDescriptor, data, conversionApi ) {
1651
+ // If passed descriptor is a creator function, call it. If not, just use passed value.
1652
+ const descriptor = typeof highlightDescriptor == 'function' ?
1653
+ highlightDescriptor( data, conversionApi ) :
1654
+ highlightDescriptor;
1655
+
1656
+ if ( !descriptor ) {
1657
+ return null;
1658
+ }
1659
+
1660
+ // Apply default descriptor priority.
1661
+ if ( !descriptor.priority ) {
1662
+ descriptor.priority = 10;
1663
+ }
1664
+
1665
+ // Default descriptor id is marker name.
1666
+ if ( !descriptor.id ) {
1667
+ descriptor.id = data.markerName;
1668
+ }
1669
+
1670
+ return descriptor;
1671
+ }
1672
+
1673
+ /**
1674
+ * An object describing how the marker highlight should be represented in the view.
1675
+ *
1676
+ * Each text node contained in a highlighted range will be wrapped in a `<span>`
1677
+ * {@link module:engine/view/attributeelement~AttributeElement view attribute element} with CSS class(es), attributes and a priority
1678
+ * described by this object.
1679
+ *
1680
+ * Additionally, each {@link module:engine/view/containerelement~ContainerElement container element} can handle displaying the highlight
1681
+ * separately by providing the `addHighlight` and `removeHighlight` custom properties. In this case:
1682
+ *
1683
+ * * The `HighlightDescriptor` object is passed to the `addHighlight` function upon conversion and should be used to apply the highlight to
1684
+ * the element.
1685
+ * * The descriptor `id` is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight with the
1686
+ * given ID from the element.
1687
+ *
1688
+ * @typedef {Object} module:engine/conversion/downcasthelpers~HighlightDescriptor
1689
+ *
1690
+ * @property {String|Array.<String>} classes A CSS class or an array of classes to set. If the descriptor is used to
1691
+ * create an {@link module:engine/view/attributeelement~AttributeElement attribute element} over text nodes, these classes will be set
1692
+ * on that attribute element. If the descriptor is applied to an element, usually these classes will be set on that element, however,
1693
+ * this depends on how the element converts the descriptor.
1694
+ *
1695
+ * @property {String} [id] Descriptor identifier. If not provided, it defaults to the converted marker's name.
1696
+ *
1697
+ * @property {Number} [priority] Descriptor priority. If not provided, it defaults to `10`. If the descriptor is used to create
1698
+ * an {@link module:engine/view/attributeelement~AttributeElement attribute element}, it will be that element's
1699
+ * {@link module:engine/view/attributeelement~AttributeElement#priority priority}. If the descriptor is applied to an element,
1700
+ * the priority will be used to determine which descriptor is more important.
1701
+ *
1702
+ * @property {Object} [attributes] Attributes to set. If the descriptor is used to create
1703
+ * an {@link module:engine/view/attributeelement~AttributeElement attribute element} over text nodes, these attributes will be set on that
1704
+ * attribute element. If the descriptor is applied to an element, usually these attributes will be set on that element, however,
1705
+ * this depends on how the element converts the descriptor.
1706
+ */