upcastdispatcher.js 32 KB

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