viewconversiondispatcher.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. /**
  2. * @license Copyright (c) 2003-2017, 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 EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  10. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  11. import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
  12. import ModelRange from '../model/range';
  13. import ModelPosition from '../model/position';
  14. import ModelTreeWalker from '../model/treewalker';
  15. import ModelNode from '../model/node';
  16. import ModelDocumentFragment from '../model/documentfragment';
  17. import { remove } from '../model/writer';
  18. /**
  19. * `ViewConversionDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of
  20. * converting given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
  21. * {@link module:engine/view/element~Element}
  22. * into another structure. In default application, {@link module:engine/view/view view} is converted to {@link module:engine/model/model}.
  23. *
  24. * During conversion process, for all {@link module:engine/view/node~Node view nodes} from the converted view document fragment,
  25. * `ViewConversionDispatcher` fires corresponding events. Special callbacks called "converters" should listen to
  26. * `ViewConversionDispatcher` for those events.
  27. *
  28. * Each callback, as a first argument, is passed a special object `data` that has `input` and `output` properties.
  29. * `input` property contains {@link module:engine/view/node~Node view node} or
  30. * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
  31. * that is converted at the moment and might be handled by the callback. `output` property should be used to save the result
  32. * of conversion. Keep in mind that the `data` parameter is customizable and may contain other values - see
  33. * {@link ~ViewConversionDispatcher#convert}. It is also shared by reference by all callbacks
  34. * listening to given event. **Note**: in view to model conversion - `data` contains `context` property that is an array
  35. * of {@link module:engine/model/element~Element model elements}. These are model elements that will be the parent of currently
  36. * converted view item. `context` property is used in examples below.
  37. *
  38. * The second parameter passed to a callback is an instance of {@link module:engine/conversion/viewconsumable~ViewConsumable}. It stores
  39. * information about what parts of processed view item are still waiting to be handled. After a piece of view item
  40. * was converted, appropriate consumable value should be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
  41. *
  42. * The third parameter passed to a callback is an instance of {@link ~ViewConversionDispatcher}
  43. * which provides additional tools for converters.
  44. *
  45. * Examples of providing callbacks for `ViewConversionDispatcher`:
  46. *
  47. * // Converter for paragraphs (<p>).
  48. * viewDispatcher.on( 'element:p', ( evt, data, consumable, conversionApi ) => {
  49. * const paragraph = new ModelElement( 'paragraph' );
  50. * const schemaQuery = {
  51. * name: 'paragraph',
  52. * inside: data.context
  53. * };
  54. *
  55. * if ( conversionApi.schema.check( schemaQuery ) ) {
  56. * if ( !consumable.consume( data.input, { name: true } ) ) {
  57. * // Before converting this paragraph's children we have to update their context by this paragraph.
  58. * data.context.push( paragraph );
  59. * const children = conversionApi.convertChildren( data.input, consumable, data );
  60. * data.context.pop();
  61. * paragraph.appendChildren( children );
  62. * data.output = paragraph;
  63. * }
  64. * }
  65. * } );
  66. *
  67. * // Converter for links (<a>).
  68. * viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => {
  69. * if ( consumable.consume( data.input, { name: true, attributes: [ 'href' ] } ) ) {
  70. * // <a> element is inline and is represented by an attribute in the model.
  71. * // This is why we are not updating `context` property.
  72. * data.output = conversionApi.convertChildren( data.input, consumable, data );
  73. *
  74. * for ( let item of Range.createFrom( data.output ) ) {
  75. * const schemaQuery = {
  76. * name: item.name || '$text',
  77. * attribute: 'link',
  78. * inside: data.context
  79. * };
  80. *
  81. * if ( conversionApi.schema.check( schemaQuery ) ) {
  82. * item.setAttribute( 'link', data.input.getAttribute( 'href' ) );
  83. * }
  84. * }
  85. * }
  86. * } );
  87. *
  88. * // Fire conversion.
  89. * // Always take care where the converted model structure will be appended to. If this `viewDocumentFragment`
  90. * // is going to be appended directly to a '$root' element, use that in `context`.
  91. * viewDispatcher.convert( viewDocumentFragment, { context: [ '$root' ] } );
  92. *
  93. * Before each conversion process, `ViewConversionDispatcher` fires {@link ~ViewConversionDispatcher#event:viewCleanup}
  94. * event which can be used to prepare tree view for conversion.
  95. *
  96. * @mixes module:utils/emittermixin~EmitterMixin
  97. * @fires viewCleanup
  98. * @fires element
  99. * @fires text
  100. * @fires documentFragment
  101. */
  102. export default class ViewConversionDispatcher {
  103. /**
  104. * Creates a `ViewConversionDispatcher` that operates using passed API.
  105. *
  106. * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi
  107. * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
  108. * by `ViewConversionDispatcher`.
  109. */
  110. constructor( conversionApi = {} ) {
  111. /**
  112. * Interface passed by dispatcher to the events callbacks.
  113. *
  114. * @member {module:engine/conversion/viewconversiondispatcher~ViewConversionApi}
  115. */
  116. this.conversionApi = extend( {}, conversionApi );
  117. // `convertItem` and `convertChildren` are bound to this `ViewConversionDispatcher` instance and
  118. // set on `conversionApi`. This way only a part of `ViewConversionDispatcher` API is exposed.
  119. this.conversionApi.convertItem = this._convertItem.bind( this );
  120. this.conversionApi.convertChildren = this._convertChildren.bind( this );
  121. }
  122. /**
  123. * Starts the conversion process. The entry point for the conversion.
  124. *
  125. * @fires element
  126. * @fires text
  127. * @fires documentFragment
  128. * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
  129. * viewItem Part of the view to be converted.
  130. * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
  131. * events. See also {@link ~ViewConversionDispatcher#event:element element event}.
  132. * @returns {module:engine/conversion/viewconversiondispatcher~ConvertedModelData} Model data that is a result of the conversion process.
  133. */
  134. convert( viewItem, additionalData = {} ) {
  135. this.fire( 'viewCleanup', viewItem );
  136. const consumable = ViewConsumable.createFrom( viewItem );
  137. const conversionResult = this._convertItem( viewItem, consumable, additionalData );
  138. let markers = new Map();
  139. if ( conversionResult instanceof ModelNode || conversionResult instanceof ModelDocumentFragment ) {
  140. markers = extractMarkersFromModelFragment( conversionResult );
  141. }
  142. return { conversionResult, markers };
  143. }
  144. /**
  145. * @private
  146. * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertItem
  147. */
  148. _convertItem( input, consumable, additionalData = {} ) {
  149. const data = extend( {}, additionalData, {
  150. input: input,
  151. output: null
  152. } );
  153. if ( input.is( 'element' ) ) {
  154. this.fire( 'element:' + input.name, data, consumable, this.conversionApi );
  155. } else if ( input.is( 'text' ) ) {
  156. this.fire( 'text', data, consumable, this.conversionApi );
  157. } else {
  158. this.fire( 'documentFragment', data, consumable, this.conversionApi );
  159. }
  160. return data.output;
  161. }
  162. /**
  163. * @private
  164. * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertChildren
  165. */
  166. _convertChildren( input, consumable, additionalData = {} ) {
  167. const viewChildren = Array.from( input.getChildren() );
  168. const convertedChildren = viewChildren.map( ( viewChild ) => this._convertItem( viewChild, consumable, additionalData ) );
  169. // Flatten and remove nulls.
  170. return convertedChildren.reduce( ( a, b ) => b ? a.concat( b ) : a, [] );
  171. }
  172. /**
  173. * Fired before the first conversion event, at the beginning of view to model conversion process.
  174. *
  175. * @event viewCleanup
  176. * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
  177. * viewItem Part of the view to be converted.
  178. */
  179. /**
  180. * Fired when {@link module:engine/view/element~Element} is converted.
  181. *
  182. * `element` is a namespace event for a class of events. Names of actually called events follow this pattern:
  183. * `element:<elementName>` where `elementName` is the name of converted element. This way listeners may listen to
  184. * all elements conversion or to conversion of specific elements.
  185. *
  186. * @event element
  187. * @param {Object} data Object containing conversion input and a placeholder for conversion output and possibly other
  188. * values (see {@link #convert}).
  189. * Keep in mind that this object is shared by reference between all callbacks that will be called.
  190. * This means that callbacks can add their own values if needed,
  191. * and those values will be available in other callbacks.
  192. * @param {module:engine/view/element~Element} data.input Converted element.
  193. * @param {*} data.output The current state of conversion result. Every change to converted element should
  194. * be reflected by setting or modifying this property.
  195. * @param {module:engine/model/schema~SchemaPath} data.context The conversion context.
  196. * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
  197. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ViewConversionDispatcher` constructor.
  198. * Besides of properties passed in constructor, it also has `convertItem` and `convertChildren` methods which are references
  199. * to {@link #_convertItem} and
  200. * {@link ~ViewConversionDispatcher#_convertChildren}. Those methods are needed to convert
  201. * the whole view-tree they were exposed in `conversionApi` for callbacks.
  202. */
  203. /**
  204. * Fired when {@link module:engine/view/text~Text} is converted.
  205. *
  206. * @event text
  207. * @see #event:element
  208. */
  209. /**
  210. * Fired when {@link module:engine/view/documentfragment~DocumentFragment} is converted.
  211. *
  212. * @event documentFragment
  213. * @see #event:element
  214. */
  215. }
  216. mix( ViewConversionDispatcher, EmitterMixin );
  217. // Traverses given model item and searches elements which marks marker range. Found element is removed from
  218. // DocumentFragment but path of this element is stored in a Map which is then returned.
  219. //
  220. // @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
  221. // @returns {Map<String, module:engine/model/range~Range>} List of static markers.
  222. function extractMarkersFromModelFragment( modelItem ) {
  223. const markerStamps = new Set();
  224. const markers = new Map();
  225. if ( modelItem.is( 'text' ) ) {
  226. return markers;
  227. }
  228. // Create ModelTreeWalker.
  229. const walker = new ModelTreeWalker( {
  230. startPosition: ModelPosition.createAt( modelItem, 0 ),
  231. ignoreElementEnd: true
  232. } );
  233. // Walk through DocumentFragment and collect marker elements.
  234. for ( const value of walker ) {
  235. // Check if current element is a marker stamp.
  236. if ( value.item.name == '$marker' ) {
  237. markerStamps.add( value.item );
  238. }
  239. }
  240. // Walk through collected marker elements store its path and remove its from the DocumentFragment.
  241. for ( const stamp of markerStamps ) {
  242. const markerName = stamp.getAttribute( 'data-name' );
  243. const currentPosition = ModelPosition.createBefore( stamp );
  244. // When marker of given name is not stored it means that we have found the beginning of the range.
  245. if ( !markers.has( markerName ) ) {
  246. markers.set( markerName, new ModelRange( ModelPosition.createFromPosition( currentPosition ) ) );
  247. // Otherwise is means that we have found end of the marker range.
  248. } else {
  249. markers.get( markerName ).end = ModelPosition.createFromPosition( currentPosition );
  250. }
  251. // Remove marker stamp element from DocumentFragment.
  252. remove( ModelRange.createOn( stamp ) );
  253. }
  254. return markers;
  255. }
  256. /**
  257. * Model data that is a result of the conversion process.
  258. *
  259. * @typedef {ConvertedModelData} module:engine/conversion/viewconversiondispatcher~ConvertedModelData
  260. * @property {module:engine/model/documentfragment~DocumentFragment|module:engine/model/element~Node} conversionResult Converted model item.
  261. * @property {Map<String, module:engine/model/range~Range>} markers List of static markers.
  262. */
  263. /**
  264. * Conversion interface that is registered for given {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}
  265. * and is passed as one of parameters when {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher dispatcher}
  266. * fires it's events.
  267. *
  268. * `ViewConversionApi` object is built by {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher} constructor.
  269. * The exact list of properties of this object is determined by the object passed to the constructor.
  270. *
  271. * @interface ViewConversionApi
  272. */
  273. /**
  274. * Starts conversion of given item by firing an appropriate event.
  275. *
  276. * Every fired event is passed (as first parameter) an object with `output` property. Every event may set and/or
  277. * modify that property. When all callbacks are done, the final value of `output` property is returned by this method.
  278. *
  279. * @method #convertItem
  280. * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
  281. * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:text
  282. * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:documentFragment
  283. * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element|module:engine/view/text~Text}
  284. * input Item to convert.
  285. * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
  286. * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
  287. * events. See also {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element element event}.
  288. * @returns {*} The result of item conversion, created and modified by callbacks attached to fired event.
  289. */
  290. /**
  291. * Starts conversion of all children of given item by firing appropriate events for all those children.
  292. *
  293. * @method #convertChildren
  294. * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
  295. * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:text
  296. * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:documentFragment
  297. * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
  298. * input Item which children will be converted.
  299. * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
  300. * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
  301. * events. See also {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element element event}.
  302. * @returns {Array.<*>} Array containing results of conversion of all children of given item.
  303. */