upcastdispatcher.js 21 KB

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