@ckeditor/ckeditor5-engine 40.0.0 → 40.1.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 (242) hide show
  1. package/LICENSE.md +3 -3
  2. package/package.json +2 -2
  3. package/src/controller/datacontroller.d.ts +334 -334
  4. package/src/controller/datacontroller.js +481 -481
  5. package/src/controller/editingcontroller.d.ts +98 -98
  6. package/src/controller/editingcontroller.js +191 -191
  7. package/src/conversion/conversion.d.ts +478 -478
  8. package/src/conversion/conversion.js +601 -601
  9. package/src/conversion/conversionhelpers.d.ts +26 -26
  10. package/src/conversion/conversionhelpers.js +32 -32
  11. package/src/conversion/downcastdispatcher.d.ts +562 -562
  12. package/src/conversion/downcastdispatcher.js +547 -547
  13. package/src/conversion/downcasthelpers.d.ts +1226 -1226
  14. package/src/conversion/downcasthelpers.js +2178 -2183
  15. package/src/conversion/mapper.d.ts +503 -503
  16. package/src/conversion/mapper.js +536 -536
  17. package/src/conversion/modelconsumable.d.ts +201 -201
  18. package/src/conversion/modelconsumable.js +333 -333
  19. package/src/conversion/upcastdispatcher.d.ts +492 -492
  20. package/src/conversion/upcastdispatcher.js +460 -460
  21. package/src/conversion/upcasthelpers.d.ts +499 -499
  22. package/src/conversion/upcasthelpers.js +950 -950
  23. package/src/conversion/viewconsumable.d.ts +369 -369
  24. package/src/conversion/viewconsumable.js +536 -532
  25. package/src/dataprocessor/basichtmlwriter.d.ts +18 -18
  26. package/src/dataprocessor/basichtmlwriter.js +20 -19
  27. package/src/dataprocessor/dataprocessor.d.ts +61 -61
  28. package/src/dataprocessor/dataprocessor.js +5 -5
  29. package/src/dataprocessor/htmldataprocessor.d.ts +76 -76
  30. package/src/dataprocessor/htmldataprocessor.js +96 -96
  31. package/src/dataprocessor/htmlwriter.d.ts +16 -16
  32. package/src/dataprocessor/htmlwriter.js +5 -5
  33. package/src/dataprocessor/xmldataprocessor.d.ts +90 -90
  34. package/src/dataprocessor/xmldataprocessor.js +108 -108
  35. package/src/dev-utils/model.d.ts +124 -124
  36. package/src/dev-utils/model.js +395 -395
  37. package/src/dev-utils/operationreplayer.d.ts +51 -51
  38. package/src/dev-utils/operationreplayer.js +112 -112
  39. package/src/dev-utils/utils.d.ts +37 -37
  40. package/src/dev-utils/utils.js +73 -73
  41. package/src/dev-utils/view.d.ts +319 -319
  42. package/src/dev-utils/view.js +967 -967
  43. package/src/index.d.ts +114 -114
  44. package/src/index.js +78 -78
  45. package/src/model/batch.d.ts +106 -106
  46. package/src/model/batch.js +96 -96
  47. package/src/model/differ.d.ts +387 -387
  48. package/src/model/differ.js +1149 -1149
  49. package/src/model/document.d.ts +272 -272
  50. package/src/model/document.js +360 -361
  51. package/src/model/documentfragment.d.ts +200 -200
  52. package/src/model/documentfragment.js +306 -306
  53. package/src/model/documentselection.d.ts +420 -420
  54. package/src/model/documentselection.js +993 -993
  55. package/src/model/element.d.ts +165 -165
  56. package/src/model/element.js +281 -281
  57. package/src/model/history.d.ts +114 -114
  58. package/src/model/history.js +207 -207
  59. package/src/model/item.d.ts +14 -14
  60. package/src/model/item.js +5 -5
  61. package/src/model/liveposition.d.ts +77 -77
  62. package/src/model/liveposition.js +93 -93
  63. package/src/model/liverange.d.ts +102 -102
  64. package/src/model/liverange.js +120 -120
  65. package/src/model/markercollection.d.ts +335 -335
  66. package/src/model/markercollection.js +403 -403
  67. package/src/model/model.d.ts +919 -919
  68. package/src/model/model.js +842 -842
  69. package/src/model/node.d.ts +256 -256
  70. package/src/model/node.js +375 -375
  71. package/src/model/nodelist.d.ts +91 -91
  72. package/src/model/nodelist.js +163 -163
  73. package/src/model/operation/attributeoperation.d.ts +103 -103
  74. package/src/model/operation/attributeoperation.js +148 -148
  75. package/src/model/operation/detachoperation.d.ts +60 -60
  76. package/src/model/operation/detachoperation.js +77 -77
  77. package/src/model/operation/insertoperation.d.ts +90 -90
  78. package/src/model/operation/insertoperation.js +135 -135
  79. package/src/model/operation/markeroperation.d.ts +91 -91
  80. package/src/model/operation/markeroperation.js +107 -107
  81. package/src/model/operation/mergeoperation.d.ts +100 -100
  82. package/src/model/operation/mergeoperation.js +167 -167
  83. package/src/model/operation/moveoperation.d.ts +96 -96
  84. package/src/model/operation/moveoperation.js +164 -164
  85. package/src/model/operation/nooperation.d.ts +38 -38
  86. package/src/model/operation/nooperation.js +48 -48
  87. package/src/model/operation/operation.d.ts +96 -96
  88. package/src/model/operation/operation.js +59 -62
  89. package/src/model/operation/operationfactory.d.ts +18 -18
  90. package/src/model/operation/operationfactory.js +44 -44
  91. package/src/model/operation/renameoperation.d.ts +83 -83
  92. package/src/model/operation/renameoperation.js +115 -115
  93. package/src/model/operation/rootattributeoperation.d.ts +98 -98
  94. package/src/model/operation/rootattributeoperation.js +155 -155
  95. package/src/model/operation/rootoperation.d.ts +76 -76
  96. package/src/model/operation/rootoperation.js +90 -90
  97. package/src/model/operation/splitoperation.d.ts +109 -109
  98. package/src/model/operation/splitoperation.js +194 -194
  99. package/src/model/operation/transform.d.ts +100 -100
  100. package/src/model/operation/transform.js +1985 -1985
  101. package/src/model/operation/utils.d.ts +71 -71
  102. package/src/model/operation/utils.js +217 -213
  103. package/src/model/position.d.ts +539 -539
  104. package/src/model/position.js +979 -979
  105. package/src/model/range.d.ts +458 -458
  106. package/src/model/range.js +875 -875
  107. package/src/model/rootelement.d.ts +60 -60
  108. package/src/model/rootelement.js +74 -74
  109. package/src/model/schema.d.ts +1186 -1186
  110. package/src/model/schema.js +1242 -1242
  111. package/src/model/selection.d.ts +482 -482
  112. package/src/model/selection.js +789 -789
  113. package/src/model/text.d.ts +66 -66
  114. package/src/model/text.js +85 -85
  115. package/src/model/textproxy.d.ts +144 -144
  116. package/src/model/textproxy.js +189 -189
  117. package/src/model/treewalker.d.ts +186 -186
  118. package/src/model/treewalker.js +244 -244
  119. package/src/model/typecheckable.d.ts +285 -285
  120. package/src/model/typecheckable.js +16 -16
  121. package/src/model/utils/autoparagraphing.d.ts +37 -37
  122. package/src/model/utils/autoparagraphing.js +63 -63
  123. package/src/model/utils/deletecontent.d.ts +58 -58
  124. package/src/model/utils/deletecontent.js +488 -488
  125. package/src/model/utils/findoptimalinsertionrange.d.ts +32 -32
  126. package/src/model/utils/findoptimalinsertionrange.js +57 -57
  127. package/src/model/utils/getselectedcontent.d.ts +30 -30
  128. package/src/model/utils/getselectedcontent.js +125 -125
  129. package/src/model/utils/insertcontent.d.ts +46 -46
  130. package/src/model/utils/insertcontent.js +705 -705
  131. package/src/model/utils/insertobject.d.ts +44 -44
  132. package/src/model/utils/insertobject.js +139 -139
  133. package/src/model/utils/modifyselection.d.ts +48 -48
  134. package/src/model/utils/modifyselection.js +186 -186
  135. package/src/model/utils/selection-post-fixer.d.ts +74 -74
  136. package/src/model/utils/selection-post-fixer.js +260 -260
  137. package/src/model/writer.d.ts +851 -851
  138. package/src/model/writer.js +1306 -1306
  139. package/src/view/attributeelement.d.ts +108 -108
  140. package/src/view/attributeelement.js +184 -184
  141. package/src/view/containerelement.d.ts +49 -49
  142. package/src/view/containerelement.js +80 -80
  143. package/src/view/datatransfer.d.ts +79 -79
  144. package/src/view/datatransfer.js +98 -98
  145. package/src/view/document.d.ts +184 -184
  146. package/src/view/document.js +122 -120
  147. package/src/view/documentfragment.d.ts +153 -149
  148. package/src/view/documentfragment.js +234 -228
  149. package/src/view/documentselection.d.ts +306 -306
  150. package/src/view/documentselection.js +256 -256
  151. package/src/view/domconverter.d.ts +652 -640
  152. package/src/view/domconverter.js +1473 -1450
  153. package/src/view/downcastwriter.d.ts +996 -996
  154. package/src/view/downcastwriter.js +1696 -1696
  155. package/src/view/editableelement.d.ts +62 -62
  156. package/src/view/editableelement.js +62 -62
  157. package/src/view/element.d.ts +468 -468
  158. package/src/view/element.js +724 -724
  159. package/src/view/elementdefinition.d.ts +87 -87
  160. package/src/view/elementdefinition.js +5 -5
  161. package/src/view/emptyelement.d.ts +41 -41
  162. package/src/view/emptyelement.js +73 -73
  163. package/src/view/filler.d.ts +111 -111
  164. package/src/view/filler.js +150 -150
  165. package/src/view/item.d.ts +14 -14
  166. package/src/view/item.js +5 -5
  167. package/src/view/matcher.d.ts +486 -486
  168. package/src/view/matcher.js +507 -507
  169. package/src/view/node.d.ts +163 -163
  170. package/src/view/node.js +228 -228
  171. package/src/view/observer/arrowkeysobserver.d.ts +45 -45
  172. package/src/view/observer/arrowkeysobserver.js +40 -40
  173. package/src/view/observer/bubblingemittermixin.d.ts +166 -166
  174. package/src/view/observer/bubblingemittermixin.js +172 -172
  175. package/src/view/observer/bubblingeventinfo.d.ts +47 -47
  176. package/src/view/observer/bubblingeventinfo.js +37 -37
  177. package/src/view/observer/clickobserver.d.ts +43 -43
  178. package/src/view/observer/clickobserver.js +29 -29
  179. package/src/view/observer/compositionobserver.d.ts +82 -82
  180. package/src/view/observer/compositionobserver.js +60 -60
  181. package/src/view/observer/domeventdata.d.ts +50 -50
  182. package/src/view/observer/domeventdata.js +47 -47
  183. package/src/view/observer/domeventobserver.d.ts +73 -73
  184. package/src/view/observer/domeventobserver.js +79 -79
  185. package/src/view/observer/fakeselectionobserver.d.ts +47 -47
  186. package/src/view/observer/fakeselectionobserver.js +91 -91
  187. package/src/view/observer/focusobserver.d.ts +82 -82
  188. package/src/view/observer/focusobserver.js +86 -86
  189. package/src/view/observer/inputobserver.d.ts +86 -86
  190. package/src/view/observer/inputobserver.js +164 -164
  191. package/src/view/observer/keyobserver.d.ts +66 -66
  192. package/src/view/observer/keyobserver.js +39 -39
  193. package/src/view/observer/mouseobserver.d.ts +89 -89
  194. package/src/view/observer/mouseobserver.js +29 -29
  195. package/src/view/observer/mutationobserver.d.ts +86 -86
  196. package/src/view/observer/mutationobserver.js +206 -206
  197. package/src/view/observer/observer.d.ts +89 -89
  198. package/src/view/observer/observer.js +84 -84
  199. package/src/view/observer/selectionobserver.d.ts +148 -148
  200. package/src/view/observer/selectionobserver.js +202 -202
  201. package/src/view/observer/tabobserver.d.ts +46 -46
  202. package/src/view/observer/tabobserver.js +42 -42
  203. package/src/view/placeholder.d.ts +96 -96
  204. package/src/view/placeholder.js +267 -267
  205. package/src/view/position.d.ts +189 -189
  206. package/src/view/position.js +324 -324
  207. package/src/view/range.d.ts +279 -279
  208. package/src/view/range.js +430 -430
  209. package/src/view/rawelement.d.ts +73 -73
  210. package/src/view/rawelement.js +105 -105
  211. package/src/view/renderer.d.ts +265 -265
  212. package/src/view/renderer.js +1000 -999
  213. package/src/view/rooteditableelement.d.ts +41 -41
  214. package/src/view/rooteditableelement.js +69 -69
  215. package/src/view/selection.d.ts +375 -375
  216. package/src/view/selection.js +559 -559
  217. package/src/view/styles/background.d.ts +33 -33
  218. package/src/view/styles/background.js +74 -74
  219. package/src/view/styles/border.d.ts +43 -43
  220. package/src/view/styles/border.js +316 -316
  221. package/src/view/styles/margin.d.ts +29 -29
  222. package/src/view/styles/margin.js +34 -34
  223. package/src/view/styles/padding.d.ts +29 -29
  224. package/src/view/styles/padding.js +34 -34
  225. package/src/view/styles/utils.d.ts +93 -93
  226. package/src/view/styles/utils.js +219 -219
  227. package/src/view/stylesmap.d.ts +675 -675
  228. package/src/view/stylesmap.js +765 -766
  229. package/src/view/text.d.ts +74 -74
  230. package/src/view/text.js +93 -93
  231. package/src/view/textproxy.d.ts +97 -97
  232. package/src/view/textproxy.js +124 -124
  233. package/src/view/treewalker.d.ts +195 -195
  234. package/src/view/treewalker.js +327 -327
  235. package/src/view/typecheckable.d.ts +448 -448
  236. package/src/view/typecheckable.js +19 -19
  237. package/src/view/uielement.d.ts +96 -96
  238. package/src/view/uielement.js +183 -182
  239. package/src/view/upcastwriter.d.ts +417 -417
  240. package/src/view/upcastwriter.js +359 -359
  241. package/src/view/view.d.ts +487 -487
  242. 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
+ }