@ckeditor/ckeditor5-engine 40.0.0 → 40.2.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 (243) hide show
  1. package/CHANGELOG.md +39 -39
  2. package/LICENSE.md +3 -3
  3. package/package.json +2 -2
  4. package/src/controller/datacontroller.d.ts +334 -334
  5. package/src/controller/datacontroller.js +481 -481
  6. package/src/controller/editingcontroller.d.ts +98 -98
  7. package/src/controller/editingcontroller.js +191 -191
  8. package/src/conversion/conversion.d.ts +478 -478
  9. package/src/conversion/conversion.js +601 -601
  10. package/src/conversion/conversionhelpers.d.ts +26 -26
  11. package/src/conversion/conversionhelpers.js +32 -32
  12. package/src/conversion/downcastdispatcher.d.ts +562 -562
  13. package/src/conversion/downcastdispatcher.js +548 -547
  14. package/src/conversion/downcasthelpers.d.ts +1226 -1226
  15. package/src/conversion/downcasthelpers.js +2178 -2183
  16. package/src/conversion/mapper.d.ts +503 -503
  17. package/src/conversion/mapper.js +536 -536
  18. package/src/conversion/modelconsumable.d.ts +201 -201
  19. package/src/conversion/modelconsumable.js +333 -333
  20. package/src/conversion/upcastdispatcher.d.ts +492 -492
  21. package/src/conversion/upcastdispatcher.js +460 -460
  22. package/src/conversion/upcasthelpers.d.ts +499 -499
  23. package/src/conversion/upcasthelpers.js +950 -950
  24. package/src/conversion/viewconsumable.d.ts +369 -369
  25. package/src/conversion/viewconsumable.js +536 -532
  26. package/src/dataprocessor/basichtmlwriter.d.ts +18 -18
  27. package/src/dataprocessor/basichtmlwriter.js +20 -19
  28. package/src/dataprocessor/dataprocessor.d.ts +61 -61
  29. package/src/dataprocessor/dataprocessor.js +5 -5
  30. package/src/dataprocessor/htmldataprocessor.d.ts +76 -76
  31. package/src/dataprocessor/htmldataprocessor.js +96 -96
  32. package/src/dataprocessor/htmlwriter.d.ts +16 -16
  33. package/src/dataprocessor/htmlwriter.js +5 -5
  34. package/src/dataprocessor/xmldataprocessor.d.ts +90 -90
  35. package/src/dataprocessor/xmldataprocessor.js +108 -108
  36. package/src/dev-utils/model.d.ts +124 -124
  37. package/src/dev-utils/model.js +395 -395
  38. package/src/dev-utils/operationreplayer.d.ts +51 -51
  39. package/src/dev-utils/operationreplayer.js +112 -112
  40. package/src/dev-utils/utils.d.ts +37 -37
  41. package/src/dev-utils/utils.js +73 -73
  42. package/src/dev-utils/view.d.ts +319 -319
  43. package/src/dev-utils/view.js +967 -967
  44. package/src/index.d.ts +114 -114
  45. package/src/index.js +78 -78
  46. package/src/model/batch.d.ts +106 -106
  47. package/src/model/batch.js +96 -96
  48. package/src/model/differ.d.ts +387 -387
  49. package/src/model/differ.js +1149 -1149
  50. package/src/model/document.d.ts +272 -272
  51. package/src/model/document.js +360 -361
  52. package/src/model/documentfragment.d.ts +200 -200
  53. package/src/model/documentfragment.js +306 -306
  54. package/src/model/documentselection.d.ts +420 -420
  55. package/src/model/documentselection.js +993 -993
  56. package/src/model/element.d.ts +165 -165
  57. package/src/model/element.js +281 -281
  58. package/src/model/history.d.ts +114 -114
  59. package/src/model/history.js +207 -207
  60. package/src/model/item.d.ts +14 -14
  61. package/src/model/item.js +5 -5
  62. package/src/model/liveposition.d.ts +77 -77
  63. package/src/model/liveposition.js +93 -93
  64. package/src/model/liverange.d.ts +102 -102
  65. package/src/model/liverange.js +120 -120
  66. package/src/model/markercollection.d.ts +335 -335
  67. package/src/model/markercollection.js +403 -403
  68. package/src/model/model.d.ts +919 -919
  69. package/src/model/model.js +842 -842
  70. package/src/model/node.d.ts +256 -256
  71. package/src/model/node.js +375 -375
  72. package/src/model/nodelist.d.ts +91 -91
  73. package/src/model/nodelist.js +163 -163
  74. package/src/model/operation/attributeoperation.d.ts +103 -103
  75. package/src/model/operation/attributeoperation.js +148 -148
  76. package/src/model/operation/detachoperation.d.ts +60 -60
  77. package/src/model/operation/detachoperation.js +77 -77
  78. package/src/model/operation/insertoperation.d.ts +90 -90
  79. package/src/model/operation/insertoperation.js +135 -135
  80. package/src/model/operation/markeroperation.d.ts +91 -91
  81. package/src/model/operation/markeroperation.js +107 -107
  82. package/src/model/operation/mergeoperation.d.ts +100 -100
  83. package/src/model/operation/mergeoperation.js +167 -167
  84. package/src/model/operation/moveoperation.d.ts +96 -96
  85. package/src/model/operation/moveoperation.js +164 -164
  86. package/src/model/operation/nooperation.d.ts +38 -38
  87. package/src/model/operation/nooperation.js +48 -48
  88. package/src/model/operation/operation.d.ts +96 -96
  89. package/src/model/operation/operation.js +59 -62
  90. package/src/model/operation/operationfactory.d.ts +18 -18
  91. package/src/model/operation/operationfactory.js +44 -44
  92. package/src/model/operation/renameoperation.d.ts +83 -83
  93. package/src/model/operation/renameoperation.js +115 -115
  94. package/src/model/operation/rootattributeoperation.d.ts +98 -98
  95. package/src/model/operation/rootattributeoperation.js +155 -155
  96. package/src/model/operation/rootoperation.d.ts +76 -76
  97. package/src/model/operation/rootoperation.js +90 -90
  98. package/src/model/operation/splitoperation.d.ts +109 -109
  99. package/src/model/operation/splitoperation.js +194 -194
  100. package/src/model/operation/transform.d.ts +100 -100
  101. package/src/model/operation/transform.js +1985 -1985
  102. package/src/model/operation/utils.d.ts +71 -71
  103. package/src/model/operation/utils.js +217 -213
  104. package/src/model/position.d.ts +539 -539
  105. package/src/model/position.js +979 -979
  106. package/src/model/range.d.ts +458 -458
  107. package/src/model/range.js +875 -875
  108. package/src/model/rootelement.d.ts +60 -60
  109. package/src/model/rootelement.js +74 -74
  110. package/src/model/schema.d.ts +1186 -1186
  111. package/src/model/schema.js +1242 -1242
  112. package/src/model/selection.d.ts +482 -482
  113. package/src/model/selection.js +789 -789
  114. package/src/model/text.d.ts +66 -66
  115. package/src/model/text.js +85 -85
  116. package/src/model/textproxy.d.ts +144 -144
  117. package/src/model/textproxy.js +189 -189
  118. package/src/model/treewalker.d.ts +186 -186
  119. package/src/model/treewalker.js +244 -244
  120. package/src/model/typecheckable.d.ts +285 -285
  121. package/src/model/typecheckable.js +16 -16
  122. package/src/model/utils/autoparagraphing.d.ts +37 -37
  123. package/src/model/utils/autoparagraphing.js +63 -63
  124. package/src/model/utils/deletecontent.d.ts +58 -58
  125. package/src/model/utils/deletecontent.js +488 -488
  126. package/src/model/utils/findoptimalinsertionrange.d.ts +32 -32
  127. package/src/model/utils/findoptimalinsertionrange.js +57 -57
  128. package/src/model/utils/getselectedcontent.d.ts +30 -30
  129. package/src/model/utils/getselectedcontent.js +125 -125
  130. package/src/model/utils/insertcontent.d.ts +46 -46
  131. package/src/model/utils/insertcontent.js +705 -705
  132. package/src/model/utils/insertobject.d.ts +44 -44
  133. package/src/model/utils/insertobject.js +139 -139
  134. package/src/model/utils/modifyselection.d.ts +48 -48
  135. package/src/model/utils/modifyselection.js +186 -186
  136. package/src/model/utils/selection-post-fixer.d.ts +74 -74
  137. package/src/model/utils/selection-post-fixer.js +260 -260
  138. package/src/model/writer.d.ts +851 -851
  139. package/src/model/writer.js +1306 -1306
  140. package/src/view/attributeelement.d.ts +108 -108
  141. package/src/view/attributeelement.js +184 -184
  142. package/src/view/containerelement.d.ts +49 -49
  143. package/src/view/containerelement.js +80 -80
  144. package/src/view/datatransfer.d.ts +79 -79
  145. package/src/view/datatransfer.js +98 -98
  146. package/src/view/document.d.ts +184 -184
  147. package/src/view/document.js +122 -120
  148. package/src/view/documentfragment.d.ts +153 -149
  149. package/src/view/documentfragment.js +234 -228
  150. package/src/view/documentselection.d.ts +306 -306
  151. package/src/view/documentselection.js +256 -256
  152. package/src/view/domconverter.d.ts +652 -640
  153. package/src/view/domconverter.js +1473 -1450
  154. package/src/view/downcastwriter.d.ts +996 -996
  155. package/src/view/downcastwriter.js +1696 -1696
  156. package/src/view/editableelement.d.ts +62 -62
  157. package/src/view/editableelement.js +62 -62
  158. package/src/view/element.d.ts +468 -468
  159. package/src/view/element.js +724 -724
  160. package/src/view/elementdefinition.d.ts +87 -87
  161. package/src/view/elementdefinition.js +5 -5
  162. package/src/view/emptyelement.d.ts +41 -41
  163. package/src/view/emptyelement.js +73 -73
  164. package/src/view/filler.d.ts +111 -111
  165. package/src/view/filler.js +150 -150
  166. package/src/view/item.d.ts +14 -14
  167. package/src/view/item.js +5 -5
  168. package/src/view/matcher.d.ts +486 -486
  169. package/src/view/matcher.js +507 -507
  170. package/src/view/node.d.ts +163 -163
  171. package/src/view/node.js +228 -228
  172. package/src/view/observer/arrowkeysobserver.d.ts +45 -45
  173. package/src/view/observer/arrowkeysobserver.js +40 -40
  174. package/src/view/observer/bubblingemittermixin.d.ts +166 -166
  175. package/src/view/observer/bubblingemittermixin.js +172 -172
  176. package/src/view/observer/bubblingeventinfo.d.ts +47 -47
  177. package/src/view/observer/bubblingeventinfo.js +37 -37
  178. package/src/view/observer/clickobserver.d.ts +43 -43
  179. package/src/view/observer/clickobserver.js +29 -29
  180. package/src/view/observer/compositionobserver.d.ts +82 -82
  181. package/src/view/observer/compositionobserver.js +60 -60
  182. package/src/view/observer/domeventdata.d.ts +50 -50
  183. package/src/view/observer/domeventdata.js +47 -47
  184. package/src/view/observer/domeventobserver.d.ts +73 -73
  185. package/src/view/observer/domeventobserver.js +79 -79
  186. package/src/view/observer/fakeselectionobserver.d.ts +47 -47
  187. package/src/view/observer/fakeselectionobserver.js +91 -91
  188. package/src/view/observer/focusobserver.d.ts +82 -82
  189. package/src/view/observer/focusobserver.js +86 -86
  190. package/src/view/observer/inputobserver.d.ts +86 -86
  191. package/src/view/observer/inputobserver.js +164 -164
  192. package/src/view/observer/keyobserver.d.ts +66 -66
  193. package/src/view/observer/keyobserver.js +39 -39
  194. package/src/view/observer/mouseobserver.d.ts +89 -89
  195. package/src/view/observer/mouseobserver.js +29 -29
  196. package/src/view/observer/mutationobserver.d.ts +86 -86
  197. package/src/view/observer/mutationobserver.js +206 -206
  198. package/src/view/observer/observer.d.ts +89 -89
  199. package/src/view/observer/observer.js +84 -84
  200. package/src/view/observer/selectionobserver.d.ts +148 -148
  201. package/src/view/observer/selectionobserver.js +202 -202
  202. package/src/view/observer/tabobserver.d.ts +46 -46
  203. package/src/view/observer/tabobserver.js +42 -42
  204. package/src/view/placeholder.d.ts +96 -96
  205. package/src/view/placeholder.js +267 -267
  206. package/src/view/position.d.ts +189 -189
  207. package/src/view/position.js +324 -324
  208. package/src/view/range.d.ts +279 -279
  209. package/src/view/range.js +430 -430
  210. package/src/view/rawelement.d.ts +73 -73
  211. package/src/view/rawelement.js +105 -105
  212. package/src/view/renderer.d.ts +265 -265
  213. package/src/view/renderer.js +1000 -999
  214. package/src/view/rooteditableelement.d.ts +41 -41
  215. package/src/view/rooteditableelement.js +69 -69
  216. package/src/view/selection.d.ts +375 -375
  217. package/src/view/selection.js +559 -559
  218. package/src/view/styles/background.d.ts +33 -33
  219. package/src/view/styles/background.js +74 -74
  220. package/src/view/styles/border.d.ts +43 -43
  221. package/src/view/styles/border.js +316 -316
  222. package/src/view/styles/margin.d.ts +29 -29
  223. package/src/view/styles/margin.js +34 -34
  224. package/src/view/styles/padding.d.ts +29 -29
  225. package/src/view/styles/padding.js +34 -34
  226. package/src/view/styles/utils.d.ts +93 -93
  227. package/src/view/styles/utils.js +219 -219
  228. package/src/view/stylesmap.d.ts +675 -675
  229. package/src/view/stylesmap.js +765 -766
  230. package/src/view/text.d.ts +74 -74
  231. package/src/view/text.js +93 -93
  232. package/src/view/textproxy.d.ts +97 -97
  233. package/src/view/textproxy.js +124 -124
  234. package/src/view/treewalker.d.ts +195 -195
  235. package/src/view/treewalker.js +327 -327
  236. package/src/view/typecheckable.d.ts +448 -448
  237. package/src/view/typecheckable.js +19 -19
  238. package/src/view/uielement.d.ts +96 -96
  239. package/src/view/uielement.js +183 -182
  240. package/src/view/upcastwriter.d.ts +417 -417
  241. package/src/view/upcastwriter.js +359 -359
  242. package/src/view/view.d.ts +487 -487
  243. package/src/view/view.js +546 -546
