transform.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* bender-tags: treemodel, operation */
  6. 'use strict';
  7. import transform from '/ckeditor5/core/treemodel/delta/transform.js';
  8. import Document from '/ckeditor5/core/treemodel/document.js';
  9. import Element from '/ckeditor5/core/treemodel/element.js';
  10. import Position from '/ckeditor5/core/treemodel/position.js';
  11. import Range from '/ckeditor5/core/treemodel/range.js';
  12. import Delta from '/ckeditor5/core/treemodel/delta/delta.js';
  13. import InsertDelta from '/ckeditor5/core/treemodel/delta/insertdelta.js';
  14. import MergeDelta from '/ckeditor5/core/treemodel/delta/mergedelta.js';
  15. import SplitDelta from '/ckeditor5/core/treemodel/delta/splitdelta.js';
  16. import InsertOperation from '/ckeditor5/core/treemodel/operation/insertoperation.js';
  17. import MoveOperation from '/ckeditor5/core/treemodel/operation/moveoperation.js';
  18. import RemoveOperation from '/ckeditor5/core/treemodel/operation/removeoperation.js';
  19. import ReinsertOperation from '/ckeditor5/core/treemodel/operation/reinsertoperation.js';
  20. import treeModelTestUtils from '/tests/core/treemodel/_utils/utils.js';
  21. const getNodesAndText = treeModelTestUtils.getNodesAndText;
  22. function getInsertDelta( position, nodes, version ) {
  23. let delta = new InsertDelta();
  24. delta.addOperation( new InsertOperation( position, nodes, version ) );
  25. return delta;
  26. }
  27. function getMergeDelta( position, howManyInPrev, howManyInNext, version ) {
  28. let delta = new MergeDelta();
  29. let sourcePosition = Position.createFromPosition( position );
  30. sourcePosition.path.push( 0 );
  31. let targetPosition = Position.createFromPosition( position );
  32. targetPosition.offset--;
  33. targetPosition.path.push( howManyInPrev );
  34. delta.addOperation( new MoveOperation( sourcePosition, howManyInNext, targetPosition, version ) );
  35. delta.addOperation( new RemoveOperation( position, 1, version + 1 ) );
  36. return delta;
  37. }
  38. function getSplitDelta( position, nodeCopy, howManyMove, version ) {
  39. let delta = new SplitDelta();
  40. let insertPosition = Position.createFromPosition( position );
  41. insertPosition.path = insertPosition.getParentPath();
  42. insertPosition.offset++;
  43. let targetPosition = Position.createFromPosition( insertPosition );
  44. targetPosition.path.push( 0 );
  45. delta.addOperation( new InsertOperation( insertPosition, [ nodeCopy ], version ) );
  46. delta.addOperation( new MoveOperation( position, howManyMove, targetPosition, version + 1 ) );
  47. return delta;
  48. }
  49. function expectDelta( delta, expected ) {
  50. expect( delta ).to.be.instanceof( expected.type );
  51. expect( delta.operations.length ).to.equal( expected.operations.length );
  52. for ( let i = 0; i < delta.operations.length; i++ ) {
  53. expectOperation( delta.operations[ i ], expected.operations[ i ] );
  54. }
  55. }
  56. function expectOperation( op, params ) {
  57. for ( let i in params ) {
  58. if ( params.hasOwnProperty( i ) ) {
  59. if ( i == 'type' ) {
  60. expect( op ).to.be.instanceof( params[ i ] );
  61. }
  62. else if ( i == 'nodes' ) {
  63. expect( op.nodeList._nodes ).to.deep.equal( params[ i ] );
  64. } else if ( params[ i ] instanceof Position || params[ i ] instanceof Range ) {
  65. expect( op[ i ].isEqual( params[ i ] ) ).to.be.true;
  66. } else {
  67. expect( op[ i ] ).to.equal( params[ i ] );
  68. }
  69. }
  70. }
  71. }
  72. function applyDelta( delta, document ) {
  73. for ( let op of delta.operations ) {
  74. document.applyOperation( op );
  75. }
  76. }
  77. describe( 'transform', () => {
  78. let doc, root, gy, baseVersion;
  79. beforeEach( () => {
  80. doc = new Document();
  81. root = doc.createRoot( 'root' );
  82. root.insertChildren( 0, [
  83. new Element( 'x' ),
  84. new Element( 'x' ),
  85. new Element( 'x', [], 'a' ),
  86. new Element( 'div', [], [
  87. new Element( 'x' ),
  88. new Element( 'x' ),
  89. new Element( 'x', [], 'a' ),
  90. new Element( 'div', [], [
  91. new Element( 'x' ),
  92. new Element( 'x' ),
  93. new Element( 'x', [], 'abcd' ),
  94. new Element( 'p', [], 'abcfoobarxyz' )
  95. ] )
  96. ] )
  97. ] );
  98. gy = doc.graveyard;
  99. baseVersion = doc.version;
  100. } );
  101. describe( 'InsertDelta by', () => {
  102. let insertDelta, insertPosition, nodeA, nodeB;
  103. beforeEach( () => {
  104. nodeA = new Element( 'a' );
  105. nodeB = new Element( 'b' );
  106. insertPosition = new Position( root, [ 3, 3, 3 ] );
  107. insertDelta = getInsertDelta( insertPosition, [ nodeA, nodeB ], baseVersion );
  108. } );
  109. describe( 'MergeDelta', () => {
  110. it( 'merge in same position as insert', () => {
  111. let mergeDelta = getMergeDelta( insertPosition, 4, 12, baseVersion );
  112. let transformed = transform( insertDelta, mergeDelta );
  113. baseVersion = mergeDelta.operations.length;
  114. // Expected: MergeOperation gets reversed (by special case of SplitDelta that has ReinsertOperation
  115. // instead of InsertOperation. Then InsertDelta is applied as is.
  116. expect( transformed.length ).to.equal( 2 );
  117. expectDelta( transformed[ 0 ], {
  118. type: SplitDelta,
  119. operations: [
  120. {
  121. type: ReinsertOperation,
  122. sourcePosition: new Position( gy, [ 0 ] ),
  123. howMany: 1,
  124. targetPosition: new Position( root, [ 3, 3, 3 ] ),
  125. baseVersion: baseVersion
  126. },
  127. {
  128. type: MoveOperation,
  129. sourcePosition: new Position( root, [ 3, 3, 2, 4 ] ),
  130. howMany: 12,
  131. targetPosition: new Position( root, [ 3, 3, 3, 0 ] ),
  132. baseVersion: baseVersion + 1
  133. }
  134. ]
  135. } );
  136. expectDelta( transformed[ 1 ], {
  137. type: InsertDelta,
  138. operations: [
  139. {
  140. type: InsertOperation,
  141. position: Position.createFromPosition( insertPosition ),
  142. nodes: [ nodeA, nodeB ],
  143. baseVersion: baseVersion + 2
  144. }
  145. ]
  146. } );
  147. // Test if deltas do what they should after applying transformed delta.
  148. applyDelta( mergeDelta, doc );
  149. applyDelta( transformed[ 0 ], doc );
  150. applyDelta( transformed[ 1 ], doc );
  151. let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
  152. // Merge between X with "abcd" and P with "abcfoobarxyz" should be reversed and AB should be inserted between X and P.
  153. expect( nodesAndText ).to.equal( 'XXXXXabcdXAABBPabcfoobarxyzP' );
  154. } );
  155. it( 'merge the node that is parent of insert position (sticky move test)', () => {
  156. let mergeDelta = getMergeDelta( new Position( root, [ 3, 3 ] ), 1, 4, baseVersion );
  157. let transformed = transform( insertDelta, mergeDelta );
  158. baseVersion = mergeDelta.operations.length;
  159. // Expected: InsertOperation in InsertDelta has it's path updated.
  160. expect( transformed.length ).to.equal( 1 );
  161. expectDelta( transformed[ 0 ], {
  162. type: InsertDelta,
  163. operations: [
  164. {
  165. type: InsertOperation,
  166. position: new Position( root, [ 3, 2, 4 ] ),
  167. nodes: [ nodeA, nodeB ],
  168. baseVersion: baseVersion
  169. }
  170. ]
  171. } );
  172. // Test if deltas do what they should after applying transformed delta.
  173. applyDelta( mergeDelta, doc );
  174. applyDelta( transformed[ 0 ], doc );
  175. let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 2, 0 ] ), 7 ) );
  176. // Merge between X with "a" and DIV should be applied. AB should be inserted in new, correct position.
  177. expect( nodesAndText ).to.equal( 'aXXXXXabcdXAABBPabcfoobarxyzP' );
  178. } );
  179. it( 'merge at affected position but resolved by default OT', () => {
  180. let mergeDelta = getMergeDelta( new Position( root, [ 3 ] ), 1, 4, baseVersion );
  181. let transformed = transform( insertDelta, mergeDelta );
  182. baseVersion = mergeDelta.operations.length;
  183. // Expected: InsertOperation in InsertDelta has it's path updated.
  184. expect( transformed.length ).to.equal( 1 );
  185. expectDelta( transformed[ 0 ], {
  186. type: InsertDelta,
  187. operations: [
  188. {
  189. type: InsertOperation,
  190. position: new Position( root, [ 2, 4, 3 ] ),
  191. nodes: [ nodeA, nodeB ],
  192. baseVersion: baseVersion
  193. }
  194. ]
  195. } );
  196. // Test if deltas do what they should after applying transformed delta.
  197. applyDelta( mergeDelta, doc );
  198. applyDelta( transformed[ 0 ], doc );
  199. let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 2, 0 ] ), 5 ) );
  200. // Merge between X with "a" and DIV should be applied. AB should be inserted in new, correct position.
  201. expect( nodesAndText ).to.equal( 'aXXXXXaXDIVXXXXXabcdXAABBPabcfoobarxyzPDIV' );
  202. } );
  203. } );
  204. } );
  205. describe( 'MergeDelta by', () => {
  206. let mergeDelta, mergePosition;
  207. beforeEach( () => {
  208. mergePosition = new Position( root, [ 3, 3, 3 ] );
  209. mergeDelta = getMergeDelta( mergePosition, 4, 12, baseVersion );
  210. } );
  211. describe( 'InsertDelta', () => {
  212. let nodeA, nodeB;
  213. beforeEach( () => {
  214. nodeA = new Element( 'a' );
  215. nodeB = new Element( 'b' );
  216. } );
  217. it( 'insert at same position as merge', () => {
  218. let insertDelta = getInsertDelta( mergePosition, [ nodeA, nodeB ], baseVersion );
  219. let transformed = transform( mergeDelta, insertDelta );
  220. // Expected: MergeDelta gets ignored and is not applied.
  221. expect( transformed.length ).to.equal( 1 );
  222. expectDelta( transformed[ 0 ], {
  223. type: Delta,
  224. operations: []
  225. } );
  226. // Test if deltas do what they should after applying transformed delta.
  227. applyDelta( insertDelta, doc );
  228. applyDelta( transformed[ 0 ], doc );
  229. let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
  230. // InsertDelta is applied. Merge between X and P is discarded.
  231. expect( nodesAndText ).to.equal( 'XXXXXabcdXAABBPabcfoobarxyzP' );
  232. } );
  233. it( 'insert inside merged node (sticky move test)', () => {
  234. let insertDelta = getInsertDelta( new Position( root, [ 3, 3, 3, 12 ] ), [ nodeA, nodeB ], baseVersion );
  235. let transformed = transform( mergeDelta, insertDelta );
  236. baseVersion = insertDelta.operations.length;
  237. expect( transformed.length ).to.equal( 1 );
  238. // Expected: MoveOperation in MergeDelta has it's "range" expanded.
  239. expectDelta( transformed[ 0 ], {
  240. type: MergeDelta,
  241. operations: [
  242. {
  243. type: MoveOperation,
  244. sourcePosition: new Position( root, [ 3, 3, 3, 0 ] ),
  245. howMany: 14,
  246. targetPosition: new Position( root, [ 3, 3, 2, 4 ] ),
  247. baseVersion: baseVersion
  248. },
  249. {
  250. // This is `MoveOperation` instead of `RemoveOperation` because during OT,
  251. // `RemoveOperation` may get converted to `MoveOperation`. Still, this expectation is
  252. // correct because `RemoveOperation` is deriving from `MoveOperation`. So we can expect
  253. // that something that was `RemoveOperation` is a `MoveOperation`.
  254. type: MoveOperation,
  255. sourcePosition: new Position( root, [ 3, 3, 3 ] ),
  256. howMany: 1,
  257. targetPosition: new Position( gy, [ 0 ] ),
  258. baseVersion: baseVersion + 1
  259. }
  260. ]
  261. } );
  262. // Test if deltas do what they should after applying transformed delta.
  263. applyDelta( insertDelta, doc );
  264. applyDelta( transformed[ 0 ], doc );
  265. let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 3 ) );
  266. // InsertDelta is applied. Merge between X and P is discarded.
  267. expect( nodesAndText ).to.equal( 'XXXXXabcdabcfoobarxyzAABBX' );
  268. } );
  269. } );
  270. } );
  271. describe( 'SplitDelta by', () => {
  272. let splitDeltaA, splitPosition;
  273. beforeEach( () => {
  274. splitPosition = new Position( root, [ 3, 3, 3, 3 ] );
  275. splitDeltaA = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
  276. } );
  277. describe( 'SplitDelta', () => {
  278. it( 'split in same parent and offset', () => {
  279. let splitDeltaB = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
  280. let transformed = transform( splitDeltaA, splitDeltaB );
  281. expect( transformed.length ).to.equal( 1 );
  282. expectDelta( transformed[ 0 ], {
  283. type: Delta,
  284. operations: []
  285. } );
  286. // Test if deltas do what they should after applying transformed delta.
  287. applyDelta( splitDeltaB, doc );
  288. applyDelta( transformed[ 0 ], doc );
  289. let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 5 ) );
  290. // Incoming split delta is discarded. Only one new element is created after applying both split deltas.
  291. // There are no empty P elements.
  292. expect( nodesAndText ).to.equal( 'XXXXXabcdXPabcPPfoobarxyzP' );
  293. } );
  294. it( 'split in same parent, incoming delta splits closer', () => {
  295. let splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 5 ] ), new Element( 'p' ), 7, baseVersion );
  296. let transformed = transform( splitDeltaA, splitDeltaB );
  297. baseVersion = splitDeltaB.operations.length;
  298. expect( transformed.length ).to.equal( 1 );
  299. expectDelta( transformed[ 0 ], {
  300. type: SplitDelta,
  301. operations: [
  302. {
  303. type: InsertOperation,
  304. position: new Position( root, [ 3, 3, 4 ] ),
  305. baseVersion: baseVersion
  306. },
  307. {
  308. type: MoveOperation,
  309. sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
  310. howMany: 2,
  311. targetPosition: new Position( root, [ 3, 3, 4, 0 ] ),
  312. baseVersion: baseVersion + 1
  313. }
  314. ]
  315. } );
  316. // Test if deltas do what they should after applying transformed delta.
  317. applyDelta( splitDeltaB, doc );
  318. applyDelta( transformed[ 0 ], doc );
  319. let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
  320. // P element is correctly split, there are three P elements, letters in P elements are in correct order.
  321. expect( nodesAndText ).to.equal( 'XXXXXabcdXPabcPPfoPPobarxyzP' );
  322. } );
  323. it( 'split in same parent, incoming delta splits further', () => {
  324. let splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 1 ] ), new Element( 'p' ), 11, baseVersion );
  325. let transformed = transform( splitDeltaA, splitDeltaB );
  326. baseVersion = splitDeltaB.operations.length;
  327. expect( transformed.length ).to.equal( 1 );
  328. expectDelta( transformed[ 0 ], {
  329. type: SplitDelta,
  330. operations: [
  331. {
  332. type: InsertOperation,
  333. position: new Position( root, [ 3, 3, 5 ] ),
  334. baseVersion: baseVersion
  335. },
  336. {
  337. type: MoveOperation,
  338. sourcePosition: new Position( root, [ 3, 3, 4, 2 ] ),
  339. howMany: 9,
  340. targetPosition: new Position( root, [ 3, 3, 5, 0 ] ),
  341. baseVersion: baseVersion + 1
  342. }
  343. ]
  344. } );
  345. // Test if deltas do what they should after applying transformed delta.
  346. applyDelta( splitDeltaB, doc );
  347. applyDelta( transformed[ 0 ], doc );
  348. let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
  349. // P element is correctly split, there are three P elements, letters in P elements are in correct order.
  350. expect( nodesAndText ).to.equal( 'XXXXXabcdXPaPPbcPPfoobarxyzP' );
  351. } );
  352. } );
  353. } );
  354. } );