8
0

modelconsumable.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  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/modelconsumable
  7. */
  8. import TextProxy from '../model/textproxy';
  9. /**
  10. * Manages a list of consumable values for {@link module:engine/model/item~Item model items}.
  11. *
  12. * Consumables are various aspects of the model. A model item can be broken down into singular properties that might be
  13. * taken into consideration when converting that item.
  14. *
  15. * `ModelConsumable` is used by {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} while analyzing changed
  16. * parts of {@link module:engine/model/document~Document the document}. The added / changed / removed model items are broken down
  17. * into singular properties (the item itself and it's attributes). All those parts are saved in `ModelConsumable`. Then,
  18. * during conversion, when given part of model item is converted (i.e. the view element has been inserted into the view,
  19. * but without attributes), consumable value is removed from `ModelConsumable`.
  20. *
  21. * For model items, `ModelConsumable` stores consumable values of one of following types: `insert`, `addattribute:<attributeKey>`,
  22. * `changeattributes:<attributeKey>`, `removeattributes:<attributeKey>`.
  23. *
  24. * In most cases, it is enough to let {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}
  25. * gather consumable values, so there is no need to use
  26. * {@link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly.
  27. * However, it is important to understand how consumable values can be
  28. * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}.
  29. * See {@link module:engine/conversion/downcasthelpers default downcast converters} for more information.
  30. *
  31. * Keep in mind, that one conversion event may have multiple callbacks (converters) attached to it. Each of those is
  32. * able to convert one or more parts of the model. However, when one of those callbacks actually converts
  33. * something, other should not, because they would duplicate the results. Using `ModelConsumable` helps avoiding
  34. * this situation, because callbacks should only convert those values, which were not yet consumed from `ModelConsumable`.
  35. *
  36. * Consuming multiple values in a single callback:
  37. *
  38. * // Converter for custom `image` element that might have a `caption` element inside which changes
  39. * // how the image is displayed in the view:
  40. * //
  41. * // Model:
  42. * //
  43. * // [image]
  44. * // └─ [caption]
  45. * // └─ foo
  46. * //
  47. * // View:
  48. * //
  49. * // <figure>
  50. * // ├─ <img />
  51. * // └─ <caption>
  52. * // └─ foo
  53. * modelConversionDispatcher.on( 'insert:image', ( evt, data, conversionApi ) => {
  54. * // First, consume the `image` element.
  55. * conversionApi.consumable.consume( data.item, 'insert' );
  56. *
  57. * // Just create normal image element for the view.
  58. * // Maybe it will be "decorated" later.
  59. * const viewImage = new ViewElement( 'img' );
  60. * const insertPosition = conversionApi.mapper.toViewPosition( data.range.start );
  61. * const viewWriter = conversionApi.writer;
  62. *
  63. * // Check if the `image` element has children.
  64. * if ( data.item.childCount > 0 ) {
  65. * const modelCaption = data.item.getChild( 0 );
  66. *
  67. * // `modelCaption` insertion change is consumed from consumable values.
  68. * // It will not be converted by other converters, but it's children (probably some text) will be.
  69. * // Through mapping, converters for text will know where to insert contents of `modelCaption`.
  70. * if ( conversionApi.consumable.consume( modelCaption, 'insert' ) ) {
  71. * const viewCaption = new ViewElement( 'figcaption' );
  72. *
  73. * const viewImageHolder = new ViewElement( 'figure', null, [ viewImage, viewCaption ] );
  74. *
  75. * conversionApi.mapper.bindElements( modelCaption, viewCaption );
  76. * conversionApi.mapper.bindElements( data.item, viewImageHolder );
  77. * viewWriter.insert( insertPosition, viewImageHolder );
  78. * }
  79. * } else {
  80. * conversionApi.mapper.bindElements( data.item, viewImage );
  81. * viewWriter.insert( insertPosition, viewImage );
  82. * }
  83. *
  84. * evt.stop();
  85. * } );
  86. */
  87. export default class ModelConsumable {
  88. /**
  89. * Creates an empty consumables list.
  90. */
  91. constructor() {
  92. /**
  93. * Contains list of consumable values.
  94. *
  95. * @private
  96. * @member {Map} module:engine/conversion/modelconsumable~ModelConsumable#_consumable
  97. */
  98. this._consumable = new Map();
  99. /**
  100. * For each {@link module:engine/model/textproxy~TextProxy} added to `ModelConsumable`, this registry holds parent
  101. * of that `TextProxy` and start and end indices of that `TextProxy`. This allows identification of `TextProxy`
  102. * instances that points to the same part of the model but are different instances. Each distinct `TextProxy`
  103. * is given unique `Symbol` which is then registered as consumable. This process is transparent for `ModelConsumable`
  104. * API user because whenever `TextProxy` is added, tested, consumed or reverted, internal mechanisms of
  105. * `ModelConsumable` translates `TextProxy` to that unique `Symbol`.
  106. *
  107. * @private
  108. * @member {Map} module:engine/conversion/modelconsumable~ModelConsumable#_textProxyRegistry
  109. */
  110. this._textProxyRegistry = new Map();
  111. }
  112. /**
  113. * Adds a consumable value to the consumables list and links it with given model item.
  114. *
  115. * modelConsumable.add( modelElement, 'insert' ); // Add `modelElement` insertion change to consumable values.
  116. * modelConsumable.add( modelElement, 'addAttribute:bold' ); // Add `bold` attribute insertion on `modelElement` change.
  117. * modelConsumable.add( modelElement, 'removeAttribute:bold' ); // Add `bold` attribute removal on `modelElement` change.
  118. * modelConsumable.add( modelSelection, 'selection' ); // Add `modelSelection` to consumable values.
  119. * modelConsumable.add( modelRange, 'range' ); // Add `modelRange` to consumable values.
  120. *
  121. * @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item
  122. * Model item, range or selection that has the consumable.
  123. * @param {String} type Consumable type. Will be normalized to a proper form, that is either `<word>` or `<part>:<part>`.
  124. * Second colon and everything after will be cut. Passing event name is a safe and good practice.
  125. */
  126. add( item, type ) {
  127. type = _normalizeConsumableType( type );
  128. if ( item instanceof TextProxy ) {
  129. item = this._getSymbolForTextProxy( item );
  130. }
  131. if ( !this._consumable.has( item ) ) {
  132. this._consumable.set( item, new Map() );
  133. }
  134. this._consumable.get( item ).set( type, true );
  135. }
  136. /**
  137. * Removes given consumable value from given model item.
  138. *
  139. * modelConsumable.consume( modelElement, 'insert' ); // Remove `modelElement` insertion change from consumable values.
  140. * modelConsumable.consume( modelElement, 'addAttribute:bold' ); // Remove `bold` attribute insertion on `modelElement` change.
  141. * modelConsumable.consume( modelElement, 'removeAttribute:bold' ); // Remove `bold` attribute removal on `modelElement` change.
  142. * modelConsumable.consume( modelSelection, 'selection' ); // Remove `modelSelection` from consumable values.
  143. * modelConsumable.consume( modelRange, 'range' ); // Remove 'modelRange' from consumable values.
  144. *
  145. * @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item
  146. * Model item, range or selection from which consumable will be consumed.
  147. * @param {String} type Consumable type. Will be normalized to a proper form, that is either `<word>` or `<part>:<part>`.
  148. * Second colon and everything after will be cut. Passing event name is a safe and good practice.
  149. * @returns {Boolean} `true` if consumable value was available and was consumed, `false` otherwise.
  150. */
  151. consume( item, type ) {
  152. type = _normalizeConsumableType( type );
  153. if ( item instanceof TextProxy ) {
  154. item = this._getSymbolForTextProxy( item );
  155. }
  156. if ( this.test( item, type ) ) {
  157. this._consumable.get( item ).set( type, false );
  158. return true;
  159. } else {
  160. return false;
  161. }
  162. }
  163. /**
  164. * Tests whether there is a consumable value of given type connected with given model item.
  165. *
  166. * modelConsumable.test( modelElement, 'insert' ); // Check for `modelElement` insertion change.
  167. * modelConsumable.test( modelElement, 'addAttribute:bold' ); // Check for `bold` attribute insertion on `modelElement` change.
  168. * modelConsumable.test( modelElement, 'removeAttribute:bold' ); // Check for `bold` attribute removal on `modelElement` change.
  169. * modelConsumable.test( modelSelection, 'selection' ); // Check if `modelSelection` is consumable.
  170. * modelConsumable.test( modelRange, 'range' ); // Check if `modelRange` is consumable.
  171. *
  172. * @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item
  173. * Model item, range or selection to be tested.
  174. * @param {String} type Consumable type. Will be normalized to a proper form, that is either `<word>` or `<part>:<part>`.
  175. * Second colon and everything after will be cut. Passing event name is a safe and good practice.
  176. * @returns {null|Boolean} `null` if such consumable was never added, `false` if the consumable values was
  177. * already consumed or `true` if it was added and not consumed yet.
  178. */
  179. test( item, type ) {
  180. type = _normalizeConsumableType( type );
  181. if ( item instanceof TextProxy ) {
  182. item = this._getSymbolForTextProxy( item );
  183. }
  184. const itemConsumables = this._consumable.get( item );
  185. if ( itemConsumables === undefined ) {
  186. return null;
  187. }
  188. const value = itemConsumables.get( type );
  189. if ( value === undefined ) {
  190. return null;
  191. }
  192. return value;
  193. }
  194. /**
  195. * Reverts consuming of consumable value.
  196. *
  197. * modelConsumable.revert( modelElement, 'insert' ); // Revert consuming `modelElement` insertion change.
  198. * modelConsumable.revert( modelElement, 'addAttribute:bold' ); // Revert consuming `bold` attribute insert from `modelElement`.
  199. * modelConsumable.revert( modelElement, 'removeAttribute:bold' ); // Revert consuming `bold` attribute remove from `modelElement`.
  200. * modelConsumable.revert( modelSelection, 'selection' ); // Revert consuming `modelSelection`.
  201. * modelConsumable.revert( modelRange, 'range' ); // Revert consuming `modelRange`.
  202. *
  203. * @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item
  204. * Model item, range or selection to be reverted.
  205. * @param {String} type Consumable type.
  206. * @returns {null|Boolean} `true` if consumable has been reversed, `false` otherwise. `null` if the consumable has
  207. * never been added.
  208. */
  209. revert( item, type ) {
  210. type = _normalizeConsumableType( type );
  211. if ( item instanceof TextProxy ) {
  212. item = this._getSymbolForTextProxy( item );
  213. }
  214. const test = this.test( item, type );
  215. if ( test === false ) {
  216. this._consumable.get( item ).set( type, true );
  217. return true;
  218. } else if ( test === true ) {
  219. return false;
  220. }
  221. return null;
  222. }
  223. /**
  224. * Gets a unique symbol for passed {@link module:engine/model/textproxy~TextProxy} instance. All `TextProxy` instances that
  225. * have same parent, same start index and same end index will get the same symbol.
  226. *
  227. * Used internally to correctly consume `TextProxy` instances.
  228. *
  229. * @private
  230. * @param {module:engine/model/textproxy~TextProxy} textProxy `TextProxy` instance to get a symbol for.
  231. * @returns {Symbol} Symbol representing all equal instances of `TextProxy`.
  232. */
  233. _getSymbolForTextProxy( textProxy ) {
  234. let symbol = null;
  235. const startMap = this._textProxyRegistry.get( textProxy.startOffset );
  236. if ( startMap ) {
  237. const endMap = startMap.get( textProxy.endOffset );
  238. if ( endMap ) {
  239. symbol = endMap.get( textProxy.parent );
  240. }
  241. }
  242. if ( !symbol ) {
  243. symbol = this._addSymbolForTextProxy( textProxy.startOffset, textProxy.endOffset, textProxy.parent );
  244. }
  245. return symbol;
  246. }
  247. /**
  248. * Adds a symbol for given properties that characterizes a {@link module:engine/model/textproxy~TextProxy} instance.
  249. *
  250. * Used internally to correctly consume `TextProxy` instances.
  251. *
  252. * @private
  253. * @param {Number} startIndex Text proxy start index in it's parent.
  254. * @param {Number} endIndex Text proxy end index in it's parent.
  255. * @param {module:engine/model/element~Element} parent Text proxy parent.
  256. * @returns {Symbol} Symbol generated for given properties.
  257. */
  258. _addSymbolForTextProxy( start, end, parent ) {
  259. const symbol = Symbol( 'textProxySymbol' );
  260. let startMap, endMap;
  261. startMap = this._textProxyRegistry.get( start );
  262. if ( !startMap ) {
  263. startMap = new Map();
  264. this._textProxyRegistry.set( start, startMap );
  265. }
  266. endMap = startMap.get( end );
  267. if ( !endMap ) {
  268. endMap = new Map();
  269. startMap.set( end, endMap );
  270. }
  271. endMap.set( parent, symbol );
  272. return symbol;
  273. }
  274. }
  275. // Returns a normalized consumable type name from given string. A normalized consumable type name is a string that has
  276. // at most one colon, for example: `insert` or `addMarker:highlight`. If string to normalize has more "parts" (more colons),
  277. // the other parts are dropped, for example: `addattribute:bold:$text` -> `addattributes:bold`.
  278. //
  279. // @param {String} type Consumable type.
  280. // @returns {String} Normalized consumable type.
  281. function _normalizeConsumableType( type ) {
  282. const parts = type.split( ':' );
  283. return parts.length > 1 ? parts[ 0 ] + ':' + parts[ 1 ] : parts[ 0 ];
  284. }