@@ -1,460 +1,460 @@
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
- * @module engine/conversion/upcastdispatcher
7
- */
8
- import ViewConsumable from './viewconsumable';
9
- import ModelRange from '../model/range';
10
- import ModelPosition from '../model/position';
11
- import { SchemaContext } from '../model/schema'; // eslint-disable-line no-duplicate-imports
12
- import { isParagraphable, wrapInParagraph } from '../model/utils/autoparagraphing';
13
- import { CKEditorError, EmitterMixin } from '@ckeditor/ckeditor5-utils';
14
- /**
15
- * Upcast dispatcher is a central point of the view-to-model conversion, which is a process of
16
- * converting a given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
17
- * {@link module:engine/view/element~Element view element} into a correct model structure.
18
- *
19
- * During the conversion process, the dispatcher fires events for all {@link module:engine/view/node~Node view nodes}
20
- * from the converted view document fragment.
21
- * Special callbacks called "converters" should listen to these events in order to convert the view nodes.
22
- *
23
- * The second parameter of the callback is the `data` object with the following properties:
24
- *
25
- * * `data.viewItem` contains a {@link module:engine/view/node~Node view node} or a
26
- * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
27
- * that is converted at the moment and might be handled by the callback.
28
- * * `data.modelRange` is used to point to the result
29
- * of the current conversion (e.g. the element that is being inserted)
30
- * and is always a {@link module:engine/model/range~Range} when the conversion succeeds.
31
- * * `data.modelCursor` is a {@link module:engine/model/position~Position position} on which the converter should insert
32
- * the newly created items.
33
- *
34
- * The third parameter of the callback is an instance of {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi}
35
- * which provides additional tools for converters.
36
- *
37
- * You can read more about conversion in the {@glink framework/deep-dive/conversion/upcast Upcast conversion} guide.
38
- *
39
- * Examples of event-based converters:
40
- *
41
- * ```ts
42
- * // A converter for links (<a>).
43
- * editor.data.upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
44
- * if ( conversionApi.consumable.consume( data.viewItem, { name: true, attributes: [ 'href' ] } ) ) {
45
- * // The <a> element is inline and is represented by an attribute in the model.
46
- * // This is why you need to convert only children.
47
- * const { modelRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
48
- *
49
- * for ( let item of modelRange.getItems() ) {
50
- * if ( conversionApi.schema.checkAttribute( item, 'linkHref' ) ) {
51
- * conversionApi.writer.setAttribute( 'linkHref', data.viewItem.getAttribute( 'href' ), item );
52
- * }
53
- * }
54
- * }
55
- * } );
56
- *
57
- * // Convert <p> element's font-size style.
58
- * // Note: You should use a low-priority observer in order to ensure that
59
- * // it is executed after the element-to-element converter.
60
- * editor.data.upcastDispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
61
- * const { consumable, schema, writer } = conversionApi;
62
- *
63
- * if ( !consumable.consume( data.viewItem, { style: 'font-size' } ) ) {
64
- * return;
65
- * }
66
- *
67
- * const fontSize = data.viewItem.getStyle( 'font-size' );
68
- *
69
- * // Do not go for the model element after data.modelCursor because it might happen
70
- * // that a single view element was converted to multiple model elements. Get all of them.
71
- * for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
72
- * if ( schema.checkAttribute( item, 'fontSize' ) ) {
73
- * writer.setAttribute( 'fontSize', fontSize, item );
74
- * }
75
- * }
76
- * }, { priority: 'low' } );
77
- *
78
- * // Convert all elements which have no custom converter into a paragraph (autoparagraphing).
79
- * editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
80
- * // Check if an element can be converted.
81
- * if ( !conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
82
- * // When an element is already consumed by higher priority converters, do nothing.
83
- * return;
84
- * }
85
- *
86
- * const paragraph = conversionApi.writer.createElement( 'paragraph' );
87
- *
88
- * // Try to safely insert a paragraph at the model cursor - it will find an allowed parent for the current element.
89
- * if ( !conversionApi.safeInsert( paragraph, data.modelCursor ) ) {
90
- * // When an element was not inserted, it means that you cannot insert a paragraph at this position.
91
- * return;
92
- * }
93
- *
94
- * // Consume the inserted element.
95
- * conversionApi.consumable.consume( data.viewItem, { name: data.viewItem.name } ) );
96
- *
97
- * // Convert the children to a paragraph.
98
- * const { modelRange } = conversionApi.convertChildren( data.viewItem, paragraph ) );
99
- *
100
- * // Update `modelRange` and `modelCursor` in the `data` as a conversion result.
101
- * conversionApi.updateConversionResult( paragraph, data );
102
- * }, { priority: 'low' } );
103
- * ```
104
- *
105
- * @fires viewCleanup
106
- * @fires element
107
- * @fires text
108
- * @fires documentFragment
109
- */
110
- export default class UpcastDispatcher extends EmitterMixin() {
111
- /**
112
- * Creates an upcast dispatcher that operates using the passed API.
113
- *
114
- * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi
115
- * @param conversionApi Additional properties for an interface that will be passed to events fired
116
- * by the upcast dispatcher.
117
- */
118
- constructor(conversionApi) {
119
- super();
120
- /**
121
- * The list of elements that were created during splitting.
122
- *
123
- * After the conversion process, the list is cleared.
124
- */
125
- this._splitParts = new Map();
126
- /**
127
- * The list of cursor parent elements that were created during splitting.
128
- *
129
- * After the conversion process the list is cleared.
130
- */
131
- this._cursorParents = new Map();
132
- /**
133
- * The position in the temporary structure where the converted content is inserted. The structure reflects the context of
134
- * the target position where the content will be inserted. This property is built based on the context parameter of the
135
- * convert method.
136
- */
137
- this._modelCursor = null;
138
- /**
139
- * The list of elements that were created during the splitting but should not get removed on conversion end even if they are empty.
140
- *
141
- * The list is cleared after the conversion process.
142
- */
143
- this._emptyElementsToKeep = new Set();
144
- this.conversionApi = {
145
- ...conversionApi,
146
- consumable: null,
147
- writer: null,
148
- store: null,
149
- convertItem: (viewItem, modelCursor) => this._convertItem(viewItem, modelCursor),
150
- convertChildren: (viewElement, positionOrElement) => this._convertChildren(viewElement, positionOrElement),
151
- safeInsert: (modelNode, position) => this._safeInsert(modelNode, position),
152
- updateConversionResult: (modelElement, data) => this._updateConversionResult(modelElement, data),
153
- // Advanced API - use only if custom position handling is needed.
154
- splitToAllowedParent: (modelNode, modelCursor) => this._splitToAllowedParent(modelNode, modelCursor),
155
- getSplitParts: modelElement => this._getSplitParts(modelElement),
156
- keepEmptyElement: modelElement => this._keepEmptyElement(modelElement)
157
- };
158
- }
159
- /**
160
- * Starts the conversion process. The entry point for the conversion.
161
- *
162
- * @fires element
163
- * @fires text
164
- * @fires documentFragment
165
- * @param viewElement The part of the view to be converted.
166
- * @param writer An instance of the model writer.
167
- * @param context Elements will be converted according to this context.
168
- * @returns Model data that is the result of the conversion process
169
- * wrapped in `DocumentFragment`. Converted marker elements will be set as the document fragment's
170
- * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
171
- */
172
- convert(viewElement, writer, context = ['$root']) {
173
- this.fire('viewCleanup', viewElement);
174
- // Create context tree and set position in the top element.
175
- // Items will be converted according to this position.
176
- this._modelCursor = createContextTree(context, writer);
177
- // Store writer in conversion as a conversion API
178
- // to be sure that conversion process will use the same batch.
179
- this.conversionApi.writer = writer;
180
- // Create consumable values list for conversion process.
181
- this.conversionApi.consumable = ViewConsumable.createFrom(viewElement);
182
- // Custom data stored by converter for conversion process.
183
- this.conversionApi.store = {};
184
- // Do the conversion.
185
- const { modelRange } = this._convertItem(viewElement, this._modelCursor);
186
- // Conversion result is always a document fragment so let's create it.
187
- const documentFragment = writer.createDocumentFragment();
188
- // When there is a conversion result.
189
- if (modelRange) {
190
- // Remove all empty elements that were create while splitting.
191
- this._removeEmptyElements();
192
- // Move all items that were converted in context tree to the document fragment.
193
- for (const item of Array.from(this._modelCursor.parent.getChildren())) {
194
- writer.append(item, documentFragment);
195
- }
196
- // Extract temporary markers elements from model and set as static markers collection.
197
- documentFragment.markers = extractMarkersFromModelFragment(documentFragment, writer);
198
- }
199
- // Clear context position.
200
- this._modelCursor = null;
201
- // Clear split elements & parents lists.
202
- this._splitParts.clear();
203
- this._cursorParents.clear();
204
- this._emptyElementsToKeep.clear();
205
- // Clear conversion API.
206
- this.conversionApi.writer = null;
207
- this.conversionApi.store = null;
208
- // Return fragment as conversion result.
209
- return documentFragment;
210
- }
211
- /**
212
- * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertItem
213
- */
214
- _convertItem(viewItem, modelCursor) {
215
- const data = { viewItem, modelCursor, modelRange: null };
216
- if (viewItem.is('element')) {
217
- this.fire(`element:${viewItem.name}`, data, this.conversionApi);
218
- }
219
- else if (viewItem.is('$text')) {
220
- this.fire('text', data, this.conversionApi);
221
- }
222
- else {
223
- this.fire('documentFragment', data, this.conversionApi);
224
- }
225
- // Handle incorrect conversion result.
226
- if (data.modelRange && !(data.modelRange instanceof ModelRange)) {
227
- /**
228
- * Incorrect conversion result was dropped.
229
- *
230
- * {@link module:engine/model/range~Range Model range} should be a conversion result.
231
- *
232
- * @error view-conversion-dispatcher-incorrect-result
233
- */
234
- throw new CKEditorError('view-conversion-dispatcher-incorrect-result', this);
235
- }
236
- return { modelRange: data.modelRange, modelCursor: data.modelCursor };
237
- }
238
- /**
239
- * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertChildren
240
- */
241
- _convertChildren(viewItem, elementOrModelCursor) {
242
- let nextModelCursor = elementOrModelCursor.is('position') ?
243
- elementOrModelCursor : ModelPosition._createAt(elementOrModelCursor, 0);
244
- const modelRange = new ModelRange(nextModelCursor);
245
- for (const viewChild of Array.from(viewItem.getChildren())) {
246
- const result = this._convertItem(viewChild, nextModelCursor);
247
- if (result.modelRange instanceof ModelRange) {
248
- modelRange.end = result.modelRange.end;
249
- nextModelCursor = result.modelCursor;
250
- }
251
- }
252
- return { modelRange, modelCursor: nextModelCursor };
253
- }
254
- /**
255
- * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#safeInsert
256
- */
257
- _safeInsert(modelNode, position) {
258
- // Find allowed parent for element that we are going to insert.
259
- // If current parent does not allow to insert element but one of the ancestors does
260
- // then split nodes to allowed parent.
261
- const splitResult = this._splitToAllowedParent(modelNode, position);
262
- // When there is no split result it means that we can't insert element to model tree, so let's skip it.
263
- if (!splitResult) {
264
- return false;
265
- }
266
- // Insert element on allowed position.
267
- this.conversionApi.writer.insert(modelNode, splitResult.position);
268
- return true;
269
- }
270
- /**
271
- * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#updateConversionResult
272
- */
273
- _updateConversionResult(modelElement, data) {
274
- const parts = this._getSplitParts(modelElement);
275
- const writer = this.conversionApi.writer;
276
- // Set conversion result range - only if not set already.
277
- if (!data.modelRange) {
278
- data.modelRange = writer.createRange(writer.createPositionBefore(modelElement), writer.createPositionAfter(parts[parts.length - 1]));
279
- }
280
- const savedCursorParent = this._cursorParents.get(modelElement);
281
- // Now we need to check where the `modelCursor` should be.
282
- if (savedCursorParent) {
283
- // If we split parent to insert our element then we want to continue conversion in the new part of the split parent.
284
- //
285
- // before: <allowed><notAllowed>foo[]</notAllowed></allowed>
286
- // after: <allowed><notAllowed>foo</notAllowed> <converted></converted> <notAllowed>[]</notAllowed></allowed>
287
- data.modelCursor = writer.createPositionAt(savedCursorParent, 0);
288
- }
289
- else {
290
- // Otherwise just continue after inserted element.
291
- data.modelCursor = data.modelRange.end;
292
- }
293
- }
294
- /**
295
- * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#splitToAllowedParent
296
- */
297
- _splitToAllowedParent(node, modelCursor) {
298
- const { schema, writer } = this.conversionApi;
299
- // Try to find allowed parent.
300
- let allowedParent = schema.findAllowedParent(modelCursor, node);
301
- if (allowedParent) {
302
- // When current position parent allows to insert node then return this position.
303
- if (allowedParent === modelCursor.parent) {
304
- return { position: modelCursor };
305
- }
306
- // When allowed parent is in context tree (it's outside the converted tree).
307
- if (this._modelCursor.parent.getAncestors().includes(allowedParent)) {
308
- allowedParent = null;
309
- }
310
- }
311
- if (!allowedParent) {
312
- // Check if the node wrapped with a paragraph would be accepted by the schema.
313
- if (!isParagraphable(modelCursor, node, schema)) {
314
- return null;
315
- }
316
- return {
317
- position: wrapInParagraph(modelCursor, writer)
318
- };
319
- }
320
- // Split element to allowed parent.
321
- const splitResult = this.conversionApi.writer.split(modelCursor, allowedParent);
322
- // Using the range returned by `model.Writer#split`, we will pair original elements with their split parts.
323
- //
324
- // The range returned from the writer spans "over the split" or, precisely saying, from the end of the original element (the one
325
- // that got split) to the beginning of the other part of that element:
326
- //
327
- // <limit><a><b><c>X[]Y</c></b><a></limit> ->
328
- // <limit><a><b><c>X[</c></b></a><a><b><c>]Y</c></b></a>
329
- //
330
- // After the split there cannot be any full node between the positions in `splitRange`. The positions are touching.
331
- // Also, because of how splitting works, it is easy to notice, that "closing tags" are in the reverse order than "opening tags".
332
- // Also, since we split all those elements, each of them has to have the other part.
333
- //
334
- // With those observations in mind, we will pair the original elements with their split parts by saving "closing tags" and matching
335
- // them with "opening tags" in the reverse order. For that we can use a stack.
336
- const stack = [];
337
- for (const treeWalkerValue of splitResult.range.getWalker()) {
338
- if (treeWalkerValue.type == 'elementEnd') {
339
- stack.push(treeWalkerValue.item);
340
- }
341
- else {
342
- // There should not be any text nodes after the element is split, so the only other value is `elementStart`.
343
- const originalPart = stack.pop();
344
- const splitPart = treeWalkerValue.item;
345
- this._registerSplitPair(originalPart, splitPart);
346
- }
347
- }
348
- const cursorParent = splitResult.range.end.parent;
349
- this._cursorParents.set(node, cursorParent);
350
- return {
351
- position: splitResult.position,
352
- cursorParent
353
- };
354
- }
355
- /**
356
- * Registers that a `splitPart` element is a split part of the `originalPart` element.
357
- *
358
- * The data set by this method is used by {@link #_getSplitParts} and {@link #_removeEmptyElements}.
359
- */
360
- _registerSplitPair(originalPart, splitPart) {
361
- if (!this._splitParts.has(originalPart)) {
362
- this._splitParts.set(originalPart, [originalPart]);
363
- }
364
- const list = this._splitParts.get(originalPart);
365
- this._splitParts.set(splitPart, list);
366
- list.push(splitPart);
367
- }
368
- /**
369
- * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#getSplitParts
370
- */
371
- _getSplitParts(element) {
372
- let parts;
373
- if (!this._splitParts.has(element)) {
374
- parts = [element];
375
- }
376
- else {
377
- parts = this._splitParts.get(element);
378
- }
379
- return parts;
380
- }
381
- /**
382
- * Mark an element that were created during the splitting to not get removed on conversion end even if it is empty.
383
- */
384
- _keepEmptyElement(element) {
385
- this._emptyElementsToKeep.add(element);
386
- }
387
- /**
388
- * Checks if there are any empty elements created while splitting and removes them.
389
- *
390
- * This method works recursively to re-check empty elements again after at least one element was removed in the initial call,
391
- * as some elements might have become empty after other empty elements were removed from them.
392
- */
393
- _removeEmptyElements() {
394
- let anyRemoved = false;
395
- for (const element of this._splitParts.keys()) {
396
- if (element.isEmpty && !this._emptyElementsToKeep.has(element)) {
397
- this.conversionApi.writer.remove(element);
398
- this._splitParts.delete(element);
399
- anyRemoved = true;
400
- }
401
- }
402
- if (anyRemoved) {
403
- this._removeEmptyElements();
404
- }
405
- }
406
- }
407
- /**
408
- * Traverses given model item and searches elements which marks marker range. Found element is removed from
409
- * DocumentFragment but path of this element is stored in a Map which is then returned.
410
- *
411
- * @param modelItem Fragment of model.
412
- * @returns List of static markers.
413
- */
414
- function extractMarkersFromModelFragment(modelItem, writer) {
415
- const markerElements = new Set();
416
- const markers = new Map();
417
- // Create ModelTreeWalker.
418
- const range = ModelRange._createIn(modelItem).getItems();
419
- // Walk through DocumentFragment and collect marker elements.
420
- for (const item of range) {
421
- // Check if current element is a marker.
422
- if (item.is('element', '$marker')) {
423
- markerElements.add(item);
424
- }
425
- }
426
- // Walk through collected marker elements store its path and remove its from the DocumentFragment.
427
- for (const markerElement of markerElements) {
428
- const markerName = markerElement.getAttribute('data-name');
429
- const currentPosition = writer.createPositionBefore(markerElement);
430
- // When marker of given name is not stored it means that we have found the beginning of the range.
431
- if (!markers.has(markerName)) {
432
- markers.set(markerName, new ModelRange(currentPosition.clone()));
433
- // Otherwise is means that we have found end of the marker range.
434
- }
435
- else {
436
- markers.get(markerName).end = currentPosition.clone();
437
- }
438
- // Remove marker element from DocumentFragment.
439
- writer.remove(markerElement);
440
- }
441
- return markers;
442
- }
443
- /**
444
- * Creates model fragment according to given context and returns position in the bottom (the deepest) element.
445
- */
446
- function createContextTree(contextDefinition, writer) {
447
- let position;
448
- for (const item of new SchemaContext(contextDefinition)) {
449
- const attributes = {};
450
- for (const key of item.getAttributeKeys()) {
451
- attributes[key] = item.getAttribute(key);
452
- }
453
- const current = writer.createElement(item.name, attributes);
454
- if (position) {
455
- writer.insert(current, position);
456
- }
457
- position = ModelPosition._createAt(current, 0);
458
- }
459
- return position;
460
- }
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
+ * @module engine/conversion/upcastdispatcher
7
+ */
8
+ import ViewConsumable from './viewconsumable';
9
+ import ModelRange from '../model/range';
10
+ import ModelPosition from '../model/position';
11
+ import { SchemaContext } from '../model/schema'; // eslint-disable-line no-duplicate-imports
12
+ import { isParagraphable, wrapInParagraph } from '../model/utils/autoparagraphing';
13
+ import { CKEditorError, EmitterMixin } from '@ckeditor/ckeditor5-utils';
14
+ /**
15
+ * Upcast dispatcher is a central point of the view-to-model conversion, which is a process of
16
+ * converting a given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
17
+ * {@link module:engine/view/element~Element view element} into a correct model structure.
18
+ *
19
+ * During the conversion process, the dispatcher fires events for all {@link module:engine/view/node~Node view nodes}
20
+ * from the converted view document fragment.
21
+ * Special callbacks called "converters" should listen to these events in order to convert the view nodes.
22
+ *
23
+ * The second parameter of the callback is the `data` object with the following properties:
24
+ *
25
+ * * `data.viewItem` contains a {@link module:engine/view/node~Node view node} or a
26
+ * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
27
+ * that is converted at the moment and might be handled by the callback.
28
+ * * `data.modelRange` is used to point to the result
29
+ * of the current conversion (e.g. the element that is being inserted)
30
+ * and is always a {@link module:engine/model/range~Range} when the conversion succeeds.
31
+ * * `data.modelCursor` is a {@link module:engine/model/position~Position position} on which the converter should insert
32
+ * the newly created items.
33
+ *
34
+ * The third parameter of the callback is an instance of {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi}
35
+ * which provides additional tools for converters.
36
+ *
37
+ * You can read more about conversion in the {@glink framework/deep-dive/conversion/upcast Upcast conversion} guide.
38
+ *
39
+ * Examples of event-based converters:
40
+ *
41
+ * ```ts
42
+ * // A converter for links (<a>).
43
+ * editor.data.upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
44
+ * if ( conversionApi.consumable.consume( data.viewItem, { name: true, attributes: [ 'href' ] } ) ) {
45
+ * // The <a> element is inline and is represented by an attribute in the model.
46
+ * // This is why you need to convert only children.
47
+ * const { modelRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
48
+ *
49
+ * for ( let item of modelRange.getItems() ) {
50
+ * if ( conversionApi.schema.checkAttribute( item, 'linkHref' ) ) {
51
+ * conversionApi.writer.setAttribute( 'linkHref', data.viewItem.getAttribute( 'href' ), item );
52
+ * }
53
+ * }
54
+ * }
55
+ * } );
56
+ *
57
+ * // Convert <p> element's font-size style.
58
+ * // Note: You should use a low-priority observer in order to ensure that
59
+ * // it is executed after the element-to-element converter.
60
+ * editor.data.upcastDispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
61
+ * const { consumable, schema, writer } = conversionApi;
62
+ *
63
+ * if ( !consumable.consume( data.viewItem, { style: 'font-size' } ) ) {
64
+ * return;
65
+ * }
66
+ *
67
+ * const fontSize = data.viewItem.getStyle( 'font-size' );
68
+ *
69
+ * // Do not go for the model element after data.modelCursor because it might happen
70
+ * // that a single view element was converted to multiple model elements. Get all of them.
71
+ * for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
72
+ * if ( schema.checkAttribute( item, 'fontSize' ) ) {
73
+ * writer.setAttribute( 'fontSize', fontSize, item );
74
+ * }
75
+ * }
76
+ * }, { priority: 'low' } );
77
+ *
78
+ * // Convert all elements which have no custom converter into a paragraph (autoparagraphing).
79
+ * editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
80
+ * // Check if an element can be converted.
81
+ * if ( !conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
82
+ * // When an element is already consumed by higher priority converters, do nothing.
83
+ * return;
84
+ * }
85
+ *
86
+ * const paragraph = conversionApi.writer.createElement( 'paragraph' );
87
+ *
88
+ * // Try to safely insert a paragraph at the model cursor - it will find an allowed parent for the current element.
89
+ * if ( !conversionApi.safeInsert( paragraph, data.modelCursor ) ) {
90
+ * // When an element was not inserted, it means that you cannot insert a paragraph at this position.
91
+ * return;
92
+ * }
93
+ *
94
+ * // Consume the inserted element.
95
+ * conversionApi.consumable.consume( data.viewItem, { name: data.viewItem.name } ) );
96
+ *
97
+ * // Convert the children to a paragraph.
98
+ * const { modelRange } = conversionApi.convertChildren( data.viewItem, paragraph ) );
99
+ *
100
+ * // Update `modelRange` and `modelCursor` in the `data` as a conversion result.
101
+ * conversionApi.updateConversionResult( paragraph, data );
102
+ * }, { priority: 'low' } );
103
+ * ```
104
+ *
105
+ * @fires viewCleanup
106
+ * @fires element
107
+ * @fires text
108
+ * @fires documentFragment
109
+ */
110
+ export default class UpcastDispatcher extends EmitterMixin() {
111
+ /**
112
+ * Creates an upcast dispatcher that operates using the passed API.
113
+ *
114
+ * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi
115
+ * @param conversionApi Additional properties for an interface that will be passed to events fired
116
+ * by the upcast dispatcher.
117
+ */
118
+ constructor(conversionApi) {
119
+ super();
120
+ /**
121
+ * The list of elements that were created during splitting.
122
+ *
123
+ * After the conversion process, the list is cleared.
124
+ */
125
+ this._splitParts = new Map();
126
+ /**
127
+ * The list of cursor parent elements that were created during splitting.
128
+ *
129
+ * After the conversion process the list is cleared.
130
+ */
131
+ this._cursorParents = new Map();
132
+ /**
133
+ * The position in the temporary structure where the converted content is inserted. The structure reflects the context of
134
+ * the target position where the content will be inserted. This property is built based on the context parameter of the
135
+ * convert method.
136
+ */
137
+ this._modelCursor = null;
138
+ /**
139
+ * The list of elements that were created during the splitting but should not get removed on conversion end even if they are empty.
140
+ *
141
+ * The list is cleared after the conversion process.
142
+ */
143
+ this._emptyElementsToKeep = new Set();
144
+ this.conversionApi = {
145
+ ...conversionApi,
146
+ consumable: null,
147
+ writer: null,
148
+ store: null,
149
+ convertItem: (viewItem, modelCursor) => this._convertItem(viewItem, modelCursor),
150
+ convertChildren: (viewElement, positionOrElement) => this._convertChildren(viewElement, positionOrElement),
151
+ safeInsert: (modelNode, position) => this._safeInsert(modelNode, position),
152
+ updateConversionResult: (modelElement, data) => this._updateConversionResult(modelElement, data),
153
+ // Advanced API - use only if custom position handling is needed.
154
+ splitToAllowedParent: (modelNode, modelCursor) => this._splitToAllowedParent(modelNode, modelCursor),
155
+ getSplitParts: modelElement => this._getSplitParts(modelElement),
156
+ keepEmptyElement: modelElement => this._keepEmptyElement(modelElement)
157
+ };
158
+ }
159
+ /**
160
+ * Starts the conversion process. The entry point for the conversion.
161
+ *
162
+ * @fires element
163
+ * @fires text
164
+ * @fires documentFragment
165
+ * @param viewElement The part of the view to be converted.
166
+ * @param writer An instance of the model writer.
167
+ * @param context Elements will be converted according to this context.
168
+ * @returns Model data that is the result of the conversion process
169
+ * wrapped in `DocumentFragment`. Converted marker elements will be set as the document fragment's
170
+ * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
171
+ */
172
+ convert(viewElement, writer, context = ['$root']) {
173
+ this.fire('viewCleanup', viewElement);
174
+ // Create context tree and set position in the top element.
175
+ // Items will be converted according to this position.
176
+ this._modelCursor = createContextTree(context, writer);
177
+ // Store writer in conversion as a conversion API
178
+ // to be sure that conversion process will use the same batch.
179
+ this.conversionApi.writer = writer;
180
+ // Create consumable values list for conversion process.
181
+ this.conversionApi.consumable = ViewConsumable.createFrom(viewElement);
182
+ // Custom data stored by converter for conversion process.
183
+ this.conversionApi.store = {};
184
+ // Do the conversion.
185
+ const { modelRange } = this._convertItem(viewElement, this._modelCursor);
186
+ // Conversion result is always a document fragment so let's create it.
187
+ const documentFragment = writer.createDocumentFragment();
188
+ // When there is a conversion result.
189
+ if (modelRange) {
190
+ // Remove all empty elements that were created while splitting.
191
+ this._removeEmptyElements();
192
+ // Move all items that were converted in context tree to the document fragment.
193
+ for (const item of Array.from(this._modelCursor.parent.getChildren())) {
194
+ writer.append(item, documentFragment);
195
+ }
196
+ // Extract temporary markers elements from model and set as static markers collection.
197
+ documentFragment.markers = extractMarkersFromModelFragment(documentFragment, writer);
198
+ }
199
+ // Clear context position.
200
+ this._modelCursor = null;
201
+ // Clear split elements & parents lists.
202
+ this._splitParts.clear();
203
+ this._cursorParents.clear();
204
+ this._emptyElementsToKeep.clear();
205
+ // Clear conversion API.
206
+ this.conversionApi.writer = null;
207
+ this.conversionApi.store = null;
208
+ // Return fragment as conversion result.
209
+ return documentFragment;
210
+ }
211
+ /**
212
+ * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertItem
213
+ */
214
+ _convertItem(viewItem, modelCursor) {
215
+ const data = { viewItem, modelCursor, modelRange: null };
216
+ if (viewItem.is('element')) {
217
+ this.fire(`element:${viewItem.name}`, data, this.conversionApi);
218
+ }
219
+ else if (viewItem.is('$text')) {
220
+ this.fire('text', data, this.conversionApi);
221
+ }
222
+ else {
223
+ this.fire('documentFragment', data, this.conversionApi);
224
+ }
225
+ // Handle incorrect conversion result.
226
+ if (data.modelRange && !(data.modelRange instanceof ModelRange)) {
227
+ /**
228
+ * Incorrect conversion result was dropped.
229
+ *
230
+ * {@link module:engine/model/range~Range Model range} should be a conversion result.
231
+ *
232
+ * @error view-conversion-dispatcher-incorrect-result
233
+ */
234
+ throw new CKEditorError('view-conversion-dispatcher-incorrect-result', this);
235
+ }
236
+ return { modelRange: data.modelRange, modelCursor: data.modelCursor };
237
+ }
238
+ /**
239
+ * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertChildren
240
+ */
241
+ _convertChildren(viewItem, elementOrModelCursor) {
242
+ let nextModelCursor = elementOrModelCursor.is('position') ?
243
+ elementOrModelCursor : ModelPosition._createAt(elementOrModelCursor, 0);
244
+ const modelRange = new ModelRange(nextModelCursor);
245
+ for (const viewChild of Array.from(viewItem.getChildren())) {
246
+ const result = this._convertItem(viewChild, nextModelCursor);
247
+ if (result.modelRange instanceof ModelRange) {
248
+ modelRange.end = result.modelRange.end;
249
+ nextModelCursor = result.modelCursor;
250
+ }
251
+ }
252
+ return { modelRange, modelCursor: nextModelCursor };
253
+ }
254
+ /**
255
+ * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#safeInsert
256
+ */
257
+ _safeInsert(modelNode, position) {
258
+ // Find allowed parent for element that we are going to insert.
259
+ // If current parent does not allow to insert element but one of the ancestors does
260
+ // then split nodes to allowed parent.
261
+ const splitResult = this._splitToAllowedParent(modelNode, position);
262
+ // When there is no split result it means that we can't insert element to model tree, so let's skip it.
263
+ if (!splitResult) {
264
+ return false;
265
+ }
266
+ // Insert element on allowed position.
267
+ this.conversionApi.writer.insert(modelNode, splitResult.position);
268
+ return true;
269
+ }
270
+ /**
271
+ * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#updateConversionResult
272
+ */
273
+ _updateConversionResult(modelElement, data) {
274
+ const parts = this._getSplitParts(modelElement);
275
+ const writer = this.conversionApi.writer;
276
+ // Set conversion result range - only if not set already.
277
+ if (!data.modelRange) {
278
+ data.modelRange = writer.createRange(writer.createPositionBefore(modelElement), writer.createPositionAfter(parts[parts.length - 1]));
279
+ }
280
+ const savedCursorParent = this._cursorParents.get(modelElement);
281
+ // Now we need to check where the `modelCursor` should be.
282
+ if (savedCursorParent) {
283
+ // If we split parent to insert our element then we want to continue conversion in the new part of the split parent.
284
+ //
285
+ // before: <allowed><notAllowed>foo[]</notAllowed></allowed>
286
+ // after: <allowed><notAllowed>foo</notAllowed> <converted></converted> <notAllowed>[]</notAllowed></allowed>
287
+ data.modelCursor = writer.createPositionAt(savedCursorParent, 0);
288
+ }
289
+ else {
290
+ // Otherwise just continue after inserted element.
291
+ data.modelCursor = data.modelRange.end;
292
+ }
293
+ }
294
+ /**
295
+ * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#splitToAllowedParent
296
+ */
297
+ _splitToAllowedParent(node, modelCursor) {
298
+ const { schema, writer } = this.conversionApi;
299
+ // Try to find allowed parent.
300
+ let allowedParent = schema.findAllowedParent(modelCursor, node);
301
+ if (allowedParent) {
302
+ // When current position parent allows to insert node then return this position.
303
+ if (allowedParent === modelCursor.parent) {
304
+ return { position: modelCursor };
305
+ }
306
+ // When allowed parent is in context tree (it's outside the converted tree).
307
+ if (this._modelCursor.parent.getAncestors().includes(allowedParent)) {
308
+ allowedParent = null;
309
+ }
310
+ }
311
+ if (!allowedParent) {
312
+ // Check if the node wrapped with a paragraph would be accepted by the schema.
313
+ if (!isParagraphable(modelCursor, node, schema)) {
314
+ return null;
315
+ }
316
+ return {
317
+ position: wrapInParagraph(modelCursor, writer)
318
+ };
319
+ }
320
+ // Split element to allowed parent.
321
+ const splitResult = this.conversionApi.writer.split(modelCursor, allowedParent);
322
+ // Using the range returned by `model.Writer#split`, we will pair original elements with their split parts.
323
+ //
324
+ // The range returned from the writer spans "over the split" or, precisely saying, from the end of the original element (the one
325
+ // that got split) to the beginning of the other part of that element:
326
+ //
327
+ // <limit><a><b><c>X[]Y</c></b><a></limit> ->
328
+ // <limit><a><b><c>X[</c></b></a><a><b><c>]Y</c></b></a>
329
+ //
330
+ // After the split there cannot be any full node between the positions in `splitRange`. The positions are touching.
331
+ // Also, because of how splitting works, it is easy to notice, that "closing tags" are in the reverse order than "opening tags".
332
+ // Also, since we split all those elements, each of them has to have the other part.
333
+ //
334
+ // With those observations in mind, we will pair the original elements with their split parts by saving "closing tags" and matching
335
+ // them with "opening tags" in the reverse order. For that we can use a stack.
336
+ const stack = [];
337
+ for (const treeWalkerValue of splitResult.range.getWalker()) {
338
+ if (treeWalkerValue.type == 'elementEnd') {
339
+ stack.push(treeWalkerValue.item);
340
+ }
341
+ else {
342
+ // There should not be any text nodes after the element is split, so the only other value is `elementStart`.
343
+ const originalPart = stack.pop();
344
+ const splitPart = treeWalkerValue.item;
345
+ this._registerSplitPair(originalPart, splitPart);
346
+ }
347
+ }
348
+ const cursorParent = splitResult.range.end.parent;
349
+ this._cursorParents.set(node, cursorParent);
350
+ return {
351
+ position: splitResult.position,
352
+ cursorParent
353
+ };
354
+ }
355
+ /**
356
+ * Registers that a `splitPart` element is a split part of the `originalPart` element.
357
+ *
358
+ * The data set by this method is used by {@link #_getSplitParts} and {@link #_removeEmptyElements}.
359
+ */
360
+ _registerSplitPair(originalPart, splitPart) {
361
+ if (!this._splitParts.has(originalPart)) {
362
+ this._splitParts.set(originalPart, [originalPart]);
363
+ }
364
+ const list = this._splitParts.get(originalPart);
365
+ this._splitParts.set(splitPart, list);
366
+ list.push(splitPart);
367
+ }
368
+ /**
369
+ * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#getSplitParts
370
+ */
371
+ _getSplitParts(element) {
372
+ let parts;
373
+ if (!this._splitParts.has(element)) {
374
+ parts = [element];
375
+ }
376
+ else {
377
+ parts = this._splitParts.get(element);
378
+ }
379
+ return parts;
380
+ }
381
+ /**
382
+ * Mark an element that were created during the splitting to not get removed on conversion end even if it is empty.
383
+ */
384
+ _keepEmptyElement(element) {
385
+ this._emptyElementsToKeep.add(element);
386
+ }
387
+ /**
388
+ * Checks if there are any empty elements created while splitting and removes them.
389
+ *
390
+ * This method works recursively to re-check empty elements again after at least one element was removed in the initial call,
391
+ * as some elements might have become empty after other empty elements were removed from them.
392
+ */
393
+ _removeEmptyElements() {
394
+ let anyRemoved = false;
395
+ for (const element of this._splitParts.keys()) {
396
+ if (element.isEmpty && !this._emptyElementsToKeep.has(element)) {
397
+ this.conversionApi.writer.remove(element);
398
+ this._splitParts.delete(element);
399
+ anyRemoved = true;
400
+ }
401
+ }
402
+ if (anyRemoved) {
403
+ this._removeEmptyElements();
404
+ }
405
+ }
406
+ }
407
+ /**
408
+ * Traverses given model item and searches elements which marks marker range. Found element is removed from
409
+ * DocumentFragment but path of this element is stored in a Map which is then returned.
410
+ *
411
+ * @param modelItem Fragment of model.
412
+ * @returns List of static markers.
413
+ */
414
+ function extractMarkersFromModelFragment(modelItem, writer) {
415
+ const markerElements = new Set();
416
+ const markers = new Map();
417
+ // Create ModelTreeWalker.
418
+ const range = ModelRange._createIn(modelItem).getItems();
419
+ // Walk through DocumentFragment and collect marker elements.
420
+ for (const item of range) {
421
+ // Check if current element is a marker.
422
+ if (item.is('element', '$marker')) {
423
+ markerElements.add(item);
424
+ }
425
+ }
426
+ // Walk through collected marker elements store its path and remove its from the DocumentFragment.
427
+ for (const markerElement of markerElements) {
428
+ const markerName = markerElement.getAttribute('data-name');
429
+ const currentPosition = writer.createPositionBefore(markerElement);
430
+ // When marker of given name is not stored it means that we have found the beginning of the range.
431
+ if (!markers.has(markerName)) {
432
+ markers.set(markerName, new ModelRange(currentPosition.clone()));
433
+ // Otherwise is means that we have found end of the marker range.
434
+ }
435
+ else {
436
+ markers.get(markerName).end = currentPosition.clone();
437
+ }
438
+ // Remove marker element from DocumentFragment.
439
+ writer.remove(markerElement);
440
+ }
441
+ return markers;
442
+ }
443
+ /**
444
+ * Creates model fragment according to given context and returns position in the bottom (the deepest) element.
445
+ */
446
+ function createContextTree(contextDefinition, writer) {
447
+ let position;
448
+ for (const item of new SchemaContext(contextDefinition)) {
449
+ const attributes = {};
450
+ for (const key of item.getAttributeKeys()) {
451
+ attributes[key] = item.getAttribute(key);
452
+ }
453
+ const current = writer.createElement(item.name, attributes);
454
+ if (position) {
455
+ writer.insert(current, position);
456
+ }
457
+ position = ModelPosition._createAt(current, 0);
458
+ }
459
+ return position;
460
+ }