transform.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/model/operation/transform
  7. */
  8. import InsertOperation from './insertoperation';
  9. import AttributeOperation from './attributeoperation';
  10. import RootAttributeOperation from './rootattributeoperation';
  11. import RenameOperation from './renameoperation';
  12. import MarkerOperation from './markeroperation';
  13. import MoveOperation from './moveoperation';
  14. import RemoveOperation from './removeoperation';
  15. import NoOperation from './nooperation';
  16. import Range from '../range';
  17. import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
  18. /**
  19. * Transforms given {@link module:engine/model/operation/operation~Operation operation}
  20. * by another {@link module:engine/model/operation/operation~Operation operation}
  21. * and returns the result of that transformation as an array containing
  22. * one or more {@link module:engine/model/operation/operation~Operation operations}.
  23. *
  24. * Operations work on specified positions, passed to them when they are created.
  25. * Whenever {@link module:engine/model/document~Document document}
  26. * changes, we have to reflect those modifications by updating or "transforming" operations which are not yet applied.
  27. * When an operation is transformed, its parameters may change based on the operation by which it is transformed.
  28. * If the transform-by operation applied any modifications to the Tree Data Model which affect positions or nodes
  29. * connected with transformed operation, those changes will be reflected in the parameters of the returned operation(s).
  30. *
  31. * Whenever the {@link module:engine/model/document~Document document}
  32. * has different {@link module:engine/model/document~Document#version}
  33. * than the operation you want to {@link module:engine/model/document~Document#applyOperation apply}, you need to transform that
  34. * operation by all operations which were already applied to the {@link module:engine/model/document~Document document} and have greater
  35. * {@link module:engine/model/document~Document#version} than the operation being applied. Transform them in the same order as those
  36. * operations which were applied. This way all modifications done to the Tree Data Model will be reflected
  37. * in the operation parameters and the operation will "operate" on "up-to-date" version of the Tree Data Model.
  38. * This is mostly the case with Operational Transformations but it might be needed in particular features as well.
  39. *
  40. * In some cases, when given operation apply changes to the same nodes as this operation, two or more operations need
  41. * to be created as one would not be able to reflect the combination of these operations.
  42. * This is why an array is returned instead of a single object. All returned operations have to be applied
  43. * (or further transformed) to get an effect which was intended in pre-transformed operation.
  44. *
  45. * Sometimes two operations are in conflict. This happens when they modify the same node in a different way, i.e.
  46. * set different value for the same attribute or move the node into different positions. When this happens,
  47. * we need to decide which operation is more important. We can't assume that operation `a` or operation `b` is always
  48. * more important. In Operational Transformations algorithms we often need to get a result of transforming
  49. * `a` by `b` and also `b` by `a`. In both transformations the same operation has to be the important one. If we assume
  50. * that first or the second passed operation is always more important we won't be able to solve this case.
  51. *
  52. * @external module:engine/model/model.operation
  53. * @function module:engine/model/operation/transform~transform
  54. * @param {module:engine/model/operation/operation~Operation} a Operation that will be transformed.
  55. * @param {module:engine/model/operation/operation~Operation} b Operation to transform by.
  56. * @param {Boolean} isAMoreImportantThanB Flag indicating whether the operation which will be transformed (`a`) should be treated
  57. * as more important when resolving conflicts.
  58. * @returns {Array.<module:engine/model/operation/operation~Operation>} Result of the transformation.
  59. */
  60. export default transform;
  61. const ot = {
  62. InsertOperation: {
  63. // Transforms InsertOperation `a` by InsertOperation `b`. Accepts a flag stating whether `a` is more important
  64. // than `b` when it comes to resolving conflicts. Returns results as an array of operations.
  65. InsertOperation( a, b, isStrong ) {
  66. // Transformed operations are always new instances, not references to the original operations.
  67. const transformed = a.clone();
  68. // Transform insert position by the other operation position.
  69. transformed.position = transformed.position._getTransformedByInsertion( b.position, b.nodes.maxOffset, !isStrong );
  70. return [ transformed ];
  71. },
  72. AttributeOperation: doNotUpdate,
  73. RootAttributeOperation: doNotUpdate,
  74. RenameOperation: doNotUpdate,
  75. MarkerOperation: doNotUpdate,
  76. // Transforms InsertOperation `a` by MoveOperation `b`. Accepts a flag stating whether `a` is more important
  77. // than `b` when it comes to resolving conflicts. Returns results as an array of operations.
  78. MoveOperation( a, b, isStrong ) {
  79. const transformed = a.clone();
  80. // Transform insert position by the other operation parameters.
  81. transformed.position = a.position._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !isStrong, b.isSticky );
  82. return [ transformed ];
  83. }
  84. },
  85. AttributeOperation: {
  86. // Transforms AttributeOperation `a` by InsertOperation `b`. Returns results as an array of operations.
  87. InsertOperation( a, b ) {
  88. // Transform this operation's range.
  89. const ranges = a.range._getTransformedByInsertion( b.position, b.nodes.maxOffset, true, false );
  90. // Map transformed range(s) to operations and return them.
  91. return ranges.reverse().map( range => {
  92. return new AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion );
  93. } );
  94. },
  95. // Transforms AttributeOperation `a` by AttributeOperation `b`. Accepts a flag stating whether `a` is more important
  96. // than `b` when it comes to resolving conflicts. Returns results as an array of operations.
  97. AttributeOperation( a, b, isStrong ) {
  98. if ( a.key === b.key ) {
  99. // If operations attributes are in conflict, check if their ranges intersect and manage them properly.
  100. // First, we want to apply change to the part of a range that has not been changed by the other operation.
  101. const operations = a.range.getDifference( b.range ).map( range => {
  102. return new AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion );
  103. } );
  104. // Then we take care of the common part of ranges.
  105. const common = a.range.getIntersection( b.range );
  106. if ( common ) {
  107. // If this operation is more important, we also want to apply change to the part of the
  108. // original range that has already been changed by the other operation. Since that range
  109. // got changed we also have to update `oldValue`.
  110. if ( isStrong ) {
  111. operations.push( new AttributeOperation( common, b.key, b.newValue, a.newValue, a.baseVersion ) );
  112. } else if ( operations.length === 0 ) {
  113. operations.push( new NoOperation( 0 ) );
  114. }
  115. }
  116. return operations;
  117. } else {
  118. // If operations don't conflict, simply return an array containing just a clone of this operation.
  119. return [ a.clone() ];
  120. }
  121. },
  122. RootAttributeOperation: doNotUpdate,
  123. RenameOperation: doNotUpdate,
  124. MarkerOperation: doNotUpdate,
  125. // Transforms AttributeOperation `a` by MoveOperation `b`. Returns results as an array of operations.
  126. MoveOperation( a, b ) {
  127. // Convert MoveOperation properties into a range.
  128. const rangeB = Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
  129. // This will aggregate transformed ranges.
  130. let ranges = [];
  131. // Special case when MoveOperation is in fact a RemoveOperation. RemoveOperation not only moves nodes but also
  132. // creates a "holder" element for them in graveyard. If there was a RemoveOperation pointing to an offset
  133. // before this AttributeOperation, we have to increment AttributeOperation's offset.
  134. if ( b instanceof RemoveOperation && b._needsHolderElement &&
  135. a.range.root == b.targetPosition.root && a.range.start.path[ 0 ] >= b._holderElementOffset
  136. ) {
  137. // Do not change original operation!
  138. a = a.clone();
  139. a.range.start.path[ 0 ]++;
  140. a.range.end.path[ 0 ]++;
  141. }
  142. // Difference is a part of changed range that is modified by AttributeOperation but is not affected
  143. // by MoveOperation. This can be zero, one or two ranges (if moved range is inside changed range).
  144. // Right now we will make a simplification and join difference ranges and transform them as one. We will cover rangeB later.
  145. const difference = joinRanges( a.range.getDifference( rangeB ) );
  146. // Common is a range of nodes that is affected by MoveOperation. So it got moved to other place.
  147. const common = a.range.getIntersection( rangeB );
  148. if ( difference !== null ) {
  149. // MoveOperation removes nodes from their original position. We acknowledge this by proper transformation.
  150. // Take the start and the end of the range and transform them by deletion of moved nodes.
  151. // Note that if rangeB was inside AttributeOperation range, only difference.end will be transformed.
  152. // This nicely covers the joining simplification we did in the previous step.
  153. difference.start = difference.start._getTransformedByDeletion( b.sourcePosition, b.howMany );
  154. difference.end = difference.end._getTransformedByDeletion( b.sourcePosition, b.howMany );
  155. // MoveOperation pastes nodes into target position. We acknowledge this by proper transformation.
  156. // Note that since we operate on transformed difference range, we should transform by
  157. // previously transformed target position.
  158. // Note that we do not use Position._getTransformedByMove on range boundaries because we need to
  159. // transform by insertion a range as a whole, since newTargetPosition might be inside that range.
  160. ranges = difference._getTransformedByInsertion( b.getMovedRangeStart(), b.howMany, true, false ).reverse();
  161. }
  162. if ( common !== null ) {
  163. // Here we do not need to worry that newTargetPosition is inside moved range, because that
  164. // would mean that the MoveOperation targets into itself, and that is incorrect operation.
  165. // Instead, we calculate the new position of that part of original range.
  166. common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
  167. common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
  168. ranges.push( common );
  169. }
  170. // Map transformed range(s) to operations and return them.
  171. return ranges.map( range => {
  172. return new AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion );
  173. } );
  174. }
  175. },
  176. RootAttributeOperation: {
  177. InsertOperation: doNotUpdate,
  178. AttributeOperation: doNotUpdate,
  179. // Transforms RootAttributeOperation `a` by RootAttributeOperation `b`. Accepts a flag stating whether `a` is more important
  180. // than `b` when it comes to resolving conflicts. Returns results as an array of operations.
  181. RootAttributeOperation( a, b, isStrong ) {
  182. if ( a.root === b.root && a.key === b.key ) {
  183. if ( ( a.newValue !== b.newValue && !isStrong ) || a.newValue === b.newValue ) {
  184. return [ new NoOperation( a.baseVersion ) ];
  185. }
  186. }
  187. return [ a.clone() ];
  188. },
  189. RenameOperation: doNotUpdate,
  190. MarkerOperation: doNotUpdate,
  191. MoveOperation: doNotUpdate
  192. },
  193. RenameOperation: {
  194. // Transforms RenameOperation `a` by InsertOperation `b`. Returns results as an array of operations.
  195. InsertOperation( a, b ) {
  196. // Clone the operation, we don't want to alter the original operation.
  197. const clone = a.clone();
  198. // Transform this operation's position.
  199. clone.position = clone.position._getTransformedByInsertion( b.position, b.nodes.maxOffset, true );
  200. return [ clone ];
  201. },
  202. AttributeOperation: doNotUpdate,
  203. RootAttributeOperation: doNotUpdate,
  204. // Transforms RenameOperation `a` by RenameOperation `b`. Accepts a flag stating whether `a` is more important
  205. // than `b` when it comes to resolving conflicts. Returns results as an array of operations.
  206. RenameOperation( a, b, isStrong ) {
  207. // Clone the operation, we don't want to alter the original operation.
  208. const clone = a.clone();
  209. if ( a.position.isEqual( b.position ) ) {
  210. if ( isStrong ) {
  211. clone.oldName = b.newName;
  212. } else {
  213. return [ new NoOperation( a.baseVersion ) ];
  214. }
  215. }
  216. return [ clone ];
  217. },
  218. MarkerOperation: doNotUpdate,
  219. // Transforms RenameOperation `a` by MoveOperation `b`. Returns results as an array of operations.
  220. MoveOperation( a, b ) {
  221. const clone = a.clone();
  222. const isSticky = clone.position.isEqual( b.sourcePosition );
  223. clone.position = clone.position._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, true, isSticky );
  224. return [ clone ];
  225. }
  226. },
  227. MarkerOperation: {
  228. // Transforms MarkerOperation `a` by InsertOperation `b`. Returns results as an array of operations.
  229. InsertOperation( a, b ) {
  230. // Clone the operation, we don't want to alter the original operation.
  231. const clone = a.clone();
  232. if ( clone.oldRange ) {
  233. clone.oldRange = clone.oldRange._getTransformedByInsertion( b.position, b.nodes.maxOffset, false, false )[ 0 ];
  234. }
  235. if ( clone.newRange ) {
  236. clone.newRange = clone.newRange._getTransformedByInsertion( b.position, b.nodes.maxOffset, false, false )[ 0 ];
  237. }
  238. return [ clone ];
  239. },
  240. AttributeOperation: doNotUpdate,
  241. RootAttributeOperation: doNotUpdate,
  242. RenameOperation: doNotUpdate,
  243. // Transforms MarkerOperation `a` by MarkerOperation `b`. Accepts a flag stating whether `a` is more important
  244. // than `b` when it comes to resolving conflicts. Returns results as an array of operations.
  245. MarkerOperation( a, b, isStrong ) {
  246. // Clone the operation, we don't want to alter the original operation.
  247. const clone = a.clone();
  248. if ( a.name == b.name ) {
  249. if ( isStrong ) {
  250. clone.oldRange = b.newRange;
  251. } else {
  252. return [ new NoOperation( a.baseVersion ) ];
  253. }
  254. }
  255. return [ clone ];
  256. },
  257. // Transforms MarkerOperation `a` by MoveOperation `b`. Returns results as an array of operations.
  258. MoveOperation( a, b ) {
  259. // Clone the operation, we don't want to alter the original operation.
  260. const clone = a.clone();
  261. if ( clone.oldRange ) {
  262. const oldRanges = clone.oldRange._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany );
  263. clone.oldRange = Range.createFromRanges( oldRanges );
  264. }
  265. if ( clone.newRange ) {
  266. const newRanges = clone.newRange._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany );
  267. clone.newRange = Range.createFromRanges( newRanges );
  268. }
  269. return [ clone ];
  270. }
  271. },
  272. MoveOperation: {
  273. // Transforms MoveOperation `a` by InsertOperation `b`. Accepts a flag stating whether `a` is more important
  274. // than `b` when it comes to resolving conflicts. Returns results as an array of operations.
  275. InsertOperation( a, b, isStrong ) {
  276. // Create range from MoveOperation properties and transform it by insertion.
  277. let range = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
  278. range = range._getTransformedByInsertion( b.position, b.nodes.maxOffset, false, a.isSticky )[ 0 ];
  279. const result = new a.constructor(
  280. range.start,
  281. range.end.offset - range.start.offset,
  282. a instanceof RemoveOperation ?
  283. a.baseVersion :
  284. a.targetPosition._getTransformedByInsertion( b.position, b.nodes.maxOffset, !isStrong ),
  285. a instanceof RemoveOperation ? undefined : a.baseVersion
  286. );
  287. result.isSticky = a.isSticky;
  288. if ( a instanceof RemoveOperation ) {
  289. result._needsHolderElement = a._needsHolderElement;
  290. result._holderElementOffset = a._holderElementOffset;
  291. }
  292. return [ result ];
  293. },
  294. AttributeOperation: doNotUpdate,
  295. RootAttributeOperation: doNotUpdate,
  296. RenameOperation: doNotUpdate,
  297. MarkerOperation: doNotUpdate,
  298. // Transforms MoveOperation `a` by MoveOperation `b`. Accepts a flag stating whether `a` is more important
  299. // than `b` when it comes to resolving conflicts. Returns results as an array of operations.
  300. MoveOperation( a, b, isStrong ) {
  301. // Special case when both move operations' target positions are inside nodes that are
  302. // being moved by the other move operation. So in other words, we move ranges into inside of each other.
  303. // This case can't be solved reasonably (on the other hand, it should not happen often).
  304. if ( moveTargetIntoMovedRange( a, b ) && moveTargetIntoMovedRange( b, a ) ) {
  305. // Instead of transforming operation, we return a reverse of the operation that we transform by.
  306. // So when the results of this "transformation" will be applied, `b` MoveOperation will get reversed.
  307. return [ b.getReversed() ];
  308. }
  309. // Special case when operation transformed by is RemoveOperation. RemoveOperation not only moves nodes but also
  310. // (usually) creates a "holder" element for them in graveyard. This has to be taken into consideration when
  311. // transforming operations that operate in graveyard root.
  312. if ( b instanceof RemoveOperation && b._needsHolderElement ) {
  313. a = a.clone();
  314. const aSourceOffset = a.sourcePosition.path[ 0 ];
  315. const aTargetOffset = a.targetPosition.path[ 0 ];
  316. // We can't use Position#_getTransformedByInsertion because it works a bit differently and we would get wrong results.
  317. // That's because `sourcePosition`/`targetPosition` is, for example, `[ 1, 0 ]`, while `insertionPosition` is
  318. // `[ 1 ]`. In this case, `sourcePosition`/`targetPosition` would always be transformed but in fact, only
  319. // "holderElement" offsets should be looked at (so `[ 1 ]` in case of `sourcePosition`/`targetPosition`, not `[ 1, 0 ]` ).
  320. // This is why we are doing it "by hand".
  321. if ( a.sourcePosition.root == b.targetPosition.root ) {
  322. if ( aSourceOffset > b._holderElementOffset || aSourceOffset == b._holderElementOffset ) {
  323. a.sourcePosition.path[ 0 ]++;
  324. }
  325. }
  326. if ( a.targetPosition.root == b.targetPosition.root ) {
  327. if ( aTargetOffset > b._holderElementOffset || ( aTargetOffset == b._holderElementOffset && isStrong ) ) {
  328. a.targetPosition.path[ 0 ]++;
  329. }
  330. }
  331. }
  332. // If only one of operations is a remove operation, we force remove operation to be the "stronger" one
  333. // to provide more expected results.
  334. if ( a instanceof RemoveOperation && !( b instanceof RemoveOperation ) ) {
  335. isStrong = true;
  336. } else if ( !( a instanceof RemoveOperation ) && b instanceof RemoveOperation ) {
  337. isStrong = false;
  338. }
  339. // Create ranges from MoveOperations properties.
  340. const rangeA = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
  341. const rangeB = Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
  342. // This will aggregate transformed ranges.
  343. const ranges = [];
  344. // All the other non-special cases are treated by generic algorithm below.
  345. const difference = joinRanges( rangeA.getDifference( rangeB ) );
  346. if ( difference ) {
  347. difference.start = difference.start._getTransformedByMove(
  348. b.sourcePosition,
  349. b.targetPosition,
  350. b.howMany,
  351. !a.isSticky,
  352. false
  353. );
  354. difference.end = difference.end._getTransformedByMove(
  355. b.sourcePosition,
  356. b.targetPosition,
  357. b.howMany,
  358. a.isSticky,
  359. false
  360. );
  361. ranges.push( difference );
  362. }
  363. // Then, we have to manage the common part of both move ranges.
  364. const common = rangeA.getIntersection( rangeB );
  365. // If MoveOperations has common range it can be one of two:
  366. // * on the same tree level - it means that we move the same nodes into different places
  367. // * on deeper tree level - it means that we move nodes that are inside moved nodes
  368. // The operations are conflicting only if they try to move exactly same nodes, so only in the first case.
  369. // That means that we transform common part in two cases:
  370. // * `rangeA` is "deeper" than `rangeB` so it does not collide
  371. // * `rangeA` is at the same level but is stronger than `rangeB`.
  372. const aCompB = compareArrays( a.sourcePosition.getParentPath(), b.sourcePosition.getParentPath() );
  373. // If the `b` MoveOperation points inside the `a` MoveOperation range, the common part will be included in
  374. // range(s) that (is) are results of processing `difference`. If that's the case, we cannot include it again.
  375. const bTargetsToA = rangeA.containsPosition( b.targetPosition ) ||
  376. ( rangeA.start.isEqual( b.targetPosition ) && a.isSticky ) ||
  377. ( rangeA.end.isEqual( b.targetPosition ) && a.isSticky );
  378. // If the `b` MoveOperation range contains both whole `a` range and target position we do an exception and
  379. // transform `a` operation. Normally, when same nodes are moved, we stick with stronger operation's target.
  380. // Here it is a move inside larger range so there is no conflict because after all, all nodes from
  381. // smaller range will be moved to larger range target. The effect of this transformation feels natural.
  382. // Also if we wouldn't do that, we would get different results on both sides of transformation (i.e. in
  383. // collaborative editing).
  384. const aIsInside = rangeB.containsRange( rangeA ) &&
  385. (
  386. rangeB.containsPosition( a.targetPosition ) ||
  387. rangeB.start.isEqual( a.targetPosition ) ||
  388. rangeB.end.isEqual( a.targetPosition )
  389. );
  390. if ( common !== null && ( aCompB === 'extension' || ( aCompB === 'same' && isStrong ) || aIsInside ) && !bTargetsToA ) {
  391. // Here we do not need to worry that newTargetPosition is inside moved range, because that
  392. // would mean that the MoveOperation targets into itself, and that is incorrect operation.
  393. // Instead, we calculate the new position of that part of original range.
  394. common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
  395. common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
  396. // We have to take care of proper range order.
  397. if ( difference && rangeA.start.isBefore( rangeB.start ) ) {
  398. ranges.push( common );
  399. } else {
  400. ranges.unshift( common );
  401. }
  402. }
  403. if ( ranges.length === 0 ) {
  404. // At this point we transformed this operation's source ranges it means that nothing should be changed.
  405. // But since we need to return an instance of Operation we return an array with NoOperation.
  406. if ( a instanceof RemoveOperation ) {
  407. // If `a` operation was RemoveOperation, we cannot convert it to NoOperation.
  408. // This is because RemoveOperation creates a holder in graveyard.
  409. // Even if we "remove nothing" we need a RemoveOperation to create holder element
  410. // so that the tree structure is synchronised between clients.
  411. // Note that this can happen only if both operations are remove operations, because in
  412. // other case RemoveOperation would be forced to be stronger and there would be a common range to move.
  413. a = a.clone();
  414. a.howMany = 0;
  415. a.sourcePosition = b.targetPosition;
  416. return [ a ];
  417. } else {
  418. return [ new NoOperation( a.baseVersion ) ];
  419. }
  420. }
  421. // Target position also could be affected by the other MoveOperation. We will transform it.
  422. const newTargetPosition = a.targetPosition._getTransformedByMove(
  423. b.sourcePosition,
  424. b.targetPosition,
  425. b.howMany,
  426. !isStrong,
  427. b.isSticky || aIsInside
  428. );
  429. // Map transformed range(s) to operations and return them.
  430. return ranges.reverse().map( ( range, i ) => {
  431. // We want to keep correct operation class.
  432. const result = new a.constructor(
  433. range.start,
  434. range.end.offset - range.start.offset,
  435. a instanceof RemoveOperation ? a.baseVersion : newTargetPosition,
  436. a instanceof RemoveOperation ? undefined : a.baseVersion
  437. );
  438. result.isSticky = a.isSticky;
  439. if ( a instanceof RemoveOperation ) {
  440. // Transformed `RemoveOperation` needs graveyard holder only when the original operation needed it.
  441. // If `RemoveOperation` got split into two or more operations, only first operation needs graveyard holder.
  442. result._needsHolderElement = a._needsHolderElement && i === 0;
  443. result._holderElementOffset = a._holderElementOffset;
  444. }
  445. return result;
  446. } );
  447. }
  448. }
  449. };
  450. function transform( a, b, isStrong ) {
  451. let group, algorithm;
  452. if ( a instanceof InsertOperation ) {
  453. group = ot.InsertOperation;
  454. } else if ( a instanceof AttributeOperation ) {
  455. group = ot.AttributeOperation;
  456. } else if ( a instanceof RootAttributeOperation ) {
  457. group = ot.RootAttributeOperation;
  458. } else if ( a instanceof RenameOperation ) {
  459. group = ot.RenameOperation;
  460. } else if ( a instanceof MarkerOperation ) {
  461. group = ot.MarkerOperation;
  462. } else if ( a instanceof MoveOperation ) {
  463. group = ot.MoveOperation;
  464. } else {
  465. algorithm = doNotUpdate;
  466. }
  467. if ( group ) {
  468. if ( b instanceof InsertOperation ) {
  469. algorithm = group.InsertOperation;
  470. } else if ( b instanceof AttributeOperation ) {
  471. algorithm = group.AttributeOperation;
  472. } else if ( b instanceof RootAttributeOperation ) {
  473. algorithm = group.RootAttributeOperation;
  474. } else if ( b instanceof RenameOperation ) {
  475. algorithm = group.RenameOperation;
  476. } else if ( b instanceof MarkerOperation ) {
  477. algorithm = group.MarkerOperation;
  478. } else if ( b instanceof MoveOperation ) {
  479. algorithm = group.MoveOperation;
  480. } else {
  481. algorithm = doNotUpdate;
  482. }
  483. }
  484. const transformed = algorithm( a, b, isStrong );
  485. return updateBaseVersions( a.baseVersion, transformed );
  486. }
  487. // When we don't want to update an operation, we create and return a clone of it.
  488. // Returns the operation in "unified format" - wrapped in an Array.
  489. function doNotUpdate( operation ) {
  490. return [ operation.clone() ];
  491. }
  492. // Takes an Array of operations and sets consecutive base versions for them, starting from given base version.
  493. // Returns the passed array.
  494. function updateBaseVersions( baseVersion, operations ) {
  495. for ( let i = 0; i < operations.length; i++ ) {
  496. operations[ i ].baseVersion = baseVersion + i + 1;
  497. }
  498. return operations;
  499. }
  500. // Checks whether MoveOperation targetPosition is inside a node from the moved range of the other MoveOperation.
  501. function moveTargetIntoMovedRange( a, b ) {
  502. return a.targetPosition._getTransformedByDeletion( b.sourcePosition, b.howMany ) === null;
  503. }
  504. // Gets an array of Ranges and produces one Range out of it. The root of a new range will be same as
  505. // the root of the first range in the array. If any of given ranges has different root than the first range,
  506. // it will be discarded.
  507. function joinRanges( ranges ) {
  508. if ( ranges.length === 0 ) {
  509. return null;
  510. } else if ( ranges.length == 1 ) {
  511. return ranges[ 0 ];
  512. } else {
  513. ranges[ 0 ].end = ranges[ ranges.length - 1 ].end;
  514. return ranges[ 0 ];
  515. }
  516. }