upcastdispatcher.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793
  1. /**
  2. * @license Copyright (c) 2003-2020, 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 the model to 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 view element} into a correct model structure.
  19. *
  20. * During the conversion process, the dispatcher fires events for all {@link module:engine/view/node~Node view nodes}
  21. * from the converted view document fragment.
  22. * Special callbacks called "converters" should listen to these events in order to convert these view nodes.
  23. *
  24. * The second parameter of the callback is the `data` object with the following properties:
  25. *
  26. * * `data.viewItem` 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.
  29. * * `data.modelRange` is used to point to the result
  30. * of the current conversion (e.g. the element that is being inserted)
  31. * and is always a {@link module:engine/model/range~Range} when the succeeds.
  32. * * `data.modelCursor` is a {@link module:engine/model/position~Position position} on which the converter should insert
  33. * newly created items.
  34. *
  35. * The third parameter of the callback is an instance of {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi}
  36. * which provides additional tools for converters.
  37. *
  38. * You can read more about conversion in the following guides:
  39. *
  40. * * {@glink framework/guides/deep-dive/conversion/conversion-introduction Advanced conversion concepts — attributes}
  41. * * {@glink framework/guides/deep-dive/conversion/custom-element-conversion Custom element conversion}
  42. *
  43. * Examples of event-based converters:
  44. *
  45. * // Converter for links (<a>).
  46. * editor.data.upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
  47. * if ( conversionApi.consumable.consume( data.viewItem, { name: true, attributes: [ 'href' ] } ) ) {
  48. * // <a> element is inline and is represented by an attribute in the model.
  49. * // This is why we need to convert only children.
  50. * const { modelRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
  51. *
  52. * for ( let item of modelRange.getItems() ) {
  53. * if ( conversionApi.schema.checkAttribute( item, 'linkHref' ) ) {
  54. * conversionApi.writer.setAttribute( 'linkHref', data.viewItem.getAttribute( 'href' ), item );
  55. * }
  56. * }
  57. * }
  58. * } );
  59. *
  60. * // Convert <p>'s font-size style.
  61. * // Note: You should use a low-priority observer in order to ensure that
  62. * // it's executed after the element-to-element converter.
  63. * editor.data.upcastDispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
  64. * const { consumable, schema, writer } = conversionApi;
  65. *
  66. * if ( !consumable.consume( data.viewItem, { style: 'font-size' } ) ) {
  67. * return;
  68. * }
  69. *
  70. * const fontSize = data.viewItem.getStyle( 'font-size' );
  71. *
  72. * // Don't go for the model element after data.modelCursor because it might happen
  73. * // that a single view element was converted to multiple model elements. Get all of them.
  74. * for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
  75. * if ( schema.checkAttribute( item, 'fontSize' ) ) {
  76. * writer.setAttribute( 'fontSize', fontSize, item );
  77. * }
  78. * }
  79. * }, { priority: 'low' } );
  80. *
  81. * // Convert all elements which have no custom converter into a paragraph (autoparagraphing).
  82. * editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
  83. * // Check if element can be converted.
  84. * if ( !conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
  85. * // When element is already consumed by higher priority converters then do nothing.
  86. * return;
  87. * }
  88. *
  89. * const paragraph = conversionApi.writer.createElement( 'paragraph' );
  90. *
  91. * // Try to safely insert paragraph at model cursor - it will find an allowed parent for a current element.
  92. * if ( !conversionApi.safeInsert( paragraph, data.modelCursor ) ) {
  93. * // When element was not inserted it means that we can't insert paragraph at this position.
  94. * return;
  95. * }
  96. *
  97. * // Consume the inserted element.
  98. * conversionApi.consumable.consume( data.viewItem, { name: data.viewItem.name } ) );
  99. *
  100. * // Convert children to paragraph.
  101. * const { modelRange } = conversionApi.convertChildren( data.viewItem, paragraph ) );
  102. *
  103. * // Update `modelRange` and `modelCursor` in a `data` as a conversion result.
  104. * conversionApi.updateConversionResult( paragraph, data );
  105. * }, { priority: 'low' } );
  106. *
  107. * @mixes module:utils/emittermixin~EmitterMixin
  108. * @fires viewCleanup
  109. * @fires element
  110. * @fires text
  111. * @fires documentFragment
  112. */
  113. export default class UpcastDispatcher {
  114. /**
  115. * Creates a `UpcastDispatcher` that operates using passed API.
  116. *
  117. * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi
  118. * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
  119. * by `UpcastDispatcher`.
  120. */
  121. constructor( conversionApi = {} ) {
  122. /**
  123. * List of the elements that were created during splitting.
  124. *
  125. * After conversion process the list is cleared.
  126. *
  127. * @private
  128. * @type {Map.<module:engine/model/element~Element,Array.<module:engine/model/element~Element>>}
  129. */
  130. this._splitParts = new Map();
  131. /**
  132. * List of cursor parent elements that were created during splitting.
  133. *
  134. * After conversion process the list is cleared.
  135. *
  136. * @private
  137. * @type {Map.<module:engine/model/element~Element,Array.<module:engine/model/element~Element>>}
  138. */
  139. this._cursorParents = new Map();
  140. /**
  141. * Position in the temporary structure where the converted content is inserted. The structure reflect the context of
  142. * the target position where the content will be inserted. This property is build based on the context parameter of the
  143. * convert method.
  144. *
  145. * @private
  146. * @type {module:engine/model/position~Position|null}
  147. */
  148. this._modelCursor = null;
  149. /**
  150. * Interface passed by dispatcher to the events callbacks.
  151. *
  152. * @member {module:engine/conversion/upcastdispatcher~UpcastConversionApi}
  153. */
  154. this.conversionApi = Object.assign( {}, conversionApi );
  155. // The below methods are bound to this `UpcastDispatcher` instance and set on `conversionApi`.
  156. // This way only a part of `UpcastDispatcher` API is exposed.
  157. this.conversionApi.convertItem = this._convertItem.bind( this );
  158. this.conversionApi.convertChildren = this._convertChildren.bind( this );
  159. this.conversionApi.safeInsert = this._safeInsert.bind( this );
  160. this.conversionApi.updateConversionResult = this._updateConversionResult.bind( this );
  161. // Advanced API - use only if custom position handling is needed.
  162. this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.bind( this );
  163. this.conversionApi.getSplitParts = this._getSplitParts.bind( this );
  164. }
  165. /**
  166. * Starts the conversion process. The entry point for the conversion.
  167. *
  168. * @fires element
  169. * @fires text
  170. * @fires documentFragment
  171. * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
  172. * Part of the view to be converted.
  173. * @param {module:engine/model/writer~Writer} writer Instance of model writer.
  174. * @param {module:engine/model/schema~SchemaContextDefinition} [context=['$root']] Elements will be converted according to this context.
  175. * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process
  176. * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's
  177. * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
  178. */
  179. convert( viewItem, writer, context = [ '$root' ] ) {
  180. this.fire( 'viewCleanup', viewItem );
  181. // Create context tree and set position in the top element.
  182. // Items will be converted according to this position.
  183. this._modelCursor = createContextTree( context, writer );
  184. // Store writer in conversion as a conversion API
  185. // to be sure that conversion process will use the same batch.
  186. this.conversionApi.writer = writer;
  187. // Create consumable values list for conversion process.
  188. this.conversionApi.consumable = ViewConsumable.createFrom( viewItem );
  189. // Custom data stored by converter for conversion process.
  190. this.conversionApi.store = {};
  191. // Do the conversion.
  192. const { modelRange } = this._convertItem( viewItem, this._modelCursor );
  193. // Conversion result is always a document fragment so let's create it.
  194. const documentFragment = writer.createDocumentFragment();
  195. // When there is a conversion result.
  196. if ( modelRange ) {
  197. // Remove all empty elements that were create while splitting.
  198. this._removeEmptyElements();
  199. // Move all items that were converted in context tree to the document fragment.
  200. for ( const item of Array.from( this._modelCursor.parent.getChildren() ) ) {
  201. writer.append( item, documentFragment );
  202. }
  203. // Extract temporary markers elements from model and set as static markers collection.
  204. documentFragment.markers = extractMarkersFromModelFragment( documentFragment, writer );
  205. }
  206. // Clear context position.
  207. this._modelCursor = null;
  208. // Clear split elements & parents lists.
  209. this._splitParts.clear();
  210. this._cursorParents.clear();
  211. // Clear conversion API.
  212. this.conversionApi.writer = null;
  213. this.conversionApi.store = null;
  214. // Return fragment as conversion result.
  215. return documentFragment;
  216. }
  217. /**
  218. * @private
  219. * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertItem
  220. */
  221. _convertItem( viewItem, modelCursor ) {
  222. const data = Object.assign( { viewItem, modelCursor, modelRange: null } );
  223. if ( viewItem.is( 'element' ) ) {
  224. this.fire( 'element:' + viewItem.name, data, this.conversionApi );
  225. } else if ( viewItem.is( '$text' ) ) {
  226. this.fire( 'text', data, this.conversionApi );
  227. } else {
  228. this.fire( 'documentFragment', data, this.conversionApi );
  229. }
  230. // Handle incorrect conversion result.
  231. if ( data.modelRange && !( data.modelRange instanceof ModelRange ) ) {
  232. /**
  233. * Incorrect conversion result was dropped.
  234. *
  235. * {@link module:engine/model/range~Range Model range} should be a conversion result.
  236. *
  237. * @error view-conversion-dispatcher-incorrect-result
  238. */
  239. throw new CKEditorError( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.', this );
  240. }
  241. return { modelRange: data.modelRange, modelCursor: data.modelCursor };
  242. }
  243. /**
  244. * @private
  245. * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertChildren
  246. */
  247. _convertChildren( viewItem, elementOrModelCursor ) {
  248. let nextModelCursor = elementOrModelCursor.is( 'position' ) ?
  249. elementOrModelCursor : ModelPosition._createAt( elementOrModelCursor, 0 );
  250. const modelRange = new ModelRange( nextModelCursor );
  251. for ( const viewChild of Array.from( viewItem.getChildren() ) ) {
  252. const result = this._convertItem( viewChild, nextModelCursor );
  253. if ( result.modelRange instanceof ModelRange ) {
  254. modelRange.end = result.modelRange.end;
  255. nextModelCursor = result.modelCursor;
  256. }
  257. }
  258. return { modelRange, modelCursor: nextModelCursor };
  259. }
  260. /**
  261. * @private
  262. * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#safeInsert
  263. */
  264. _safeInsert( modelElement, position ) {
  265. // Find allowed parent for element that we are going to insert.
  266. // If current parent does not allow to insert element but one of the ancestors does
  267. // then split nodes to allowed parent.
  268. const splitResult = this._splitToAllowedParent( modelElement, position );
  269. // When there is no split result it means that we can't insert element to model tree, so let's skip it.
  270. if ( !splitResult ) {
  271. return false;
  272. }
  273. // Insert element on allowed position.
  274. this.conversionApi.writer.insert( modelElement, splitResult.position );
  275. return true;
  276. }
  277. /**
  278. * @private
  279. * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#updateConversionResult
  280. */
  281. _updateConversionResult( modelElement, data ) {
  282. const parts = this._getSplitParts( modelElement );
  283. const writer = this.conversionApi.writer;
  284. // Set conversion result range - only if not set already.
  285. if ( !data.modelRange ) {
  286. data.modelRange = writer.createRange(
  287. writer.createPositionBefore( modelElement ),
  288. writer.createPositionAfter( parts[ parts.length - 1 ] )
  289. );
  290. }
  291. const savedCursorParent = this._cursorParents.get( modelElement );
  292. // Now we need to check where the `modelCursor` should be.
  293. if ( savedCursorParent ) {
  294. // If we split parent to insert our element then we want to continue conversion in the new part of the split parent.
  295. //
  296. // before: <allowed><notAllowed>foo[]</notAllowed></allowed>
  297. // after: <allowed><notAllowed>foo</notAllowed> <converted></converted> <notAllowed>[]</notAllowed></allowed>
  298. data.modelCursor = writer.createPositionAt( savedCursorParent, 0 );
  299. } else {
  300. // Otherwise just continue after inserted element.
  301. data.modelCursor = data.modelRange.end;
  302. }
  303. }
  304. /**
  305. * @private
  306. * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#splitToAllowedParent
  307. */
  308. _splitToAllowedParent( node, modelCursor ) {
  309. // Try to find allowed parent.
  310. const allowedParent = this.conversionApi.schema.findAllowedParent( modelCursor, node );
  311. // When there is no parent that allows to insert node then return `null`.
  312. if ( !allowedParent ) {
  313. return null;
  314. }
  315. // When current position parent allows to insert node then return this position.
  316. if ( allowedParent === modelCursor.parent ) {
  317. return { position: modelCursor };
  318. }
  319. // When allowed parent is in context tree.
  320. if ( this._modelCursor.parent.getAncestors().includes( allowedParent ) ) {
  321. return null;
  322. }
  323. // Split element to allowed parent.
  324. const splitResult = this.conversionApi.writer.split( modelCursor, allowedParent );
  325. // Using the range returned by `model.Writer#split`, we will pair original elements with their split parts.
  326. //
  327. // The range returned from the writer spans "over the split" or, precisely saying, from the end of the original element (the one
  328. // that got split) to the beginning of the other part of that element:
  329. //
  330. // <limit><a><b><c>X[]Y</c></b><a></limit> ->
  331. // <limit><a><b><c>X[</c></b></a><a><b><c>]Y</c></b></a>
  332. //
  333. // After the split there cannot be any full node between the positions in `splitRange`. The positions are touching.
  334. // Also, because of how splitting works, it is easy to notice, that "closing tags" are in the reverse order than "opening tags".
  335. // Also, since we split all those elements, each of them has to have the other part.
  336. //
  337. // With those observations in mind, we will pair the original elements with their split parts by saving "closing tags" and matching
  338. // them with "opening tags" in the reverse order. For that we can use a stack.
  339. const stack = [];
  340. for ( const treeWalkerValue of splitResult.range.getWalker() ) {
  341. if ( treeWalkerValue.type == 'elementEnd' ) {
  342. stack.push( treeWalkerValue.item );
  343. } else {
  344. // There should not be any text nodes after the element is split, so the only other value is `elementStart`.
  345. const originalPart = stack.pop();
  346. const splitPart = treeWalkerValue.item;
  347. this._registerSplitPair( originalPart, splitPart );
  348. }
  349. }
  350. const cursorParent = splitResult.range.end.parent;
  351. this._cursorParents.set( node, cursorParent );
  352. return {
  353. position: splitResult.position,
  354. cursorParent
  355. };
  356. }
  357. /**
  358. * Registers that `splitPart` element is a split part of the `originalPart` element.
  359. *
  360. * Data set by this method is used by {@link #_getSplitParts} and {@link #_removeEmptyElements}.
  361. *
  362. * @private
  363. * @param {module:engine/model/element~Element} originalPart
  364. * @param {module:engine/model/element~Element} splitPart
  365. */
  366. _registerSplitPair( originalPart, splitPart ) {
  367. if ( !this._splitParts.has( originalPart ) ) {
  368. this._splitParts.set( originalPart, [ originalPart ] );
  369. }
  370. const list = this._splitParts.get( originalPart );
  371. this._splitParts.set( splitPart, list );
  372. list.push( splitPart );
  373. }
  374. /**
  375. * @private
  376. * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#getSplitParts
  377. */
  378. _getSplitParts( element ) {
  379. let parts;
  380. if ( !this._splitParts.has( element ) ) {
  381. parts = [ element ];
  382. } else {
  383. parts = this._splitParts.get( element );
  384. }
  385. return parts;
  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. * @private
  394. */
  395. _removeEmptyElements() {
  396. let anyRemoved = false;
  397. for ( const element of this._splitParts.keys() ) {
  398. if ( element.isEmpty ) {
  399. this.conversionApi.writer.remove( element );
  400. this._splitParts.delete( element );
  401. anyRemoved = true;
  402. }
  403. }
  404. if ( anyRemoved ) {
  405. this._removeEmptyElements();
  406. }
  407. }
  408. /**
  409. * Fired before the first conversion event, at the beginning of upcast (view to model conversion) process.
  410. *
  411. * @event viewCleanup
  412. * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
  413. * viewItem Part of the view to be converted.
  414. */
  415. /**
  416. * Fired when {@link module:engine/view/element~Element} is converted.
  417. *
  418. * `element` is a namespace event for a class of events. Names of actually called events follow this pattern:
  419. * `element:<elementName>` where `elementName` is the name of converted element. This way listeners may listen to
  420. * all elements conversion or to conversion of specific elements.
  421. *
  422. * @event element
  423. * @param {module:engine/conversion/upcastdispatcher~UpcastConversionData} data Conversion data. Keep in mind that this object is shared
  424. * by reference between all callbacks that will be called. This means that callbacks can override values if needed, and those values
  425. * will be available in other callbacks.
  426. * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion utilities to be used by callback.
  427. */
  428. /**
  429. * Fired when {@link module:engine/view/text~Text} is converted.
  430. *
  431. * @event text
  432. * @see #event:element
  433. */
  434. /**
  435. * Fired when {@link module:engine/view/documentfragment~DocumentFragment} is converted.
  436. *
  437. * @event documentFragment
  438. * @see #event:element
  439. */
  440. }
  441. mix( UpcastDispatcher, EmitterMixin );
  442. // Traverses given model item and searches elements which marks marker range. Found element is removed from
  443. // DocumentFragment but path of this element is stored in a Map which is then returned.
  444. //
  445. // @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
  446. // @returns {Map<String, module:engine/model/range~Range>} List of static markers.
  447. function extractMarkersFromModelFragment( modelItem, writer ) {
  448. const markerElements = new Set();
  449. const markers = new Map();
  450. // Create ModelTreeWalker.
  451. const range = ModelRange._createIn( modelItem ).getItems();
  452. // Walk through DocumentFragment and collect marker elements.
  453. for ( const item of range ) {
  454. // Check if current element is a marker.
  455. if ( item.name == '$marker' ) {
  456. markerElements.add( item );
  457. }
  458. }
  459. // Walk through collected marker elements store its path and remove its from the DocumentFragment.
  460. for ( const markerElement of markerElements ) {
  461. const markerName = markerElement.getAttribute( 'data-name' );
  462. const currentPosition = writer.createPositionBefore( markerElement );
  463. // When marker of given name is not stored it means that we have found the beginning of the range.
  464. if ( !markers.has( markerName ) ) {
  465. markers.set( markerName, new ModelRange( currentPosition.clone() ) );
  466. // Otherwise is means that we have found end of the marker range.
  467. } else {
  468. markers.get( markerName ).end = currentPosition.clone();
  469. }
  470. // Remove marker element from DocumentFragment.
  471. writer.remove( markerElement );
  472. }
  473. return markers;
  474. }
  475. // Creates model fragment according to given context and returns position in the bottom (the deepest) element.
  476. function createContextTree( contextDefinition, writer ) {
  477. let position;
  478. for ( const item of new SchemaContext( contextDefinition ) ) {
  479. const attributes = {};
  480. for ( const key of item.getAttributeKeys() ) {
  481. attributes[ key ] = item.getAttribute( key );
  482. }
  483. const current = writer.createElement( item.name, attributes );
  484. if ( position ) {
  485. writer.append( current, position );
  486. }
  487. position = ModelPosition._createAt( current, 0 );
  488. }
  489. return position;
  490. }
  491. /**
  492. * A set of conversion utils available as the third parameter of
  493. * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher upcast dispatcher}'s events.
  494. *
  495. * @interface module:engine/conversion/upcastdispatcher~UpcastConversionApi
  496. */
  497. /**
  498. * Starts conversion of given item by firing an appropriate event.
  499. *
  500. * Every fired event is passed (as first parameter) an object with `modelRange` property. Every event may set and/or
  501. * modify that property. When all callbacks are done, the final value of `modelRange` property is returned by this method.
  502. * The `modelRange` must be {@link module:engine/model/range~Range model range} or `null` (as set by default).
  503. *
  504. * @method #convertItem
  505. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
  506. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
  507. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
  508. * @param {module:engine/view/item~Item} viewItem Item to convert.
  509. * @param {module:engine/model/position~Position} modelCursor Position of conversion.
  510. * @returns {Object} result Conversion result.
  511. * @returns {module:engine/model/range~Range|null} result.modelRange Model range containing result of item conversion,
  512. * created and modified by callbacks attached to fired event, or `null` if the conversion result was incorrect.
  513. * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
  514. */
  515. /**
  516. * Starts conversion of all children of given item by firing appropriate events for all those children.
  517. *
  518. * @method #convertChildren
  519. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
  520. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
  521. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
  522. * @param {module:engine/view/item~Item} viewItem Element which children should be converted.
  523. * @param {module:engine/model/position~Position|module:engine/model/element~Element} positionOrElement Position or element of conversion.
  524. * @returns {Object} result Conversion result.
  525. * @returns {module:engine/model/range~Range} result.modelRange Model range containing results of conversion of all children of given item.
  526. * When no children was converted then range is collapsed.
  527. * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
  528. */
  529. /**
  530. * Safely inserts an element to the document checking {@link module:engine/model/schema~Schema schema} to find allowed parent for
  531. * an element that we are going to insert starting from given position. If current parent does not allow to insert element
  532. * but one of the ancestors does then split nodes to allowed parent.
  533. *
  534. * If schema allows to insert node in given position, nothing is split.
  535. *
  536. * If it was not possible to find allowed parent, `false` is returned, nothing is split.
  537. *
  538. * Otherwise, ancestors are split.
  539. *
  540. * For instance, if `<image>` is not allowed in `<paragraph>` but is allowed in `$root`:
  541. *
  542. * <paragraph>foo[]bar</paragraph>
  543. *
  544. * -> safe insert for `<image>` will split ->
  545. *
  546. * <paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
  547. *
  548. * Example usage:
  549. *
  550. * const myElement = conversionApi.writer.createElement( 'myElement' );
  551. *
  552. * if ( !conversionApi.safeInsert( myElement, data.modelCursor ) ) {
  553. * return;
  554. * }
  555. *
  556. * The split result is saved and {@link #updateConversionResult} should be used to update
  557. * {@link module:engine/conversion/upcastdispatcher~UpcastConversionData conversion data}.
  558. *
  559. * @method #safeInsert
  560. * @param {module:engine/model/node~Node} node Node to insert.
  561. * @param {module:engine/model/position~Position} position Position on which element is going to be inserted.
  562. * @returns {Boolean} Split result. If it was not possible to find allowed position `false` is returned.
  563. */
  564. /**
  565. * Updates the conversion result and sets proper {@link module:engine/conversion/upcastdispatcher~UpcastConversionData#modelRange} and
  566. * next {@link module:engine/conversion/upcastdispatcher~UpcastConversionData#modelCursor} after the conversion.
  567. * Used together with {@link #safeInsert} enables you to easily convert elements without worrying if the node was split
  568. * during its children conversion.
  569. *
  570. * Example of a usage in a converter code:
  571. *
  572. * const myElement = conversionApi.writer.createElement( 'myElement' );
  573. *
  574. * if ( !conversionApi.safeInsert( myElement, data.modelCursor ) ) {
  575. * return;
  576. * }
  577. *
  578. * // Children conversion may split `myElement`.
  579. * conversionApi.convertChildren( data.viewItem, myElement );
  580. *
  581. * conversionApi.updateConversionResult( myElement, data );
  582. *
  583. * @method #updateConversionResult
  584. * @param {module:engine/model/element~Element} element
  585. * @param {module:engine/conversion/upcastdispatcher~UpcastConversionData} data Conversion data.
  586. * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion utilities to be used by callback.
  587. */
  588. /**
  589. * Checks {@link module:engine/model/schema~Schema schema} to find allowed parent for element that we are going to insert
  590. * starting from given position. If current parent does not allow to insert element but one of the ancestors does then
  591. * split nodes to allowed parent.
  592. *
  593. * If schema allows to insert node in given position, nothing is split and object with that position is returned.
  594. *
  595. * If it was not possible to find allowed parent, `null` is returned, nothing is split.
  596. *
  597. * Otherwise, ancestors are split and object with position and the copy of the split element is returned.
  598. *
  599. * For instance, if `<image>` is not allowed in `<paragraph>` but is allowed in `$root`:
  600. *
  601. * <paragraph>foo[]bar</paragraph>
  602. *
  603. * -> split for `<image>` ->
  604. *
  605. * <paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
  606. *
  607. * In the sample above position between `<paragraph>` elements will be returned as `position` and the second `paragraph`
  608. * as `cursorParent`.
  609. *
  610. * **Note:** This is an advanced method. For most cases {@link #safeInsert} and {@link #updateConversionResult} should be used.
  611. *
  612. * @method #splitToAllowedParent
  613. * @param {module:engine/model/position~Position} position Position on which element is going to be inserted.
  614. * @param {module:engine/model/node~Node} node Node to insert.
  615. * @returns {Object|null} Split result. If it was not possible to find allowed position `null` is returned.
  616. * @returns {module:engine/model/position~Position} position between split elements.
  617. * @returns {module:engine/model/element~Element} [cursorParent] Element inside which cursor should be placed to
  618. * continue conversion. When element is not defined it means that there was no split.
  619. */
  620. /**
  621. * Returns all the split parts of given `element` that were created during upcasting through using {@link #splitToAllowedParent}.
  622. * It enables you to easily track those elements and continue processing them after they are split during their children conversion.
  623. *
  624. * <paragraph>Foo<image />bar<image />baz</paragraph> ->
  625. * <paragraph>Foo</paragraph><image /><paragraph>bar</paragraph><image /><paragraph>baz</paragraph>
  626. *
  627. * For a reference to any of above paragraphs, the function will return all three paragraphs (the original element included),
  628. * sorted in the order of their creation (the original element is the first one).
  629. *
  630. * If given `element` was not split, an array with single element is returned.
  631. *
  632. * Example of a usage in a converter code:
  633. *
  634. * const myElement = conversionApi.writer.createElement( 'myElement' );
  635. *
  636. * // Children conversion may split `myElement`.
  637. * conversionApi.convertChildren( data.viewItem, data.modelCursor );
  638. *
  639. * const splitParts = conversionApi.getSplitParts( myElement );
  640. * const lastSplitPart = splitParts[ splitParts.length - 1 ];
  641. *
  642. * // Setting `data.modelRange` basing on split parts:
  643. * data.modelRange = conversionApi.writer.createRange(
  644. * conversionApi.writer.createPositionBefore( myElement ),
  645. * conversionApi.writer.createPositionAfter( lastSplitPart )
  646. * );
  647. *
  648. * // Setting `data.modelCursor` to continue after the last split element:
  649. * data.modelCursor = conversionApi.writer.createPositionAfter( lastSplitPart );
  650. *
  651. * **Tip:** if you are unable to get a reference to the original element (for example because the code is split into multiple converters
  652. * or even classes) but it was already converted, you might want to check first element in `data.modelRange`. This is a common situation
  653. * if an attribute converter is separated from an element converter.
  654. *
  655. * **Note:** This is an advanced method. For most cases {@link #safeInsert} and {@link #updateConversionResult} should be used.
  656. *
  657. * @method #getSplitParts
  658. * @param {module:engine/model/element~Element} element
  659. * @returns {Array.<module:engine/model/element~Element>}
  660. */
  661. /**
  662. * Stores information about what parts of processed view item are still waiting to be handled. After a piece of view item
  663. * was converted, appropriate consumable value should be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
  664. *
  665. * @member {module:engine/conversion/viewconsumable~ViewConsumable} #consumable
  666. */
  667. /**
  668. * Custom data stored by converters for conversion process. Custom properties of this object can be defined and use to
  669. * pass parameters between converters.
  670. *
  671. * The difference between this property and `data` parameter of
  672. * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element} is that `data` parameters allows you
  673. * to pass parameters within a single event and `store` within the whole conversion.
  674. *
  675. * @member {Object} #store
  676. */
  677. /**
  678. * The model's schema instance.
  679. *
  680. * @member {module:engine/model/schema~Schema} #schema
  681. */
  682. /**
  683. * The {@link module:engine/model/writer~Writer} instance used to manipulate data during conversion.
  684. *
  685. * @member {module:engine/model/writer~Writer} #writer
  686. */
  687. /**
  688. * Conversion data.
  689. *
  690. * **Note:** Keep in mind that this object is shared by reference between all conversion callbacks that will be called.
  691. * This means that callbacks can override values if needed, and those values will be available in other callbacks.
  692. *
  693. * @typedef {Object} module:engine/conversion/upcastdispatcher~UpcastConversionData
  694. *
  695. * @property {module:engine/view/item~Item} viewItem Converted item.
  696. * @property {module:engine/model/position~Position} modelCursor Position where a converter should start changes.
  697. * Change this value for the next converter to tell where the conversion should continue.
  698. * @property {module:engine/model/range~Range} [modelRange] The current state of conversion result. Every change to
  699. * converted element should be reflected by setting or modifying this property.
  700. */