@ckeditor/ckeditor5-engine 39.0.2 → 40.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 (241) hide show
  1. package/package.json +2 -2
  2. package/src/controller/datacontroller.d.ts +334 -334
  3. package/src/controller/datacontroller.js +481 -481
  4. package/src/controller/editingcontroller.d.ts +98 -98
  5. package/src/controller/editingcontroller.js +191 -191
  6. package/src/conversion/conversion.d.ts +478 -478
  7. package/src/conversion/conversion.js +601 -601
  8. package/src/conversion/conversionhelpers.d.ts +26 -26
  9. package/src/conversion/conversionhelpers.js +32 -32
  10. package/src/conversion/downcastdispatcher.d.ts +562 -562
  11. package/src/conversion/downcastdispatcher.js +547 -547
  12. package/src/conversion/downcasthelpers.d.ts +1226 -1226
  13. package/src/conversion/downcasthelpers.js +2183 -2183
  14. package/src/conversion/mapper.d.ts +503 -503
  15. package/src/conversion/mapper.js +536 -536
  16. package/src/conversion/modelconsumable.d.ts +201 -201
  17. package/src/conversion/modelconsumable.js +333 -333
  18. package/src/conversion/upcastdispatcher.d.ts +492 -492
  19. package/src/conversion/upcastdispatcher.js +460 -460
  20. package/src/conversion/upcasthelpers.d.ts +499 -499
  21. package/src/conversion/upcasthelpers.js +950 -950
  22. package/src/conversion/viewconsumable.d.ts +369 -369
  23. package/src/conversion/viewconsumable.js +532 -532
  24. package/src/dataprocessor/basichtmlwriter.d.ts +18 -18
  25. package/src/dataprocessor/basichtmlwriter.js +19 -19
  26. package/src/dataprocessor/dataprocessor.d.ts +61 -61
  27. package/src/dataprocessor/dataprocessor.js +5 -5
  28. package/src/dataprocessor/htmldataprocessor.d.ts +76 -76
  29. package/src/dataprocessor/htmldataprocessor.js +96 -96
  30. package/src/dataprocessor/htmlwriter.d.ts +16 -16
  31. package/src/dataprocessor/htmlwriter.js +5 -5
  32. package/src/dataprocessor/xmldataprocessor.d.ts +90 -90
  33. package/src/dataprocessor/xmldataprocessor.js +108 -108
  34. package/src/dev-utils/model.d.ts +124 -124
  35. package/src/dev-utils/model.js +395 -395
  36. package/src/dev-utils/operationreplayer.d.ts +51 -51
  37. package/src/dev-utils/operationreplayer.js +112 -112
  38. package/src/dev-utils/utils.d.ts +37 -37
  39. package/src/dev-utils/utils.js +73 -73
  40. package/src/dev-utils/view.d.ts +319 -319
  41. package/src/dev-utils/view.js +967 -967
  42. package/src/index.d.ts +114 -114
  43. package/src/index.js +78 -78
  44. package/src/model/batch.d.ts +106 -106
  45. package/src/model/batch.js +96 -96
  46. package/src/model/differ.d.ts +387 -387
  47. package/src/model/differ.js +1149 -1149
  48. package/src/model/document.d.ts +272 -272
  49. package/src/model/document.js +361 -361
  50. package/src/model/documentfragment.d.ts +200 -200
  51. package/src/model/documentfragment.js +306 -306
  52. package/src/model/documentselection.d.ts +420 -420
  53. package/src/model/documentselection.js +993 -993
  54. package/src/model/element.d.ts +165 -165
  55. package/src/model/element.js +281 -281
  56. package/src/model/history.d.ts +114 -114
  57. package/src/model/history.js +207 -207
  58. package/src/model/item.d.ts +14 -14
  59. package/src/model/item.js +5 -5
  60. package/src/model/liveposition.d.ts +77 -77
  61. package/src/model/liveposition.js +93 -93
  62. package/src/model/liverange.d.ts +102 -102
  63. package/src/model/liverange.js +120 -120
  64. package/src/model/markercollection.d.ts +335 -335
  65. package/src/model/markercollection.js +403 -403
  66. package/src/model/model.d.ts +919 -919
  67. package/src/model/model.js +842 -842
  68. package/src/model/node.d.ts +256 -256
  69. package/src/model/node.js +375 -375
  70. package/src/model/nodelist.d.ts +91 -91
  71. package/src/model/nodelist.js +163 -163
  72. package/src/model/operation/attributeoperation.d.ts +103 -103
  73. package/src/model/operation/attributeoperation.js +148 -148
  74. package/src/model/operation/detachoperation.d.ts +60 -60
  75. package/src/model/operation/detachoperation.js +77 -77
  76. package/src/model/operation/insertoperation.d.ts +90 -90
  77. package/src/model/operation/insertoperation.js +135 -135
  78. package/src/model/operation/markeroperation.d.ts +91 -91
  79. package/src/model/operation/markeroperation.js +107 -107
  80. package/src/model/operation/mergeoperation.d.ts +100 -100
  81. package/src/model/operation/mergeoperation.js +167 -167
  82. package/src/model/operation/moveoperation.d.ts +96 -96
  83. package/src/model/operation/moveoperation.js +164 -164
  84. package/src/model/operation/nooperation.d.ts +38 -38
  85. package/src/model/operation/nooperation.js +48 -48
  86. package/src/model/operation/operation.d.ts +96 -96
  87. package/src/model/operation/operation.js +62 -62
  88. package/src/model/operation/operationfactory.d.ts +18 -18
  89. package/src/model/operation/operationfactory.js +44 -44
  90. package/src/model/operation/renameoperation.d.ts +83 -83
  91. package/src/model/operation/renameoperation.js +115 -115
  92. package/src/model/operation/rootattributeoperation.d.ts +98 -98
  93. package/src/model/operation/rootattributeoperation.js +155 -155
  94. package/src/model/operation/rootoperation.d.ts +76 -76
  95. package/src/model/operation/rootoperation.js +90 -90
  96. package/src/model/operation/splitoperation.d.ts +109 -109
  97. package/src/model/operation/splitoperation.js +194 -194
  98. package/src/model/operation/transform.d.ts +100 -100
  99. package/src/model/operation/transform.js +1985 -1985
  100. package/src/model/operation/utils.d.ts +71 -71
  101. package/src/model/operation/utils.js +213 -213
  102. package/src/model/position.d.ts +539 -539
  103. package/src/model/position.js +979 -979
  104. package/src/model/range.d.ts +458 -458
  105. package/src/model/range.js +875 -875
  106. package/src/model/rootelement.d.ts +60 -60
  107. package/src/model/rootelement.js +74 -74
  108. package/src/model/schema.d.ts +1186 -1186
  109. package/src/model/schema.js +1242 -1242
  110. package/src/model/selection.d.ts +482 -482
  111. package/src/model/selection.js +789 -789
  112. package/src/model/text.d.ts +66 -66
  113. package/src/model/text.js +85 -85
  114. package/src/model/textproxy.d.ts +144 -144
  115. package/src/model/textproxy.js +189 -189
  116. package/src/model/treewalker.d.ts +186 -186
  117. package/src/model/treewalker.js +244 -244
  118. package/src/model/typecheckable.d.ts +285 -285
  119. package/src/model/typecheckable.js +16 -16
  120. package/src/model/utils/autoparagraphing.d.ts +37 -37
  121. package/src/model/utils/autoparagraphing.js +63 -63
  122. package/src/model/utils/deletecontent.d.ts +58 -58
  123. package/src/model/utils/deletecontent.js +488 -488
  124. package/src/model/utils/findoptimalinsertionrange.d.ts +32 -32
  125. package/src/model/utils/findoptimalinsertionrange.js +57 -57
  126. package/src/model/utils/getselectedcontent.d.ts +30 -30
  127. package/src/model/utils/getselectedcontent.js +125 -125
  128. package/src/model/utils/insertcontent.d.ts +46 -46
  129. package/src/model/utils/insertcontent.js +705 -705
  130. package/src/model/utils/insertobject.d.ts +44 -44
  131. package/src/model/utils/insertobject.js +139 -139
  132. package/src/model/utils/modifyselection.d.ts +48 -48
  133. package/src/model/utils/modifyselection.js +186 -186
  134. package/src/model/utils/selection-post-fixer.d.ts +74 -74
  135. package/src/model/utils/selection-post-fixer.js +260 -260
  136. package/src/model/writer.d.ts +851 -851
  137. package/src/model/writer.js +1306 -1306
  138. package/src/view/attributeelement.d.ts +108 -108
  139. package/src/view/attributeelement.js +184 -184
  140. package/src/view/containerelement.d.ts +49 -49
  141. package/src/view/containerelement.js +80 -80
  142. package/src/view/datatransfer.d.ts +79 -79
  143. package/src/view/datatransfer.js +98 -98
  144. package/src/view/document.d.ts +184 -184
  145. package/src/view/document.js +120 -120
  146. package/src/view/documentfragment.d.ts +149 -149
  147. package/src/view/documentfragment.js +228 -228
  148. package/src/view/documentselection.d.ts +306 -306
  149. package/src/view/documentselection.js +256 -256
  150. package/src/view/domconverter.d.ts +640 -640
  151. package/src/view/domconverter.js +1450 -1425
  152. package/src/view/downcastwriter.d.ts +996 -996
  153. package/src/view/downcastwriter.js +1696 -1696
  154. package/src/view/editableelement.d.ts +62 -62
  155. package/src/view/editableelement.js +62 -62
  156. package/src/view/element.d.ts +468 -468
  157. package/src/view/element.js +724 -724
  158. package/src/view/elementdefinition.d.ts +87 -87
  159. package/src/view/elementdefinition.js +5 -5
  160. package/src/view/emptyelement.d.ts +41 -41
  161. package/src/view/emptyelement.js +73 -73
  162. package/src/view/filler.d.ts +111 -111
  163. package/src/view/filler.js +150 -150
  164. package/src/view/item.d.ts +14 -14
  165. package/src/view/item.js +5 -5
  166. package/src/view/matcher.d.ts +486 -486
  167. package/src/view/matcher.js +507 -507
  168. package/src/view/node.d.ts +163 -163
  169. package/src/view/node.js +228 -228
  170. package/src/view/observer/arrowkeysobserver.d.ts +45 -45
  171. package/src/view/observer/arrowkeysobserver.js +40 -40
  172. package/src/view/observer/bubblingemittermixin.d.ts +166 -166
  173. package/src/view/observer/bubblingemittermixin.js +172 -172
  174. package/src/view/observer/bubblingeventinfo.d.ts +47 -47
  175. package/src/view/observer/bubblingeventinfo.js +37 -37
  176. package/src/view/observer/clickobserver.d.ts +43 -43
  177. package/src/view/observer/clickobserver.js +29 -29
  178. package/src/view/observer/compositionobserver.d.ts +82 -82
  179. package/src/view/observer/compositionobserver.js +60 -60
  180. package/src/view/observer/domeventdata.d.ts +50 -50
  181. package/src/view/observer/domeventdata.js +47 -47
  182. package/src/view/observer/domeventobserver.d.ts +73 -73
  183. package/src/view/observer/domeventobserver.js +79 -79
  184. package/src/view/observer/fakeselectionobserver.d.ts +47 -47
  185. package/src/view/observer/fakeselectionobserver.js +91 -91
  186. package/src/view/observer/focusobserver.d.ts +82 -82
  187. package/src/view/observer/focusobserver.js +86 -86
  188. package/src/view/observer/inputobserver.d.ts +86 -86
  189. package/src/view/observer/inputobserver.js +164 -164
  190. package/src/view/observer/keyobserver.d.ts +66 -66
  191. package/src/view/observer/keyobserver.js +39 -39
  192. package/src/view/observer/mouseobserver.d.ts +89 -89
  193. package/src/view/observer/mouseobserver.js +29 -29
  194. package/src/view/observer/mutationobserver.d.ts +86 -86
  195. package/src/view/observer/mutationobserver.js +206 -206
  196. package/src/view/observer/observer.d.ts +89 -89
  197. package/src/view/observer/observer.js +84 -84
  198. package/src/view/observer/selectionobserver.d.ts +148 -148
  199. package/src/view/observer/selectionobserver.js +202 -202
  200. package/src/view/observer/tabobserver.d.ts +46 -46
  201. package/src/view/observer/tabobserver.js +42 -42
  202. package/src/view/placeholder.d.ts +96 -96
  203. package/src/view/placeholder.js +267 -267
  204. package/src/view/position.d.ts +189 -189
  205. package/src/view/position.js +324 -324
  206. package/src/view/range.d.ts +279 -279
  207. package/src/view/range.js +430 -430
  208. package/src/view/rawelement.d.ts +73 -73
  209. package/src/view/rawelement.js +105 -105
  210. package/src/view/renderer.d.ts +265 -265
  211. package/src/view/renderer.js +999 -999
  212. package/src/view/rooteditableelement.d.ts +41 -41
  213. package/src/view/rooteditableelement.js +69 -69
  214. package/src/view/selection.d.ts +375 -375
  215. package/src/view/selection.js +559 -559
  216. package/src/view/styles/background.d.ts +33 -33
  217. package/src/view/styles/background.js +74 -74
  218. package/src/view/styles/border.d.ts +43 -43
  219. package/src/view/styles/border.js +316 -316
  220. package/src/view/styles/margin.d.ts +29 -29
  221. package/src/view/styles/margin.js +34 -34
  222. package/src/view/styles/padding.d.ts +29 -29
  223. package/src/view/styles/padding.js +34 -34
  224. package/src/view/styles/utils.d.ts +93 -93
  225. package/src/view/styles/utils.js +219 -219
  226. package/src/view/stylesmap.d.ts +675 -675
  227. package/src/view/stylesmap.js +766 -766
  228. package/src/view/text.d.ts +74 -74
  229. package/src/view/text.js +93 -93
  230. package/src/view/textproxy.d.ts +97 -97
  231. package/src/view/textproxy.js +124 -124
  232. package/src/view/treewalker.d.ts +195 -195
  233. package/src/view/treewalker.js +327 -327
  234. package/src/view/typecheckable.d.ts +448 -448
  235. package/src/view/typecheckable.js +19 -19
  236. package/src/view/uielement.d.ts +96 -96
  237. package/src/view/uielement.js +182 -182
  238. package/src/view/upcastwriter.d.ts +417 -417
  239. package/src/view/upcastwriter.js +359 -359
  240. package/src/view/view.d.ts +487 -487
  241. package/src/view/view.js +546 -546
