8
0

upcastdispatcher.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. 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';
  12. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  13. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  14. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  15. /**
  16. * `UpcastDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of
  17. * converting given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
  18. * {@link module:engine/view/element~Element} into another structure.
  19. * In default application, {@link module:engine/view/view view} is converted to {@link module:engine/model/model}.
  20. *
  21. * During conversion process, for all {@link module:engine/view/node~Node view nodes} from the converted view document fragment,
  22. * `UpcastDispatcher` fires corresponding events. Special callbacks called "converters" should listen to
  23. * `UpcastDispatcher` for those events.
  24. *
  25. * Each callback, as the second argument, is passed a special object `data` that has `viewItem`, `modelCursor` and
  26. * `modelRange` properties. `viewItem` property contains {@link module:engine/view/node~Node view node} or
  27. * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
  28. * that is converted at the moment and might be handled by the callback. `modelRange` property should be used to save the result
  29. * of conversion and is always a {@link module:engine/model/range~Range} when conversion result is correct.
  30. * `modelCursor` property is a {@link module:engine/model/position~Position position} on which conversion result will be inserted
  31. * and is a context according to {@link module:engine/model/schema~Schema schema} will be checked before the conversion.
  32. * See also {@link ~UpcastDispatcher#convert}. It is also shared by reference by all callbacks listening to given event.
  33. *
  34. * The third parameter passed to a callback is an instance of {@link ~UpcastDispatcher}
  35. * which provides additional tools for converters.
  36. *
  37. * Examples of providing callbacks for `UpcastDispatcher`:
  38. *
  39. * // Converter for links (<a>).
  40. * editor.data.upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
  41. * if ( conversionApi.consumable.consume( data.viewItem, { name: true, attributes: [ 'href' ] } ) ) {
  42. * // <a> element is inline and is represented by an attribute in the model.
  43. * // This is why we need to convert only children.
  44. * const { modelRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
  45. *
  46. * for ( let item of modelRange.getItems() ) {
  47. * if ( conversionApi.schema.checkAttribute( item, 'linkHref' ) ) {
  48. * conversionApi.writer.setAttribute( 'linkHref', data.viewItem.getAttribute( 'href' ), item );
  49. * }
  50. * }
  51. * }
  52. * } );
  53. *
  54. * // Convert <p>'s font-size style.
  55. * // Note: You should use a low-priority observer in order to ensure that
  56. * // it's executed after the element-to-element converter.
  57. * editor.data.upcastDispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
  58. * const { consumable, schema, writer } = conversionApi;
  59. *
  60. * if ( !consumable.consume( data.viewItem, { style: 'font-size' } ) ) {
  61. * return;
  62. * }
  63. *
  64. * const fontSize = data.viewItem.getStyle( 'font-size' );
  65. *
  66. * // Don't go for the model element after data.modelCursor because it might happen
  67. * // that a single view element was converted to multiple model elements. Get all of them.
  68. * for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
  69. * if ( schema.checkAttribute( item, 'fontSize' ) ) {
  70. * writer.setAttribute( 'fontSize', fontSize, item );
  71. * }
  72. * }
  73. * }, { priority: 'low' } );
  74. *
  75. * // Convert all elements which have no custom converter into paragraph (autoparagraphing).
  76. * editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
  77. * // When element is already consumed by higher priority converters then do nothing.
  78. * if ( conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
  79. * const paragraph = conversionApi.writer.createElement( 'paragraph' );
  80. *
  81. * // Find allowed parent for paragraph that we are going to insert. If current parent does not allow
  82. * // to insert paragraph but one of the ancestors does then split nodes to allowed parent.
  83. * const splitResult = conversionApi.splitToAllowedParent( paragraph, data.modelCursor );
  84. *
  85. * // When there is no split result it means that we can't insert paragraph in this position.
  86. * if ( splitResult ) {
  87. * // Insert paragraph in allowed position.
  88. * conversionApi.writer.insert( paragraph, splitResult.position );
  89. *
  90. * // Convert children to paragraph.
  91. * const { modelRange } = conversionApi.convertChildren(
  92. * data.viewItem,
  93. * conversionApi.writer.createPositionAt( paragraph, 0 )
  94. * );
  95. *
  96. * // Set as conversion result, attribute converters may use this property.
  97. * data.modelRange = conversionApi.writer.createRange(
  98. * conversionApi.writer.createPositionBefore( paragraph ),
  99. * modelRange.end
  100. * );
  101. *
  102. * // Continue conversion inside paragraph.
  103. * data.modelCursor = data.modelRange.end;
  104. * }
  105. * }
  106. * }
  107. * }, { priority: 'low' } );
  108. *
  109. * Before each conversion process, `UpcastDispatcher` fires {@link ~UpcastDispatcher#event:viewCleanup}
  110. * event which can be used to prepare tree view for conversion.
  111. *
  112. * @mixes module:utils/emittermixin~EmitterMixin
  113. * @fires viewCleanup
  114. * @fires element
  115. * @fires text
  116. * @fires documentFragment
  117. */
  118. export default class UpcastDispatcher {
  119. /**
  120. * Creates a `UpcastDispatcher` that operates using passed API.
  121. *
  122. * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi
  123. * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
  124. * by `UpcastDispatcher`.
  125. */
  126. constructor( conversionApi = {} ) {
  127. /**
  128. * List of the elements that were created during splitting.
  129. *
  130. * After conversion process the list is cleared.
  131. *
  132. * @private
  133. * @type {Map.<module:engine/model/element~Element,Array.<module:engine/model/element~Element>>}
  134. */
  135. this._splitParts = new Map();
  136. /**
  137. * Position in the temporary structure where the converted content is inserted. The structure reflect the context of
  138. * the target position where the content will be inserted. This property is build based on the context parameter of the
  139. * convert method.
  140. *
  141. * @private
  142. * @type {module:engine/model/position~Position|null}
  143. */
  144. this._modelCursor = null;
  145. /**
  146. * Interface passed by dispatcher to the events callbacks.
  147. *
  148. * @member {module:engine/conversion/upcastdispatcher~UpcastConversionApi}
  149. */
  150. this.conversionApi = Object.assign( {}, conversionApi );
  151. // `convertItem`, `convertChildren` and `splitToAllowedParent` are bound to this `UpcastDispatcher`
  152. // instance and set on `conversionApi`. This way only a part of `UpcastDispatcher` API is exposed.
  153. this.conversionApi.convertItem = this._convertItem.bind( this );
  154. this.conversionApi.convertChildren = this._convertChildren.bind( this );
  155. this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.bind( this );
  156. this.conversionApi.getSplitParts = this._getSplitParts.bind( this );
  157. }
  158. /**
  159. * Starts the conversion process. The entry point for the conversion.
  160. *
  161. * @fires element
  162. * @fires text
  163. * @fires documentFragment
  164. * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
  165. * Part of the view to be converted.
  166. * @param {module:engine/model/writer~Writer} writer Instance of model writer.
  167. * @param {module:engine/model/schema~SchemaContextDefinition} [context=['$root']] Elements will be converted according to this context.
  168. * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process
  169. * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's
  170. * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
  171. */
  172. convert( viewItem, writer, context = [ '$root' ] ) {
  173. this.fire( 'viewCleanup', viewItem );
  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( viewItem );
  182. // Custom data stored by converter for conversion process.
  183. this.conversionApi.store = {};
  184. // Do the conversion.
  185. const { modelRange } = this._convertItem( viewItem, 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 lists.
  202. this._splitParts.clear();
  203. // Clear conversion API.
  204. this.conversionApi.writer = null;
  205. this.conversionApi.store = null;
  206. // Return fragment as conversion result.
  207. return documentFragment;
  208. }
  209. /**
  210. * @private
  211. * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertItem
  212. */
  213. _convertItem( viewItem, modelCursor ) {
  214. const data = Object.assign( { viewItem, modelCursor, modelRange: null } );
  215. if ( viewItem.is( 'element' ) ) {
  216. this.fire( 'element:' + viewItem.name, data, this.conversionApi );
  217. } else if ( viewItem.is( 'text' ) ) {
  218. this.fire( 'text', data, this.conversionApi );
  219. } else {
  220. this.fire( 'documentFragment', data, this.conversionApi );
  221. }
  222. // Handle incorrect conversion result.
  223. if ( data.modelRange && !( data.modelRange instanceof ModelRange ) ) {
  224. /**
  225. * Incorrect conversion result was dropped.
  226. *
  227. * {@link module:engine/model/range~Range Model range} should be a conversion result.
  228. *
  229. * @error view-conversion-dispatcher-incorrect-result
  230. */
  231. throw new CKEditorError( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.', this );
  232. }
  233. return { modelRange: data.modelRange, modelCursor: data.modelCursor };
  234. }
  235. /**
  236. * @private
  237. * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertChildren
  238. */
  239. _convertChildren( viewItem, modelCursor ) {
  240. const modelRange = new ModelRange( modelCursor );
  241. let nextModelCursor = modelCursor;
  242. for ( const viewChild of Array.from( viewItem.getChildren() ) ) {
  243. const result = this._convertItem( viewChild, nextModelCursor );
  244. if ( result.modelRange instanceof ModelRange ) {
  245. modelRange.end = result.modelRange.end;
  246. nextModelCursor = result.modelCursor;
  247. }
  248. }
  249. return { modelRange, modelCursor: nextModelCursor };
  250. }
  251. /**
  252. * @private
  253. * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#splitToAllowedParent
  254. */
  255. _splitToAllowedParent( node, modelCursor ) {
  256. // Try to find allowed parent.
  257. const allowedParent = this.conversionApi.schema.findAllowedParent( modelCursor, node );
  258. // When there is no parent that allows to insert node then return `null`.
  259. if ( !allowedParent ) {
  260. return null;
  261. }
  262. // When current position parent allows to insert node then return this position.
  263. if ( allowedParent === modelCursor.parent ) {
  264. return { position: modelCursor };
  265. }
  266. // When allowed parent is in context tree.
  267. if ( this._modelCursor.parent.getAncestors().includes( allowedParent ) ) {
  268. return null;
  269. }
  270. // Split element to allowed parent.
  271. const splitResult = this.conversionApi.writer.split( modelCursor, allowedParent );
  272. // Using the range returned by `model.Writer#split`, we will pair original elements with their split parts.
  273. //
  274. // The range returned from the writer spans "over the split" or, precisely saying, from the end of the original element (the one
  275. // that got split) to the beginning of the other part of that element:
  276. //
  277. // <limit><a><b><c>X[]Y</c></b><a></limit> ->
  278. // <limit><a><b><c>X[</c></b></a><a><b><c>]Y</c></b></a>
  279. //
  280. // After the split there cannot be any full node between the positions in `splitRange`. The positions are touching.
  281. // Also, because of how splitting works, it is easy to notice, that "closing tags" are in the reverse order than "opening tags".
  282. // Also, since we split all those elements, each of them has to have the other part.
  283. //
  284. // With those observations in mind, we will pair the original elements with their split parts by saving "closing tags" and matching
  285. // them with "opening tags" in the reverse order. For that we can use a stack.
  286. const stack = [];
  287. for ( const treeWalkerValue of splitResult.range.getWalker() ) {
  288. if ( treeWalkerValue.type == 'elementEnd' ) {
  289. stack.push( treeWalkerValue.item );
  290. } else {
  291. // There should not be any text nodes after the element is split, so the only other value is `elementStart`.
  292. const originalPart = stack.pop();
  293. const splitPart = treeWalkerValue.item;
  294. this._registerSplitPair( originalPart, splitPart );
  295. }
  296. }
  297. return {
  298. position: splitResult.position,
  299. cursorParent: splitResult.range.end.parent
  300. };
  301. }
  302. /**
  303. * Registers that `splitPart` element is a split part of the `originalPart` element.
  304. *
  305. * Data set by this method is used by {@link #_getSplitParts} and {@link #_removeEmptyElements}.
  306. *
  307. * @private
  308. * @param {module:engine/model/element~Element} originalPart
  309. * @param {module:engine/model/element~Element} splitPart
  310. */
  311. _registerSplitPair( originalPart, splitPart ) {
  312. if ( !this._splitParts.has( originalPart ) ) {
  313. this._splitParts.set( originalPart, [ originalPart ] );
  314. }
  315. const list = this._splitParts.get( originalPart );
  316. this._splitParts.set( splitPart, list );
  317. list.push( splitPart );
  318. }
  319. /**
  320. * @private
  321. * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#getSplitParts
  322. */
  323. _getSplitParts( element ) {
  324. let parts;
  325. if ( !this._splitParts.has( element ) ) {
  326. parts = [ element ];
  327. } else {
  328. parts = this._splitParts.get( element );
  329. }
  330. return parts;
  331. }
  332. /**
  333. * Checks if there are any empty elements created while splitting and removes them.
  334. *
  335. * This method works recursively to re-check empty elements again after at least one element was removed in the initial call,
  336. * as some elements might have become empty after other empty elements were removed from them.
  337. *
  338. * @private
  339. */
  340. _removeEmptyElements() {
  341. let anyRemoved = false;
  342. for ( const element of this._splitParts.keys() ) {
  343. if ( element.isEmpty ) {
  344. this.conversionApi.writer.remove( element );
  345. this._splitParts.delete( element );
  346. anyRemoved = true;
  347. }
  348. }
  349. if ( anyRemoved ) {
  350. this._removeEmptyElements();
  351. }
  352. }
  353. /**
  354. * Fired before the first conversion event, at the beginning of upcast (view to model conversion) process.
  355. *
  356. * @event viewCleanup
  357. * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
  358. * viewItem Part of the view to be converted.
  359. */
  360. /**
  361. * Fired when {@link module:engine/view/element~Element} is converted.
  362. *
  363. * `element` is a namespace event for a class of events. Names of actually called events follow this pattern:
  364. * `element:<elementName>` where `elementName` is the name of converted element. This way listeners may listen to
  365. * all elements conversion or to conversion of specific elements.
  366. *
  367. * @event element
  368. * @param {Object} data Conversion data. Keep in mind that this object is shared by reference between all
  369. * callbacks that will be called. This means that callbacks can override values if needed, and those values will
  370. * be available in other callbacks.
  371. * @param {module:engine/view/item~Item} data.viewItem Converted item.
  372. * @param {module:engine/model/position~Position} data.modelCursor Position where a converter should start changes.
  373. * Change this value for the next converter to tell where the conversion should continue.
  374. * @param {module:engine/model/range~Range} data.modelRange The current state of conversion result. Every change to
  375. * converted element should be reflected by setting or modifying this property.
  376. * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion utilities to be used by callback.
  377. */
  378. /**
  379. * Fired when {@link module:engine/view/text~Text} is converted.
  380. *
  381. * @event text
  382. * @see #event:element
  383. */
  384. /**
  385. * Fired when {@link module:engine/view/documentfragment~DocumentFragment} is converted.
  386. *
  387. * @event documentFragment
  388. * @see #event:element
  389. */
  390. }
  391. mix( UpcastDispatcher, EmitterMixin );
  392. // Traverses given model item and searches elements which marks marker range. Found element is removed from
  393. // DocumentFragment but path of this element is stored in a Map which is then returned.
  394. //
  395. // @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
  396. // @returns {Map<String, module:engine/model/range~Range>} List of static markers.
  397. function extractMarkersFromModelFragment( modelItem, writer ) {
  398. const markerElements = new Set();
  399. const markers = new Map();
  400. // Create ModelTreeWalker.
  401. const range = ModelRange._createIn( modelItem ).getItems();
  402. // Walk through DocumentFragment and collect marker elements.
  403. for ( const item of range ) {
  404. // Check if current element is a marker.
  405. if ( item.name == '$marker' ) {
  406. markerElements.add( item );
  407. }
  408. }
  409. // Walk through collected marker elements store its path and remove its from the DocumentFragment.
  410. for ( const markerElement of markerElements ) {
  411. const markerName = markerElement.getAttribute( 'data-name' );
  412. const currentPosition = writer.createPositionBefore( markerElement );
  413. // When marker of given name is not stored it means that we have found the beginning of the range.
  414. if ( !markers.has( markerName ) ) {
  415. markers.set( markerName, new ModelRange( currentPosition.clone() ) );
  416. // Otherwise is means that we have found end of the marker range.
  417. } else {
  418. markers.get( markerName ).end = currentPosition.clone();
  419. }
  420. // Remove marker element from DocumentFragment.
  421. writer.remove( markerElement );
  422. }
  423. return markers;
  424. }
  425. // Creates model fragment according to given context and returns position in the bottom (the deepest) element.
  426. function createContextTree( contextDefinition, writer ) {
  427. let position;
  428. for ( const item of new SchemaContext( contextDefinition ) ) {
  429. const attributes = {};
  430. for ( const key of item.getAttributeKeys() ) {
  431. attributes[ key ] = item.getAttribute( key );
  432. }
  433. const current = writer.createElement( item.name, attributes );
  434. if ( position ) {
  435. writer.append( current, position );
  436. }
  437. position = ModelPosition._createAt( current, 0 );
  438. }
  439. return position;
  440. }
  441. /**
  442. * Conversion interface that is registered for given {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}
  443. * and is passed as one of parameters when {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher dispatcher}
  444. * fires it's events.
  445. *
  446. * @interface module:engine/conversion/upcastdispatcher~UpcastConversionApi
  447. */
  448. /**
  449. * Starts conversion of given item by firing an appropriate event.
  450. *
  451. * Every fired event is passed (as first parameter) an object with `modelRange` property. Every event may set and/or
  452. * modify that property. When all callbacks are done, the final value of `modelRange` property is returned by this method.
  453. * The `modelRange` must be {@link module:engine/model/range~Range model range} or `null` (as set by default).
  454. *
  455. * @method #convertItem
  456. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
  457. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
  458. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
  459. * @param {module:engine/view/item~Item} viewItem Item to convert.
  460. * @param {module:engine/model/position~Position} modelCursor Position of conversion.
  461. * @returns {Object} result Conversion result.
  462. * @returns {module:engine/model/range~Range|null} result.modelRange Model range containing result of item conversion,
  463. * created and modified by callbacks attached to fired event, or `null` if the conversion result was incorrect.
  464. * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
  465. */
  466. /**
  467. * Starts conversion of all children of given item by firing appropriate events for all those children.
  468. *
  469. * @method #convertChildren
  470. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
  471. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
  472. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
  473. * @param {module:engine/view/item~Item} viewItem Element which children should be converted.
  474. * @param {module:engine/model/position~Position} modelCursor Position of conversion.
  475. * @returns {Object} result Conversion result.
  476. * @returns {module:engine/model/range~Range} result.modelRange Model range containing results of conversion of all children of given item.
  477. * When no children was converted then range is collapsed.
  478. * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
  479. */
  480. /**
  481. * Checks {@link module:engine/model/schema~Schema schema} to find allowed parent for element that we are going to insert
  482. * starting from given position. If current parent does not allow to insert element but one of the ancestors does then
  483. * split nodes to allowed parent.
  484. *
  485. * If schema allows to insert node in given position, nothing is split and object with that position is returned.
  486. *
  487. * If it was not possible to find allowed parent, `null` is returned, nothing is split.
  488. *
  489. * Otherwise, ancestors are split and object with position and the copy of the split element is returned.
  490. *
  491. * For instance, if `<image>` is not allowed in `<paragraph>` but is allowed in `$root`:
  492. *
  493. * <paragraph>foo[]bar</paragraph>
  494. *
  495. * -> split for `<image>` ->
  496. *
  497. * <paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
  498. *
  499. * In the sample above position between `<paragraph>` elements will be returned as `position` and the second `paragraph`
  500. * as `cursorParent`.
  501. *
  502. * @method #splitToAllowedParent
  503. * @param {module:engine/model/position~Position} position Position on which element is going to be inserted.
  504. * @param {module:engine/model/node~Node} node Node to insert.
  505. * @returns {Object|null} Split result. If it was not possible to find allowed position `null` is returned.
  506. * @returns {module:engine/model/position~Position} position between split elements.
  507. * @returns {module:engine/model/element~Element} [cursorParent] Element inside which cursor should be placed to
  508. * continue conversion. When element is not defined it means that there was no split.
  509. */
  510. /**
  511. * Returns all the split parts of given `element` that were created during upcasting through using {@link #splitToAllowedParent}.
  512. * It enables you to easily track those elements and continue processing them after they are split during their children conversion.
  513. *
  514. * <paragraph>Foo<image />bar<image />baz</paragraph> ->
  515. * <paragraph>Foo</paragraph><image /><paragraph>bar</paragraph><image /><paragraph>baz</paragraph>
  516. *
  517. * For a reference to any of above paragraphs, the function will return all three paragraphs (the original element included),
  518. * sorted in the order of their creation (the original element is the first one).
  519. *
  520. * If given `element` was not split, an array with single element is returned.
  521. *
  522. * Example of a usage in a converter code:
  523. *
  524. * const myElement = conversionApi.writer.createElement( 'myElement' );
  525. *
  526. * // Children conversion may split `myElement`.
  527. * conversionApi.convertChildren( myElement, modelCursor );
  528. *
  529. * const splitParts = conversionApi.getSplitParts( myElement );
  530. * const lastSplitPart = splitParts[ splitParts.length - 1 ];
  531. *
  532. * // Setting `data.modelRange` basing on split parts:
  533. * data.modelRange = conversionApi.writer.createRange(
  534. * conversionApi.writer.createPositionBefore( myElement ),
  535. * conversionApi.writer.createPositionAfter( lastSplitPart )
  536. * );
  537. *
  538. * // Setting `data.modelCursor` to continue after the last split element:
  539. * data.modelCursor = conversionApi.writer.createPositionAfter( lastSplitPart );
  540. *
  541. * **Tip:** if you are unable to get a reference to the original element (for example because the code is split into multiple converters
  542. * or even classes) but it was already converted, you might want to check first element in `data.modelRange`. This is a common situation
  543. * if an attribute converter is separated from an element converter.
  544. *
  545. * @method #getSplitParts
  546. * @param {module:engine/model/element~Element} element
  547. * @returns {Array.<module:engine/model/element~Element>}
  548. */
  549. /**
  550. * Stores information about what parts of processed view item are still waiting to be handled. After a piece of view item
  551. * was converted, appropriate consumable value should be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
  552. *
  553. * @member {module:engine/conversion/viewconsumable~ViewConsumable} #consumable
  554. */
  555. /**
  556. * Custom data stored by converters for conversion process. Custom properties of this object can be defined and use to
  557. * pass parameters between converters.
  558. *
  559. * The difference between this property and `data` parameter of
  560. * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element} is that `data` parameters allows you
  561. * to pass parameters within a single event and `store` within the whole conversion.
  562. *
  563. * @member {Object} #store
  564. */
  565. /**
  566. * The model's schema instance.
  567. *
  568. * @member {module:engine/model/schema~Schema} #schema
  569. */
  570. /**
  571. * The {@link module:engine/model/writer~Writer} instance used to manipulate data during conversion.
  572. *
  573. * @member {module:engine/model/writer~Writer} #writer
  574. */