8
0

viewconversiondispatcher.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/conversion/viewconversiondispatcher
  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. * `ViewConversionDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of
  17. * converting given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
  18. * {@link module:engine/view/element~Element} into another structure.
  19. * In default application, {@link module:engine/view/view view} is converted to {@link module:engine/model/model}.
  20. *
  21. * During conversion process, for all {@link module:engine/view/node~Node view nodes} from the converted view document fragment,
  22. * `ViewConversionDispatcher` fires corresponding events. Special callbacks called "converters" should listen to
  23. * `ViewConversionDispatcher` for those events.
  24. *
  25. * Each callback, as a first argument, is passed a special object `data` that has `viewItem`, `modelCursor` and
  26. * `modelRange` properties. `viewItem` property contains {@link module:engine/view/node~Node view node} or
  27. * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
  28. * that is converted at the moment and might be handled by the callback. `modelRange` property should be used to save the result
  29. * of conversion and is always a {@link module:engine/model/range~Range} when conversion result is correct.
  30. * `modelCursor` property is a {@link module:engine/model/position~Position position} on which conversion result will be inserted
  31. * and is a context according to {@link module:engine/model/schema~Schema schema} will be checked before the conversion.
  32. * See also {@link ~ViewConversionDispatcher#convert}. It is also shared by reference by all callbacks listening to given event.
  33. *
  34. * The third parameter passed to a callback is an instance of {@link ~ViewConversionDispatcher}
  35. * which provides additional tools for converters.
  36. *
  37. * Examples of providing callbacks for `ViewConversionDispatcher`:
  38. *
  39. * // Converter for paragraphs (<p>).
  40. * viewDispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
  41. * // Create paragraph element.
  42. * const paragraph = conversionApi.createElement( 'paragraph' );
  43. *
  44. * // Check if paragraph is allowed on current cursor position.
  45. * if ( conversionApi.schema.checkChild( data.modelCursor, paragraph ) ) {
  46. * // Try to consume value from consumable list.
  47. * if ( !conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
  48. * // Insert paragraph on cursor position.
  49. * conversionApi.writer.insert( paragraph, data.modelCursor );
  50. *
  51. * // Convert paragraph children to paragraph element.
  52. * conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( paragraph ) );
  53. *
  54. * // Wrap paragraph in range and set as conversion result.
  55. * data.modelRange = ModelRange.createOn( paragraph );
  56. *
  57. * // Continue conversion just after paragraph.
  58. * data.modelCursor = data.modelRange.end;
  59. * }
  60. * }
  61. * } );
  62. *
  63. * // Converter for links (<a>).
  64. * viewDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
  65. * if ( conversionApi.consumable.consume( data.viewItem, { name: true, attributes: [ 'href' ] } ) ) {
  66. * // <a> element is inline and is represented by an attribute in the model.
  67. * // This is why we need to convert only children.
  68. * const { modelRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
  69. *
  70. * for ( let item of modelRange.getItems() ) {
  71. * if ( conversionApi.schema.checkAttribute( item, 'linkHref' ) ) {
  72. * conversionApi.writer.setAttribute( 'linkHref', data.viewItem.getAttribute( 'href' ), item );
  73. * }
  74. * }
  75. * }
  76. * } );
  77. *
  78. * // Fire conversion.
  79. * // Always take care where the converted model structure will be appended to. If this `viewDocumentFragment`
  80. * // is going to be appended directly to a '$root' element, use that in `context`.
  81. * viewDispatcher.convert( viewDocumentFragment, { context: [ '$root' ] } );
  82. *
  83. * Before each conversion process, `ViewConversionDispatcher` fires {@link ~ViewConversionDispatcher#event:viewCleanup}
  84. * event which can be used to prepare tree view for conversion.
  85. *
  86. * @mixes module:utils/emittermixin~EmitterMixin
  87. * @fires viewCleanup
  88. * @fires element
  89. * @fires text
  90. * @fires documentFragment
  91. */
  92. export default class ViewConversionDispatcher {
  93. /**
  94. * Creates a `ViewConversionDispatcher` that operates using passed API.
  95. *
  96. * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi
  97. * @param {module:engine/model/model~Model} model Data model.
  98. * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
  99. * by `ViewConversionDispatcher`.
  100. */
  101. constructor( model, conversionApi = {} ) {
  102. /**
  103. * Data model.
  104. *
  105. * @private
  106. * @type {module:engine/model/model~Model}
  107. */
  108. this._model = model;
  109. /**
  110. * List of elements that will be checked after conversion process and if element in the list will be empty it
  111. * will be removed from conversion result.
  112. *
  113. * After conversion process list is cleared.
  114. *
  115. * @protected
  116. * @type {Set<module:engine/model/element~Element>}
  117. */
  118. this._removeIfEmpty = new Set();
  119. /**
  120. * Position where conversion result will be inserted. Note that it's not exactly position in one of the
  121. * {@link module:engine/model/document~Document#roots document roots} but it's only a similar position.
  122. * At the beginning of conversion process fragment of model tree is created according to given context and this
  123. * position is created in the top element of created fragment. Then {@link module:engine/view/item~Item View items}
  124. * are converted to this position what makes possible to precisely check converted items by
  125. * {@link module:engine/model/schema~Schema}.
  126. *
  127. * After conversion process position is cleared.
  128. *
  129. * @private
  130. * @type {module:engine/model/position~Position|null}
  131. */
  132. this._modelCursor = null;
  133. /**
  134. * Interface passed by dispatcher to the events callbacks.
  135. *
  136. * @member {module:engine/conversion/viewconversiondispatcher~ViewConversionApi}
  137. */
  138. this.conversionApi = Object.assign( {}, conversionApi );
  139. // `convertItem`, `convertChildren` and `splitToAllowedParent` are bound to this `ViewConversionDispatcher`
  140. // instance and set on `conversionApi`. This way only a part of `ViewConversionDispatcher` API is exposed.
  141. this.conversionApi.convertItem = this._convertItem.bind( this );
  142. this.conversionApi.convertChildren = this._convertChildren.bind( this );
  143. this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.bind( this );
  144. }
  145. /**
  146. * Starts the conversion process. The entry point for the conversion.
  147. *
  148. * @fires element
  149. * @fires text
  150. * @fires documentFragment
  151. * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
  152. * Part of the view to be converted.
  153. * @param {module:engine/model/schema~SchemaContextDefinition} [context=['$root']] Elements will be converted according to this context.
  154. * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process
  155. * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's
  156. * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
  157. */
  158. convert( viewItem, context = [ '$root' ] ) {
  159. return this._model.change( writer => {
  160. this.fire( 'viewCleanup', viewItem );
  161. // Create context tree and set position in the top element.
  162. // Items will be converted according to this position.
  163. this._modelCursor = createContextTree( context, writer );
  164. // Store writer in conversion as a conversion API
  165. // to be sure that conversion process will use the same batch.
  166. this.conversionApi.writer = writer;
  167. // Create consumable values list for conversion process.
  168. this.conversionApi.consumable = ViewConsumable.createFrom( viewItem );
  169. // Custom data stored by converter for conversion process.
  170. this.conversionApi.store = {};
  171. // Do the conversion.
  172. const { modelRange } = this._convertItem( viewItem, this._modelCursor );
  173. // Conversion result is always a document fragment so let's create this fragment.
  174. const documentFragment = writer.createDocumentFragment();
  175. // When there is a conversion result.
  176. if ( modelRange ) {
  177. // Remove all empty elements that was added to #_removeIfEmpty list.
  178. this._removeEmptyElements();
  179. // Move all items that was converted to context tree to document fragment.
  180. for ( const item of Array.from( this._modelCursor.parent.getChildren() ) ) {
  181. writer.append( item, documentFragment );
  182. }
  183. // Extract temporary markers elements from model and set as static markers collection.
  184. documentFragment.markers = extractMarkersFromModelFragment( documentFragment, writer );
  185. }
  186. // Clear context position.
  187. this._modelCursor = null;
  188. // Clear split elements.
  189. this._removeIfEmpty.clear();
  190. // Clear conversion API.
  191. this.conversionApi.writer = null;
  192. this.conversionApi.store = null;
  193. // Return fragment as conversion result.
  194. return documentFragment;
  195. } );
  196. }
  197. /**
  198. * @private
  199. * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertItem
  200. */
  201. _convertItem( viewItem, modelCursor ) {
  202. const data = Object.assign( { viewItem, modelCursor, modelRange: null } );
  203. if ( viewItem.is( 'element' ) ) {
  204. this.fire( 'element:' + viewItem.name, data, this.conversionApi );
  205. } else if ( viewItem.is( 'text' ) ) {
  206. this.fire( 'text', data, this.conversionApi );
  207. } else {
  208. this.fire( 'documentFragment', data, this.conversionApi );
  209. }
  210. // Handle incorrect conversion result.
  211. if ( data.modelRange && !( data.modelRange instanceof ModelRange ) ) {
  212. /**
  213. * Incorrect conversion result was dropped.
  214. *
  215. * {@link module:engine/model/range~Range Model range} should be a conversion result.
  216. *
  217. * @error view-conversion-dispatcher-incorrect-result
  218. */
  219. throw new CKEditorError( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.' );
  220. }
  221. return { modelRange: data.modelRange, modelCursor: data.modelCursor };
  222. }
  223. /**
  224. * @private
  225. * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertChildren
  226. */
  227. _convertChildren( viewItem, modelCursor ) {
  228. const modelRange = new ModelRange( modelCursor );
  229. let nextModelCursor = modelCursor;
  230. for ( const viewChild of Array.from( viewItem.getChildren() ) ) {
  231. const result = this._convertItem( viewChild, nextModelCursor );
  232. if ( result.modelRange instanceof ModelRange ) {
  233. modelRange.end = result.modelRange.end;
  234. nextModelCursor = result.modelCursor;
  235. }
  236. }
  237. return { modelRange, modelCursor: nextModelCursor };
  238. }
  239. /**
  240. * @private
  241. * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#splitToAllowedParent
  242. */
  243. _splitToAllowedParent( node, modelCursor ) {
  244. // Try to find allowed parent.
  245. const allowedParent = this.conversionApi.schema.findAllowedParent( node, modelCursor );
  246. // When there is no parent that allows to insert node then return `null`.
  247. if ( !allowedParent ) {
  248. return null;
  249. }
  250. // When current position parent allows to insert node then return this position.
  251. if ( allowedParent === modelCursor.parent ) {
  252. return { position: modelCursor };
  253. }
  254. // When allowed parent is in context tree.
  255. if ( this._modelCursor.parent.getAncestors().includes( allowedParent ) ) {
  256. return null;
  257. }
  258. // Split element to allowed parent.
  259. const splitResult = this.conversionApi.writer.split( modelCursor, allowedParent );
  260. // Remember all elements that are created as a result of split.
  261. // This is important because at the end of conversion we want to remove all empty split elements.
  262. //
  263. // Loop through positions between elements in range (except split result position) and collect parents.
  264. // <notSplit><split1><split2>[pos]</split2>[pos]</split1>[omit]<split1>[pos]<split2>[pos]</split2></split1></notSplit>
  265. for ( const position of splitResult.range.getPositions() ) {
  266. if ( !position.isEqual( splitResult.position ) ) {
  267. this._removeIfEmpty.add( position.parent );
  268. }
  269. }
  270. return {
  271. position: splitResult.position,
  272. cursorParent: splitResult.range.end.parent
  273. };
  274. }
  275. /**
  276. * Checks if {@link #_removeIfEmpty} contains empty elements and remove them.
  277. * We need to do it smart because there could be elements that are not empty because contains
  278. * other empty elements and after removing its children they become available to remove.
  279. * We need to continue iterating over split elements as long as any element will be removed.
  280. *
  281. * @private
  282. */
  283. _removeEmptyElements() {
  284. let removed = false;
  285. for ( const element of this._removeIfEmpty ) {
  286. if ( element.isEmpty ) {
  287. this.conversionApi.writer.remove( element );
  288. this._removeIfEmpty.delete( element );
  289. removed = true;
  290. }
  291. }
  292. if ( removed ) {
  293. this._removeEmptyElements();
  294. }
  295. }
  296. /**
  297. * Fired before the first conversion event, at the beginning of view to model conversion process.
  298. *
  299. * @event viewCleanup
  300. * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
  301. * viewItem Part of the view to be converted.
  302. */
  303. /**
  304. * Fired when {@link module:engine/view/element~Element} is converted.
  305. *
  306. * `element` is a namespace event for a class of events. Names of actually called events follow this pattern:
  307. * `element:<elementName>` where `elementName` is the name of converted element. This way listeners may listen to
  308. * all elements conversion or to conversion of specific elements.
  309. *
  310. * @event element
  311. * @param {Object} data Object containing viewItem to convert, modelCursor as a conversion position and placeholder
  312. * for modelRange that is a conversion result. Keep in mind that this object is shared by reference between all
  313. * callbacks that will be called. This means that callbacks can override values if needed, and those values will
  314. * be available in other callbacks.
  315. * @param {module:engine/view/item~Item} data.viewItem Converted item.
  316. * @param {module:engine/model/position~Position} data.modelCursor Target position for current item.
  317. * @param {module:engine/model/range~Range} data.modelRange The current state of conversion result. Every change to
  318. * converted element should be reflected by setting or modifying this property.
  319. * @param {ViewConversionApi} conversionApi Conversion interface to be used by callback, passed in
  320. * `ViewConversionDispatcher` constructor.
  321. */
  322. /**
  323. * Fired when {@link module:engine/view/text~Text} is converted.
  324. *
  325. * @event text
  326. * @see #event:element
  327. */
  328. /**
  329. * Fired when {@link module:engine/view/documentfragment~DocumentFragment} is converted.
  330. *
  331. * @event documentFragment
  332. * @see #event:element
  333. */
  334. }
  335. mix( ViewConversionDispatcher, EmitterMixin );
  336. // Traverses given model item and searches elements which marks marker range. Found element is removed from
  337. // DocumentFragment but path of this element is stored in a Map which is then returned.
  338. //
  339. // @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
  340. // @returns {Map<String, module:engine/model/range~Range>} List of static markers.
  341. function extractMarkersFromModelFragment( modelItem, writer ) {
  342. const markerElements = new Set();
  343. const markers = new Map();
  344. // Create ModelTreeWalker.
  345. const range = ModelRange.createIn( modelItem ).getItems();
  346. // Walk through DocumentFragment and collect marker elements.
  347. for ( const item of range ) {
  348. // Check if current element is a marker.
  349. if ( item.name == '$marker' ) {
  350. markerElements.add( item );
  351. }
  352. }
  353. // Walk through collected marker elements store its path and remove its from the DocumentFragment.
  354. for ( const markerElement of markerElements ) {
  355. const markerName = markerElement.getAttribute( 'data-name' );
  356. const currentPosition = ModelPosition.createBefore( markerElement );
  357. // When marker of given name is not stored it means that we have found the beginning of the range.
  358. if ( !markers.has( markerName ) ) {
  359. markers.set( markerName, new ModelRange( ModelPosition.createFromPosition( currentPosition ) ) );
  360. // Otherwise is means that we have found end of the marker range.
  361. } else {
  362. markers.get( markerName ).end = ModelPosition.createFromPosition( currentPosition );
  363. }
  364. // Remove marker element from DocumentFragment.
  365. writer.remove( markerElement );
  366. }
  367. return markers;
  368. }
  369. // Creates model fragment according to given context and returns position in top element.
  370. function createContextTree( contextDefinition, writer ) {
  371. let position;
  372. for ( const item of new SchemaContext( contextDefinition ) ) {
  373. const attributes = {};
  374. for ( const key of item.getAttributeKeys() ) {
  375. attributes[ key ] = item.getAttribute( key );
  376. }
  377. const current = writer.createElement( item.name, attributes );
  378. if ( position ) {
  379. writer.append( current, position );
  380. }
  381. position = ModelPosition.createAt( current );
  382. }
  383. return position;
  384. }
  385. /**
  386. * Conversion interface that is registered for given {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}
  387. * and is passed as one of parameters when {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher dispatcher}
  388. * fires it's events.
  389. *
  390. * @interface ViewConversionApi
  391. */
  392. /**
  393. * Starts conversion of given item by firing an appropriate event.
  394. *
  395. * Every fired event is passed (as first parameter) an object with `modelRange` property. Every event may set and/or
  396. * modify that property. When all callbacks are done, the final value of `modelRange` property is returned by this method.
  397. * The `modelRange` must be {@link module:engine/model/range~Range model range} or `null` (as set by default).
  398. *
  399. * @method #convertItem
  400. * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
  401. * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:text
  402. * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:documentFragment
  403. * @param {module:engine/view/item~Item} viewItem Item to convert.
  404. * @param {module:engine/model/position~Position} modelCursor Position of conversion.
  405. * @returns {Object} result Conversion result.
  406. * @returns {module:engine/model/range~Range|null} result.modelRange Model range containing result of item conversion,
  407. * created and modified by callbacks attached to fired event, or `null` if the conversion result was incorrect.
  408. * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
  409. */
  410. /**
  411. * Starts conversion of all children of given item by firing appropriate events for all those children.
  412. *
  413. * @method #convertChildren
  414. * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
  415. * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:text
  416. * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:documentFragment
  417. * @param {module:engine/view/item~Item} viewItem Item to convert.
  418. * @param {module:engine/model/position~Position} modelCursor Position of conversion.
  419. * @returns {Object} result Conversion result.
  420. * @returns {module:engine/model/range~Range} result.modelRange Model range containing results of conversion of all children of given item.
  421. * When no children was converted then range is collapsed.
  422. * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
  423. */
  424. /**
  425. * Find allowed parent for element that we are going to insert starting from given position.
  426. * If current parent does not allow to insert element but one of the ancestors does then split nodes to allowed parent.
  427. *
  428. * @method #splitToAllowedParent
  429. * @param {module:engine/model/position~Position} position Position on which element is going to be inserted.
  430. * @param {module:engine/model/node~Node} node Node to insert.
  431. * @returns {Object} Split result.
  432. * @returns {module:engine/model/position~Position} position between split elements.
  433. * @returns {module:engine/model/element~Element} [cursorParent] Element inside which cursor should be placed to
  434. * continue conversion. When element is not defined it means that there was no split.
  435. */
  436. /**
  437. * Instance of {@link module:engine/conversion/viewconsumable~ViewConsumable}. It stores
  438. * information about what parts of processed view item are still waiting to be handled. After a piece of view item
  439. * was converted, appropriate consumable value should be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
  440. *
  441. * @param {Object} #consumable
  442. */
  443. /**
  444. * Custom data stored by converter for conversion process.
  445. *
  446. * @param {Object} #store
  447. */