@@ -1,2183 +1,2183 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
- */
5
- /**
6
- * Contains downcast (model-to-view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
7
- *
8
- * @module engine/conversion/downcasthelpers
9
- */
10
- import ModelRange from '../model/range';
11
- import ModelSelection from '../model/selection';
12
- import ModelDocumentSelection from '../model/documentselection';
13
- import ModelElement from '../model/element';
14
- import ModelPosition from '../model/position';
15
- import ViewAttributeElement from '../view/attributeelement';
16
- import ConversionHelpers from './conversionhelpers';
17
- import { CKEditorError, toArray } from '@ckeditor/ckeditor5-utils';
18
- import { cloneDeep } from 'lodash-es';
19
- /**
20
- * Downcast conversion helper functions.
21
- *
22
- * Learn more about {@glink framework/deep-dive/conversion/downcast downcast helpers}.
23
- *
24
- * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
25
- */
26
- export default class DowncastHelpers extends ConversionHelpers {
27
- /**
28
- * Model element to view element conversion helper.
29
- *
30
- * This conversion results in creating a view element. For example, model `<paragraph>Foo</paragraph>` becomes `<p>Foo</p>` in the view.
31
- *
32
- * ```ts
33
- * editor.conversion.for( 'downcast' ).elementToElement( {
34
- * model: 'paragraph',
35
- * view: 'p'
36
- * } );
37
- *
38
- * editor.conversion.for( 'downcast' ).elementToElement( {
39
- * model: 'paragraph',
40
- * view: 'div',
41
- * converterPriority: 'high'
42
- * } );
43
- *
44
- * editor.conversion.for( 'downcast' ).elementToElement( {
45
- * model: 'fancyParagraph',
46
- * view: {
47
- * name: 'p',
48
- * classes: 'fancy'
49
- * }
50
- * } );
51
- *
52
- * editor.conversion.for( 'downcast' ).elementToElement( {
53
- * model: 'heading',
54
- * view: ( modelElement, conversionApi ) => {
55
- * const { writer } = conversionApi;
56
- *
57
- * return writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
58
- * }
59
- * } );
60
- * ```
61
- *
62
- * The element-to-element conversion supports the reconversion mechanism. It can be enabled by using either the `attributes` or
63
- * the `children` props on a model description. You will find a couple examples below.
64
- *
65
- * In order to reconvert an element if any of its direct children have been added or removed, use the `children` property on a `model`
66
- * description. For example, this model:
67
- *
68
- * ```xml
69
- * <box>
70
- * <paragraph>Some text.</paragraph>
71
- * </box>
72
- * ```
73
- *
74
- * will be converted into this structure in the view:
75
- *
76
- * ```html
77
- * <div class="box" data-type="single">
78
- * <p>Some text.</p>
79
- * </div>
80
- * ```
81
- *
82
- * But if more items were inserted in the model:
83
- *
84
- * ```xml
85
- * <box>
86
- * <paragraph>Some text.</paragraph>
87
- * <paragraph>Other item.</paragraph>
88
- * </box>
89
- * ```
90
- *
91
- * it will be converted into this structure in the view (note the element `data-type` change):
92
- *
93
- * ```html
94
- * <div class="box" data-type="multiple">
95
- * <p>Some text.</p>
96
- * <p>Other item.</p>
97
- * </div>
98
- * ```
99
- *
100
- * Such a converter would look like this (note that the `paragraph` elements are converted separately):
101
- *
102
- * ```ts
103
- * editor.conversion.for( 'downcast' ).elementToElement( {
104
- * model: {
105
- * name: 'box',
106
- * children: true
107
- * },
108
- * view: ( modelElement, conversionApi ) => {
109
- * const { writer } = conversionApi;
110
- *
111
- * return writer.createContainerElement( 'div', {
112
- * class: 'box',
113
- * 'data-type': modelElement.childCount == 1 ? 'single' : 'multiple'
114
- * } );
115
- * }
116
- * } );
117
- * ```
118
- *
119
- * In order to reconvert element if any of its attributes have been updated, use the `attributes` property on a `model`
120
- * description. For example, this model:
121
- *
122
- * ```xml
123
- * <heading level="2">Some text.</heading>
124
- * ```
125
- *
126
- * will be converted into this structure in the view:
127
- *
128
- * ```html
129
- * <h2>Some text.</h2>
130
- * ```
131
- *
132
- * But if the `heading` element's `level` attribute has been updated to `3` for example, then
133
- * it will be converted into this structure in the view:
134
- *
135
- * ```html
136
- * <h3>Some text.</h3>
137
- * ```
138
- *
139
- * Such a converter would look as follows:
140
- *
141
- * ```ts
142
- * editor.conversion.for( 'downcast' ).elementToElement( {
143
- * model: {
144
- * name: 'heading',
145
- * attributes: 'level'
146
- * },
147
- * view: ( modelElement, conversionApi ) => {
148
- * const { writer } = conversionApi;
149
- *
150
- * return writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
151
- * }
152
- * } );
153
- * ```
154
- *
155
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
156
- * to the conversion process.
157
- *
158
- * You can read more about the element-to-element conversion in the
159
- * {@glink framework/deep-dive/conversion/downcast downcast conversion} guide.
160
- *
161
- * @param config Conversion configuration.
162
- * @param config.model The description or a name of the model element to convert.
163
- * @param config.model.attributes The list of attribute names that should be consumed while creating
164
- * the view element. Note that the view will be reconverted if any of the listed attributes changes.
165
- * @param config.model.children Specifies whether the view element requires reconversion if the list
166
- * of the model child nodes changed.
167
- * @param config.view A view element definition or a function that takes the model element and
168
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
169
- * as parameters and returns a view container element.
170
- */
171
- elementToElement(config) {
172
- return this.add(downcastElementToElement(config));
173
- }
174
- /**
175
- * The model element to view structure (several elements) conversion helper.
176
- *
177
- * This conversion results in creating a view structure with one or more slots defined for the child nodes.
178
- * For example, a model `<table>` may become this structure in the view:
179
- *
180
- * ```html
181
- * <figure class="table">
182
- * <table>
183
- * <tbody>${ slot for table rows }</tbody>
184
- * </table>
185
- * </figure>
186
- * ```
187
- *
188
- * The children of the model's `<table>` element will be inserted into the `<tbody>` element.
189
- * If the `elementToElement()` helper was used, the children would be inserted into the `<figure>`.
190
- *
191
- * An example converter that converts the following model structure:
192
- *
193
- * ```xml
194
- * <wrappedParagraph>Some text.</wrappedParagraph>
195
- * ```
196
- *
197
- * into this structure in the view:
198
- *
199
- * ```html
200
- * <div class="wrapper">
201
- * <p>Some text.</p>
202
- * </div>
203
- * ```
204
- *
205
- * would look like this:
206
- *
207
- * ```ts
208
- * editor.conversion.for( 'downcast' ).elementToStructure( {
209
- * model: 'wrappedParagraph',
210
- * view: ( modelElement, conversionApi ) => {
211
- * const { writer } = conversionApi;
212
- *
213
- * const wrapperViewElement = writer.createContainerElement( 'div', { class: 'wrapper' } );
214
- * const paragraphViewElement = writer.createContainerElement( 'p' );
215
- *
216
- * writer.insert( writer.createPositionAt( wrapperViewElement, 0 ), paragraphViewElement );
217
- * writer.insert( writer.createPositionAt( paragraphViewElement, 0 ), writer.createSlot() );
218
- *
219
- * return wrapperViewElement;
220
- * }
221
- * } );
222
- * ```
223
- *
224
- * The `createSlot()` function can also take a callback that allows filtering which children of the model element
225
- * should be converted into this slot.
226
- *
227
- * Imagine a table feature where for this model structure:
228
- *
229
- * ```xml
230
- * <table headingRows="1">
231
- * <tableRow> ... table cells 1 ... </tableRow>
232
- * <tableRow> ... table cells 2 ... </tableRow>
233
- * <tableRow> ... table cells 3 ... </tableRow>
234
- * <caption>Caption text</caption>
235
- * </table>
236
- * ```
237
- *
238
- * we want to generate this view structure:
239
- *
240
- * ```html
241
- * <figure class="table">
242
- * <table>
243
- * <thead>
244
- * <tr> ... table cells 1 ... </tr>
245
- * </thead>
246
- * <tbody>
247
- * <tr> ... table cells 2 ... </tr>
248
- * <tr> ... table cells 3 ... </tr>
249
- * </tbody>
250
- * </table>
251
- * <figcaption>Caption text</figcaption>
252
- * </figure>
253
- * ```
254
- *
255
- * The converter has to take the `headingRows` attribute into consideration when allocating the `<tableRow>` elements
256
- * into the `<tbody>` and `<thead>` elements. Hence, we need two slots and need to define proper filter callbacks for them.
257
- *
258
- * Additionally, all elements other than `<tableRow>` should be placed outside the `<table>` tag.
259
- * In the example above, this will handle the table caption.
260
- *
261
- * Such a converter would look like this:
262
- *
263
- * ```ts
264
- * editor.conversion.for( 'downcast' ).elementToStructure( {
265
- * model: {
266
- * name: 'table',
267
- * attributes: [ 'headingRows' ]
268
- * },
269
- * view: ( modelElement, conversionApi ) => {
270
- * const { writer } = conversionApi;
271
- *
272
- * const figureElement = writer.createContainerElement( 'figure', { class: 'table' } );
273
- * const tableElement = writer.createContainerElement( 'table' );
274
- *
275
- * writer.insert( writer.createPositionAt( figureElement, 0 ), tableElement );
276
- *
277
- * const headingRows = modelElement.getAttribute( 'headingRows' ) || 0;
278
- *
279
- * if ( headingRows > 0 ) {
280
- * const tableHead = writer.createContainerElement( 'thead' );
281
- *
282
- * const headSlot = writer.createSlot( node => node.is( 'element', 'tableRow' ) && node.index < headingRows );
283
- *
284
- * writer.insert( writer.createPositionAt( tableElement, 'end' ), tableHead );
285
- * writer.insert( writer.createPositionAt( tableHead, 0 ), headSlot );
286
- * }
287
- *
288
- * if ( headingRows < tableUtils.getRows( table ) ) {
289
- * const tableBody = writer.createContainerElement( 'tbody' );
290
- *
291
- * const bodySlot = writer.createSlot( node => node.is( 'element', 'tableRow' ) && node.index >= headingRows );
292
- *
293
- * writer.insert( writer.createPositionAt( tableElement, 'end' ), tableBody );
294
- * writer.insert( writer.createPositionAt( tableBody, 0 ), bodySlot );
295
- * }
296
- *
297
- * const restSlot = writer.createSlot( node => !node.is( 'element', 'tableRow' ) );
298
- *
299
- * writer.insert( writer.createPositionAt( figureElement, 'end' ), restSlot );
300
- *
301
- * return figureElement;
302
- * }
303
- * } );
304
- * ```
305
- *
306
- * Note: The children of a model element that's being converted must be allocated in the same order in the view
307
- * in which they are placed in the model.
308
- *
309
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
310
- * to the conversion process.
311
- *
312
- * @param config Conversion configuration.
313
- * @param config.model The description or a name of the model element to convert.
314
- * @param config.model.name The name of the model element to convert.
315
- * @param config.model.attributes The list of attribute names that should be consumed while creating
316
- * the view structure. Note that the view will be reconverted if any of the listed attributes will change.
317
- * @param config.view A function that takes the model element and
318
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as parameters
319
- * and returns a view container element with slots for model child nodes to be converted into.
320
- */
321
- elementToStructure(config) {
322
- return this.add(downcastElementToStructure(config));
323
- }
324
- /**
325
- * Model attribute to view element conversion helper.
326
- *
327
- * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with
328
- * `"Foo"` as data and the `bold` attribute becomes `<strong>Foo</strong>` in the view.
329
- *
330
- * ```ts
331
- * editor.conversion.for( 'downcast' ).attributeToElement( {
332
- * model: 'bold',
333
- * view: 'strong'
334
- * } );
335
- *
336
- * editor.conversion.for( 'downcast' ).attributeToElement( {
337
- * model: 'bold',
338
- * view: 'b',
339
- * converterPriority: 'high'
340
- * } );
341
- *
342
- * editor.conversion.for( 'downcast' ).attributeToElement( {
343
- * model: 'invert',
344
- * view: {
345
- * name: 'span',
346
- * classes: [ 'font-light', 'bg-dark' ]
347
- * }
348
- * } );
349
- *
350
- * editor.conversion.for( 'downcast' ).attributeToElement( {
351
- * model: {
352
- * key: 'fontSize',
353
- * values: [ 'big', 'small' ]
354
- * },
355
- * view: {
356
- * big: {
357
- * name: 'span',
358
- * styles: {
359
- * 'font-size': '1.2em'
360
- * }
361
- * },
362
- * small: {
363
- * name: 'span',
364
- * styles: {
365
- * 'font-size': '0.8em'
366
- * }
367
- * }
368
- * }
369
- * } );
370
- *
371
- * editor.conversion.for( 'downcast' ).attributeToElement( {
372
- * model: 'bold',
373
- * view: ( modelAttributeValue, conversionApi ) => {
374
- * const { writer } = conversionApi;
375
- *
376
- * return writer.createAttributeElement( 'span', {
377
- * style: 'font-weight:' + modelAttributeValue
378
- * } );
379
- * }
380
- * } );
381
- *
382
- * editor.conversion.for( 'downcast' ).attributeToElement( {
383
- * model: {
384
- * key: 'color',
385
- * name: '$text'
386
- * },
387
- * view: ( modelAttributeValue, conversionApi ) => {
388
- * const { writer } = conversionApi;
389
- *
390
- * return writer.createAttributeElement( 'span', {
391
- * style: 'color:' + modelAttributeValue
392
- * } );
393
- * }
394
- * } );
395
- * ```
396
- *
397
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
398
- * to the conversion process.
399
- *
400
- * @param config Conversion configuration.
401
- * @param config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
402
- * of `String`s with possible values if the model attribute is an enumerable.
403
- * @param config.view A view element definition or a function
404
- * that takes the model attribute value and
405
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as parameters and returns a view
406
- * attribute element. If `config.model.values` is given, `config.view` should be an object assigning values from `config.model.values`
407
- * to view element definitions or functions.
408
- * @param config.converterPriority Converter priority.
409
- */
410
- attributeToElement(config) {
411
- return this.add(downcastAttributeToElement(config));
412
- }
413
- /**
414
- * Model attribute to view attribute conversion helper.
415
- *
416
- * This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example,
417
- * `<imageInline src='foo.jpg'></imageInline>` is converted to `<img src='foo.jpg'></img>`.
418
- *
419
- * ```ts
420
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
421
- * model: 'source',
422
- * view: 'src'
423
- * } );
424
- *
425
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
426
- * model: 'source',
427
- * view: 'href',
428
- * converterPriority: 'high'
429
- * } );
430
- *
431
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
432
- * model: {
433
- * name: 'imageInline',
434
- * key: 'source'
435
- * },
436
- * view: 'src'
437
- * } );
438
- *
439
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
440
- * model: {
441
- * name: 'styled',
442
- * values: [ 'dark', 'light' ]
443
- * },
444
- * view: {
445
- * dark: {
446
- * key: 'class',
447
- * value: [ 'styled', 'styled-dark' ]
448
- * },
449
- * light: {
450
- * key: 'class',
451
- * value: [ 'styled', 'styled-light' ]
452
- * }
453
- * }
454
- * } );
455
- *
456
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
457
- * model: 'styled',
458
- * view: modelAttributeValue => ( {
459
- * key: 'class',
460
- * value: 'styled-' + modelAttributeValue
461
- * } )
462
- * } );
463
- * ```
464
- *
465
- * **Note**: Downcasting to a style property requires providing `value` as an object:
466
- *
467
- * ```ts
468
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
469
- * model: 'lineHeight',
470
- * view: modelAttributeValue => ( {
471
- * key: 'style',
472
- * value: {
473
- * 'line-height': modelAttributeValue,
474
- * 'border-bottom': '1px dotted #ba2'
475
- * }
476
- * } )
477
- * } );
478
- * ```
479
- *
480
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
481
- * to the conversion process.
482
- *
483
- * @param config Conversion configuration.
484
- * @param config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
485
- * the attribute key, possible values and, optionally, an element name to convert from.
486
- * @param config.view A view attribute key, or a `{ key, value }` object or a function that takes the model attribute value and
487
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
488
- * as parameters and returns a `{ key, value }` object. If the `key` is `'class'`, the `value` can be a `String` or an
489
- * array of `String`s. If the `key` is `'style'`, the `value` is an object with key-value pairs. In other cases, `value` is a `String`.
490
- * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
491
- * `{ key, value }` objects or a functions.
492
- * @param config.converterPriority Converter priority.
493
- */
494
- attributeToAttribute(config) {
495
- return this.add(downcastAttributeToAttribute(config));
496
- }
497
- /**
498
- * Model marker to view element conversion helper.
499
- *
500
- * **Note**: This method should be used mainly for editing the downcast and it is recommended
501
- * to use the {@link #markerToData `#markerToData()`} helper instead.
502
- *
503
- * This helper may produce invalid HTML code (e.g. a span between table cells).
504
- * It should only be used when you are sure that the produced HTML will be semantically correct.
505
- *
506
- * This conversion results in creating a view element on the boundaries of the converted marker. If the converted marker
507
- * is collapsed, only one element is created. For example, a model marker set like this: `<paragraph>F[oo b]ar</paragraph>`
508
- * becomes `<p>F<span data-marker="search"></span>oo b<span data-marker="search"></span>ar</p>` in the view.
509
- *
510
- * ```ts
511
- * editor.conversion.for( 'editingDowncast' ).markerToElement( {
512
- * model: 'search',
513
- * view: 'marker-search'
514
- * } );
515
- *
516
- * editor.conversion.for( 'editingDowncast' ).markerToElement( {
517
- * model: 'search',
518
- * view: 'search-result',
519
- * converterPriority: 'high'
520
- * } );
521
- *
522
- * editor.conversion.for( 'editingDowncast' ).markerToElement( {
523
- * model: 'search',
524
- * view: {
525
- * name: 'span',
526
- * attributes: {
527
- * 'data-marker': 'search'
528
- * }
529
- * }
530
- * } );
531
- *
532
- * editor.conversion.for( 'editingDowncast' ).markerToElement( {
533
- * model: 'search',
534
- * view: ( markerData, conversionApi ) => {
535
- * const { writer } = conversionApi;
536
- *
537
- * return writer.createUIElement( 'span', {
538
- * 'data-marker': 'search',
539
- * 'data-start': markerData.isOpening
540
- * } );
541
- * }
542
- * } );
543
- * ```
544
- *
545
- * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function
546
- * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
547
- * as a parameters and should return an instance of the
548
- * {@link module:engine/view/uielement~UIElement view UI element}. The `data` object and
549
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi `conversionApi`} are passed from
550
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
551
- * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` for
552
- * the marker end boundary element.
553
- *
554
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
555
- * to the conversion process.
556
- *
557
- * @param config Conversion configuration.
558
- * @param config.model The name of the model marker (or model marker group) to convert.
559
- * @param config.view A view element definition or a function that takes the model marker data and
560
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
561
- * and returns a view UI element.
562
- * @param config.converterPriority Converter priority.
563
- */
564
- markerToElement(config) {
565
- return this.add(downcastMarkerToElement(config));
566
- }
567
- /**
568
- * Model marker to highlight conversion helper.
569
- *
570
- * This conversion results in creating a highlight on view nodes. For this kind of conversion,
571
- * the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor} should be provided.
572
- *
573
- * For text nodes, a `<span>` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes
574
- * in the converted marker range. For example, a model marker set like this: `<paragraph>F[oo b]ar</paragraph>` becomes
575
- * `<p>F<span class="comment">oo b</span>ar</p>` in the view.
576
- *
577
- * {@link module:engine/view/containerelement~ContainerElement} may provide a custom way of handling highlight. Most often,
578
- * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in `<span>`).
579
- * For example, a model marker set like this:
580
- * `[<imageInline src="foo.jpg"></imageInline>]` becomes `<img src="foo.jpg" class="comment"></img>` in the view.
581
- *
582
- * For container elements, the conversion is two-step. While the converter processes the highlight descriptor and passes it
583
- * to a container element, it is the container element instance itself that applies values from the highlight descriptor.
584
- * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that.
585
- *
586
- * ```ts
587
- * editor.conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
588
- *
589
- * editor.conversion.for( 'downcast' ).markerToHighlight( {
590
- * model: 'comment',
591
- * view: { classes: 'comment' },
592
- * converterPriority: 'high'
593
- * } );
594
- *
595
- * editor.conversion.for( 'downcast' ).markerToHighlight( {
596
- * model: 'comment',
597
- * view: ( data, conversionApi ) => {
598
- * // Assuming that the marker name is in a form of comment:commentType:commentId.
599
- * const [ , commentType, commentId ] = data.markerName.split( ':' );
600
- *
601
- * return {
602
- * classes: [ 'comment', 'comment-' + commentType ],
603
- * attributes: { 'data-comment-id': commentId }
604
- * };
605
- * }
606
- * } );
607
- * ```
608
- *
609
- * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function
610
- * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
611
- * as the parameters and should return a
612
- * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor}.
613
- * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
614
- *
615
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
616
- * to the conversion process.
617
- *
618
- * @param config Conversion configuration.
619
- * @param config.model The name of the model marker (or model marker group) to convert.
620
- * @param config.view A highlight descriptor that will be used for highlighting or a function that takes the model marker data and
621
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
622
- * and returns a highlight descriptor.
623
- * @param config.converterPriority Converter priority.
624
- */
625
- markerToHighlight(config) {
626
- return this.add(downcastMarkerToHighlight(config));
627
- }
628
- /**
629
- * Model marker converter for data downcast.
630
- *
631
- * This conversion creates a representation for model marker boundaries in the view:
632
- *
633
- * * If the marker boundary is before or after a model element, a view attribute is set on a corresponding view element.
634
- * * In other cases, a view element with the specified tag name is inserted at the corresponding view position.
635
- *
636
- * Typically, the marker names use the `group:uniqueId:otherData` convention. For example: `comment:e34zfk9k2n459df53sjl34:zx32c`.
637
- * The default configuration for this conversion is that the first part is the `group` part and the rest of
638
- * the marker name becomes the `name` part.
639
- *
640
- * Tag and attribute names and values are generated from the marker name:
641
- *
642
- * * The templates for attributes are `data-[group]-start-before="[name]"`, `data-[group]-start-after="[name]"`,
643
- * `data-[group]-end-before="[name]"` and `data-[group]-end-after="[name]"`.
644
- * * The templates for view elements are `<[group]-start name="[name]">` and `<[group]-end name="[name]">`.
645
- *
646
- * Attributes mark whether the given marker's start or end boundary is before or after the given element.
647
- * The `data-[group]-start-before` and `data-[group]-end-after` attributes are favored.
648
- * The other two are used when the former two cannot be used.
649
- *
650
- * The conversion configuration can take a function that will generate different group and name parts.
651
- * 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
652
- * properties: `group` and `name`. If the function returns a falsy value, the conversion will not take place.
653
- *
654
- * Basic usage:
655
- *
656
- * ```ts
657
- * // Using the default conversion.
658
- * // In this case, all markers with names starting with 'comment:' will be converted.
659
- * // The `group` parameter will be set to `comment`.
660
- * // The `name` parameter will be the rest of the marker name (without the `:`).
661
- * editor.conversion.for( 'dataDowncast' ).markerToData( {
662
- * model: 'comment'
663
- * } );
664
- * ```
665
- *
666
- * An example of a view that may be generated by this conversion (assuming a marker with the name `comment:commentId:uid` marked
667
- * by `[]`):
668
- *
669
- * ```
670
- * // Model:
671
- * <paragraph>Foo[bar</paragraph>
672
- * <imageBlock src="abc.jpg"></imageBlock>]
673
- *
674
- * // View:
675
- * <p>Foo<comment-start name="commentId:uid"></comment-start>bar</p>
676
- * <figure data-comment-end-after="commentId:uid" class="image"><img src="abc.jpg" /></figure>
677
- * ```
678
- *
679
- * In the example above, the comment starts before "bar" and ends after the image.
680
- *
681
- * If the `name` part is empty, the following view may be generated:
682
- *
683
- * ```html
684
- * <p>Foo <myMarker-start></myMarker-start>bar</p>
685
- * <figure data-myMarker-end-after="" class="image"><img src="abc.jpg" /></figure>
686
- * ```
687
- *
688
- * **Note:** A situation where some markers have the `name` part and some do not, is incorrect and should be avoided.
689
- *
690
- * Examples where `data-group-start-after` and `data-group-end-before` are used:
691
- *
692
- * ```
693
- * // Model:
694
- * <blockQuote>[]<paragraph>Foo</paragraph></blockQuote>
695
- *
696
- * // View:
697
- * <blockquote><p data-group-end-before="name" data-group-start-before="name">Foo</p></blockquote>
698
- * ```
699
- *
700
- * Similarly, when a marker is collapsed after the last element:
701
- *
702
- * ```
703
- * // Model:
704
- * <blockQuote><paragraph>Foo</paragraph>[]</blockQuote>
705
- *
706
- * // View:
707
- * <blockquote><p data-group-end-after="name" data-group-start-after="name">Foo</p></blockquote>
708
- * ```
709
- *
710
- * When there are multiple markers from the same group stored in the same attribute of the same element, their
711
- * name parts are put together in the attribute value, for example: `data-group-start-before="name1,name2,name3"`.
712
- *
713
- * Other examples of usage:
714
- *
715
- * ```ts
716
- * // Using a custom function which is the same as the default conversion:
717
- * editor.conversion.for( 'dataDowncast' ).markerToData( {
718
- * model: 'comment',
719
- * view: markerName => ( {
720
- * group: 'comment',
721
- * name: markerName.substr( 8 ) // Removes 'comment:' part.
722
- * } )
723
- * } );
724
- *
725
- * // Using the converter priority:
726
- * editor.conversion.for( 'dataDowncast' ).markerToData( {
727
- * model: 'comment',
728
- * view: markerName => ( {
729
- * group: 'comment',
730
- * name: markerName.substr( 8 ) // Removes 'comment:' part.
731
- * } ),
732
- * converterPriority: 'high'
733
- * } );
734
- * ```
735
- *
736
- * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline.
737
- *
738
- * See the {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} API guide to learn how to
739
- * add a converter to the conversion process.
740
- *
741
- * @param config Conversion configuration.
742
- * @param config.model The name of the model marker (or the model marker group) to convert.
743
- * @param config.view A function that takes the model marker name and
744
- * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as the parameters
745
- * and returns an object with the `group` and `name` properties.
746
- * @param config.converterPriority Converter priority.
747
- */
748
- markerToData(config) {
749
- return this.add(downcastMarkerToData(config));
750
- }
751
- }
752
- /**
753
- * Function factory that creates a default downcast converter for text insertion changes.
754
- *
755
- * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
756
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
757
- *
758
- * ```ts
759
- * modelDispatcher.on( 'insert:$text', insertText() );
760
- * ```
761
- *
762
- * @returns Insert text event converter.
763
- */
764
- export function insertText() {
765
- return (evt, data, conversionApi) => {
766
- if (!conversionApi.consumable.consume(data.item, evt.name)) {
767
- return;
768
- }
769
- const viewWriter = conversionApi.writer;
770
- const viewPosition = conversionApi.mapper.toViewPosition(data.range.start);
771
- const viewText = viewWriter.createText(data.item.data);
772
- viewWriter.insert(viewPosition, viewText);
773
- };
774
- }
775
- /**
776
- * Function factory that creates a default downcast converter for triggering attributes and children conversion.
777
- *
778
- * @returns The converter.
779
- */
780
- export function insertAttributesAndChildren() {
781
- return (evt, data, conversionApi) => {
782
- conversionApi.convertAttributes(data.item);
783
- // Start converting children of the current item.
784
- // In case of reconversion children were already re-inserted or converted separately.
785
- if (!data.reconversion && data.item.is('element') && !data.item.isEmpty) {
786
- conversionApi.convertChildren(data.item);
787
- }
788
- };
789
- }
790
- /**
791
- * Function factory that creates a default downcast converter for node remove changes.
792
- *
793
- * ```ts
794
- * modelDispatcher.on( 'remove', remove() );
795
- * ```
796
- *
797
- * @returns Remove event converter.
798
- */
799
- export function remove() {
800
- return (evt, data, conversionApi) => {
801
- // Find the view range start position by mapping the model position at which the remove happened.
802
- const viewStart = conversionApi.mapper.toViewPosition(data.position);
803
- const modelEnd = data.position.getShiftedBy(data.length);
804
- const viewEnd = conversionApi.mapper.toViewPosition(modelEnd, { isPhantom: true });
805
- const viewRange = conversionApi.writer.createRange(viewStart, viewEnd);
806
- // Trim the range to remove in case some UI elements are on the view range boundaries.
807
- const removed = conversionApi.writer.remove(viewRange.getTrimmed());
808
- // After the range is removed, unbind all view elements from the model.
809
- // Range inside view document fragment is used to unbind deeply.
810
- for (const child of conversionApi.writer.createRangeIn(removed).getItems()) {
811
- conversionApi.mapper.unbindViewElement(child, { defer: true });
812
- }
813
- };
814
- }
815
- /**
816
- * Creates a `<span>` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from the information
817
- * provided by the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor} object. If the priority
818
- * is not provided in the descriptor, the default priority will be used.
819
- */
820
- export function createViewElementFromHighlightDescriptor(writer, descriptor) {
821
- const viewElement = writer.createAttributeElement('span', descriptor.attributes);
822
- if (descriptor.classes) {
823
- viewElement._addClass(descriptor.classes);
824
- }
825
- if (typeof descriptor.priority === 'number') {
826
- viewElement._priority = descriptor.priority;
827
- }
828
- viewElement._id = descriptor.id;
829
- return viewElement;
830
- }
831
- /**
832
- * Function factory that creates a converter which converts a non-collapsed {@link module:engine/model/selection~Selection model selection}
833
- * to a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
834
- * value from the `consumable` object and maps model positions from the selection to view positions.
835
- *
836
- * ```ts
837
- * modelDispatcher.on( 'selection', convertRangeSelection() );
838
- * ```
839
- *
840
- * @returns Selection converter.
841
- */
842
- export function convertRangeSelection() {
843
- return (evt, data, conversionApi) => {
844
- const selection = data.selection;
845
- if (selection.isCollapsed) {
846
- return;
847
- }
848
- if (!conversionApi.consumable.consume(selection, 'selection')) {
849
- return;
850
- }
851
- const viewRanges = [];
852
- for (const range of selection.getRanges()) {
853
- viewRanges.push(conversionApi.mapper.toViewRange(range));
854
- }
855
- conversionApi.writer.setSelection(viewRanges, { backward: selection.isBackward });
856
- };
857
- }
858
- /**
859
- * Function factory that creates a converter which converts a collapsed {@link module:engine/model/selection~Selection model selection} to
860
- * a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
861
- * value from the `consumable` object, maps the model selection position to the view position and breaks
862
- * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position.
863
- *
864
- * ```ts
865
- * modelDispatcher.on( 'selection', convertCollapsedSelection() );
866
- * ```
867
- *
868
- * An example of the view state before and after converting the collapsed selection:
869
- *
870
- * ```
871
- * <p><strong>f^oo<strong>bar</p>
872
- * -> <p><strong>f</strong>^<strong>oo</strong>bar</p>
873
- * ```
874
- *
875
- * By breaking attribute elements like `<strong>`, the selection is in a correct element. Then, when the selection attribute is
876
- * converted, broken attributes might be merged again, or the position where the selection is may be wrapped
877
- * with different, appropriate attribute elements.
878
- *
879
- * See also {@link module:engine/conversion/downcasthelpers~cleanSelection} which does a clean-up
880
- * by merging attributes.
881
- *
882
- * @returns Selection converter.
883
- */
884
- export function convertCollapsedSelection() {
885
- return (evt, data, conversionApi) => {
886
- const selection = data.selection;
887
- if (!selection.isCollapsed) {
888
- return;
889
- }
890
- if (!conversionApi.consumable.consume(selection, 'selection')) {
891
- return;
892
- }
893
- const viewWriter = conversionApi.writer;
894
- const modelPosition = selection.getFirstPosition();
895
- const viewPosition = conversionApi.mapper.toViewPosition(modelPosition);
896
- const brokenPosition = viewWriter.breakAttributes(viewPosition);
897
- viewWriter.setSelection(brokenPosition);
898
- };
899
- }
900
- /**
901
- * Function factory that creates a converter which cleans artifacts after the previous
902
- * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty
903
- * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merges sibling attributes at all start and end
904
- * positions of all ranges.
905
- *
906
- * ```
907
- * <p><strong>^</strong></p>
908
- * -> <p>^</p>
909
- *
910
- * <p><strong>foo</strong>^<strong>bar</strong>bar</p>
911
- * -> <p><strong>foo^bar<strong>bar</p>
912
- *
913
- * <p><strong>foo</strong><em>^</em><strong>bar</strong>bar</p>
914
- * -> <p><strong>foo^bar<strong>bar</p>
915
- * ```
916
- *
917
- * This listener should be assigned before any converter for the new selection:
918
- *
919
- * ```ts
920
- * modelDispatcher.on( 'cleanSelection', cleanSelection() );
921
- * ```
922
- *
923
- * See {@link module:engine/conversion/downcasthelpers~convertCollapsedSelection}
924
- * which does the opposite by breaking attributes in the selection position.
925
- *
926
- * @returns Selection converter.
927
- */
928
- export function cleanSelection() {
929
- return (evt, data, conversionApi) => {
930
- const viewWriter = conversionApi.writer;
931
- const viewSelection = viewWriter.document.selection;
932
- for (const range of viewSelection.getRanges()) {
933
- // Not collapsed selection should not have artifacts.
934
- if (range.isCollapsed) {
935
- // Position might be in the node removed by the view writer.
936
- if (range.end.parent.isAttached()) {
937
- conversionApi.writer.mergeAttributes(range.start);
938
- }
939
- }
940
- }
941
- viewWriter.setSelection(null);
942
- };
943
- }
944
- /**
945
- * Function factory that creates a converter which converts the set/change/remove attribute changes from the model to the view.
946
- * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the
947
- * selection will be put inside it.
948
- *
949
- * Attributes from the model are converted to a view element that will be wrapping these view nodes that are bound to
950
- * model elements having the given attribute. This is useful for attributes like `bold` that may be set on text nodes in the model
951
- * but are represented as an element in the view:
952
- *
953
- * ```
954
- * [paragraph] MODEL ====> VIEW <p>
955
- * |- a {bold: true} |- <b>
956
- * |- b {bold: true} | |- ab
957
- * |- c |- c
958
- * ```
959
- *
960
- * Passed `Function` will be provided with the attribute value and then all the parameters of the
961
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute` event}.
962
- * It is expected that the function returns an {@link module:engine/view/element~Element}.
963
- * The result of the function will be the wrapping element.
964
- * When the provided `Function` does not return any element, no conversion will take place.
965
- *
966
- * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
967
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
968
- *
969
- * ```ts
970
- * modelDispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, { writer } ) => {
971
- * return writer.createAttributeElement( 'strong' );
972
- * } );
973
- * ```
974
- *
975
- * @internal
976
- * @param elementCreator Function returning a view element that will be used for wrapping.
977
- * @returns Set/change attribute converter.
978
- */
979
- export function wrap(elementCreator) {
980
- return (evt, data, conversionApi) => {
981
- if (!conversionApi.consumable.test(data.item, evt.name)) {
982
- return;
983
- }
984
- // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
985
- // or the attribute was removed.
986
- const oldViewElement = elementCreator(data.attributeOldValue, conversionApi, data);
987
- // Create node to wrap with.
988
- const newViewElement = elementCreator(data.attributeNewValue, conversionApi, data);
989
- if (!oldViewElement && !newViewElement) {
990
- return;
991
- }
992
- conversionApi.consumable.consume(data.item, evt.name);
993
- const viewWriter = conversionApi.writer;
994
- const viewSelection = viewWriter.document.selection;
995
- if (data.item instanceof ModelSelection || data.item instanceof ModelDocumentSelection) {
996
- // Selection attribute conversion.
997
- viewWriter.wrap(viewSelection.getFirstRange(), newViewElement);
998
- }
999
- else {
1000
- // Node attribute conversion.
1001
- let viewRange = conversionApi.mapper.toViewRange(data.range);
1002
- // First, unwrap the range from current wrapper.
1003
- if (data.attributeOldValue !== null && oldViewElement) {
1004
- viewRange = viewWriter.unwrap(viewRange, oldViewElement);
1005
- }
1006
- if (data.attributeNewValue !== null && newViewElement) {
1007
- viewWriter.wrap(viewRange, newViewElement);
1008
- }
1009
- }
1010
- };
1011
- }
1012
- /**
1013
- * Function factory that creates a converter which converts node insertion changes from the model to the view.
1014
- * The function passed will be provided with all the parameters of the dispatcher's
1015
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}.
1016
- * It is expected that the function returns an {@link module:engine/view/element~Element}.
1017
- * The result of the function will be inserted into the view.
1018
- *
1019
- * The converter automatically consumes the corresponding value from the consumables list and binds the model and view elements.
1020
- *
1021
- * ```ts
1022
- * downcastDispatcher.on(
1023
- * 'insert:myElem',
1024
- * insertElement( ( modelItem, { writer } ) => {
1025
- * const text = writer.createText( 'myText' );
1026
- * const myElem = writer.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text );
1027
- *
1028
- * // Do something fancy with `myElem` using `modelItem` or other parameters.
1029
- *
1030
- * return myElem;
1031
- * }
1032
- * ) );
1033
- * ```
1034
- *
1035
- * @internal
1036
- * @param elementCreator Function returning a view element, which will be inserted.
1037
- * @param consumer Function defining element consumption process.
1038
- * By default this function just consume passed item insertion.
1039
- * @returns Insert element event converter.
1040
- */
1041
- export function insertElement(elementCreator, consumer = defaultConsumer) {
1042
- return (evt, data, conversionApi) => {
1043
- if (!consumer(data.item, conversionApi.consumable, { preflight: true })) {
1044
- return;
1045
- }
1046
- const viewElement = elementCreator(data.item, conversionApi, data);
1047
- if (!viewElement) {
1048
- return;
1049
- }
1050
- // Consume an element insertion and all present attributes that are specified as a reconversion triggers.
1051
- consumer(data.item, conversionApi.consumable);
1052
- const viewPosition = conversionApi.mapper.toViewPosition(data.range.start);
1053
- conversionApi.mapper.bindElements(data.item, viewElement);
1054
- conversionApi.writer.insert(viewPosition, viewElement);
1055
- // Convert attributes before converting children.
1056
- conversionApi.convertAttributes(data.item);
1057
- // Convert children or reinsert previous view elements.
1058
- reinsertOrConvertNodes(viewElement, data.item.getChildren(), conversionApi, { reconversion: data.reconversion });
1059
- };
1060
- }
1061
- /**
1062
- * Function factory that creates a converter which converts a single model node insertion to a view structure.
1063
- *
1064
- * It is expected that the passed element creator function returns an {@link module:engine/view/element~Element} with attached slots
1065
- * created with `writer.createSlot()` to indicate where child nodes should be converted.
1066
- *
1067
- * @see module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
1068
- *
1069
- * @internal
1070
- * @param elementCreator Function returning a view structure, which will be inserted.
1071
- * @param consumer A callback that is expected to consume all the consumables
1072
- * that were used by the element creator.
1073
- * @returns Insert element event converter.
1074
- */
1075
- export function insertStructure(elementCreator, consumer) {
1076
- return (evt, data, conversionApi) => {
1077
- if (!consumer(data.item, conversionApi.consumable, { preflight: true })) {
1078
- return;
1079
- }
1080
- const slotsMap = new Map();
1081
- conversionApi.writer._registerSlotFactory(createSlotFactory(data.item, slotsMap, conversionApi));
1082
- // View creation.
1083
- const viewElement = elementCreator(data.item, conversionApi, data);
1084
- conversionApi.writer._clearSlotFactory();
1085
- if (!viewElement) {
1086
- return;
1087
- }
1088
- // Check if all children are covered by slots and there is no child that landed in multiple slots.
1089
- validateSlotsChildren(data.item, slotsMap, conversionApi);
1090
- // Consume an element insertion and all present attributes that are specified as a reconversion triggers.
1091
- consumer(data.item, conversionApi.consumable);
1092
- const viewPosition = conversionApi.mapper.toViewPosition(data.range.start);
1093
- conversionApi.mapper.bindElements(data.item, viewElement);
1094
- conversionApi.writer.insert(viewPosition, viewElement);
1095
- // Convert attributes before converting children.
1096
- conversionApi.convertAttributes(data.item);
1097
- // Fill view slots with previous view elements or create new ones.
1098
- fillSlots(viewElement, slotsMap, conversionApi, { reconversion: data.reconversion });
1099
- };
1100
- }
1101
- /**
1102
- * Function factory that creates a converter which converts marker adding change to the
1103
- * {@link module:engine/view/uielement~UIElement view UI element}.
1104
- *
1105
- * The view UI element that will be added to the view depends on the passed parameter. See {@link ~insertElement}.
1106
- * In case of a non-collapsed range, the UI element will not wrap nodes but separate elements will be placed at the beginning
1107
- * and at the end of the range.
1108
- *
1109
- * This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
1110
- *
1111
- * @internal
1112
- * @param elementCreator A view UI element or a function returning the view element that will be inserted.
1113
- * @returns Insert element event converter.
1114
- */
1115
- export function insertUIElement(elementCreator) {
1116
- return (evt, data, conversionApi) => {
1117
- // Create two view elements. One will be inserted at the beginning of marker, one at the end.
1118
- // If marker is collapsed, only "opening" element will be inserted.
1119
- data.isOpening = true;
1120
- const viewStartElement = elementCreator(data, conversionApi);
1121
- data.isOpening = false;
1122
- const viewEndElement = elementCreator(data, conversionApi);
1123
- if (!viewStartElement || !viewEndElement) {
1124
- return;
1125
- }
1126
- const markerRange = data.markerRange;
1127
- // Marker that is collapsed has consumable build differently that non-collapsed one.
1128
- // For more information see `addMarker` event description.
1129
- // If marker's range is collapsed - check if it can be consumed.
1130
- if (markerRange.isCollapsed && !conversionApi.consumable.consume(markerRange, evt.name)) {
1131
- return;
1132
- }
1133
- // If marker's range is not collapsed - consume all items inside.
1134
- for (const value of markerRange) {
1135
- if (!conversionApi.consumable.consume(value.item, evt.name)) {
1136
- return;
1137
- }
1138
- }
1139
- const mapper = conversionApi.mapper;
1140
- const viewWriter = conversionApi.writer;
1141
- // Add "opening" element.
1142
- viewWriter.insert(mapper.toViewPosition(markerRange.start), viewStartElement);
1143
- conversionApi.mapper.bindElementToMarker(viewStartElement, data.markerName);
1144
- // Add "closing" element only if range is not collapsed.
1145
- if (!markerRange.isCollapsed) {
1146
- viewWriter.insert(mapper.toViewPosition(markerRange.end), viewEndElement);
1147
- conversionApi.mapper.bindElementToMarker(viewEndElement, data.markerName);
1148
- }
1149
- evt.stop();
1150
- };
1151
- }
1152
- /**
1153
- * Function factory that returns a default downcast converter for removing a {@link module:engine/view/uielement~UIElement UI element}
1154
- * based on marker remove change.
1155
- *
1156
- * This converter unbinds elements from the marker name.
1157
- *
1158
- * @returns Removed UI element converter.
1159
- */
1160
- function removeUIElement() {
1161
- return (evt, data, conversionApi) => {
1162
- const elements = conversionApi.mapper.markerNameToElements(data.markerName);
1163
- if (!elements) {
1164
- return;
1165
- }
1166
- for (const element of elements) {
1167
- conversionApi.mapper.unbindElementFromMarkerName(element, data.markerName);
1168
- conversionApi.writer.clear(conversionApi.writer.createRangeOn(element), element);
1169
- }
1170
- conversionApi.writer.clearClonedElementsGroup(data.markerName);
1171
- evt.stop();
1172
- };
1173
- }
1174
- /**
1175
- * Function factory that creates a default converter for model markers.
1176
- *
1177
- * See {@link DowncastHelpers#markerToData} for more information what type of view is generated.
1178
- *
1179
- * This converter binds created UI elements and affected view elements with the marker name
1180
- * using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
1181
- *
1182
- * @returns Add marker converter.
1183
- */
1184
- function insertMarkerData(viewCreator) {
1185
- return (evt, data, conversionApi) => {
1186
- const viewMarkerData = viewCreator(data.markerName, conversionApi);
1187
- if (!viewMarkerData) {
1188
- return;
1189
- }
1190
- const markerRange = data.markerRange;
1191
- if (!conversionApi.consumable.consume(markerRange, evt.name)) {
1192
- return;
1193
- }
1194
- // Adding closing data first to keep the proper order in the view.
1195
- handleMarkerBoundary(markerRange, false, conversionApi, data, viewMarkerData);
1196
- handleMarkerBoundary(markerRange, true, conversionApi, data, viewMarkerData);
1197
- evt.stop();
1198
- };
1199
- }
1200
- /**
1201
- * Helper function for `insertMarkerData()` that marks a marker boundary at the beginning or end of given `range`.
1202
- */
1203
- function handleMarkerBoundary(range, isStart, conversionApi, data, viewMarkerData) {
1204
- const modelPosition = isStart ? range.start : range.end;
1205
- const elementAfter = modelPosition.nodeAfter && modelPosition.nodeAfter.is('element') ? modelPosition.nodeAfter : null;
1206
- const elementBefore = modelPosition.nodeBefore && modelPosition.nodeBefore.is('element') ? modelPosition.nodeBefore : null;
1207
- if (elementAfter || elementBefore) {
1208
- let modelElement;
1209
- let isBefore;
1210
- // If possible, we want to add `data-group-start-before` and `data-group-end-after` attributes.
1211
- if (isStart && elementAfter || !isStart && !elementBefore) {
1212
- // [<elementAfter>...</elementAfter> -> <elementAfter data-group-start-before="...">...</elementAfter>
1213
- // <parent>]<elementAfter> -> <parent><elementAfter data-group-end-before="...">
1214
- modelElement = elementAfter;
1215
- isBefore = true;
1216
- }
1217
- else {
1218
- // <elementBefore>...</elementBefore>] -> <elementBefore data-group-end-after="...">...</elementBefore>
1219
- // </elementBefore>[</parent> -> </elementBefore data-group-start-after="..."></parent>
1220
- modelElement = elementBefore;
1221
- isBefore = false;
1222
- }
1223
- const viewElement = conversionApi.mapper.toViewElement(modelElement);
1224
- // In rare circumstances, the model element may be not mapped to any view element and that would cause an error.
1225
- // One of those situations is a soft break inside code block.
1226
- if (viewElement) {
1227
- insertMarkerAsAttribute(viewElement, isStart, isBefore, conversionApi, data, viewMarkerData);
1228
- return;
1229
- }
1230
- }
1231
- const viewPosition = conversionApi.mapper.toViewPosition(modelPosition);
1232
- insertMarkerAsElement(viewPosition, isStart, conversionApi, data, viewMarkerData);
1233
- }
1234
- /**
1235
- * Helper function for `insertMarkerData()` that marks a marker boundary in the view as an attribute on a view element.
1236
- */
1237
- function insertMarkerAsAttribute(viewElement, isStart, isBefore, conversionApi, data, viewMarkerData) {
1238
- const attributeName = `data-${viewMarkerData.group}-${isStart ? 'start' : 'end'}-${isBefore ? 'before' : 'after'}`;
1239
- const markerNames = viewElement.hasAttribute(attributeName) ? viewElement.getAttribute(attributeName).split(',') : [];
1240
- // Adding marker name at the beginning to have the same order in the attribute as there is with marker elements.
1241
- markerNames.unshift(viewMarkerData.name);
1242
- conversionApi.writer.setAttribute(attributeName, markerNames.join(','), viewElement);
1243
- conversionApi.mapper.bindElementToMarker(viewElement, data.markerName);
1244
- }
1245
- /**
1246
- * Helper function for `insertMarkerData()` that marks a marker boundary in the view as a separate view ui element.
1247
- */
1248
- function insertMarkerAsElement(position, isStart, conversionApi, data, viewMarkerData) {
1249
- const viewElementName = `${viewMarkerData.group}-${isStart ? 'start' : 'end'}`;
1250
- const attrs = viewMarkerData.name ? { 'name': viewMarkerData.name } : null;
1251
- const viewElement = conversionApi.writer.createUIElement(viewElementName, attrs);
1252
- conversionApi.writer.insert(position, viewElement);
1253
- conversionApi.mapper.bindElementToMarker(viewElement, data.markerName);
1254
- }
1255
- /**
1256
- * Function factory that creates a converter for removing a model marker data added by the {@link #insertMarkerData} converter.
1257
- *
1258
- * @returns Remove marker converter.
1259
- */
1260
- function removeMarkerData(viewCreator) {
1261
- return (evt, data, conversionApi) => {
1262
- const viewData = viewCreator(data.markerName, conversionApi);
1263
- if (!viewData) {
1264
- return;
1265
- }
1266
- const elements = conversionApi.mapper.markerNameToElements(data.markerName);
1267
- if (!elements) {
1268
- return;
1269
- }
1270
- for (const element of elements) {
1271
- conversionApi.mapper.unbindElementFromMarkerName(element, data.markerName);
1272
- if (element.is('containerElement')) {
1273
- removeMarkerFromAttribute(`data-${viewData.group}-start-before`, element);
1274
- removeMarkerFromAttribute(`data-${viewData.group}-start-after`, element);
1275
- removeMarkerFromAttribute(`data-${viewData.group}-end-before`, element);
1276
- removeMarkerFromAttribute(`data-${viewData.group}-end-after`, element);
1277
- }
1278
- else {
1279
- conversionApi.writer.clear(conversionApi.writer.createRangeOn(element), element);
1280
- }
1281
- }
1282
- conversionApi.writer.clearClonedElementsGroup(data.markerName);
1283
- evt.stop();
1284
- function removeMarkerFromAttribute(attributeName, element) {
1285
- if (element.hasAttribute(attributeName)) {
1286
- const markerNames = new Set(element.getAttribute(attributeName).split(','));
1287
- markerNames.delete(viewData.name);
1288
- if (markerNames.size == 0) {
1289
- conversionApi.writer.removeAttribute(attributeName, element);
1290
- }
1291
- else {
1292
- conversionApi.writer.setAttribute(attributeName, Array.from(markerNames).join(','), element);
1293
- }
1294
- }
1295
- }
1296
- };
1297
- }
1298
- /**
1299
- * Function factory that creates a converter which converts the set/change/remove attribute changes from the model to the view.
1300
- *
1301
- * Attributes from the model are converted to the view element attributes in the view. You may provide a custom function to generate
1302
- * a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view element
1303
- * attributes on a one-to-one basis.
1304
- *
1305
- * *Note:** The provided attribute creator should always return the same `key` for a given attribute from the model.
1306
- *
1307
- * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
1308
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
1309
- *
1310
- * ```ts
1311
- * modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( value, data ) => {
1312
- * // Change attribute key from `customAttr` to `class` in the view.
1313
- * const key = 'class';
1314
- * let value = data.attributeNewValue;
1315
- *
1316
- * // Force attribute value to 'empty' if the model element is empty.
1317
- * if ( data.item.childCount === 0 ) {
1318
- * value = 'empty';
1319
- * }
1320
- *
1321
- * // Return the key-value pair.
1322
- * return { key, value };
1323
- * } ) );
1324
- * ```
1325
- *
1326
- * @param attributeCreator Function returning an object with two properties: `key` and `value`, which
1327
- * represent the attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
1328
- * The function is passed the model attribute value as the first parameter and additional data about the change as the second parameter.
1329
- * @returns Set/change attribute converter.
1330
- */
1331
- function changeAttribute(attributeCreator) {
1332
- return (evt, data, conversionApi) => {
1333
- if (!conversionApi.consumable.test(data.item, evt.name)) {
1334
- return;
1335
- }
1336
- const oldAttribute = attributeCreator(data.attributeOldValue, conversionApi, data);
1337
- const newAttribute = attributeCreator(data.attributeNewValue, conversionApi, data);
1338
- if (!oldAttribute && !newAttribute) {
1339
- return;
1340
- }
1341
- conversionApi.consumable.consume(data.item, evt.name);
1342
- const viewElement = conversionApi.mapper.toViewElement(data.item);
1343
- const viewWriter = conversionApi.writer;
1344
- // If model item cannot be mapped to a view element, it means item is not an `Element` instance but a `TextProxy` node.
1345
- // Only elements can have attributes in a view so do not proceed for anything else (#1587).
1346
- if (!viewElement) {
1347
- /**
1348
- * This error occurs when a {@link module:engine/model/textproxy~TextProxy text node's} attribute is to be downcasted
1349
- * by an {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `Attribute to Attribute converter`}.
1350
- * In most cases it is caused by converters misconfiguration when only "generic" converter is defined:
1351
- *
1352
- * ```ts
1353
- * editor.conversion.for( 'downcast' ).attributeToAttribute( {
1354
- * model: 'attribute-name',
1355
- * view: 'attribute-name'
1356
- * } ) );
1357
- * ```
1358
- *
1359
- * and given attribute is used on text node, for example:
1360
- *
1361
- * ```ts
1362
- * model.change( writer => {
1363
- * writer.insertText( 'Foo', { 'attribute-name': 'bar' }, parent, 0 );
1364
- * } );
1365
- * ```
1366
- *
1367
- * In such cases, to convert the same attribute for both {@link module:engine/model/element~Element}
1368
- * and {@link module:engine/model/textproxy~TextProxy `Text`} nodes, text specific
1369
- * {@link module:engine/conversion/conversion~Conversion#attributeToElement `Attribute to Element converter`}
1370
- * with higher {@link module:utils/priorities~PriorityString priority} must also be defined:
1371
- *
1372
- * ```ts
1373
- * editor.conversion.for( 'downcast' ).attributeToElement( {
1374
- * model: {
1375
- * key: 'attribute-name',
1376
- * name: '$text'
1377
- * },
1378
- * view: ( value, { writer } ) => {
1379
- * return writer.createAttributeElement( 'span', { 'attribute-name': value } );
1380
- * },
1381
- * converterPriority: 'high'
1382
- * } ) );
1383
- * ```
1384
- *
1385
- * @error conversion-attribute-to-attribute-on-text
1386
- */
1387
- throw new CKEditorError('conversion-attribute-to-attribute-on-text', conversionApi.dispatcher, data);
1388
- }
1389
- // First remove the old attribute if there was one.
1390
- if (data.attributeOldValue !== null && oldAttribute) {
1391
- if (oldAttribute.key == 'class') {
1392
- const classes = toArray(oldAttribute.value);
1393
- for (const className of classes) {
1394
- viewWriter.removeClass(className, viewElement);
1395
- }
1396
- }
1397
- else if (oldAttribute.key == 'style') {
1398
- const keys = Object.keys(oldAttribute.value);
1399
- for (const key of keys) {
1400
- viewWriter.removeStyle(key, viewElement);
1401
- }
1402
- }
1403
- else {
1404
- viewWriter.removeAttribute(oldAttribute.key, viewElement);
1405
- }
1406
- }
1407
- // Then set the new attribute.
1408
- if (data.attributeNewValue !== null && newAttribute) {
1409
- if (newAttribute.key == 'class') {
1410
- const classes = toArray(newAttribute.value);
1411
- for (const className of classes) {
1412
- viewWriter.addClass(className, viewElement);
1413
- }
1414
- }
1415
- else if (newAttribute.key == 'style') {
1416
- const keys = Object.keys(newAttribute.value);
1417
- for (const key of keys) {
1418
- viewWriter.setStyle(key, newAttribute.value[key], viewElement);
1419
- }
1420
- }
1421
- else {
1422
- viewWriter.setAttribute(newAttribute.key, newAttribute.value, viewElement);
1423
- }
1424
- }
1425
- };
1426
- }
1427
- /**
1428
- * Function factory that creates a converter which converts the text inside marker's range. The converter wraps the text with
1429
- * {@link module:engine/view/attributeelement~AttributeElement} created from the provided descriptor.
1430
- * See {link module:engine/conversion/downcasthelpers~createViewElementFromHighlightDescriptor}.
1431
- *
1432
- * It can also be used to convert the selection that is inside a marker. In that case, an empty attribute element will be
1433
- * created and the selection will be put inside it.
1434
- *
1435
- * If the highlight descriptor does not provide the `priority` property, `10` will be used.
1436
- *
1437
- * If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
1438
- *
1439
- * This converter binds the created {@link module:engine/view/attributeelement~AttributeElement attribute elemens} with the marker name
1440
- * using the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
1441
- */
1442
- function highlightText(highlightDescriptor) {
1443
- return (evt, data, conversionApi) => {
1444
- if (!data.item) {
1445
- return;
1446
- }
1447
- if (!(data.item instanceof ModelSelection || data.item instanceof ModelDocumentSelection) && !data.item.is('$textProxy')) {
1448
- return;
1449
- }
1450
- const descriptor = prepareDescriptor(highlightDescriptor, data, conversionApi);
1451
- if (!descriptor) {
1452
- return;
1453
- }
1454
- if (!conversionApi.consumable.consume(data.item, evt.name)) {
1455
- return;
1456
- }
1457
- const viewWriter = conversionApi.writer;
1458
- const viewElement = createViewElementFromHighlightDescriptor(viewWriter, descriptor);
1459
- const viewSelection = viewWriter.document.selection;
1460
- if (data.item instanceof ModelSelection || data.item instanceof ModelDocumentSelection) {
1461
- viewWriter.wrap(viewSelection.getFirstRange(), viewElement);
1462
- }
1463
- else {
1464
- const viewRange = conversionApi.mapper.toViewRange(data.range);
1465
- const rangeAfterWrap = viewWriter.wrap(viewRange, viewElement);
1466
- for (const element of rangeAfterWrap.getItems()) {
1467
- if (element.is('attributeElement') && element.isSimilar(viewElement)) {
1468
- conversionApi.mapper.bindElementToMarker(element, data.markerName);
1469
- // One attribute element is enough, because all of them are bound together by the view writer.
1470
- // Mapper uses this binding to get all the elements no matter how many of them are registered in the mapper.
1471
- break;
1472
- }
1473
- }
1474
- }
1475
- };
1476
- }
1477
- /**
1478
- * Converter function factory. It creates a function which applies the marker's highlight to an element inside the marker's range.
1479
- *
1480
- * The converter checks if an element has the `addHighlight` function stored as a
1481
- * {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight.
1482
- * In such case the converter will consume all element's children, assuming that they were handled by the element itself.
1483
- *
1484
- * When the `addHighlight` custom property is not present, the element is not converted in any special way.
1485
- * This means that converters will proceed to convert the element's child nodes.
1486
- *
1487
- * If the highlight descriptor does not provide the `priority` property, `10` will be used.
1488
- *
1489
- * If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
1490
- *
1491
- * This converter binds altered {@link module:engine/view/containerelement~ContainerElement container elements} with the marker name using
1492
- * the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
1493
- */
1494
- function highlightElement(highlightDescriptor) {
1495
- return (evt, data, conversionApi) => {
1496
- if (!data.item) {
1497
- return;
1498
- }
1499
- if (!(data.item instanceof ModelElement)) {
1500
- return;
1501
- }
1502
- const descriptor = prepareDescriptor(highlightDescriptor, data, conversionApi);
1503
- if (!descriptor) {
1504
- return;
1505
- }
1506
- if (!conversionApi.consumable.test(data.item, evt.name)) {
1507
- return;
1508
- }
1509
- const viewElement = conversionApi.mapper.toViewElement(data.item);
1510
- if (viewElement && viewElement.getCustomProperty('addHighlight')) {
1511
- // Consume element itself.
1512
- conversionApi.consumable.consume(data.item, evt.name);
1513
- // Consume all children nodes.
1514
- for (const value of ModelRange._createIn(data.item)) {
1515
- conversionApi.consumable.consume(value.item, evt.name);
1516
- }
1517
- const addHighlightCallback = viewElement.getCustomProperty('addHighlight');
1518
- addHighlightCallback(viewElement, descriptor, conversionApi.writer);
1519
- conversionApi.mapper.bindElementToMarker(viewElement, data.markerName);
1520
- }
1521
- };
1522
- }
1523
- /**
1524
- * Function factory that creates a converter which converts the removing model marker to the view.
1525
- *
1526
- * Both text nodes and elements are handled by this converter but they are handled a bit differently.
1527
- *
1528
- * Text nodes are unwrapped using the {@link module:engine/view/attributeelement~AttributeElement attribute element} created from the
1529
- * provided highlight descriptor. See {link module:engine/conversion/downcasthelpers~HighlightDescriptor}.
1530
- *
1531
- * For elements, the converter checks if an element has the `removeHighlight` function stored as a
1532
- * {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight.
1533
- * In such case, the children of that element will not be converted.
1534
- *
1535
- * When `removeHighlight` is not present, the element is not converted in any special way.
1536
- * The converter will proceed to convert the element's child nodes instead.
1537
- *
1538
- * If the highlight descriptor does not provide the `priority` property, `10` will be used.
1539
- *
1540
- * If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
1541
- *
1542
- * This converter unbinds elements from the marker name.
1543
- */
1544
- function removeHighlight(highlightDescriptor) {
1545
- return (evt, data, conversionApi) => {
1546
- // This conversion makes sense only for non-collapsed range.
1547
- if (data.markerRange.isCollapsed) {
1548
- return;
1549
- }
1550
- const descriptor = prepareDescriptor(highlightDescriptor, data, conversionApi);
1551
- if (!descriptor) {
1552
- return;
1553
- }
1554
- // View element that will be used to unwrap `AttributeElement`s.
1555
- const viewHighlightElement = createViewElementFromHighlightDescriptor(conversionApi.writer, descriptor);
1556
- // Get all elements bound with given marker name.
1557
- const elements = conversionApi.mapper.markerNameToElements(data.markerName);
1558
- if (!elements) {
1559
- return;
1560
- }
1561
- for (const element of elements) {
1562
- conversionApi.mapper.unbindElementFromMarkerName(element, data.markerName);
1563
- if (element.is('attributeElement')) {
1564
- conversionApi.writer.unwrap(conversionApi.writer.createRangeOn(element), viewHighlightElement);
1565
- }
1566
- else {
1567
- // if element.is( 'containerElement' ).
1568
- const removeHighlightCallback = element.getCustomProperty('removeHighlight');
1569
- removeHighlightCallback(element, descriptor.id, conversionApi.writer);
1570
- }
1571
- }
1572
- conversionApi.writer.clearClonedElementsGroup(data.markerName);
1573
- evt.stop();
1574
- };
1575
- }
1576
- /**
1577
- * Model element to view element conversion helper.
1578
- *
1579
- * See {@link ~DowncastHelpers#elementToElement `.elementToElement()` downcast helper} for examples and config params description.
1580
- *
1581
- * @param config Conversion configuration.
1582
- * @param config.model The description or a name of the model element to convert.
1583
- * @param config.model.attributes List of attributes triggering element reconversion.
1584
- * @param config.model.children Should reconvert element if the list of model child nodes changed.
1585
- * @returns Conversion helper.
1586
- */
1587
- function downcastElementToElement(config) {
1588
- const model = normalizeModelElementConfig(config.model);
1589
- const view = normalizeToElementConfig(config.view, 'container');
1590
- // Trigger reconversion on children list change if element is a subject to any reconversion.
1591
- // This is required to be able to trigger Differ#refreshItem() on a direct child of the reconverted element.
1592
- if (model.attributes.length) {
1593
- model.children = true;
1594
- }
1595
- return (dispatcher) => {
1596
- dispatcher.on(`insert:${model.name}`, insertElement(view, createConsumer(model)), { priority: config.converterPriority || 'normal' });
1597
- if (model.children || model.attributes.length) {
1598
- dispatcher.on('reduceChanges', createChangeReducer(model), { priority: 'low' });
1599
- }
1600
- };
1601
- }
1602
- /**
1603
- * Model element to view structure conversion helper.
1604
- *
1605
- * See {@link ~DowncastHelpers#elementToStructure `.elementToStructure()` downcast helper} for examples and config params description.
1606
- *
1607
- * @param config Conversion configuration.
1608
- * @returns Conversion helper.
1609
- */
1610
- function downcastElementToStructure(config) {
1611
- const model = normalizeModelElementConfig(config.model);
1612
- const view = normalizeToElementConfig(config.view, 'container');
1613
- // Trigger reconversion on children list change because it always needs to use slots to put children in proper places.
1614
- // This is required to be able to trigger Differ#refreshItem() on a direct child of the reconverted element.
1615
- model.children = true;
1616
- return (dispatcher) => {
1617
- if (dispatcher._conversionApi.schema.checkChild(model.name, '$text')) {
1618
- /**
1619
- * This error occurs when a {@link module:engine/model/element~Element model element} is downcasted
1620
- * via {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure} helper but the element was
1621
- * allowed to host `$text` by the {@link module:engine/model/schema~Schema model schema}.
1622
- *
1623
- * For instance, this may be the result of `myElement` allowing the content of
1624
- * {@glink framework/deep-dive/schema#generic-items `$block`} in its schema definition:
1625
- *
1626
- * ```ts
1627
- * // Element definition in schema.
1628
- * schema.register( 'myElement', {
1629
- * allowContentOf: '$block',
1630
- *
1631
- * // ...
1632
- * } );
1633
- *
1634
- * // ...
1635
- *
1636
- * // Conversion of myElement with the use of elementToStructure().
1637
- * editor.conversion.for( 'downcast' ).elementToStructure( {
1638
- * model: 'myElement',
1639
- * view: ( modelElement, { writer } ) => {
1640
- * // ...
1641
- * }
1642
- * } );
1643
- * ```
1644
- *
1645
- * In such case, {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} helper
1646
- * can be used instead to get around this problem:
1647
- *
1648
- * ```ts
1649
- * editor.conversion.for( 'downcast' ).elementToElement( {
1650
- * model: 'myElement',
1651
- * view: ( modelElement, { writer } ) => {
1652
- * // ...
1653
- * }
1654
- * } );
1655
- * ```
1656
- *
1657
- * @error conversion-element-to-structure-disallowed-text
1658
- * @param {String} elementName The name of the element the structure is to be created for.
1659
- */
1660
- throw new CKEditorError('conversion-element-to-structure-disallowed-text', dispatcher, { elementName: model.name });
1661
- }
1662
- dispatcher.on(`insert:${model.name}`, insertStructure(view, createConsumer(model)), { priority: config.converterPriority || 'normal' });
1663
- dispatcher.on('reduceChanges', createChangeReducer(model), { priority: 'low' });
1664
- };
1665
- }
1666
- /**
1667
- * Model attribute to view element conversion helper.
1668
- *
1669
- * See {@link ~DowncastHelpers#attributeToElement `.attributeToElement()` downcast helper} for examples.
1670
- *
1671
- * @param config Conversion configuration.
1672
- * @param config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
1673
- * of `String`s with possible values if the model attribute is an enumerable.
1674
- * @param config.view A view element definition or a function that takes the model attribute value and
1675
- * {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} as parameters and returns a view attribute element.
1676
- * If `config.model.values` is given, `config.view` should be an object assigning values from `config.model.values` to view element
1677
- * definitions or functions.
1678
- * @param config.converterPriority Converter priority.
1679
- * @returns Conversion helper.
1680
- */
1681
- function downcastAttributeToElement(config) {
1682
- config = cloneDeep(config);
1683
- let model = config.model;
1684
- if (typeof model == 'string') {
1685
- model = { key: model };
1686
- }
1687
- let eventName = `attribute:${model.key}`;
1688
- if (model.name) {
1689
- eventName += ':' + model.name;
1690
- }
1691
- if (model.values) {
1692
- for (const modelValue of model.values) {
1693
- config.view[modelValue] = normalizeToElementConfig(config.view[modelValue], 'attribute');
1694
- }
1695
- }
1696
- else {
1697
- config.view = normalizeToElementConfig(config.view, 'attribute');
1698
- }
1699
- const elementCreator = getFromAttributeCreator(config);
1700
- return (dispatcher) => {
1701
- dispatcher.on(eventName, wrap(elementCreator), { priority: config.converterPriority || 'normal' });
1702
- };
1703
- }
1704
- /**
1705
- * Model attribute to view attribute conversion helper.
1706
- *
1707
- * See {@link ~DowncastHelpers#attributeToAttribute `.attributeToAttribute()` downcast helper} for examples.
1708
- *
1709
- * @param config Conversion configuration.
1710
- * @param config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
1711
- * the attribute key, possible values and, optionally, an element name to convert from.
1712
- * @param config.view A view attribute key, or a `{ key, value }` object or a function that takes the model attribute value and returns
1713
- * a `{ key, value }` object.
1714
- * If `key` is `'class'`, `value` can be a `String` or an array of `String`s. If `key` is `'style'`, `value` is an object with
1715
- * key-value pairs. In other cases, `value` is a `String`.
1716
- * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
1717
- * `{ key, value }` objects or a functions.
1718
- * @param config.converterPriority Converter priority.
1719
- * @returns Conversion helper.
1720
- */
1721
- function downcastAttributeToAttribute(config) {
1722
- config = cloneDeep(config);
1723
- let model = config.model;
1724
- if (typeof model == 'string') {
1725
- model = { key: model };
1726
- }
1727
- let eventName = `attribute:${model.key}`;
1728
- if (model.name) {
1729
- eventName += ':' + model.name;
1730
- }
1731
- if (model.values) {
1732
- for (const modelValue of model.values) {
1733
- config.view[modelValue] = normalizeToAttributeConfig(config.view[modelValue]);
1734
- }
1735
- }
1736
- else {
1737
- config.view = normalizeToAttributeConfig(config.view);
1738
- }
1739
- const elementCreator = getFromAttributeCreator(config);
1740
- return (dispatcher) => {
1741
- dispatcher.on(eventName, changeAttribute(elementCreator), { priority: config.converterPriority || 'normal' });
1742
- };
1743
- }
1744
- /**
1745
- * Model marker to view element conversion helper.
1746
- *
1747
- * See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
1748
- *
1749
- * @param config Conversion configuration.
1750
- * @param config.model The name of the model marker (or model marker group) to convert.
1751
- * @param config.view A view element definition or a function that takes the model marker data as a parameter and returns a view UI element.
1752
- * @param config.converterPriority Converter priority.
1753
- * @returns Conversion helper.
1754
- */
1755
- function downcastMarkerToElement(config) {
1756
- const view = normalizeToElementConfig(config.view, 'ui');
1757
- return (dispatcher) => {
1758
- dispatcher.on(`addMarker:${config.model}`, insertUIElement(view), { priority: config.converterPriority || 'normal' });
1759
- dispatcher.on(`removeMarker:${config.model}`, removeUIElement(), { priority: config.converterPriority || 'normal' });
1760
- };
1761
- }
1762
- /**
1763
- * Model marker to view data conversion helper.
1764
- *
1765
- * See {@link ~DowncastHelpers#markerToData `markerToData()` downcast helper} to learn more.
1766
- *
1767
- * @returns Conversion helper.
1768
- */
1769
- function downcastMarkerToData(config) {
1770
- config = cloneDeep(config);
1771
- const group = config.model;
1772
- let view = config.view;
1773
- // Default conversion.
1774
- if (!view) {
1775
- view = markerName => ({
1776
- group,
1777
- name: markerName.substr(config.model.length + 1)
1778
- });
1779
- }
1780
- return (dispatcher) => {
1781
- dispatcher.on(`addMarker:${group}`, insertMarkerData(view), { priority: config.converterPriority || 'normal' });
1782
- dispatcher.on(`removeMarker:${group}`, removeMarkerData(view), { priority: config.converterPriority || 'normal' });
1783
- };
1784
- }
1785
- /**
1786
- * Model marker to highlight conversion helper.
1787
- *
1788
- * See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
1789
- *
1790
- * @param config Conversion configuration.
1791
- * @param config.model The name of the model marker (or model marker group) to convert.
1792
- * @param config.view A highlight descriptor that will be used for highlighting or a function that takes
1793
- * the model marker data as a parameter and returns a highlight descriptor.
1794
- * @param config.converterPriority Converter priority.
1795
- * @returns Conversion helper.
1796
- */
1797
- function downcastMarkerToHighlight(config) {
1798
- return (dispatcher) => {
1799
- dispatcher.on(`addMarker:${config.model}`, highlightText(config.view), { priority: config.converterPriority || 'normal' });
1800
- dispatcher.on(`addMarker:${config.model}`, highlightElement(config.view), { priority: config.converterPriority || 'normal' });
1801
- dispatcher.on(`removeMarker:${config.model}`, removeHighlight(config.view), { priority: config.converterPriority || 'normal' });
1802
- };
1803
- }
1804
- /**
1805
- * Takes `config.model`, and converts it to an object with normalized structure.
1806
- *
1807
- * @param model Model configuration or element name.
1808
- */
1809
- function normalizeModelElementConfig(model) {
1810
- if (typeof model == 'string') {
1811
- model = { name: model };
1812
- }
1813
- // List of attributes that should trigger reconversion.
1814
- if (!model.attributes) {
1815
- model.attributes = [];
1816
- }
1817
- else if (!Array.isArray(model.attributes)) {
1818
- model.attributes = [model.attributes];
1819
- }
1820
- // Whether a children insertion/deletion should trigger reconversion.
1821
- model.children = !!model.children;
1822
- return model;
1823
- }
1824
- /**
1825
- * Takes `config.view`, and if it is an {@link module:engine/view/elementdefinition~ElementDefinition}, converts it
1826
- * to a function (because lower level converters accept only element creator functions).
1827
- *
1828
- * @param view View configuration.
1829
- * @param viewElementType View element type to create.
1830
- * @returns Element creator function to use in lower level converters.
1831
- */
1832
- function normalizeToElementConfig(view, viewElementType) {
1833
- if (typeof view == 'function') {
1834
- // If `view` is already a function, don't do anything.
1835
- return view;
1836
- }
1837
- return ((modelData, conversionApi) => createViewElementFromDefinition(view, conversionApi, viewElementType));
1838
- }
1839
- /**
1840
- * Creates a view element instance from the provided {@link module:engine/view/elementdefinition~ElementDefinition} and class.
1841
- */
1842
- function createViewElementFromDefinition(viewElementDefinition, conversionApi, viewElementType) {
1843
- if (typeof viewElementDefinition == 'string') {
1844
- // If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property.
1845
- viewElementDefinition = { name: viewElementDefinition };
1846
- }
1847
- let element;
1848
- const viewWriter = conversionApi.writer;
1849
- const attributes = Object.assign({}, viewElementDefinition.attributes);
1850
- if (viewElementType == 'container') {
1851
- element = viewWriter.createContainerElement(viewElementDefinition.name, attributes);
1852
- }
1853
- else if (viewElementType == 'attribute') {
1854
- const options = {
1855
- priority: viewElementDefinition.priority || ViewAttributeElement.DEFAULT_PRIORITY
1856
- };
1857
- element = viewWriter.createAttributeElement(viewElementDefinition.name, attributes, options);
1858
- }
1859
- else {
1860
- // 'ui'.
1861
- element = viewWriter.createUIElement(viewElementDefinition.name, attributes);
1862
- }
1863
- if (viewElementDefinition.styles) {
1864
- const keys = Object.keys(viewElementDefinition.styles);
1865
- for (const key of keys) {
1866
- viewWriter.setStyle(key, viewElementDefinition.styles[key], element);
1867
- }
1868
- }
1869
- if (viewElementDefinition.classes) {
1870
- const classes = viewElementDefinition.classes;
1871
- if (typeof classes == 'string') {
1872
- viewWriter.addClass(classes, element);
1873
- }
1874
- else {
1875
- for (const className of classes) {
1876
- viewWriter.addClass(className, element);
1877
- }
1878
- }
1879
- }
1880
- return element;
1881
- }
1882
- function getFromAttributeCreator(config) {
1883
- if (config.model.values) {
1884
- return ((modelAttributeValue, conversionApi, data) => {
1885
- const view = config.view[modelAttributeValue];
1886
- if (view) {
1887
- return view(modelAttributeValue, conversionApi, data);
1888
- }
1889
- return null;
1890
- });
1891
- }
1892
- else {
1893
- return config.view;
1894
- }
1895
- }
1896
- /**
1897
- * Takes the configuration, adds default parameters if they do not exist and normalizes other parameters to be used in downcast converters
1898
- * for generating a view attribute.
1899
- *
1900
- * @param view View configuration.
1901
- */
1902
- function normalizeToAttributeConfig(view) {
1903
- if (typeof view == 'string') {
1904
- return modelAttributeValue => ({ key: view, value: modelAttributeValue });
1905
- }
1906
- else if (typeof view == 'object') {
1907
- // { key, value, ... }
1908
- if (view.value) {
1909
- return () => view;
1910
- }
1911
- // { key, ... }
1912
- else {
1913
- return modelAttributeValue => ({ key: view.key, value: modelAttributeValue });
1914
- }
1915
- }
1916
- else {
1917
- // function.
1918
- return view;
1919
- }
1920
- }
1921
- /**
1922
- * Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter.
1923
- */
1924
- function prepareDescriptor(highlightDescriptor, data, conversionApi) {
1925
- // If passed descriptor is a creator function, call it. If not, just use passed value.
1926
- const descriptor = typeof highlightDescriptor == 'function' ?
1927
- highlightDescriptor(data, conversionApi) :
1928
- highlightDescriptor;
1929
- if (!descriptor) {
1930
- return null;
1931
- }
1932
- // Apply default descriptor priority.
1933
- if (!descriptor.priority) {
1934
- descriptor.priority = 10;
1935
- }
1936
- // Default descriptor id is marker name.
1937
- if (!descriptor.id) {
1938
- descriptor.id = data.markerName;
1939
- }
1940
- return descriptor;
1941
- }
1942
- /**
1943
- * Creates a function that checks a single differ diff item whether it should trigger reconversion.
1944
- *
1945
- * @param model A normalized `config.model` converter configuration.
1946
- * @param model.name The name of element.
1947
- * @param model.attributes The list of attribute names that should trigger reconversion.
1948
- * @param model.children Whether the child list change should trigger reconversion.
1949
- */
1950
- function createChangeReducerCallback(model) {
1951
- return (node, change) => {
1952
- if (!node.is('element', model.name)) {
1953
- return false;
1954
- }
1955
- if (change.type == 'attribute') {
1956
- if (model.attributes.includes(change.attributeKey)) {
1957
- return true;
1958
- }
1959
- }
1960
- else {
1961
- /* istanbul ignore else: This is always true because otherwise it would not register a reducer callback. -- @preserve */
1962
- if (model.children) {
1963
- return true;
1964
- }
1965
- }
1966
- return false;
1967
- };
1968
- }
1969
- /**
1970
- * Creates a `reduceChanges` event handler for reconversion.
1971
- *
1972
- * @param model A normalized `config.model` converter configuration.
1973
- * @param model.name The name of element.
1974
- * @param model.attributes The list of attribute names that should trigger reconversion.
1975
- * @param model.children Whether the child list change should trigger reconversion.
1976
- */
1977
- function createChangeReducer(model) {
1978
- const shouldReplace = createChangeReducerCallback(model);
1979
- return (evt, data) => {
1980
- const reducedChanges = [];
1981
- if (!data.reconvertedElements) {
1982
- data.reconvertedElements = new Set();
1983
- }
1984
- for (const change of data.changes) {
1985
- // For attribute use node affected by the change.
1986
- // For insert or remove use parent element because we need to check if it's added/removed child.
1987
- const node = change.type == 'attribute' ? change.range.start.nodeAfter : change.position.parent;
1988
- if (!node || !shouldReplace(node, change)) {
1989
- reducedChanges.push(change);
1990
- continue;
1991
- }
1992
- // If it's already marked for reconversion, so skip this change, otherwise add the diff items.
1993
- if (!data.reconvertedElements.has(node)) {
1994
- data.reconvertedElements.add(node);
1995
- const position = ModelPosition._createBefore(node);
1996
- let changeIndex = reducedChanges.length;
1997
- // We need to insert remove+reinsert before any other change on and inside the re-converted element.
1998
- // This is important because otherwise we would remove element that had already been modified by the previous change.
1999
- // Note that there could be some element removed before the re-converted element, so we must not break this behavior.
2000
- for (let i = reducedChanges.length - 1; i >= 0; i--) {
2001
- const change = reducedChanges[i];
2002
- const changePosition = change.type == 'attribute' ? change.range.start : change.position;
2003
- const positionRelation = changePosition.compareWith(position);
2004
- if (positionRelation == 'before' || change.type == 'remove' && positionRelation == 'same') {
2005
- break;
2006
- }
2007
- changeIndex = i;
2008
- }
2009
- reducedChanges.splice(changeIndex, 0, {
2010
- type: 'remove',
2011
- name: node.name,
2012
- position,
2013
- length: 1
2014
- }, {
2015
- type: 'reinsert',
2016
- name: node.name,
2017
- position,
2018
- length: 1
2019
- });
2020
- }
2021
- }
2022
- data.changes = reducedChanges;
2023
- };
2024
- }
2025
- /**
2026
- * Creates a function that checks if an element and its watched attributes can be consumed and consumes them.
2027
- *
2028
- * @param model A normalized `config.model` converter configuration.
2029
- * @param model.name The name of element.
2030
- * @param model.attributes The list of attribute names that should trigger reconversion.
2031
- * @param model.children Whether the child list change should trigger reconversion.
2032
- */
2033
- function createConsumer(model) {
2034
- return (node, consumable, options = {}) => {
2035
- const events = ['insert'];
2036
- // Collect all set attributes that are triggering conversion.
2037
- for (const attributeName of model.attributes) {
2038
- if (node.hasAttribute(attributeName)) {
2039
- events.push(`attribute:${attributeName}`);
2040
- }
2041
- }
2042
- if (!events.every(event => consumable.test(node, event))) {
2043
- return false;
2044
- }
2045
- if (!options.preflight) {
2046
- events.forEach(event => consumable.consume(node, event));
2047
- }
2048
- return true;
2049
- };
2050
- }
2051
- /**
2052
- * Creates a function that create view slots.
2053
- *
2054
- * @returns Function exposed by writer as createSlot().
2055
- */
2056
- function createSlotFactory(element, slotsMap, conversionApi) {
2057
- return (writer, modeOrFilter) => {
2058
- const slot = writer.createContainerElement('$slot');
2059
- let children = null;
2060
- if (modeOrFilter === 'children') {
2061
- children = Array.from(element.getChildren());
2062
- }
2063
- else if (typeof modeOrFilter == 'function') {
2064
- children = Array.from(element.getChildren()).filter(element => modeOrFilter(element));
2065
- }
2066
- else {
2067
- /**
2068
- * Unknown slot mode was provided to `writer.createSlot()` in downcast converter.
2069
- *
2070
- * @error conversion-slot-mode-unknown
2071
- */
2072
- throw new CKEditorError('conversion-slot-mode-unknown', conversionApi.dispatcher, { modeOrFilter });
2073
- }
2074
- slotsMap.set(slot, children);
2075
- return slot;
2076
- };
2077
- }
2078
- /**
2079
- * Checks if all children are covered by slots and there is no child that landed in multiple slots.
2080
- */
2081
- function validateSlotsChildren(element, slotsMap, conversionApi) {
2082
- const childrenInSlots = Array.from(slotsMap.values()).flat();
2083
- const uniqueChildrenInSlots = new Set(childrenInSlots);
2084
- if (uniqueChildrenInSlots.size != childrenInSlots.length) {
2085
- /**
2086
- * Filters provided to `writer.createSlot()` overlap (at least two filters accept the same child element).
2087
- *
2088
- * @error conversion-slot-filter-overlap
2089
- * @param {module:engine/model/element~Element} element The element of which children would not be properly
2090
- * allocated to multiple slots.
2091
- */
2092
- throw new CKEditorError('conversion-slot-filter-overlap', conversionApi.dispatcher, { element });
2093
- }
2094
- if (uniqueChildrenInSlots.size != element.childCount) {
2095
- /**
2096
- * Filters provided to `writer.createSlot()` are incomplete and exclude at least one children element (one of
2097
- * the children elements would not be assigned to any of the slots).
2098
- *
2099
- * @error conversion-slot-filter-incomplete
2100
- * @param {module:engine/model/element~Element} element The element of which children would not be properly
2101
- * allocated to multiple slots.
2102
- */
2103
- throw new CKEditorError('conversion-slot-filter-incomplete', conversionApi.dispatcher, { element });
2104
- }
2105
- }
2106
- /**
2107
- * Fill slots with appropriate view elements.
2108
- */
2109
- function fillSlots(viewElement, slotsMap, conversionApi, options) {
2110
- // Set temporary position mapping to redirect child view elements into a proper slots.
2111
- conversionApi.mapper.on('modelToViewPosition', toViewPositionMapping, { priority: 'highest' });
2112
- let currentSlot = null;
2113
- let currentSlotNodes = null;
2114
- // Fill slots with nested view nodes.
2115
- for ([currentSlot, currentSlotNodes] of slotsMap) {
2116
- reinsertOrConvertNodes(viewElement, currentSlotNodes, conversionApi, options);
2117
- conversionApi.writer.move(conversionApi.writer.createRangeIn(currentSlot), conversionApi.writer.createPositionBefore(currentSlot));
2118
- conversionApi.writer.remove(currentSlot);
2119
- }
2120
- conversionApi.mapper.off('modelToViewPosition', toViewPositionMapping);
2121
- function toViewPositionMapping(evt, data) {
2122
- const element = data.modelPosition.nodeAfter;
2123
- // Find the proper offset within the slot.
2124
- const index = currentSlotNodes.indexOf(element);
2125
- if (index < 0) {
2126
- return;
2127
- }
2128
- data.viewPosition = data.mapper.findPositionIn(currentSlot, index);
2129
- }
2130
- }
2131
- /**
2132
- * Inserts view representation of `nodes` into the `viewElement` either by bringing back just removed view nodes
2133
- * or by triggering conversion for them.
2134
- */
2135
- function reinsertOrConvertNodes(viewElement, modelNodes, conversionApi, options) {
2136
- // Fill with nested view nodes.
2137
- for (const modelChildNode of modelNodes) {
2138
- // Try reinserting the view node for the specified model node...
2139
- if (!reinsertNode(viewElement.root, modelChildNode, conversionApi, options)) {
2140
- // ...or else convert the model element to the view.
2141
- conversionApi.convertItem(modelChildNode);
2142
- }
2143
- }
2144
- }
2145
- /**
2146
- * Checks if the view for the given model element could be reused and reinserts it to the view.
2147
- *
2148
- * @returns `false` if view element can't be reused.
2149
- */
2150
- function reinsertNode(viewRoot, modelNode, conversionApi, options) {
2151
- const { writer, mapper } = conversionApi;
2152
- // Don't reinsert if this is not a reconversion...
2153
- if (!options.reconversion) {
2154
- return false;
2155
- }
2156
- const viewChildNode = mapper.toViewElement(modelNode);
2157
- // ...or there is no view to reinsert or it was already inserted to the view structure...
2158
- if (!viewChildNode || viewChildNode.root == viewRoot) {
2159
- return false;
2160
- }
2161
- // ...or it was strictly marked as not to be reused.
2162
- if (!conversionApi.canReuseView(viewChildNode)) {
2163
- return false;
2164
- }
2165
- // Otherwise reinsert the view node.
2166
- writer.move(writer.createRangeOn(viewChildNode), mapper.toViewPosition(ModelPosition._createBefore(modelNode)));
2167
- return true;
2168
- }
2169
- /**
2170
- * The default consumer for insert events.
2171
- *
2172
- * @param item Model item.
2173
- * @param consumable The model consumable.
2174
- * @param options.preflight Whether should consume or just check if can be consumed.
2175
- */
2176
- function defaultConsumer(item, consumable, { preflight } = {}) {
2177
- if (preflight) {
2178
- return consumable.test(item, 'insert');
2179
- }
2180
- else {
2181
- return consumable.consume(item, 'insert');
2182
- }
2183
- }
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * Contains downcast (model-to-view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
7
+ *
8
+ * @module engine/conversion/downcasthelpers
9
+ */
10
+ import ModelRange from '../model/range';
11
+ import ModelSelection from '../model/selection';
12
+ import ModelDocumentSelection from '../model/documentselection';
13
+ import ModelElement from '../model/element';
14
+ import ModelPosition from '../model/position';
15
+ import ViewAttributeElement from '../view/attributeelement';
16
+ import ConversionHelpers from './conversionhelpers';
17
+ import { CKEditorError, toArray } from '@ckeditor/ckeditor5-utils';
18
+ import { cloneDeep } from 'lodash-es';
19
+ /**
20
+ * Downcast conversion helper functions.
21
+ *
22
+ * Learn more about {@glink framework/deep-dive/conversion/downcast downcast helpers}.
23
+ *
24
+ * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
25
+ */
26
+ export default class DowncastHelpers extends ConversionHelpers {
27
+ /**
28
+ * Model element to view element conversion helper.
29
+ *
30
+ * This conversion results in creating a view element. For example, model `<paragraph>Foo</paragraph>` becomes `<p>Foo</p>` in the view.
31
+ *
32
+ * ```ts
33
+ * editor.conversion.for( 'downcast' ).elementToElement( {
34
+ * model: 'paragraph',
35
+ * view: 'p'
36
+ * } );
37
+ *
38
+ * editor.conversion.for( 'downcast' ).elementToElement( {
39
+ * model: 'paragraph',
40
+ * view: 'div',
41
+ * converterPriority: 'high'
42
+ * } );
43
+ *
44
+ * editor.conversion.for( 'downcast' ).elementToElement( {
45
+ * model: 'fancyParagraph',
46
+ * view: {
47
+ * name: 'p',
48
+ * classes: 'fancy'
49
+ * }
50
+ * } );
51
+ *
52
+ * editor.conversion.for( 'downcast' ).elementToElement( {
53
+ * model: 'heading',
54
+ * view: ( modelElement, conversionApi ) => {
55
+ * const { writer } = conversionApi;
56
+ *
57
+ * return writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
58
+ * }
59
+ * } );
60
+ * ```
61
+ *
62
+ * The element-to-element conversion supports the reconversion mechanism. It can be enabled by using either the `attributes` or
63
+ * the `children` props on a model description. You will find a couple examples below.
64
+ *
65
+ * In order to reconvert an element if any of its direct children have been added or removed, use the `children` property on a `model`
66
+ * description. For example, this model:
67
+ *
68
+ * ```xml
69
+ * <box>
70
+ * <paragraph>Some text.</paragraph>
71
+ * </box>
72
+ * ```
73
+ *
74
+ * will be converted into this structure in the view:
75
+ *
76
+ * ```html
77
+ * <div class="box" data-type="single">
78
+ * <p>Some text.</p>
79
+ * </div>
80
+ * ```
81
+ *
82
+ * But if more items were inserted in the model:
83
+ *
84
+ * ```xml
85
+ * <box>
86
+ * <paragraph>Some text.</paragraph>
87
+ * <paragraph>Other item.</paragraph>
88
+ * </box>
89
+ * ```
90
+ *
91
+ * it will be converted into this structure in the view (note the element `data-type` change):
92
+ *
93
+ * ```html
94
+ * <div class="box" data-type="multiple">
95
+ * <p>Some text.</p>
96
+ * <p>Other item.</p>
97
+ * </div>
98
+ * ```
99
+ *
100
+ * Such a converter would look like this (note that the `paragraph` elements are converted separately):
101
+ *
102
+ * ```ts
103
+ * editor.conversion.for( 'downcast' ).elementToElement( {
104
+ * model: {
105
+ * name: 'box',
106
+ * children: true
107
+ * },
108
+ * view: ( modelElement, conversionApi ) => {
109
+ * const { writer } = conversionApi;
110
+ *
111
+ * return writer.createContainerElement( 'div', {
112
+ * class: 'box',
113
+ * 'data-type': modelElement.childCount == 1 ? 'single' : 'multiple'
114
+ * } );
115
+ * }
116
+ * } );
117
+ * ```
118
+ *
119
+ * In order to reconvert element if any of its attributes have been updated, use the `attributes` property on a `model`
120
+ * description. For example, this model:
121
+ *
122
+ * ```xml
123
+ * <heading level="2">Some text.</heading>
124
+ * ```
125
+ *
126
+ * will be converted into this structure in the view:
127
+ *
128
+ * ```html
129
+ * <h2>Some text.</h2>
130
+ * ```
131
+ *
132
+ * But if the `heading` element's `level` attribute has been updated to `3` for example, then
133
+ * it will be converted into this structure in the view:
134
+ *
135
+ * ```html
136
+ * <h3>Some text.</h3>
137
+ * ```
138
+ *
139
+ * Such a converter would look as follows:
140
+ *
141
+ * ```ts
142
+ * editor.conversion.for( 'downcast' ).elementToElement( {
143
+ * model: {
144
+ * name: 'heading',
145
+ * attributes: 'level'
146
+ * },
147
+ * view: ( modelElement, conversionApi ) => {
148
+ * const { writer } = conversionApi;
149
+ *
150
+ * return writer.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) );
151
+ * }
152
+ * } );
153
+ * ```
154
+ *
155
+ * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
156
+ * to the conversion process.
157
+ *
158
+ * You can read more about the element-to-element conversion in the
159
+ * {@glink framework/deep-dive/conversion/downcast downcast conversion} guide.
160
+ *
161
+ * @param config Conversion configuration.
162
+ * @param config.model The description or a name of the model element to convert.
163
+ * @param config.model.attributes The list of attribute names that should be consumed while creating
164
+ * the view element. Note that the view will be reconverted if any of the listed attributes changes.
165
+ * @param config.model.children Specifies whether the view element requires reconversion if the list
166
+ * of the model child nodes changed.
167
+ * @param config.view A view element definition or a function that takes the model element and
168
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
169
+ * as parameters and returns a view container element.
170
+ */
171
+ elementToElement(config) {
172
+ return this.add(downcastElementToElement(config));
173
+ }
174
+ /**
175
+ * The model element to view structure (several elements) conversion helper.
176
+ *
177
+ * This conversion results in creating a view structure with one or more slots defined for the child nodes.
178
+ * For example, a model `<table>` may become this structure in the view:
179
+ *
180
+ * ```html
181
+ * <figure class="table">
182
+ * <table>
183
+ * <tbody>${ slot for table rows }</tbody>
184
+ * </table>
185
+ * </figure>
186
+ * ```
187
+ *
188
+ * The children of the model's `<table>` element will be inserted into the `<tbody>` element.
189
+ * If the `elementToElement()` helper was used, the children would be inserted into the `<figure>`.
190
+ *
191
+ * An example converter that converts the following model structure:
192
+ *
193
+ * ```xml
194
+ * <wrappedParagraph>Some text.</wrappedParagraph>
195
+ * ```
196
+ *
197
+ * into this structure in the view:
198
+ *
199
+ * ```html
200
+ * <div class="wrapper">
201
+ * <p>Some text.</p>
202
+ * </div>
203
+ * ```
204
+ *
205
+ * would look like this:
206
+ *
207
+ * ```ts
208
+ * editor.conversion.for( 'downcast' ).elementToStructure( {
209
+ * model: 'wrappedParagraph',
210
+ * view: ( modelElement, conversionApi ) => {
211
+ * const { writer } = conversionApi;
212
+ *
213
+ * const wrapperViewElement = writer.createContainerElement( 'div', { class: 'wrapper' } );
214
+ * const paragraphViewElement = writer.createContainerElement( 'p' );
215
+ *
216
+ * writer.insert( writer.createPositionAt( wrapperViewElement, 0 ), paragraphViewElement );
217
+ * writer.insert( writer.createPositionAt( paragraphViewElement, 0 ), writer.createSlot() );
218
+ *
219
+ * return wrapperViewElement;
220
+ * }
221
+ * } );
222
+ * ```
223
+ *
224
+ * The `createSlot()` function can also take a callback that allows filtering which children of the model element
225
+ * should be converted into this slot.
226
+ *
227
+ * Imagine a table feature where for this model structure:
228
+ *
229
+ * ```xml
230
+ * <table headingRows="1">
231
+ * <tableRow> ... table cells 1 ... </tableRow>
232
+ * <tableRow> ... table cells 2 ... </tableRow>
233
+ * <tableRow> ... table cells 3 ... </tableRow>
234
+ * <caption>Caption text</caption>
235
+ * </table>
236
+ * ```
237
+ *
238
+ * we want to generate this view structure:
239
+ *
240
+ * ```html
241
+ * <figure class="table">
242
+ * <table>
243
+ * <thead>
244
+ * <tr> ... table cells 1 ... </tr>
245
+ * </thead>
246
+ * <tbody>
247
+ * <tr> ... table cells 2 ... </tr>
248
+ * <tr> ... table cells 3 ... </tr>
249
+ * </tbody>
250
+ * </table>
251
+ * <figcaption>Caption text</figcaption>
252
+ * </figure>
253
+ * ```
254
+ *
255
+ * The converter has to take the `headingRows` attribute into consideration when allocating the `<tableRow>` elements
256
+ * into the `<tbody>` and `<thead>` elements. Hence, we need two slots and need to define proper filter callbacks for them.
257
+ *
258
+ * Additionally, all elements other than `<tableRow>` should be placed outside the `<table>` tag.
259
+ * In the example above, this will handle the table caption.
260
+ *
261
+ * Such a converter would look like this:
262
+ *
263
+ * ```ts
264
+ * editor.conversion.for( 'downcast' ).elementToStructure( {
265
+ * model: {
266
+ * name: 'table',
267
+ * attributes: [ 'headingRows' ]
268
+ * },
269
+ * view: ( modelElement, conversionApi ) => {
270
+ * const { writer } = conversionApi;
271
+ *
272
+ * const figureElement = writer.createContainerElement( 'figure', { class: 'table' } );
273
+ * const tableElement = writer.createContainerElement( 'table' );
274
+ *
275
+ * writer.insert( writer.createPositionAt( figureElement, 0 ), tableElement );
276
+ *
277
+ * const headingRows = modelElement.getAttribute( 'headingRows' ) || 0;
278
+ *
279
+ * if ( headingRows > 0 ) {
280
+ * const tableHead = writer.createContainerElement( 'thead' );
281
+ *
282
+ * const headSlot = writer.createSlot( node => node.is( 'element', 'tableRow' ) && node.index < headingRows );
283
+ *
284
+ * writer.insert( writer.createPositionAt( tableElement, 'end' ), tableHead );
285
+ * writer.insert( writer.createPositionAt( tableHead, 0 ), headSlot );
286
+ * }
287
+ *
288
+ * if ( headingRows < tableUtils.getRows( table ) ) {
289
+ * const tableBody = writer.createContainerElement( 'tbody' );
290
+ *
291
+ * const bodySlot = writer.createSlot( node => node.is( 'element', 'tableRow' ) && node.index >= headingRows );
292
+ *
293
+ * writer.insert( writer.createPositionAt( tableElement, 'end' ), tableBody );
294
+ * writer.insert( writer.createPositionAt( tableBody, 0 ), bodySlot );
295
+ * }
296
+ *
297
+ * const restSlot = writer.createSlot( node => !node.is( 'element', 'tableRow' ) );
298
+ *
299
+ * writer.insert( writer.createPositionAt( figureElement, 'end' ), restSlot );
300
+ *
301
+ * return figureElement;
302
+ * }
303
+ * } );
304
+ * ```
305
+ *
306
+ * Note: The children of a model element that's being converted must be allocated in the same order in the view
307
+ * in which they are placed in the model.
308
+ *
309
+ * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
310
+ * to the conversion process.
311
+ *
312
+ * @param config Conversion configuration.
313
+ * @param config.model The description or a name of the model element to convert.
314
+ * @param config.model.name The name of the model element to convert.
315
+ * @param config.model.attributes The list of attribute names that should be consumed while creating
316
+ * the view structure. Note that the view will be reconverted if any of the listed attributes will change.
317
+ * @param config.view A function that takes the model element and
318
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as parameters
319
+ * and returns a view container element with slots for model child nodes to be converted into.
320
+ */
321
+ elementToStructure(config) {
322
+ return this.add(downcastElementToStructure(config));
323
+ }
324
+ /**
325
+ * Model attribute to view element conversion helper.
326
+ *
327
+ * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with
328
+ * `"Foo"` as data and the `bold` attribute becomes `<strong>Foo</strong>` in the view.
329
+ *
330
+ * ```ts
331
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
332
+ * model: 'bold',
333
+ * view: 'strong'
334
+ * } );
335
+ *
336
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
337
+ * model: 'bold',
338
+ * view: 'b',
339
+ * converterPriority: 'high'
340
+ * } );
341
+ *
342
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
343
+ * model: 'invert',
344
+ * view: {
345
+ * name: 'span',
346
+ * classes: [ 'font-light', 'bg-dark' ]
347
+ * }
348
+ * } );
349
+ *
350
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
351
+ * model: {
352
+ * key: 'fontSize',
353
+ * values: [ 'big', 'small' ]
354
+ * },
355
+ * view: {
356
+ * big: {
357
+ * name: 'span',
358
+ * styles: {
359
+ * 'font-size': '1.2em'
360
+ * }
361
+ * },
362
+ * small: {
363
+ * name: 'span',
364
+ * styles: {
365
+ * 'font-size': '0.8em'
366
+ * }
367
+ * }
368
+ * }
369
+ * } );
370
+ *
371
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
372
+ * model: 'bold',
373
+ * view: ( modelAttributeValue, conversionApi ) => {
374
+ * const { writer } = conversionApi;
375
+ *
376
+ * return writer.createAttributeElement( 'span', {
377
+ * style: 'font-weight:' + modelAttributeValue
378
+ * } );
379
+ * }
380
+ * } );
381
+ *
382
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
383
+ * model: {
384
+ * key: 'color',
385
+ * name: '$text'
386
+ * },
387
+ * view: ( modelAttributeValue, conversionApi ) => {
388
+ * const { writer } = conversionApi;
389
+ *
390
+ * return writer.createAttributeElement( 'span', {
391
+ * style: 'color:' + modelAttributeValue
392
+ * } );
393
+ * }
394
+ * } );
395
+ * ```
396
+ *
397
+ * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
398
+ * to the conversion process.
399
+ *
400
+ * @param config Conversion configuration.
401
+ * @param config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
402
+ * of `String`s with possible values if the model attribute is an enumerable.
403
+ * @param config.view A view element definition or a function
404
+ * that takes the model attribute value and
405
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as parameters and returns a view
406
+ * attribute element. If `config.model.values` is given, `config.view` should be an object assigning values from `config.model.values`
407
+ * to view element definitions or functions.
408
+ * @param config.converterPriority Converter priority.
409
+ */
410
+ attributeToElement(config) {
411
+ return this.add(downcastAttributeToElement(config));
412
+ }
413
+ /**
414
+ * Model attribute to view attribute conversion helper.
415
+ *
416
+ * This conversion results in adding an attribute to a view node, basing on an attribute from a model node. For example,
417
+ * `<imageInline src='foo.jpg'></imageInline>` is converted to `<img src='foo.jpg'></img>`.
418
+ *
419
+ * ```ts
420
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
421
+ * model: 'source',
422
+ * view: 'src'
423
+ * } );
424
+ *
425
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
426
+ * model: 'source',
427
+ * view: 'href',
428
+ * converterPriority: 'high'
429
+ * } );
430
+ *
431
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
432
+ * model: {
433
+ * name: 'imageInline',
434
+ * key: 'source'
435
+ * },
436
+ * view: 'src'
437
+ * } );
438
+ *
439
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
440
+ * model: {
441
+ * name: 'styled',
442
+ * values: [ 'dark', 'light' ]
443
+ * },
444
+ * view: {
445
+ * dark: {
446
+ * key: 'class',
447
+ * value: [ 'styled', 'styled-dark' ]
448
+ * },
449
+ * light: {
450
+ * key: 'class',
451
+ * value: [ 'styled', 'styled-light' ]
452
+ * }
453
+ * }
454
+ * } );
455
+ *
456
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
457
+ * model: 'styled',
458
+ * view: modelAttributeValue => ( {
459
+ * key: 'class',
460
+ * value: 'styled-' + modelAttributeValue
461
+ * } )
462
+ * } );
463
+ * ```
464
+ *
465
+ * **Note**: Downcasting to a style property requires providing `value` as an object:
466
+ *
467
+ * ```ts
468
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
469
+ * model: 'lineHeight',
470
+ * view: modelAttributeValue => ( {
471
+ * key: 'style',
472
+ * value: {
473
+ * 'line-height': modelAttributeValue,
474
+ * 'border-bottom': '1px dotted #ba2'
475
+ * }
476
+ * } )
477
+ * } );
478
+ * ```
479
+ *
480
+ * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
481
+ * to the conversion process.
482
+ *
483
+ * @param config Conversion configuration.
484
+ * @param config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
485
+ * the attribute key, possible values and, optionally, an element name to convert from.
486
+ * @param config.view A view attribute key, or a `{ key, value }` object or a function that takes the model attribute value and
487
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
488
+ * as parameters and returns a `{ key, value }` object. If the `key` is `'class'`, the `value` can be a `String` or an
489
+ * array of `String`s. If the `key` is `'style'`, the `value` is an object with key-value pairs. In other cases, `value` is a `String`.
490
+ * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
491
+ * `{ key, value }` objects or a functions.
492
+ * @param config.converterPriority Converter priority.
493
+ */
494
+ attributeToAttribute(config) {
495
+ return this.add(downcastAttributeToAttribute(config));
496
+ }
497
+ /**
498
+ * Model marker to view element conversion helper.
499
+ *
500
+ * **Note**: This method should be used mainly for editing the downcast and it is recommended
501
+ * to use the {@link #markerToData `#markerToData()`} helper instead.
502
+ *
503
+ * This helper may produce invalid HTML code (e.g. a span between table cells).
504
+ * It should only be used when you are sure that the produced HTML will be semantically correct.
505
+ *
506
+ * This conversion results in creating a view element on the boundaries of the converted marker. If the converted marker
507
+ * is collapsed, only one element is created. For example, a model marker set like this: `<paragraph>F[oo b]ar</paragraph>`
508
+ * becomes `<p>F<span data-marker="search"></span>oo b<span data-marker="search"></span>ar</p>` in the view.
509
+ *
510
+ * ```ts
511
+ * editor.conversion.for( 'editingDowncast' ).markerToElement( {
512
+ * model: 'search',
513
+ * view: 'marker-search'
514
+ * } );
515
+ *
516
+ * editor.conversion.for( 'editingDowncast' ).markerToElement( {
517
+ * model: 'search',
518
+ * view: 'search-result',
519
+ * converterPriority: 'high'
520
+ * } );
521
+ *
522
+ * editor.conversion.for( 'editingDowncast' ).markerToElement( {
523
+ * model: 'search',
524
+ * view: {
525
+ * name: 'span',
526
+ * attributes: {
527
+ * 'data-marker': 'search'
528
+ * }
529
+ * }
530
+ * } );
531
+ *
532
+ * editor.conversion.for( 'editingDowncast' ).markerToElement( {
533
+ * model: 'search',
534
+ * view: ( markerData, conversionApi ) => {
535
+ * const { writer } = conversionApi;
536
+ *
537
+ * return writer.createUIElement( 'span', {
538
+ * 'data-marker': 'search',
539
+ * 'data-start': markerData.isOpening
540
+ * } );
541
+ * }
542
+ * } );
543
+ * ```
544
+ *
545
+ * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function
546
+ * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
547
+ * as a parameters and should return an instance of the
548
+ * {@link module:engine/view/uielement~UIElement view UI element}. The `data` object and
549
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi `conversionApi`} are passed from
550
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
551
+ * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` for
552
+ * the marker end boundary element.
553
+ *
554
+ * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
555
+ * to the conversion process.
556
+ *
557
+ * @param config Conversion configuration.
558
+ * @param config.model The name of the model marker (or model marker group) to convert.
559
+ * @param config.view A view element definition or a function that takes the model marker data and
560
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
561
+ * and returns a view UI element.
562
+ * @param config.converterPriority Converter priority.
563
+ */
564
+ markerToElement(config) {
565
+ return this.add(downcastMarkerToElement(config));
566
+ }
567
+ /**
568
+ * Model marker to highlight conversion helper.
569
+ *
570
+ * This conversion results in creating a highlight on view nodes. For this kind of conversion,
571
+ * the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor} should be provided.
572
+ *
573
+ * For text nodes, a `<span>` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes
574
+ * in the converted marker range. For example, a model marker set like this: `<paragraph>F[oo b]ar</paragraph>` becomes
575
+ * `<p>F<span class="comment">oo b</span>ar</p>` in the view.
576
+ *
577
+ * {@link module:engine/view/containerelement~ContainerElement} may provide a custom way of handling highlight. Most often,
578
+ * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in `<span>`).
579
+ * For example, a model marker set like this:
580
+ * `[<imageInline src="foo.jpg"></imageInline>]` becomes `<img src="foo.jpg" class="comment"></img>` in the view.
581
+ *
582
+ * For container elements, the conversion is two-step. While the converter processes the highlight descriptor and passes it
583
+ * to a container element, it is the container element instance itself that applies values from the highlight descriptor.
584
+ * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that.
585
+ *
586
+ * ```ts
587
+ * editor.conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
588
+ *
589
+ * editor.conversion.for( 'downcast' ).markerToHighlight( {
590
+ * model: 'comment',
591
+ * view: { classes: 'comment' },
592
+ * converterPriority: 'high'
593
+ * } );
594
+ *
595
+ * editor.conversion.for( 'downcast' ).markerToHighlight( {
596
+ * model: 'comment',
597
+ * view: ( data, conversionApi ) => {
598
+ * // Assuming that the marker name is in a form of comment:commentType:commentId.
599
+ * const [ , commentType, commentId ] = data.markerName.split( ':' );
600
+ *
601
+ * return {
602
+ * classes: [ 'comment', 'comment-' + commentType ],
603
+ * attributes: { 'data-comment-id': commentId }
604
+ * };
605
+ * }
606
+ * } );
607
+ * ```
608
+ *
609
+ * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function
610
+ * receives the `data` object and {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API}
611
+ * as the parameters and should return a
612
+ * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor}.
613
+ * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
614
+ *
615
+ * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
616
+ * to the conversion process.
617
+ *
618
+ * @param config Conversion configuration.
619
+ * @param config.model The name of the model marker (or model marker group) to convert.
620
+ * @param config.view A highlight descriptor that will be used for highlighting or a function that takes the model marker data and
621
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as a parameters
622
+ * and returns a highlight descriptor.
623
+ * @param config.converterPriority Converter priority.
624
+ */
625
+ markerToHighlight(config) {
626
+ return this.add(downcastMarkerToHighlight(config));
627
+ }
628
+ /**
629
+ * Model marker converter for data downcast.
630
+ *
631
+ * This conversion creates a representation for model marker boundaries in the view:
632
+ *
633
+ * * If the marker boundary is before or after a model element, a view attribute is set on a corresponding view element.
634
+ * * In other cases, a view element with the specified tag name is inserted at the corresponding view position.
635
+ *
636
+ * Typically, the marker names use the `group:uniqueId:otherData` convention. For example: `comment:e34zfk9k2n459df53sjl34:zx32c`.
637
+ * The default configuration for this conversion is that the first part is the `group` part and the rest of
638
+ * the marker name becomes the `name` part.
639
+ *
640
+ * Tag and attribute names and values are generated from the marker name:
641
+ *
642
+ * * The templates for attributes are `data-[group]-start-before="[name]"`, `data-[group]-start-after="[name]"`,
643
+ * `data-[group]-end-before="[name]"` and `data-[group]-end-after="[name]"`.
644
+ * * The templates for view elements are `<[group]-start name="[name]">` and `<[group]-end name="[name]">`.
645
+ *
646
+ * Attributes mark whether the given marker's start or end boundary is before or after the given element.
647
+ * The `data-[group]-start-before` and `data-[group]-end-after` attributes are favored.
648
+ * The other two are used when the former two cannot be used.
649
+ *
650
+ * The conversion configuration can take a function that will generate different group and name parts.
651
+ * 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
652
+ * properties: `group` and `name`. If the function returns a falsy value, the conversion will not take place.
653
+ *
654
+ * Basic usage:
655
+ *
656
+ * ```ts
657
+ * // Using the default conversion.
658
+ * // In this case, all markers with names starting with 'comment:' will be converted.
659
+ * // The `group` parameter will be set to `comment`.
660
+ * // The `name` parameter will be the rest of the marker name (without the `:`).
661
+ * editor.conversion.for( 'dataDowncast' ).markerToData( {
662
+ * model: 'comment'
663
+ * } );
664
+ * ```
665
+ *
666
+ * An example of a view that may be generated by this conversion (assuming a marker with the name `comment:commentId:uid` marked
667
+ * by `[]`):
668
+ *
669
+ * ```
670
+ * // Model:
671
+ * <paragraph>Foo[bar</paragraph>
672
+ * <imageBlock src="abc.jpg"></imageBlock>]
673
+ *
674
+ * // View:
675
+ * <p>Foo<comment-start name="commentId:uid"></comment-start>bar</p>
676
+ * <figure data-comment-end-after="commentId:uid" class="image"><img src="abc.jpg" /></figure>
677
+ * ```
678
+ *
679
+ * In the example above, the comment starts before "bar" and ends after the image.
680
+ *
681
+ * If the `name` part is empty, the following view may be generated:
682
+ *
683
+ * ```html
684
+ * <p>Foo <myMarker-start></myMarker-start>bar</p>
685
+ * <figure data-myMarker-end-after="" class="image"><img src="abc.jpg" /></figure>
686
+ * ```
687
+ *
688
+ * **Note:** A situation where some markers have the `name` part and some do not, is incorrect and should be avoided.
689
+ *
690
+ * Examples where `data-group-start-after` and `data-group-end-before` are used:
691
+ *
692
+ * ```
693
+ * // Model:
694
+ * <blockQuote>[]<paragraph>Foo</paragraph></blockQuote>
695
+ *
696
+ * // View:
697
+ * <blockquote><p data-group-end-before="name" data-group-start-before="name">Foo</p></blockquote>
698
+ * ```
699
+ *
700
+ * Similarly, when a marker is collapsed after the last element:
701
+ *
702
+ * ```
703
+ * // Model:
704
+ * <blockQuote><paragraph>Foo</paragraph>[]</blockQuote>
705
+ *
706
+ * // View:
707
+ * <blockquote><p data-group-end-after="name" data-group-start-after="name">Foo</p></blockquote>
708
+ * ```
709
+ *
710
+ * When there are multiple markers from the same group stored in the same attribute of the same element, their
711
+ * name parts are put together in the attribute value, for example: `data-group-start-before="name1,name2,name3"`.
712
+ *
713
+ * Other examples of usage:
714
+ *
715
+ * ```ts
716
+ * // Using a custom function which is the same as the default conversion:
717
+ * editor.conversion.for( 'dataDowncast' ).markerToData( {
718
+ * model: 'comment',
719
+ * view: markerName => ( {
720
+ * group: 'comment',
721
+ * name: markerName.substr( 8 ) // Removes 'comment:' part.
722
+ * } )
723
+ * } );
724
+ *
725
+ * // Using the converter priority:
726
+ * editor.conversion.for( 'dataDowncast' ).markerToData( {
727
+ * model: 'comment',
728
+ * view: markerName => ( {
729
+ * group: 'comment',
730
+ * name: markerName.substr( 8 ) // Removes 'comment:' part.
731
+ * } ),
732
+ * converterPriority: 'high'
733
+ * } );
734
+ * ```
735
+ *
736
+ * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline.
737
+ *
738
+ * See the {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} API guide to learn how to
739
+ * add a converter to the conversion process.
740
+ *
741
+ * @param config Conversion configuration.
742
+ * @param config.model The name of the model marker (or the model marker group) to convert.
743
+ * @param config.view A function that takes the model marker name and
744
+ * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi downcast conversion API} as the parameters
745
+ * and returns an object with the `group` and `name` properties.
746
+ * @param config.converterPriority Converter priority.
747
+ */
748
+ markerToData(config) {
749
+ return this.add(downcastMarkerToData(config));
750
+ }
751
+ }
752
+ /**
753
+ * Function factory that creates a default downcast converter for text insertion changes.
754
+ *
755
+ * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
756
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
757
+ *
758
+ * ```ts
759
+ * modelDispatcher.on( 'insert:$text', insertText() );
760
+ * ```
761
+ *
762
+ * @returns Insert text event converter.
763
+ */
764
+ export function insertText() {
765
+ return (evt, data, conversionApi) => {
766
+ if (!conversionApi.consumable.consume(data.item, evt.name)) {
767
+ return;
768
+ }
769
+ const viewWriter = conversionApi.writer;
770
+ const viewPosition = conversionApi.mapper.toViewPosition(data.range.start);
771
+ const viewText = viewWriter.createText(data.item.data);
772
+ viewWriter.insert(viewPosition, viewText);
773
+ };
774
+ }
775
+ /**
776
+ * Function factory that creates a default downcast converter for triggering attributes and children conversion.
777
+ *
778
+ * @returns The converter.
779
+ */
780
+ export function insertAttributesAndChildren() {
781
+ return (evt, data, conversionApi) => {
782
+ conversionApi.convertAttributes(data.item);
783
+ // Start converting children of the current item.
784
+ // In case of reconversion children were already re-inserted or converted separately.
785
+ if (!data.reconversion && data.item.is('element') && !data.item.isEmpty) {
786
+ conversionApi.convertChildren(data.item);
787
+ }
788
+ };
789
+ }
790
+ /**
791
+ * Function factory that creates a default downcast converter for node remove changes.
792
+ *
793
+ * ```ts
794
+ * modelDispatcher.on( 'remove', remove() );
795
+ * ```
796
+ *
797
+ * @returns Remove event converter.
798
+ */
799
+ export function remove() {
800
+ return (evt, data, conversionApi) => {
801
+ // Find the view range start position by mapping the model position at which the remove happened.
802
+ const viewStart = conversionApi.mapper.toViewPosition(data.position);
803
+ const modelEnd = data.position.getShiftedBy(data.length);
804
+ const viewEnd = conversionApi.mapper.toViewPosition(modelEnd, { isPhantom: true });
805
+ const viewRange = conversionApi.writer.createRange(viewStart, viewEnd);
806
+ // Trim the range to remove in case some UI elements are on the view range boundaries.
807
+ const removed = conversionApi.writer.remove(viewRange.getTrimmed());
808
+ // After the range is removed, unbind all view elements from the model.
809
+ // Range inside view document fragment is used to unbind deeply.
810
+ for (const child of conversionApi.writer.createRangeIn(removed).getItems()) {
811
+ conversionApi.mapper.unbindViewElement(child, { defer: true });
812
+ }
813
+ };
814
+ }
815
+ /**
816
+ * Creates a `<span>` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from the information
817
+ * provided by the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor} object. If the priority
818
+ * is not provided in the descriptor, the default priority will be used.
819
+ */
820
+ export function createViewElementFromHighlightDescriptor(writer, descriptor) {
821
+ const viewElement = writer.createAttributeElement('span', descriptor.attributes);
822
+ if (descriptor.classes) {
823
+ viewElement._addClass(descriptor.classes);
824
+ }
825
+ if (typeof descriptor.priority === 'number') {
826
+ viewElement._priority = descriptor.priority;
827
+ }
828
+ viewElement._id = descriptor.id;
829
+ return viewElement;
830
+ }
831
+ /**
832
+ * Function factory that creates a converter which converts a non-collapsed {@link module:engine/model/selection~Selection model selection}
833
+ * to a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
834
+ * value from the `consumable` object and maps model positions from the selection to view positions.
835
+ *
836
+ * ```ts
837
+ * modelDispatcher.on( 'selection', convertRangeSelection() );
838
+ * ```
839
+ *
840
+ * @returns Selection converter.
841
+ */
842
+ export function convertRangeSelection() {
843
+ return (evt, data, conversionApi) => {
844
+ const selection = data.selection;
845
+ if (selection.isCollapsed) {
846
+ return;
847
+ }
848
+ if (!conversionApi.consumable.consume(selection, 'selection')) {
849
+ return;
850
+ }
851
+ const viewRanges = [];
852
+ for (const range of selection.getRanges()) {
853
+ viewRanges.push(conversionApi.mapper.toViewRange(range));
854
+ }
855
+ conversionApi.writer.setSelection(viewRanges, { backward: selection.isBackward });
856
+ };
857
+ }
858
+ /**
859
+ * Function factory that creates a converter which converts a collapsed {@link module:engine/model/selection~Selection model selection} to
860
+ * a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
861
+ * value from the `consumable` object, maps the model selection position to the view position and breaks
862
+ * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position.
863
+ *
864
+ * ```ts
865
+ * modelDispatcher.on( 'selection', convertCollapsedSelection() );
866
+ * ```
867
+ *
868
+ * An example of the view state before and after converting the collapsed selection:
869
+ *
870
+ * ```
871
+ * <p><strong>f^oo<strong>bar</p>
872
+ * -> <p><strong>f</strong>^<strong>oo</strong>bar</p>
873
+ * ```
874
+ *
875
+ * By breaking attribute elements like `<strong>`, the selection is in a correct element. Then, when the selection attribute is
876
+ * converted, broken attributes might be merged again, or the position where the selection is may be wrapped
877
+ * with different, appropriate attribute elements.
878
+ *
879
+ * See also {@link module:engine/conversion/downcasthelpers~cleanSelection} which does a clean-up
880
+ * by merging attributes.
881
+ *
882
+ * @returns Selection converter.
883
+ */
884
+ export function convertCollapsedSelection() {
885
+ return (evt, data, conversionApi) => {
886
+ const selection = data.selection;
887
+ if (!selection.isCollapsed) {
888
+ return;
889
+ }
890
+ if (!conversionApi.consumable.consume(selection, 'selection')) {
891
+ return;
892
+ }
893
+ const viewWriter = conversionApi.writer;
894
+ const modelPosition = selection.getFirstPosition();
895
+ const viewPosition = conversionApi.mapper.toViewPosition(modelPosition);
896
+ const brokenPosition = viewWriter.breakAttributes(viewPosition);
897
+ viewWriter.setSelection(brokenPosition);
898
+ };
899
+ }
900
+ /**
901
+ * Function factory that creates a converter which cleans artifacts after the previous
902
+ * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty
903
+ * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merges sibling attributes at all start and end
904
+ * positions of all ranges.
905
+ *
906
+ * ```
907
+ * <p><strong>^</strong></p>
908
+ * -> <p>^</p>
909
+ *
910
+ * <p><strong>foo</strong>^<strong>bar</strong>bar</p>
911
+ * -> <p><strong>foo^bar<strong>bar</p>
912
+ *
913
+ * <p><strong>foo</strong><em>^</em><strong>bar</strong>bar</p>
914
+ * -> <p><strong>foo^bar<strong>bar</p>
915
+ * ```
916
+ *
917
+ * This listener should be assigned before any converter for the new selection:
918
+ *
919
+ * ```ts
920
+ * modelDispatcher.on( 'cleanSelection', cleanSelection() );
921
+ * ```
922
+ *
923
+ * See {@link module:engine/conversion/downcasthelpers~convertCollapsedSelection}
924
+ * which does the opposite by breaking attributes in the selection position.
925
+ *
926
+ * @returns Selection converter.
927
+ */
928
+ export function cleanSelection() {
929
+ return (evt, data, conversionApi) => {
930
+ const viewWriter = conversionApi.writer;
931
+ const viewSelection = viewWriter.document.selection;
932
+ for (const range of viewSelection.getRanges()) {
933
+ // Not collapsed selection should not have artifacts.
934
+ if (range.isCollapsed) {
935
+ // Position might be in the node removed by the view writer.
936
+ if (range.end.parent.isAttached()) {
937
+ conversionApi.writer.mergeAttributes(range.start);
938
+ }
939
+ }
940
+ }
941
+ viewWriter.setSelection(null);
942
+ };
943
+ }
944
+ /**
945
+ * Function factory that creates a converter which converts the set/change/remove attribute changes from the model to the view.
946
+ * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the
947
+ * selection will be put inside it.
948
+ *
949
+ * Attributes from the model are converted to a view element that will be wrapping these view nodes that are bound to
950
+ * model elements having the given attribute. This is useful for attributes like `bold` that may be set on text nodes in the model
951
+ * but are represented as an element in the view:
952
+ *
953
+ * ```
954
+ * [paragraph] MODEL ====> VIEW <p>
955
+ * |- a {bold: true} |- <b>
956
+ * |- b {bold: true} | |- ab
957
+ * |- c |- c
958
+ * ```
959
+ *
960
+ * Passed `Function` will be provided with the attribute value and then all the parameters of the
961
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute` event}.
962
+ * It is expected that the function returns an {@link module:engine/view/element~Element}.
963
+ * The result of the function will be the wrapping element.
964
+ * When the provided `Function` does not return any element, no conversion will take place.
965
+ *
966
+ * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
967
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
968
+ *
969
+ * ```ts
970
+ * modelDispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, { writer } ) => {
971
+ * return writer.createAttributeElement( 'strong' );
972
+ * } );
973
+ * ```
974
+ *
975
+ * @internal
976
+ * @param elementCreator Function returning a view element that will be used for wrapping.
977
+ * @returns Set/change attribute converter.
978
+ */
979
+ export function wrap(elementCreator) {
980
+ return (evt, data, conversionApi) => {
981
+ if (!conversionApi.consumable.test(data.item, evt.name)) {
982
+ return;
983
+ }
984
+ // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
985
+ // or the attribute was removed.
986
+ const oldViewElement = elementCreator(data.attributeOldValue, conversionApi, data);
987
+ // Create node to wrap with.
988
+ const newViewElement = elementCreator(data.attributeNewValue, conversionApi, data);
989
+ if (!oldViewElement && !newViewElement) {
990
+ return;
991
+ }
992
+ conversionApi.consumable.consume(data.item, evt.name);
993
+ const viewWriter = conversionApi.writer;
994
+ const viewSelection = viewWriter.document.selection;
995
+ if (data.item instanceof ModelSelection || data.item instanceof ModelDocumentSelection) {
996
+ // Selection attribute conversion.
997
+ viewWriter.wrap(viewSelection.getFirstRange(), newViewElement);
998
+ }
999
+ else {
1000
+ // Node attribute conversion.
1001
+ let viewRange = conversionApi.mapper.toViewRange(data.range);
1002
+ // First, unwrap the range from current wrapper.
1003
+ if (data.attributeOldValue !== null && oldViewElement) {
1004
+ viewRange = viewWriter.unwrap(viewRange, oldViewElement);
1005
+ }
1006
+ if (data.attributeNewValue !== null && newViewElement) {
1007
+ viewWriter.wrap(viewRange, newViewElement);
1008
+ }
1009
+ }
1010
+ };
1011
+ }
1012
+ /**
1013
+ * Function factory that creates a converter which converts node insertion changes from the model to the view.
1014
+ * The function passed will be provided with all the parameters of the dispatcher's
1015
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}.
1016
+ * It is expected that the function returns an {@link module:engine/view/element~Element}.
1017
+ * The result of the function will be inserted into the view.
1018
+ *
1019
+ * The converter automatically consumes the corresponding value from the consumables list and binds the model and view elements.
1020
+ *
1021
+ * ```ts
1022
+ * downcastDispatcher.on(
1023
+ * 'insert:myElem',
1024
+ * insertElement( ( modelItem, { writer } ) => {
1025
+ * const text = writer.createText( 'myText' );
1026
+ * const myElem = writer.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text );
1027
+ *
1028
+ * // Do something fancy with `myElem` using `modelItem` or other parameters.
1029
+ *
1030
+ * return myElem;
1031
+ * }
1032
+ * ) );
1033
+ * ```
1034
+ *
1035
+ * @internal
1036
+ * @param elementCreator Function returning a view element, which will be inserted.
1037
+ * @param consumer Function defining element consumption process.
1038
+ * By default this function just consume passed item insertion.
1039
+ * @returns Insert element event converter.
1040
+ */
1041
+ export function insertElement(elementCreator, consumer = defaultConsumer) {
1042
+ return (evt, data, conversionApi) => {
1043
+ if (!consumer(data.item, conversionApi.consumable, { preflight: true })) {
1044
+ return;
1045
+ }
1046
+ const viewElement = elementCreator(data.item, conversionApi, data);
1047
+ if (!viewElement) {
1048
+ return;
1049
+ }
1050
+ // Consume an element insertion and all present attributes that are specified as a reconversion triggers.
1051
+ consumer(data.item, conversionApi.consumable);
1052
+ const viewPosition = conversionApi.mapper.toViewPosition(data.range.start);
1053
+ conversionApi.mapper.bindElements(data.item, viewElement);
1054
+ conversionApi.writer.insert(viewPosition, viewElement);
1055
+ // Convert attributes before converting children.
1056
+ conversionApi.convertAttributes(data.item);
1057
+ // Convert children or reinsert previous view elements.
1058
+ reinsertOrConvertNodes(viewElement, data.item.getChildren(), conversionApi, { reconversion: data.reconversion });
1059
+ };
1060
+ }
1061
+ /**
1062
+ * Function factory that creates a converter which converts a single model node insertion to a view structure.
1063
+ *
1064
+ * It is expected that the passed element creator function returns an {@link module:engine/view/element~Element} with attached slots
1065
+ * created with `writer.createSlot()` to indicate where child nodes should be converted.
1066
+ *
1067
+ * @see module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure
1068
+ *
1069
+ * @internal
1070
+ * @param elementCreator Function returning a view structure, which will be inserted.
1071
+ * @param consumer A callback that is expected to consume all the consumables
1072
+ * that were used by the element creator.
1073
+ * @returns Insert element event converter.
1074
+ */
1075
+ export function insertStructure(elementCreator, consumer) {
1076
+ return (evt, data, conversionApi) => {
1077
+ if (!consumer(data.item, conversionApi.consumable, { preflight: true })) {
1078
+ return;
1079
+ }
1080
+ const slotsMap = new Map();
1081
+ conversionApi.writer._registerSlotFactory(createSlotFactory(data.item, slotsMap, conversionApi));
1082
+ // View creation.
1083
+ const viewElement = elementCreator(data.item, conversionApi, data);
1084
+ conversionApi.writer._clearSlotFactory();
1085
+ if (!viewElement) {
1086
+ return;
1087
+ }
1088
+ // Check if all children are covered by slots and there is no child that landed in multiple slots.
1089
+ validateSlotsChildren(data.item, slotsMap, conversionApi);
1090
+ // Consume an element insertion and all present attributes that are specified as a reconversion triggers.
1091
+ consumer(data.item, conversionApi.consumable);
1092
+ const viewPosition = conversionApi.mapper.toViewPosition(data.range.start);
1093
+ conversionApi.mapper.bindElements(data.item, viewElement);
1094
+ conversionApi.writer.insert(viewPosition, viewElement);
1095
+ // Convert attributes before converting children.
1096
+ conversionApi.convertAttributes(data.item);
1097
+ // Fill view slots with previous view elements or create new ones.
1098
+ fillSlots(viewElement, slotsMap, conversionApi, { reconversion: data.reconversion });
1099
+ };
1100
+ }
1101
+ /**
1102
+ * Function factory that creates a converter which converts marker adding change to the
1103
+ * {@link module:engine/view/uielement~UIElement view UI element}.
1104
+ *
1105
+ * The view UI element that will be added to the view depends on the passed parameter. See {@link ~insertElement}.
1106
+ * In case of a non-collapsed range, the UI element will not wrap nodes but separate elements will be placed at the beginning
1107
+ * and at the end of the range.
1108
+ *
1109
+ * This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
1110
+ *
1111
+ * @internal
1112
+ * @param elementCreator A view UI element or a function returning the view element that will be inserted.
1113
+ * @returns Insert element event converter.
1114
+ */
1115
+ export function insertUIElement(elementCreator) {
1116
+ return (evt, data, conversionApi) => {
1117
+ // Create two view elements. One will be inserted at the beginning of marker, one at the end.
1118
+ // If marker is collapsed, only "opening" element will be inserted.
1119
+ data.isOpening = true;
1120
+ const viewStartElement = elementCreator(data, conversionApi);
1121
+ data.isOpening = false;
1122
+ const viewEndElement = elementCreator(data, conversionApi);
1123
+ if (!viewStartElement || !viewEndElement) {
1124
+ return;
1125
+ }
1126
+ const markerRange = data.markerRange;
1127
+ // Marker that is collapsed has consumable build differently that non-collapsed one.
1128
+ // For more information see `addMarker` event description.
1129
+ // If marker's range is collapsed - check if it can be consumed.
1130
+ if (markerRange.isCollapsed && !conversionApi.consumable.consume(markerRange, evt.name)) {
1131
+ return;
1132
+ }
1133
+ // If marker's range is not collapsed - consume all items inside.
1134
+ for (const value of markerRange) {
1135
+ if (!conversionApi.consumable.consume(value.item, evt.name)) {
1136
+ return;
1137
+ }
1138
+ }
1139
+ const mapper = conversionApi.mapper;
1140
+ const viewWriter = conversionApi.writer;
1141
+ // Add "opening" element.
1142
+ viewWriter.insert(mapper.toViewPosition(markerRange.start), viewStartElement);
1143
+ conversionApi.mapper.bindElementToMarker(viewStartElement, data.markerName);
1144
+ // Add "closing" element only if range is not collapsed.
1145
+ if (!markerRange.isCollapsed) {
1146
+ viewWriter.insert(mapper.toViewPosition(markerRange.end), viewEndElement);
1147
+ conversionApi.mapper.bindElementToMarker(viewEndElement, data.markerName);
1148
+ }
1149
+ evt.stop();
1150
+ };
1151
+ }
1152
+ /**
1153
+ * Function factory that returns a default downcast converter for removing a {@link module:engine/view/uielement~UIElement UI element}
1154
+ * based on marker remove change.
1155
+ *
1156
+ * This converter unbinds elements from the marker name.
1157
+ *
1158
+ * @returns Removed UI element converter.
1159
+ */
1160
+ function removeUIElement() {
1161
+ return (evt, data, conversionApi) => {
1162
+ const elements = conversionApi.mapper.markerNameToElements(data.markerName);
1163
+ if (!elements) {
1164
+ return;
1165
+ }
1166
+ for (const element of elements) {
1167
+ conversionApi.mapper.unbindElementFromMarkerName(element, data.markerName);
1168
+ conversionApi.writer.clear(conversionApi.writer.createRangeOn(element), element);
1169
+ }
1170
+ conversionApi.writer.clearClonedElementsGroup(data.markerName);
1171
+ evt.stop();
1172
+ };
1173
+ }
1174
+ /**
1175
+ * Function factory that creates a default converter for model markers.
1176
+ *
1177
+ * See {@link DowncastHelpers#markerToData} for more information what type of view is generated.
1178
+ *
1179
+ * This converter binds created UI elements and affected view elements with the marker name
1180
+ * using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
1181
+ *
1182
+ * @returns Add marker converter.
1183
+ */
1184
+ function insertMarkerData(viewCreator) {
1185
+ return (evt, data, conversionApi) => {
1186
+ const viewMarkerData = viewCreator(data.markerName, conversionApi);
1187
+ if (!viewMarkerData) {
1188
+ return;
1189
+ }
1190
+ const markerRange = data.markerRange;
1191
+ if (!conversionApi.consumable.consume(markerRange, evt.name)) {
1192
+ return;
1193
+ }
1194
+ // Adding closing data first to keep the proper order in the view.
1195
+ handleMarkerBoundary(markerRange, false, conversionApi, data, viewMarkerData);
1196
+ handleMarkerBoundary(markerRange, true, conversionApi, data, viewMarkerData);
1197
+ evt.stop();
1198
+ };
1199
+ }
1200
+ /**
1201
+ * Helper function for `insertMarkerData()` that marks a marker boundary at the beginning or end of given `range`.
1202
+ */
1203
+ function handleMarkerBoundary(range, isStart, conversionApi, data, viewMarkerData) {
1204
+ const modelPosition = isStart ? range.start : range.end;
1205
+ const elementAfter = modelPosition.nodeAfter && modelPosition.nodeAfter.is('element') ? modelPosition.nodeAfter : null;
1206
+ const elementBefore = modelPosition.nodeBefore && modelPosition.nodeBefore.is('element') ? modelPosition.nodeBefore : null;
1207
+ if (elementAfter || elementBefore) {
1208
+ let modelElement;
1209
+ let isBefore;
1210
+ // If possible, we want to add `data-group-start-before` and `data-group-end-after` attributes.
1211
+ if (isStart && elementAfter || !isStart && !elementBefore) {
1212
+ // [<elementAfter>...</elementAfter> -> <elementAfter data-group-start-before="...">...</elementAfter>
1213
+ // <parent>]<elementAfter> -> <parent><elementAfter data-group-end-before="...">
1214
+ modelElement = elementAfter;
1215
+ isBefore = true;
1216
+ }
1217
+ else {
1218
+ // <elementBefore>...</elementBefore>] -> <elementBefore data-group-end-after="...">...</elementBefore>
1219
+ // </elementBefore>[</parent> -> </elementBefore data-group-start-after="..."></parent>
1220
+ modelElement = elementBefore;
1221
+ isBefore = false;
1222
+ }
1223
+ const viewElement = conversionApi.mapper.toViewElement(modelElement);
1224
+ // In rare circumstances, the model element may be not mapped to any view element and that would cause an error.
1225
+ // One of those situations is a soft break inside code block.
1226
+ if (viewElement) {
1227
+ insertMarkerAsAttribute(viewElement, isStart, isBefore, conversionApi, data, viewMarkerData);
1228
+ return;
1229
+ }
1230
+ }
1231
+ const viewPosition = conversionApi.mapper.toViewPosition(modelPosition);
1232
+ insertMarkerAsElement(viewPosition, isStart, conversionApi, data, viewMarkerData);
1233
+ }
1234
+ /**
1235
+ * Helper function for `insertMarkerData()` that marks a marker boundary in the view as an attribute on a view element.
1236
+ */
1237
+ function insertMarkerAsAttribute(viewElement, isStart, isBefore, conversionApi, data, viewMarkerData) {
1238
+ const attributeName = `data-${viewMarkerData.group}-${isStart ? 'start' : 'end'}-${isBefore ? 'before' : 'after'}`;
1239
+ const markerNames = viewElement.hasAttribute(attributeName) ? viewElement.getAttribute(attributeName).split(',') : [];
1240
+ // Adding marker name at the beginning to have the same order in the attribute as there is with marker elements.
1241
+ markerNames.unshift(viewMarkerData.name);
1242
+ conversionApi.writer.setAttribute(attributeName, markerNames.join(','), viewElement);
1243
+ conversionApi.mapper.bindElementToMarker(viewElement, data.markerName);
1244
+ }
1245
+ /**
1246
+ * Helper function for `insertMarkerData()` that marks a marker boundary in the view as a separate view ui element.
1247
+ */
1248
+ function insertMarkerAsElement(position, isStart, conversionApi, data, viewMarkerData) {
1249
+ const viewElementName = `${viewMarkerData.group}-${isStart ? 'start' : 'end'}`;
1250
+ const attrs = viewMarkerData.name ? { 'name': viewMarkerData.name } : null;
1251
+ const viewElement = conversionApi.writer.createUIElement(viewElementName, attrs);
1252
+ conversionApi.writer.insert(position, viewElement);
1253
+ conversionApi.mapper.bindElementToMarker(viewElement, data.markerName);
1254
+ }
1255
+ /**
1256
+ * Function factory that creates a converter for removing a model marker data added by the {@link #insertMarkerData} converter.
1257
+ *
1258
+ * @returns Remove marker converter.
1259
+ */
1260
+ function removeMarkerData(viewCreator) {
1261
+ return (evt, data, conversionApi) => {
1262
+ const viewData = viewCreator(data.markerName, conversionApi);
1263
+ if (!viewData) {
1264
+ return;
1265
+ }
1266
+ const elements = conversionApi.mapper.markerNameToElements(data.markerName);
1267
+ if (!elements) {
1268
+ return;
1269
+ }
1270
+ for (const element of elements) {
1271
+ conversionApi.mapper.unbindElementFromMarkerName(element, data.markerName);
1272
+ if (element.is('containerElement')) {
1273
+ removeMarkerFromAttribute(`data-${viewData.group}-start-before`, element);
1274
+ removeMarkerFromAttribute(`data-${viewData.group}-start-after`, element);
1275
+ removeMarkerFromAttribute(`data-${viewData.group}-end-before`, element);
1276
+ removeMarkerFromAttribute(`data-${viewData.group}-end-after`, element);
1277
+ }
1278
+ else {
1279
+ conversionApi.writer.clear(conversionApi.writer.createRangeOn(element), element);
1280
+ }
1281
+ }
1282
+ conversionApi.writer.clearClonedElementsGroup(data.markerName);
1283
+ evt.stop();
1284
+ function removeMarkerFromAttribute(attributeName, element) {
1285
+ if (element.hasAttribute(attributeName)) {
1286
+ const markerNames = new Set(element.getAttribute(attributeName).split(','));
1287
+ markerNames.delete(viewData.name);
1288
+ if (markerNames.size == 0) {
1289
+ conversionApi.writer.removeAttribute(attributeName, element);
1290
+ }
1291
+ else {
1292
+ conversionApi.writer.setAttribute(attributeName, Array.from(markerNames).join(','), element);
1293
+ }
1294
+ }
1295
+ }
1296
+ };
1297
+ }
1298
+ /**
1299
+ * Function factory that creates a converter which converts the set/change/remove attribute changes from the model to the view.
1300
+ *
1301
+ * Attributes from the model are converted to the view element attributes in the view. You may provide a custom function to generate
1302
+ * a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view element
1303
+ * attributes on a one-to-one basis.
1304
+ *
1305
+ * *Note:** The provided attribute creator should always return the same `key` for a given attribute from the model.
1306
+ *
1307
+ * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
1308
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
1309
+ *
1310
+ * ```ts
1311
+ * modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( value, data ) => {
1312
+ * // Change attribute key from `customAttr` to `class` in the view.
1313
+ * const key = 'class';
1314
+ * let value = data.attributeNewValue;
1315
+ *
1316
+ * // Force attribute value to 'empty' if the model element is empty.
1317
+ * if ( data.item.childCount === 0 ) {
1318
+ * value = 'empty';
1319
+ * }
1320
+ *
1321
+ * // Return the key-value pair.
1322
+ * return { key, value };
1323
+ * } ) );
1324
+ * ```
1325
+ *
1326
+ * @param attributeCreator Function returning an object with two properties: `key` and `value`, which
1327
+ * represent the attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
1328
+ * The function is passed the model attribute value as the first parameter and additional data about the change as the second parameter.
1329
+ * @returns Set/change attribute converter.
1330
+ */
1331
+ function changeAttribute(attributeCreator) {
1332
+ return (evt, data, conversionApi) => {
1333
+ if (!conversionApi.consumable.test(data.item, evt.name)) {
1334
+ return;
1335
+ }
1336
+ const oldAttribute = attributeCreator(data.attributeOldValue, conversionApi, data);
1337
+ const newAttribute = attributeCreator(data.attributeNewValue, conversionApi, data);
1338
+ if (!oldAttribute && !newAttribute) {
1339
+ return;
1340
+ }
1341
+ conversionApi.consumable.consume(data.item, evt.name);
1342
+ const viewElement = conversionApi.mapper.toViewElement(data.item);
1343
+ const viewWriter = conversionApi.writer;
1344
+ // If model item cannot be mapped to a view element, it means item is not an `Element` instance but a `TextProxy` node.
1345
+ // Only elements can have attributes in a view so do not proceed for anything else (#1587).
1346
+ if (!viewElement) {
1347
+ /**
1348
+ * This error occurs when a {@link module:engine/model/textproxy~TextProxy text node's} attribute is to be downcasted
1349
+ * by an {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `Attribute to Attribute converter`}.
1350
+ * In most cases it is caused by converters misconfiguration when only "generic" converter is defined:
1351
+ *
1352
+ * ```ts
1353
+ * editor.conversion.for( 'downcast' ).attributeToAttribute( {
1354
+ * model: 'attribute-name',
1355
+ * view: 'attribute-name'
1356
+ * } ) );
1357
+ * ```
1358
+ *
1359
+ * and given attribute is used on text node, for example:
1360
+ *
1361
+ * ```ts
1362
+ * model.change( writer => {
1363
+ * writer.insertText( 'Foo', { 'attribute-name': 'bar' }, parent, 0 );
1364
+ * } );
1365
+ * ```
1366
+ *
1367
+ * In such cases, to convert the same attribute for both {@link module:engine/model/element~Element}
1368
+ * and {@link module:engine/model/textproxy~TextProxy `Text`} nodes, text specific
1369
+ * {@link module:engine/conversion/conversion~Conversion#attributeToElement `Attribute to Element converter`}
1370
+ * with higher {@link module:utils/priorities~PriorityString priority} must also be defined:
1371
+ *
1372
+ * ```ts
1373
+ * editor.conversion.for( 'downcast' ).attributeToElement( {
1374
+ * model: {
1375
+ * key: 'attribute-name',
1376
+ * name: '$text'
1377
+ * },
1378
+ * view: ( value, { writer } ) => {
1379
+ * return writer.createAttributeElement( 'span', { 'attribute-name': value } );
1380
+ * },
1381
+ * converterPriority: 'high'
1382
+ * } ) );
1383
+ * ```
1384
+ *
1385
+ * @error conversion-attribute-to-attribute-on-text
1386
+ */
1387
+ throw new CKEditorError('conversion-attribute-to-attribute-on-text', conversionApi.dispatcher, data);
1388
+ }
1389
+ // First remove the old attribute if there was one.
1390
+ if (data.attributeOldValue !== null && oldAttribute) {
1391
+ if (oldAttribute.key == 'class') {
1392
+ const classes = toArray(oldAttribute.value);
1393
+ for (const className of classes) {
1394
+ viewWriter.removeClass(className, viewElement);
1395
+ }
1396
+ }
1397
+ else if (oldAttribute.key == 'style') {
1398
+ const keys = Object.keys(oldAttribute.value);
1399
+ for (const key of keys) {
1400
+ viewWriter.removeStyle(key, viewElement);
1401
+ }
1402
+ }
1403
+ else {
1404
+ viewWriter.removeAttribute(oldAttribute.key, viewElement);
1405
+ }
1406
+ }
1407
+ // Then set the new attribute.
1408
+ if (data.attributeNewValue !== null && newAttribute) {
1409
+ if (newAttribute.key == 'class') {
1410
+ const classes = toArray(newAttribute.value);
1411
+ for (const className of classes) {
1412
+ viewWriter.addClass(className, viewElement);
1413
+ }
1414
+ }
1415
+ else if (newAttribute.key == 'style') {
1416
+ const keys = Object.keys(newAttribute.value);
1417
+ for (const key of keys) {
1418
+ viewWriter.setStyle(key, newAttribute.value[key], viewElement);
1419
+ }
1420
+ }
1421
+ else {
1422
+ viewWriter.setAttribute(newAttribute.key, newAttribute.value, viewElement);
1423
+ }
1424
+ }
1425
+ };
1426
+ }
1427
+ /**
1428
+ * Function factory that creates a converter which converts the text inside marker's range. The converter wraps the text with
1429
+ * {@link module:engine/view/attributeelement~AttributeElement} created from the provided descriptor.
1430
+ * See {link module:engine/conversion/downcasthelpers~createViewElementFromHighlightDescriptor}.
1431
+ *
1432
+ * It can also be used to convert the selection that is inside a marker. In that case, an empty attribute element will be
1433
+ * created and the selection will be put inside it.
1434
+ *
1435
+ * If the highlight descriptor does not provide the `priority` property, `10` will be used.
1436
+ *
1437
+ * If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
1438
+ *
1439
+ * This converter binds the created {@link module:engine/view/attributeelement~AttributeElement attribute elemens} with the marker name
1440
+ * using the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
1441
+ */
1442
+ function highlightText(highlightDescriptor) {
1443
+ return (evt, data, conversionApi) => {
1444
+ if (!data.item) {
1445
+ return;
1446
+ }
1447
+ if (!(data.item instanceof ModelSelection || data.item instanceof ModelDocumentSelection) && !data.item.is('$textProxy')) {
1448
+ return;
1449
+ }
1450
+ const descriptor = prepareDescriptor(highlightDescriptor, data, conversionApi);
1451
+ if (!descriptor) {
1452
+ return;
1453
+ }
1454
+ if (!conversionApi.consumable.consume(data.item, evt.name)) {
1455
+ return;
1456
+ }
1457
+ const viewWriter = conversionApi.writer;
1458
+ const viewElement = createViewElementFromHighlightDescriptor(viewWriter, descriptor);
1459
+ const viewSelection = viewWriter.document.selection;
1460
+ if (data.item instanceof ModelSelection || data.item instanceof ModelDocumentSelection) {
1461
+ viewWriter.wrap(viewSelection.getFirstRange(), viewElement);
1462
+ }
1463
+ else {
1464
+ const viewRange = conversionApi.mapper.toViewRange(data.range);
1465
+ const rangeAfterWrap = viewWriter.wrap(viewRange, viewElement);
1466
+ for (const element of rangeAfterWrap.getItems()) {
1467
+ if (element.is('attributeElement') && element.isSimilar(viewElement)) {
1468
+ conversionApi.mapper.bindElementToMarker(element, data.markerName);
1469
+ // One attribute element is enough, because all of them are bound together by the view writer.
1470
+ // Mapper uses this binding to get all the elements no matter how many of them are registered in the mapper.
1471
+ break;
1472
+ }
1473
+ }
1474
+ }
1475
+ };
1476
+ }
1477
+ /**
1478
+ * Converter function factory. It creates a function which applies the marker's highlight to an element inside the marker's range.
1479
+ *
1480
+ * The converter checks if an element has the `addHighlight` function stored as a
1481
+ * {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight.
1482
+ * In such case the converter will consume all element's children, assuming that they were handled by the element itself.
1483
+ *
1484
+ * When the `addHighlight` custom property is not present, the element is not converted in any special way.
1485
+ * This means that converters will proceed to convert the element's child nodes.
1486
+ *
1487
+ * If the highlight descriptor does not provide the `priority` property, `10` will be used.
1488
+ *
1489
+ * If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
1490
+ *
1491
+ * This converter binds altered {@link module:engine/view/containerelement~ContainerElement container elements} with the marker name using
1492
+ * the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
1493
+ */
1494
+ function highlightElement(highlightDescriptor) {
1495
+ return (evt, data, conversionApi) => {
1496
+ if (!data.item) {
1497
+ return;
1498
+ }
1499
+ if (!(data.item instanceof ModelElement)) {
1500
+ return;
1501
+ }
1502
+ const descriptor = prepareDescriptor(highlightDescriptor, data, conversionApi);
1503
+ if (!descriptor) {
1504
+ return;
1505
+ }
1506
+ if (!conversionApi.consumable.test(data.item, evt.name)) {
1507
+ return;
1508
+ }
1509
+ const viewElement = conversionApi.mapper.toViewElement(data.item);
1510
+ if (viewElement && viewElement.getCustomProperty('addHighlight')) {
1511
+ // Consume element itself.
1512
+ conversionApi.consumable.consume(data.item, evt.name);
1513
+ // Consume all children nodes.
1514
+ for (const value of ModelRange._createIn(data.item)) {
1515
+ conversionApi.consumable.consume(value.item, evt.name);
1516
+ }
1517
+ const addHighlightCallback = viewElement.getCustomProperty('addHighlight');
1518
+ addHighlightCallback(viewElement, descriptor, conversionApi.writer);
1519
+ conversionApi.mapper.bindElementToMarker(viewElement, data.markerName);
1520
+ }
1521
+ };
1522
+ }
1523
+ /**
1524
+ * Function factory that creates a converter which converts the removing model marker to the view.
1525
+ *
1526
+ * Both text nodes and elements are handled by this converter but they are handled a bit differently.
1527
+ *
1528
+ * Text nodes are unwrapped using the {@link module:engine/view/attributeelement~AttributeElement attribute element} created from the
1529
+ * provided highlight descriptor. See {link module:engine/conversion/downcasthelpers~HighlightDescriptor}.
1530
+ *
1531
+ * For elements, the converter checks if an element has the `removeHighlight` function stored as a
1532
+ * {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight.
1533
+ * In such case, the children of that element will not be converted.
1534
+ *
1535
+ * When `removeHighlight` is not present, the element is not converted in any special way.
1536
+ * The converter will proceed to convert the element's child nodes instead.
1537
+ *
1538
+ * If the highlight descriptor does not provide the `priority` property, `10` will be used.
1539
+ *
1540
+ * If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
1541
+ *
1542
+ * This converter unbinds elements from the marker name.
1543
+ */
1544
+ function removeHighlight(highlightDescriptor) {
1545
+ return (evt, data, conversionApi) => {
1546
+ // This conversion makes sense only for non-collapsed range.
1547
+ if (data.markerRange.isCollapsed) {
1548
+ return;
1549
+ }
1550
+ const descriptor = prepareDescriptor(highlightDescriptor, data, conversionApi);
1551
+ if (!descriptor) {
1552
+ return;
1553
+ }
1554
+ // View element that will be used to unwrap `AttributeElement`s.
1555
+ const viewHighlightElement = createViewElementFromHighlightDescriptor(conversionApi.writer, descriptor);
1556
+ // Get all elements bound with given marker name.
1557
+ const elements = conversionApi.mapper.markerNameToElements(data.markerName);
1558
+ if (!elements) {
1559
+ return;
1560
+ }
1561
+ for (const element of elements) {
1562
+ conversionApi.mapper.unbindElementFromMarkerName(element, data.markerName);
1563
+ if (element.is('attributeElement')) {
1564
+ conversionApi.writer.unwrap(conversionApi.writer.createRangeOn(element), viewHighlightElement);
1565
+ }
1566
+ else {
1567
+ // if element.is( 'containerElement' ).
1568
+ const removeHighlightCallback = element.getCustomProperty('removeHighlight');
1569
+ removeHighlightCallback(element, descriptor.id, conversionApi.writer);
1570
+ }
1571
+ }
1572
+ conversionApi.writer.clearClonedElementsGroup(data.markerName);
1573
+ evt.stop();
1574
+ };
1575
+ }
1576
+ /**
1577
+ * Model element to view element conversion helper.
1578
+ *
1579
+ * See {@link ~DowncastHelpers#elementToElement `.elementToElement()` downcast helper} for examples and config params description.
1580
+ *
1581
+ * @param config Conversion configuration.
1582
+ * @param config.model The description or a name of the model element to convert.
1583
+ * @param config.model.attributes List of attributes triggering element reconversion.
1584
+ * @param config.model.children Should reconvert element if the list of model child nodes changed.
1585
+ * @returns Conversion helper.
1586
+ */
1587
+ function downcastElementToElement(config) {
1588
+ const model = normalizeModelElementConfig(config.model);
1589
+ const view = normalizeToElementConfig(config.view, 'container');
1590
+ // Trigger reconversion on children list change if element is a subject to any reconversion.
1591
+ // This is required to be able to trigger Differ#refreshItem() on a direct child of the reconverted element.
1592
+ if (model.attributes.length) {
1593
+ model.children = true;
1594
+ }
1595
+ return (dispatcher) => {
1596
+ dispatcher.on(`insert:${model.name}`, insertElement(view, createConsumer(model)), { priority: config.converterPriority || 'normal' });
1597
+ if (model.children || model.attributes.length) {
1598
+ dispatcher.on('reduceChanges', createChangeReducer(model), { priority: 'low' });
1599
+ }
1600
+ };
1601
+ }
1602
+ /**
1603
+ * Model element to view structure conversion helper.
1604
+ *
1605
+ * See {@link ~DowncastHelpers#elementToStructure `.elementToStructure()` downcast helper} for examples and config params description.
1606
+ *
1607
+ * @param config Conversion configuration.
1608
+ * @returns Conversion helper.
1609
+ */
1610
+ function downcastElementToStructure(config) {
1611
+ const model = normalizeModelElementConfig(config.model);
1612
+ const view = normalizeToElementConfig(config.view, 'container');
1613
+ // Trigger reconversion on children list change because it always needs to use slots to put children in proper places.
1614
+ // This is required to be able to trigger Differ#refreshItem() on a direct child of the reconverted element.
1615
+ model.children = true;
1616
+ return (dispatcher) => {
1617
+ if (dispatcher._conversionApi.schema.checkChild(model.name, '$text')) {
1618
+ /**
1619
+ * This error occurs when a {@link module:engine/model/element~Element model element} is downcasted
1620
+ * via {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToStructure} helper but the element was
1621
+ * allowed to host `$text` by the {@link module:engine/model/schema~Schema model schema}.
1622
+ *
1623
+ * For instance, this may be the result of `myElement` allowing the content of
1624
+ * {@glink framework/deep-dive/schema#generic-items `$block`} in its schema definition:
1625
+ *
1626
+ * ```ts
1627
+ * // Element definition in schema.
1628
+ * schema.register( 'myElement', {
1629
+ * allowContentOf: '$block',
1630
+ *
1631
+ * // ...
1632
+ * } );
1633
+ *
1634
+ * // ...
1635
+ *
1636
+ * // Conversion of myElement with the use of elementToStructure().
1637
+ * editor.conversion.for( 'downcast' ).elementToStructure( {
1638
+ * model: 'myElement',
1639
+ * view: ( modelElement, { writer } ) => {
1640
+ * // ...
1641
+ * }
1642
+ * } );
1643
+ * ```
1644
+ *
1645
+ * In such case, {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} helper
1646
+ * can be used instead to get around this problem:
1647
+ *
1648
+ * ```ts
1649
+ * editor.conversion.for( 'downcast' ).elementToElement( {
1650
+ * model: 'myElement',
1651
+ * view: ( modelElement, { writer } ) => {
1652
+ * // ...
1653
+ * }
1654
+ * } );
1655
+ * ```
1656
+ *
1657
+ * @error conversion-element-to-structure-disallowed-text
1658
+ * @param {String} elementName The name of the element the structure is to be created for.
1659
+ */
1660
+ throw new CKEditorError('conversion-element-to-structure-disallowed-text', dispatcher, { elementName: model.name });
1661
+ }
1662
+ dispatcher.on(`insert:${model.name}`, insertStructure(view, createConsumer(model)), { priority: config.converterPriority || 'normal' });
1663
+ dispatcher.on('reduceChanges', createChangeReducer(model), { priority: 'low' });
1664
+ };
1665
+ }
1666
+ /**
1667
+ * Model attribute to view element conversion helper.
1668
+ *
1669
+ * See {@link ~DowncastHelpers#attributeToElement `.attributeToElement()` downcast helper} for examples.
1670
+ *
1671
+ * @param config Conversion configuration.
1672
+ * @param config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
1673
+ * of `String`s with possible values if the model attribute is an enumerable.
1674
+ * @param config.view A view element definition or a function that takes the model attribute value and
1675
+ * {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} as parameters and returns a view attribute element.
1676
+ * If `config.model.values` is given, `config.view` should be an object assigning values from `config.model.values` to view element
1677
+ * definitions or functions.
1678
+ * @param config.converterPriority Converter priority.
1679
+ * @returns Conversion helper.
1680
+ */
1681
+ function downcastAttributeToElement(config) {
1682
+ config = cloneDeep(config);
1683
+ let model = config.model;
1684
+ if (typeof model == 'string') {
1685
+ model = { key: model };
1686
+ }
1687
+ let eventName = `attribute:${model.key}`;
1688
+ if (model.name) {
1689
+ eventName += ':' + model.name;
1690
+ }
1691
+ if (model.values) {
1692
+ for (const modelValue of model.values) {
1693
+ config.view[modelValue] = normalizeToElementConfig(config.view[modelValue], 'attribute');
1694
+ }
1695
+ }
1696
+ else {
1697
+ config.view = normalizeToElementConfig(config.view, 'attribute');
1698
+ }
1699
+ const elementCreator = getFromAttributeCreator(config);
1700
+ return (dispatcher) => {
1701
+ dispatcher.on(eventName, wrap(elementCreator), { priority: config.converterPriority || 'normal' });
1702
+ };
1703
+ }
1704
+ /**
1705
+ * Model attribute to view attribute conversion helper.
1706
+ *
1707
+ * See {@link ~DowncastHelpers#attributeToAttribute `.attributeToAttribute()` downcast helper} for examples.
1708
+ *
1709
+ * @param config Conversion configuration.
1710
+ * @param config.model The key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
1711
+ * the attribute key, possible values and, optionally, an element name to convert from.
1712
+ * @param config.view A view attribute key, or a `{ key, value }` object or a function that takes the model attribute value and returns
1713
+ * a `{ key, value }` object.
1714
+ * If `key` is `'class'`, `value` can be a `String` or an array of `String`s. If `key` is `'style'`, `value` is an object with
1715
+ * key-value pairs. In other cases, `value` is a `String`.
1716
+ * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
1717
+ * `{ key, value }` objects or a functions.
1718
+ * @param config.converterPriority Converter priority.
1719
+ * @returns Conversion helper.
1720
+ */
1721
+ function downcastAttributeToAttribute(config) {
1722
+ config = cloneDeep(config);
1723
+ let model = config.model;
1724
+ if (typeof model == 'string') {
1725
+ model = { key: model };
1726
+ }
1727
+ let eventName = `attribute:${model.key}`;
1728
+ if (model.name) {
1729
+ eventName += ':' + model.name;
1730
+ }
1731
+ if (model.values) {
1732
+ for (const modelValue of model.values) {
1733
+ config.view[modelValue] = normalizeToAttributeConfig(config.view[modelValue]);
1734
+ }
1735
+ }
1736
+ else {
1737
+ config.view = normalizeToAttributeConfig(config.view);
1738
+ }
1739
+ const elementCreator = getFromAttributeCreator(config);
1740
+ return (dispatcher) => {
1741
+ dispatcher.on(eventName, changeAttribute(elementCreator), { priority: config.converterPriority || 'normal' });
1742
+ };
1743
+ }
1744
+ /**
1745
+ * Model marker to view element conversion helper.
1746
+ *
1747
+ * See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
1748
+ *
1749
+ * @param config Conversion configuration.
1750
+ * @param config.model The name of the model marker (or model marker group) to convert.
1751
+ * @param config.view A view element definition or a function that takes the model marker data as a parameter and returns a view UI element.
1752
+ * @param config.converterPriority Converter priority.
1753
+ * @returns Conversion helper.
1754
+ */
1755
+ function downcastMarkerToElement(config) {
1756
+ const view = normalizeToElementConfig(config.view, 'ui');
1757
+ return (dispatcher) => {
1758
+ dispatcher.on(`addMarker:${config.model}`, insertUIElement(view), { priority: config.converterPriority || 'normal' });
1759
+ dispatcher.on(`removeMarker:${config.model}`, removeUIElement(), { priority: config.converterPriority || 'normal' });
1760
+ };
1761
+ }
1762
+ /**
1763
+ * Model marker to view data conversion helper.
1764
+ *
1765
+ * See {@link ~DowncastHelpers#markerToData `markerToData()` downcast helper} to learn more.
1766
+ *
1767
+ * @returns Conversion helper.
1768
+ */
1769
+ function downcastMarkerToData(config) {
1770
+ config = cloneDeep(config);
1771
+ const group = config.model;
1772
+ let view = config.view;
1773
+ // Default conversion.
1774
+ if (!view) {
1775
+ view = markerName => ({
1776
+ group,
1777
+ name: markerName.substr(config.model.length + 1)
1778
+ });
1779
+ }
1780
+ return (dispatcher) => {
1781
+ dispatcher.on(`addMarker:${group}`, insertMarkerData(view), { priority: config.converterPriority || 'normal' });
1782
+ dispatcher.on(`removeMarker:${group}`, removeMarkerData(view), { priority: config.converterPriority || 'normal' });
1783
+ };
1784
+ }
1785
+ /**
1786
+ * Model marker to highlight conversion helper.
1787
+ *
1788
+ * See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
1789
+ *
1790
+ * @param config Conversion configuration.
1791
+ * @param config.model The name of the model marker (or model marker group) to convert.
1792
+ * @param config.view A highlight descriptor that will be used for highlighting or a function that takes
1793
+ * the model marker data as a parameter and returns a highlight descriptor.
1794
+ * @param config.converterPriority Converter priority.
1795
+ * @returns Conversion helper.
1796
+ */
1797
+ function downcastMarkerToHighlight(config) {
1798
+ return (dispatcher) => {
1799
+ dispatcher.on(`addMarker:${config.model}`, highlightText(config.view), { priority: config.converterPriority || 'normal' });
1800
+ dispatcher.on(`addMarker:${config.model}`, highlightElement(config.view), { priority: config.converterPriority || 'normal' });
1801
+ dispatcher.on(`removeMarker:${config.model}`, removeHighlight(config.view), { priority: config.converterPriority || 'normal' });
1802
+ };
1803
+ }
1804
+ /**
1805
+ * Takes `config.model`, and converts it to an object with normalized structure.
1806
+ *
1807
+ * @param model Model configuration or element name.
1808
+ */
1809
+ function normalizeModelElementConfig(model) {
1810
+ if (typeof model == 'string') {
1811
+ model = { name: model };
1812
+ }
1813
+ // List of attributes that should trigger reconversion.
1814
+ if (!model.attributes) {
1815
+ model.attributes = [];
1816
+ }
1817
+ else if (!Array.isArray(model.attributes)) {
1818
+ model.attributes = [model.attributes];
1819
+ }
1820
+ // Whether a children insertion/deletion should trigger reconversion.
1821
+ model.children = !!model.children;
1822
+ return model;
1823
+ }
1824
+ /**
1825
+ * Takes `config.view`, and if it is an {@link module:engine/view/elementdefinition~ElementDefinition}, converts it
1826
+ * to a function (because lower level converters accept only element creator functions).
1827
+ *
1828
+ * @param view View configuration.
1829
+ * @param viewElementType View element type to create.
1830
+ * @returns Element creator function to use in lower level converters.
1831
+ */
1832
+ function normalizeToElementConfig(view, viewElementType) {
1833
+ if (typeof view == 'function') {
1834
+ // If `view` is already a function, don't do anything.
1835
+ return view;
1836
+ }
1837
+ return ((modelData, conversionApi) => createViewElementFromDefinition(view, conversionApi, viewElementType));
1838
+ }
1839
+ /**
1840
+ * Creates a view element instance from the provided {@link module:engine/view/elementdefinition~ElementDefinition} and class.
1841
+ */
1842
+ function createViewElementFromDefinition(viewElementDefinition, conversionApi, viewElementType) {
1843
+ if (typeof viewElementDefinition == 'string') {
1844
+ // If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property.
1845
+ viewElementDefinition = { name: viewElementDefinition };
1846
+ }
1847
+ let element;
1848
+ const viewWriter = conversionApi.writer;
1849
+ const attributes = Object.assign({}, viewElementDefinition.attributes);
1850
+ if (viewElementType == 'container') {
1851
+ element = viewWriter.createContainerElement(viewElementDefinition.name, attributes);
1852
+ }
1853
+ else if (viewElementType == 'attribute') {
1854
+ const options = {
1855
+ priority: viewElementDefinition.priority || ViewAttributeElement.DEFAULT_PRIORITY
1856
+ };
1857
+ element = viewWriter.createAttributeElement(viewElementDefinition.name, attributes, options);
1858
+ }
1859
+ else {
1860
+ // 'ui'.
1861
+ element = viewWriter.createUIElement(viewElementDefinition.name, attributes);
1862
+ }
1863
+ if (viewElementDefinition.styles) {
1864
+ const keys = Object.keys(viewElementDefinition.styles);
1865
+ for (const key of keys) {
1866
+ viewWriter.setStyle(key, viewElementDefinition.styles[key], element);
1867
+ }
1868
+ }
1869
+ if (viewElementDefinition.classes) {
1870
+ const classes = viewElementDefinition.classes;
1871
+ if (typeof classes == 'string') {
1872
+ viewWriter.addClass(classes, element);
1873
+ }
1874
+ else {
1875
+ for (const className of classes) {
1876
+ viewWriter.addClass(className, element);
1877
+ }
1878
+ }
1879
+ }
1880
+ return element;
1881
+ }
1882
+ function getFromAttributeCreator(config) {
1883
+ if (config.model.values) {
1884
+ return ((modelAttributeValue, conversionApi, data) => {
1885
+ const view = config.view[modelAttributeValue];
1886
+ if (view) {
1887
+ return view(modelAttributeValue, conversionApi, data);
1888
+ }
1889
+ return null;
1890
+ });
1891
+ }
1892
+ else {
1893
+ return config.view;
1894
+ }
1895
+ }
1896
+ /**
1897
+ * Takes the configuration, adds default parameters if they do not exist and normalizes other parameters to be used in downcast converters
1898
+ * for generating a view attribute.
1899
+ *
1900
+ * @param view View configuration.
1901
+ */
1902
+ function normalizeToAttributeConfig(view) {
1903
+ if (typeof view == 'string') {
1904
+ return modelAttributeValue => ({ key: view, value: modelAttributeValue });
1905
+ }
1906
+ else if (typeof view == 'object') {
1907
+ // { key, value, ... }
1908
+ if (view.value) {
1909
+ return () => view;
1910
+ }
1911
+ // { key, ... }
1912
+ else {
1913
+ return modelAttributeValue => ({ key: view.key, value: modelAttributeValue });
1914
+ }
1915
+ }
1916
+ else {
1917
+ // function.
1918
+ return view;
1919
+ }
1920
+ }
1921
+ /**
1922
+ * Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter.
1923
+ */
1924
+ function prepareDescriptor(highlightDescriptor, data, conversionApi) {
1925
+ // If passed descriptor is a creator function, call it. If not, just use passed value.
1926
+ const descriptor = typeof highlightDescriptor == 'function' ?
1927
+ highlightDescriptor(data, conversionApi) :
1928
+ highlightDescriptor;
1929
+ if (!descriptor) {
1930
+ return null;
1931
+ }
1932
+ // Apply default descriptor priority.
1933
+ if (!descriptor.priority) {
1934
+ descriptor.priority = 10;
1935
+ }
1936
+ // Default descriptor id is marker name.
1937
+ if (!descriptor.id) {
1938
+ descriptor.id = data.markerName;
1939
+ }
1940
+ return descriptor;
1941
+ }
1942
+ /**
1943
+ * Creates a function that checks a single differ diff item whether it should trigger reconversion.
1944
+ *
1945
+ * @param model A normalized `config.model` converter configuration.
1946
+ * @param model.name The name of element.
1947
+ * @param model.attributes The list of attribute names that should trigger reconversion.
1948
+ * @param model.children Whether the child list change should trigger reconversion.
1949
+ */
1950
+ function createChangeReducerCallback(model) {
1951
+ return (node, change) => {
1952
+ if (!node.is('element', model.name)) {
1953
+ return false;
1954
+ }
1955
+ if (change.type == 'attribute') {
1956
+ if (model.attributes.includes(change.attributeKey)) {
1957
+ return true;
1958
+ }
1959
+ }
1960
+ else {
1961
+ /* istanbul ignore else: This is always true because otherwise it would not register a reducer callback. -- @preserve */
1962
+ if (model.children) {
1963
+ return true;
1964
+ }
1965
+ }
1966
+ return false;
1967
+ };
1968
+ }
1969
+ /**
1970
+ * Creates a `reduceChanges` event handler for reconversion.
1971
+ *
1972
+ * @param model A normalized `config.model` converter configuration.
1973
+ * @param model.name The name of element.
1974
+ * @param model.attributes The list of attribute names that should trigger reconversion.
1975
+ * @param model.children Whether the child list change should trigger reconversion.
1976
+ */
1977
+ function createChangeReducer(model) {
1978
+ const shouldReplace = createChangeReducerCallback(model);
1979
+ return (evt, data) => {
1980
+ const reducedChanges = [];
1981
+ if (!data.reconvertedElements) {
1982
+ data.reconvertedElements = new Set();
1983
+ }
1984
+ for (const change of data.changes) {
1985
+ // For attribute use node affected by the change.
1986
+ // For insert or remove use parent element because we need to check if it's added/removed child.
1987
+ const node = change.type == 'attribute' ? change.range.start.nodeAfter : change.position.parent;
1988
+ if (!node || !shouldReplace(node, change)) {
1989
+ reducedChanges.push(change);
1990
+ continue;
1991
+ }
1992
+ // If it's already marked for reconversion, so skip this change, otherwise add the diff items.
1993
+ if (!data.reconvertedElements.has(node)) {
1994
+ data.reconvertedElements.add(node);
1995
+ const position = ModelPosition._createBefore(node);
1996
+ let changeIndex = reducedChanges.length;
1997
+ // We need to insert remove+reinsert before any other change on and inside the re-converted element.
1998
+ // This is important because otherwise we would remove element that had already been modified by the previous change.
1999
+ // Note that there could be some element removed before the re-converted element, so we must not break this behavior.
2000
+ for (let i = reducedChanges.length - 1; i >= 0; i--) {
2001
+ const change = reducedChanges[i];
2002
+ const changePosition = change.type == 'attribute' ? change.range.start : change.position;
2003
+ const positionRelation = changePosition.compareWith(position);
2004
+ if (positionRelation == 'before' || change.type == 'remove' && positionRelation == 'same') {
2005
+ break;
2006
+ }
2007
+ changeIndex = i;
2008
+ }
2009
+ reducedChanges.splice(changeIndex, 0, {
2010
+ type: 'remove',
2011
+ name: node.name,
2012
+ position,
2013
+ length: 1
2014
+ }, {
2015
+ type: 'reinsert',
2016
+ name: node.name,
2017
+ position,
2018
+ length: 1
2019
+ });
2020
+ }
2021
+ }
2022
+ data.changes = reducedChanges;
2023
+ };
2024
+ }
2025
+ /**
2026
+ * Creates a function that checks if an element and its watched attributes can be consumed and consumes them.
2027
+ *
2028
+ * @param model A normalized `config.model` converter configuration.
2029
+ * @param model.name The name of element.
2030
+ * @param model.attributes The list of attribute names that should trigger reconversion.
2031
+ * @param model.children Whether the child list change should trigger reconversion.
2032
+ */
2033
+ function createConsumer(model) {
2034
+ return (node, consumable, options = {}) => {
2035
+ const events = ['insert'];
2036
+ // Collect all set attributes that are triggering conversion.
2037
+ for (const attributeName of model.attributes) {
2038
+ if (node.hasAttribute(attributeName)) {
2039
+ events.push(`attribute:${attributeName}`);
2040
+ }
2041
+ }
2042
+ if (!events.every(event => consumable.test(node, event))) {
2043
+ return false;
2044
+ }
2045
+ if (!options.preflight) {
2046
+ events.forEach(event => consumable.consume(node, event));
2047
+ }
2048
+ return true;
2049
+ };
2050
+ }
2051
+ /**
2052
+ * Creates a function that create view slots.
2053
+ *
2054
+ * @returns Function exposed by writer as createSlot().
2055
+ */
2056
+ function createSlotFactory(element, slotsMap, conversionApi) {
2057
+ return (writer, modeOrFilter) => {
2058
+ const slot = writer.createContainerElement('$slot');
2059
+ let children = null;
2060
+ if (modeOrFilter === 'children') {
2061
+ children = Array.from(element.getChildren());
2062
+ }
2063
+ else if (typeof modeOrFilter == 'function') {
2064
+ children = Array.from(element.getChildren()).filter(element => modeOrFilter(element));
2065
+ }
2066
+ else {
2067
+ /**
2068
+ * Unknown slot mode was provided to `writer.createSlot()` in downcast converter.
2069
+ *
2070
+ * @error conversion-slot-mode-unknown
2071
+ */
2072
+ throw new CKEditorError('conversion-slot-mode-unknown', conversionApi.dispatcher, { modeOrFilter });
2073
+ }
2074
+ slotsMap.set(slot, children);
2075
+ return slot;
2076
+ };
2077
+ }
2078
+ /**
2079
+ * Checks if all children are covered by slots and there is no child that landed in multiple slots.
2080
+ */
2081
+ function validateSlotsChildren(element, slotsMap, conversionApi) {
2082
+ const childrenInSlots = Array.from(slotsMap.values()).flat();
2083
+ const uniqueChildrenInSlots = new Set(childrenInSlots);
2084
+ if (uniqueChildrenInSlots.size != childrenInSlots.length) {
2085
+ /**
2086
+ * Filters provided to `writer.createSlot()` overlap (at least two filters accept the same child element).
2087
+ *
2088
+ * @error conversion-slot-filter-overlap
2089
+ * @param {module:engine/model/element~Element} element The element of which children would not be properly
2090
+ * allocated to multiple slots.
2091
+ */
2092
+ throw new CKEditorError('conversion-slot-filter-overlap', conversionApi.dispatcher, { element });
2093
+ }
2094
+ if (uniqueChildrenInSlots.size != element.childCount) {
2095
+ /**
2096
+ * Filters provided to `writer.createSlot()` are incomplete and exclude at least one children element (one of
2097
+ * the children elements would not be assigned to any of the slots).
2098
+ *
2099
+ * @error conversion-slot-filter-incomplete
2100
+ * @param {module:engine/model/element~Element} element The element of which children would not be properly
2101
+ * allocated to multiple slots.
2102
+ */
2103
+ throw new CKEditorError('conversion-slot-filter-incomplete', conversionApi.dispatcher, { element });
2104
+ }
2105
+ }
2106
+ /**
2107
+ * Fill slots with appropriate view elements.
2108
+ */
2109
+ function fillSlots(viewElement, slotsMap, conversionApi, options) {
2110
+ // Set temporary position mapping to redirect child view elements into a proper slots.
2111
+ conversionApi.mapper.on('modelToViewPosition', toViewPositionMapping, { priority: 'highest' });
2112
+ let currentSlot = null;
2113
+ let currentSlotNodes = null;
2114
+ // Fill slots with nested view nodes.
2115
+ for ([currentSlot, currentSlotNodes] of slotsMap) {
2116
+ reinsertOrConvertNodes(viewElement, currentSlotNodes, conversionApi, options);
2117
+ conversionApi.writer.move(conversionApi.writer.createRangeIn(currentSlot), conversionApi.writer.createPositionBefore(currentSlot));
2118
+ conversionApi.writer.remove(currentSlot);
2119
+ }
2120
+ conversionApi.mapper.off('modelToViewPosition', toViewPositionMapping);
2121
+ function toViewPositionMapping(evt, data) {
2122
+ const element = data.modelPosition.nodeAfter;
2123
+ // Find the proper offset within the slot.
2124
+ const index = currentSlotNodes.indexOf(element);
2125
+ if (index < 0) {
2126
+ return;
2127
+ }
2128
+ data.viewPosition = data.mapper.findPositionIn(currentSlot, index);
2129
+ }
2130
+ }
2131
+ /**
2132
+ * Inserts view representation of `nodes` into the `viewElement` either by bringing back just removed view nodes
2133
+ * or by triggering conversion for them.
2134
+ */
2135
+ function reinsertOrConvertNodes(viewElement, modelNodes, conversionApi, options) {
2136
+ // Fill with nested view nodes.
2137
+ for (const modelChildNode of modelNodes) {
2138
+ // Try reinserting the view node for the specified model node...
2139
+ if (!reinsertNode(viewElement.root, modelChildNode, conversionApi, options)) {
2140
+ // ...or else convert the model element to the view.
2141
+ conversionApi.convertItem(modelChildNode);
2142
+ }
2143
+ }
2144
+ }
2145
+ /**
2146
+ * Checks if the view for the given model element could be reused and reinserts it to the view.
2147
+ *
2148
+ * @returns `false` if view element can't be reused.
2149
+ */
2150
+ function reinsertNode(viewRoot, modelNode, conversionApi, options) {
2151
+ const { writer, mapper } = conversionApi;
2152
+ // Don't reinsert if this is not a reconversion...
2153
+ if (!options.reconversion) {
2154
+ return false;
2155
+ }
2156
+ const viewChildNode = mapper.toViewElement(modelNode);
2157
+ // ...or there is no view to reinsert or it was already inserted to the view structure...
2158
+ if (!viewChildNode || viewChildNode.root == viewRoot) {
2159
+ return false;
2160
+ }
2161
+ // ...or it was strictly marked as not to be reused.
2162
+ if (!conversionApi.canReuseView(viewChildNode)) {
2163
+ return false;
2164
+ }
2165
+ // Otherwise reinsert the view node.
2166
+ writer.move(writer.createRangeOn(viewChildNode), mapper.toViewPosition(ModelPosition._createBefore(modelNode)));
2167
+ return true;
2168
+ }
2169
+ /**
2170
+ * The default consumer for insert events.
2171
+ *
2172
+ * @param item Model item.
2173
+ * @param consumable The model consumable.
2174
+ * @param options.preflight Whether should consume or just check if can be consumed.
2175
+ */
2176
+ function defaultConsumer(item, consumable, { preflight } = {}) {
2177
+ if (preflight) {
2178
+ return consumable.test(item, 'insert');
2179
+ }
2180
+ else {
2181
+ return consumable.consume(item, 'insert');
2182
+ }
2183
+ }