@ckeditor/ckeditor5-engine 38.1.0 → 38.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +547 -547
  11. package/src/conversion/downcastdispatcher.js +538 -538
  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 +113 -113
  43. package/src/index.js +77 -77
  44. package/src/model/batch.d.ts +106 -106
  45. package/src/model/batch.js +96 -96
  46. package/src/model/differ.d.ts +373 -373
  47. package/src/model/differ.js +1090 -1090
  48. package/src/model/document.d.ts +264 -264
  49. package/src/model/document.js +356 -356
  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 +987 -987
  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 +920 -920
  67. package/src/model/model.js +843 -843
  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 +80 -80
  95. package/src/model/operation/rootoperation.js +114 -114
  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 +54 -54
  107. package/src/model/rootelement.js +68 -68
  108. package/src/model/schema.d.ts +1176 -1176
  109. package/src/model/schema.js +1237 -1237
  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 +64 -64
  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 +650 -650
  151. package/src/view/domconverter.js +1373 -1373
  152. package/src/view/downcastwriter.d.ts +996 -996
  153. package/src/view/downcastwriter.js +1696 -1696
  154. package/src/view/editableelement.d.ts +52 -52
  155. package/src/view/editableelement.js +61 -61
  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 +148 -148
  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 +85 -85
  203. package/src/view/placeholder.js +230 -230
  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 +1000 -1000
  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 +480 -480
  241. package/src/view/view.js +534 -534
@@ -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 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
+ }