upcastdispatcher.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  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. * Upcast dispatcher is a central point of the view-to-model conversion, which is a process of
  18. * converting a 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 the view nodes.
  24. *
  25. * The second parameter of the callback is the `data` object with the following properties:
  26. *
  27. * * `data.viewItem` contains a {@link module:engine/view/node~Node view node} or a
  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 conversion succeeds.
  33. * * `data.modelCursor` is a {@link module:engine/model/position~Position position} on which the converter should insert
  34. * the 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. * // A 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. * // The <a> element is inline and is represented by an attribute in the model.
  50. * // This is why you 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> element's font-size style.
  62. * // Note: You should use a low-priority observer in order to ensure that
  63. * // it is 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. * // Do not 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 an element can be converted.
  85. * if ( !conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
  86. * // When an element is already consumed by higher priority converters, do nothing.
  87. * return;
  88. * }
  89. *
  90. * const paragraph = conversionApi.writer.createElement( 'paragraph' );
  91. *
  92. * // Try to safely insert a paragraph at the model cursor - it will find an allowed parent for the current element.
  93. * if ( !conversionApi.safeInsert( paragraph, data.modelCursor ) ) {
  94. * // When an element was not inserted, it means that you cannot insert a 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 the children to a paragraph.
  102. * const { modelRange } = conversionApi.convertChildren( data.viewItem, paragraph ) );
  103. *
  104. * // Update `modelRange` and `modelCursor` in the `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 an upcast dispatcher that operates using the passed API.
  117. *
  118. * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi
  119. * @param {Object} [conversionApi] Additional properties for an interface that will be passed to events fired
  120. * by the upcast dispatcher.
  121. */
  122. constructor( conversionApi = {} ) {
  123. /**
  124. * The list of elements that were created during splitting.
  125. *
  126. * After the 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. * The list of cursor parent elements that were created during splitting.
  134. *
  135. * After the 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. * The position in the temporary structure where the converted content is inserted. The structure reflects the context of
  143. * the target position where the content will be inserted. This property is built 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. * An interface passed by the dispatcher to the event 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. * The part of the view to be converted.
  174. * @param {module:engine/model/writer~Writer} writer An instance of the 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 the result of the conversion process
  177. * wrapped in `DocumentFragment`. Converted marker elements will be set as the 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', 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 a `splitPart` element is a split part of the `originalPart` element.
  368. *
  369. * The 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 the upcast (view-to-model conversion) process.
  419. *
  420. * @event viewCleanup
  421. * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
  422. * viewItem A part of the view to be converted.
  423. */
  424. /**
  425. * Fired when an {@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 the pattern of
  428. * `element:<elementName>` where `elementName` is the name of the converted element. This way listeners may listen to
  429. * a conversion of all or just specific elements.
  430. *
  431. * @event element
  432. * @param {module:engine/conversion/upcastdispatcher~UpcastConversionData} data The conversion data. Keep in mind that this object is
  433. * shared by reference between all callbacks that will be called. This means that callbacks can override values if needed, and these
  434. * values will be available in other callbacks.
  435. * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion utilities to be used by the
  436. * callback.
  437. */
  438. /**
  439. * Fired when a {@link module:engine/view/text~Text} is converted.
  440. *
  441. * @event text
  442. * @see #event:element
  443. */
  444. /**
  445. * Fired when a {@link module:engine/view/documentfragment~DocumentFragment} is converted.
  446. *
  447. * @event documentFragment
  448. * @see #event:element
  449. */
  450. }
  451. mix( UpcastDispatcher, EmitterMixin );
  452. // Traverses given model item and searches elements which marks marker range. Found element is removed from
  453. // DocumentFragment but path of this element is stored in a Map which is then returned.
  454. //
  455. // @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
  456. // @returns {Map<String, module:engine/model/range~Range>} List of static markers.
  457. function extractMarkersFromModelFragment( modelItem, writer ) {
  458. const markerElements = new Set();
  459. const markers = new Map();
  460. // Create ModelTreeWalker.
  461. const range = ModelRange._createIn( modelItem ).getItems();
  462. // Walk through DocumentFragment and collect marker elements.
  463. for ( const item of range ) {
  464. // Check if current element is a marker.
  465. if ( item.name == '$marker' ) {
  466. markerElements.add( item );
  467. }
  468. }
  469. // Walk through collected marker elements store its path and remove its from the DocumentFragment.
  470. for ( const markerElement of markerElements ) {
  471. const markerName = markerElement.getAttribute( 'data-name' );
  472. const currentPosition = writer.createPositionBefore( markerElement );
  473. // When marker of given name is not stored it means that we have found the beginning of the range.
  474. if ( !markers.has( markerName ) ) {
  475. markers.set( markerName, new ModelRange( currentPosition.clone() ) );
  476. // Otherwise is means that we have found end of the marker range.
  477. } else {
  478. markers.get( markerName ).end = currentPosition.clone();
  479. }
  480. // Remove marker element from DocumentFragment.
  481. writer.remove( markerElement );
  482. }
  483. return markers;
  484. }
  485. // Creates model fragment according to given context and returns position in the bottom (the deepest) element.
  486. function createContextTree( contextDefinition, writer ) {
  487. let position;
  488. for ( const item of new SchemaContext( contextDefinition ) ) {
  489. const attributes = {};
  490. for ( const key of item.getAttributeKeys() ) {
  491. attributes[ key ] = item.getAttribute( key );
  492. }
  493. const current = writer.createElement( item.name, attributes );
  494. if ( position ) {
  495. writer.append( current, position );
  496. }
  497. position = ModelPosition._createAt( current, 0 );
  498. }
  499. return position;
  500. }
  501. /**
  502. * A set of conversion utilities available as the third parameter of the
  503. * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher upcast dispatcher}'s events.
  504. *
  505. * @interface module:engine/conversion/upcastdispatcher~UpcastConversionApi
  506. */
  507. /**
  508. * Starts the conversion of a given item by firing an appropriate event.
  509. *
  510. * Every fired event is passed (as the first parameter) an object with the `modelRange` property. Every event may set and/or
  511. * modify that property. When all callbacks are done, the final value of the `modelRange` property is returned by this method.
  512. * The `modelRange` must be a {@link module:engine/model/range~Range model range} or `null` (as set by default).
  513. *
  514. * @method #convertItem
  515. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
  516. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
  517. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
  518. * @param {module:engine/view/item~Item} viewItem Item to convert.
  519. * @param {module:engine/model/position~Position} modelCursor The conversion position.
  520. * @returns {Object} result The conversion result.
  521. * @returns {module:engine/model/range~Range|null} result.modelRange The model range containing the result of the item conversion,
  522. * created and modified by callbacks attached to the fired event, or `null` if the conversion result was incorrect.
  523. * @returns {module:engine/model/position~Position} result.modelCursor The position where the conversion should be continued.
  524. */
  525. /**
  526. * Starts the conversion of all children of a given item by firing appropriate events for all the children.
  527. *
  528. * @method #convertChildren
  529. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
  530. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
  531. * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
  532. * @param {module:engine/view/item~Item} viewItem An element whose children should be converted.
  533. * @param {module:engine/model/position~Position|module:engine/model/element~Element} positionOrElement A position or an element of
  534. * the conversion.
  535. * @returns {Object} result The conversion result.
  536. * @returns {module:engine/model/range~Range} result.modelRange The model range containing the results of the conversion of all children
  537. * of the given item. When no child was converted, the range is collapsed.
  538. * @returns {module:engine/model/position~Position} result.modelCursor The position where the conversion should be continued.
  539. */
  540. /**
  541. * Safely inserts an element to the document, checking the {@link module:engine/model/schema~Schema schema} to find an allowed parent for
  542. * an element that you are going to insert, starting from the given position. If the current parent does not allow to insert the element
  543. * but one of the ancestors does, then splits the nodes to allowed parent.
  544. *
  545. * If the schema allows to insert the node in a given position, nothing is split.
  546. *
  547. * If it was not possible to find an allowed parent, `false` is returned and nothing is split.
  548. *
  549. * Otherwise, ancestors are split.
  550. *
  551. * For instance, if `<image>` is not allowed in `<paragraph>` but is allowed in `$root`:
  552. *
  553. * <paragraph>foo[]bar</paragraph>
  554. *
  555. * -> safe insert for `<image>` will split ->
  556. *
  557. * <paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
  558. *
  559. * Example usage:
  560. *
  561. * const myElement = conversionApi.writer.createElement( 'myElement' );
  562. *
  563. * if ( !conversionApi.safeInsert( myElement, data.modelCursor ) ) {
  564. * return;
  565. * }
  566. *
  567. * The split result is saved and {@link #updateConversionResult} should be used to update the
  568. * {@link module:engine/conversion/upcastdispatcher~UpcastConversionData conversion data}.
  569. *
  570. * @method #safeInsert
  571. * @param {module:engine/model/node~Node} node The node to insert.
  572. * @param {module:engine/model/position~Position} position The position where an element is going to be inserted.
  573. * @returns {Boolean} The split result. If it was not possible to find an allowed position, `false` is returned.
  574. */
  575. /**
  576. * Updates the conversion result and sets a proper {@link module:engine/conversion/upcastdispatcher~UpcastConversionData#modelRange} and
  577. * the next {@link module:engine/conversion/upcastdispatcher~UpcastConversionData#modelCursor} after the conversion.
  578. * Used together with {@link #safeInsert}, it enables you to easily convert elements without worrying if the node was split
  579. * during the conversion of its children.
  580. *
  581. * A usage example in converter code:
  582. *
  583. * const myElement = conversionApi.writer.createElement( 'myElement' );
  584. *
  585. * if ( !conversionApi.safeInsert( myElement, data.modelCursor ) ) {
  586. * return;
  587. * }
  588. *
  589. * // Children conversion may split `myElement`.
  590. * conversionApi.convertChildren( data.viewItem, myElement );
  591. *
  592. * conversionApi.updateConversionResult( myElement, data );
  593. *
  594. * @method #updateConversionResult
  595. * @param {module:engine/model/element~Element} element
  596. * @param {module:engine/conversion/upcastdispatcher~UpcastConversionData} data Conversion data.
  597. * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion utilities to be used by the callback.
  598. */
  599. /**
  600. * Checks the {@link module:engine/model/schema~Schema schema} to find an allowed parent for an element that is going to be inserted
  601. * starting from the given position. If the current parent does not allow inserting an element but one of the ancestors does, the method
  602. * splits nodes to allowed parent.
  603. *
  604. * If the schema allows inserting the node in the given position, nothing is split and an object with that position is returned.
  605. *
  606. * If it was not possible to find an allowed parent, `null` is returned and nothing is split.
  607. *
  608. * Otherwise, ancestors are split and an object with a position and the copy of the split element is returned.
  609. *
  610. * For instance, if `<image>` is not allowed in `<paragraph>` but is allowed in `$root`:
  611. *
  612. * <paragraph>foo[]bar</paragraph>
  613. *
  614. * -> split for `<image>` ->
  615. *
  616. * <paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
  617. *
  618. * In the example above, the position between `<paragraph>` elements will be returned as `position` and the second `paragraph`
  619. * as `cursorParent`.
  620. *
  621. * **Note:** This is an advanced method. For most cases {@link #safeInsert} and {@link #updateConversionResult} should be used.
  622. *
  623. * @method #splitToAllowedParent
  624. * @param {module:engine/model/position~Position} position The position where the element is going to be inserted.
  625. * @param {module:engine/model/node~Node} node The node to insert.
  626. * @returns {Object|null} The split result. If it was not possible to find an allowed position, `null` is returned.
  627. * @returns {module:engine/model/position~Position} The position between split elements.
  628. * @returns {module:engine/model/element~Element} [cursorParent] The element inside which the cursor should be placed to
  629. * continue the conversion. When the element is not defined it means that there was no split.
  630. */
  631. /**
  632. * Returns all the split parts of the given `element` that were created during upcasting through using {@link #splitToAllowedParent}.
  633. * It enables you to easily track these elements and continue processing them after they are split during the conversion of their children.
  634. *
  635. * <paragraph>Foo<image />bar<image />baz</paragraph> ->
  636. * <paragraph>Foo</paragraph><image /><paragraph>bar</paragraph><image /><paragraph>baz</paragraph>
  637. *
  638. * For a reference to any of above paragraphs, the function will return all three paragraphs (the original element included),
  639. * sorted in the order of their creation (the original element is the first one).
  640. *
  641. * If the given `element` was not split, an array with a single element is returned.
  642. *
  643. * A usage example in the converter code:
  644. *
  645. * const myElement = conversionApi.writer.createElement( 'myElement' );
  646. *
  647. * // Children conversion may split `myElement`.
  648. * conversionApi.convertChildren( data.viewItem, data.modelCursor );
  649. *
  650. * const splitParts = conversionApi.getSplitParts( myElement );
  651. * const lastSplitPart = splitParts[ splitParts.length - 1 ];
  652. *
  653. * // Setting `data.modelRange` basing on split parts:
  654. * data.modelRange = conversionApi.writer.createRange(
  655. * conversionApi.writer.createPositionBefore( myElement ),
  656. * conversionApi.writer.createPositionAfter( lastSplitPart )
  657. * );
  658. *
  659. * // Setting `data.modelCursor` to continue after the last split element:
  660. * data.modelCursor = conversionApi.writer.createPositionAfter( lastSplitPart );
  661. *
  662. * **Tip:** If you are unable to get a reference to the original element (for example because the code is split into multiple converters
  663. * or even classes) but it has already been converted, you may want to check the first element in `data.modelRange`. This is a common
  664. * situation if an attribute converter is separated from an element converter.
  665. *
  666. * **Note:** This is an advanced method. For most cases {@link #safeInsert} and {@link #updateConversionResult} should be used.
  667. *
  668. * @method #getSplitParts
  669. * @param {module:engine/model/element~Element} element
  670. * @returns {Array.<module:engine/model/element~Element>}
  671. */
  672. /**
  673. * Stores information about what parts of the processed view item are still waiting to be handled. After a piece of view item
  674. * was converted, an appropriate consumable value should be
  675. * {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
  676. *
  677. * @member {module:engine/conversion/viewconsumable~ViewConsumable} #consumable
  678. */
  679. /**
  680. * Custom data stored by converters for the conversion process. Custom properties of this object can be defined and use to
  681. * pass parameters between converters.
  682. *
  683. * The difference between this property and the `data` parameter of
  684. * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element} is that the `data` parameters allow you
  685. * to pass parameters within a single event and `store` within the whole conversion.
  686. *
  687. * @member {Object} #store
  688. */
  689. /**
  690. * The model's schema instance.
  691. *
  692. * @member {module:engine/model/schema~Schema} #schema
  693. */
  694. /**
  695. * The {@link module:engine/model/writer~Writer} instance used to manipulate the data during conversion.
  696. *
  697. * @member {module:engine/model/writer~Writer} #writer
  698. */
  699. /**
  700. * Conversion data.
  701. *
  702. * **Note:** Keep in mind that this object is shared by reference between all conversion callbacks that will be called.
  703. * This means that callbacks can override values if needed, and these values will be available in other callbacks.
  704. *
  705. * @typedef {Object} module:engine/conversion/upcastdispatcher~UpcastConversionData
  706. *
  707. * @property {module:engine/view/item~Item} viewItem The converted item.
  708. * @property {module:engine/model/position~Position} modelCursor The position where the converter should start changes.
  709. * Change this value for the next converter to tell where the conversion should continue.
  710. * @property {module:engine/model/range~Range} [modelRange] The current state of conversion result. Every change to
  711. * the converted element should be reflected by setting or modifying this property.
  712. */