basic-transformations.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import { addTransformationCase, defaultTransform } from './transform.js';
  7. import Range from '../range.js';
  8. import Position from '../position.js';
  9. import NoOperation from '../operation/nooperation.js';
  10. import AttributeOperation from '../operation/attributeoperation.js';
  11. import ReinsertOperation from '../operation/reinsertoperation.js';
  12. import Delta from './delta.js';
  13. import AttributeDelta from './attributedelta.js';
  14. import InsertDelta from './insertdelta.js';
  15. import MergeDelta from './mergedelta.js';
  16. import MoveDelta from './movedelta.js';
  17. import SplitDelta from './splitdelta.js';
  18. import WeakInsertDelta from './weakinsertdelta.js';
  19. import WrapDelta from './wrapdelta.js';
  20. import UnwrapDelta from './unwrapdelta.js';
  21. import compareArrays from '../../../utils/comparearrays.js';
  22. // Provide transformations for default deltas.
  23. // Add special case for AttributeDelta x WeakInsertDelta transformation.
  24. addTransformationCase( AttributeDelta, WeakInsertDelta, ( a, b, isStrong ) => {
  25. // If nodes are weak-inserted into attribute delta range, we need to apply changes from attribute delta on them.
  26. // So first we do the normal transformation and if this special cases happens, we will add an extra delta.
  27. const deltas = defaultTransform( a, b, isStrong );
  28. if ( a.range.containsPosition( b.position ) ) {
  29. deltas.push( _getComplementaryAttrDelta( b, a ) );
  30. }
  31. return deltas;
  32. } );
  33. // Add special case for InsertDelta x MergeDelta transformation.
  34. addTransformationCase( InsertDelta, MergeDelta, ( a, b, isStrong ) => {
  35. // If insert is applied at the same position where merge happened, we reverse the merge (we treat it like it
  36. // didn't happen) and then apply the original insert operation. This is "mirrored" in MergeDelta x InsertDelta
  37. // transformation below, where we simply do not apply MergeDelta.
  38. if ( a.position.isEqual( b.position ) ) {
  39. return [
  40. b.getReversed(),
  41. a.clone()
  42. ];
  43. }
  44. return defaultTransform( a, b, isStrong );
  45. } );
  46. // Add special case for MoveDelta x MergeDelta transformation.
  47. addTransformationCase( MoveDelta, MergeDelta, ( a, b, isStrong ) => {
  48. // If move delta is supposed to move a node that has been merged, we reverse the merge (we treat it like it
  49. // didn't happen) and then apply the original move operation. This is "mirrored" in MergeDelta x MoveDelta
  50. // transformation below, where we simply do not apply MergeDelta.
  51. const operateInSameParent =
  52. a.sourcePosition.root == b.position.root &&
  53. compareArrays( a.sourcePosition.getParentPath(), b.position.getParentPath() ) === 'SAME';
  54. const mergeInsideMoveRange = a.sourcePosition.offset <= b.position.offset && a.sourcePosition.offset + a.howMany > b.position.offset;
  55. if ( operateInSameParent && mergeInsideMoveRange ) {
  56. return [
  57. b.getReversed(),
  58. a.clone()
  59. ];
  60. }
  61. return defaultTransform( a, b, isStrong );
  62. } );
  63. // Add special case for MergeDelta x InsertDelta transformation.
  64. addTransformationCase( MergeDelta, InsertDelta, ( a, b, isStrong ) => {
  65. // If merge is applied at the same position where we inserted a range of nodes we cancel the merge as it's results
  66. // may be unexpected and very weird. Even if we do some "magic" we don't know what really are users' expectations.
  67. if ( a.position.isEqual( b.position ) ) {
  68. return [ noDelta() ];
  69. }
  70. return defaultTransform( a, b, isStrong );
  71. } );
  72. // Add special case for MergeDelta x MoveDelta transformation.
  73. addTransformationCase( MergeDelta, MoveDelta, ( a, b, isStrong ) => {
  74. // If merge is applied at the position between moved nodes we cancel the merge as it's results may be unexpected and
  75. // very weird. Even if we do some "magic" we don't know what really are users' expectations.
  76. const operateInSameParent =
  77. a.position.root == b.sourcePosition.root &&
  78. compareArrays( a.position.getParentPath(), b.sourcePosition.getParentPath() ) === 'SAME';
  79. const mergeInsideMoveRange = b.sourcePosition.offset <= a.position.offset && b.sourcePosition.offset + b.howMany > a.position.offset;
  80. if ( operateInSameParent && mergeInsideMoveRange ) {
  81. return [ noDelta() ];
  82. }
  83. return defaultTransform( a, b, isStrong );
  84. } );
  85. // Add special case for SplitDelta x SplitDelta transformation.
  86. addTransformationCase( SplitDelta, SplitDelta, ( a, b, isStrong ) => {
  87. const pathA = a.position.getParentPath();
  88. const pathB = b.position.getParentPath();
  89. // The special case is for splits inside the same parent.
  90. if ( compareArrays( pathA, pathB ) == 'SAME' ) {
  91. if ( a.position.offset == b.position.offset ) {
  92. // We are applying split at the position where split already happened. Additional split is not needed.
  93. return [ noDelta() ];
  94. } else if ( a.position.offset < b.position.offset ) {
  95. // Incoming split delta splits at closer offset. So we simply have to once again split the same node,
  96. // but since it was already split (at further offset) there are less child nodes in the split node.
  97. // This means that we have to update `howMany` parameter of `MoveOperation` for that delta.
  98. const delta = a.clone();
  99. delta._moveOperation.howMany = b.position.offset - a.position.offset;
  100. // If both SplitDeltas are taking their nodes from graveyard, we have to transform their ReinsertOperations.
  101. if (
  102. a._cloneOperation instanceof ReinsertOperation &&
  103. b._cloneOperation instanceof ReinsertOperation &&
  104. a._cloneOperation.sourcePosition.offset > b._cloneOperation.sourcePosition.offset
  105. ) {
  106. delta._cloneOperation.sourcePosition.offset--;
  107. }
  108. return [ delta ];
  109. } else {
  110. // Incoming split delta splits at further offset. We have to simulate that we are not splitting the
  111. // original split node but the node after it, which got created by the other split delta.
  112. // To do so, we increment offsets so it looks like the split delta was created in the next node.
  113. const delta = a.clone();
  114. delta._cloneOperation.position.offset++;
  115. delta._moveOperation.sourcePosition.path[ delta._moveOperation.sourcePosition.path.length - 2 ]++;
  116. delta._moveOperation.targetPosition.path[ delta._moveOperation.targetPosition.path.length - 2 ]++;
  117. delta._moveOperation.sourcePosition.offset = a.position.offset - b.position.offset;
  118. // If both SplitDeltas are taking their nodes from graveyard, we have to transform their ReinsertOperations.
  119. if (
  120. a._cloneOperation instanceof ReinsertOperation &&
  121. b._cloneOperation instanceof ReinsertOperation &&
  122. a._cloneOperation.sourcePosition.offset > b._cloneOperation.sourcePosition.offset
  123. ) {
  124. delta._cloneOperation.sourcePosition.offset--;
  125. }
  126. return [ delta ];
  127. }
  128. }
  129. return defaultTransform( a, b, isStrong );
  130. } );
  131. // Add special case for SplitDelta x UnwrapDelta transformation.
  132. addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, isStrong ) => {
  133. // If incoming split delta tries to split a node that just got unwrapped, there is actually nothing to split,
  134. // so we discard that delta.
  135. if ( compareArrays( b.position.path, a.position.getParentPath() ) === 'SAME' ) {
  136. return [ noDelta() ];
  137. }
  138. return defaultTransform( a, b, isStrong );
  139. } );
  140. // Add special case for SplitDelta x WrapDelta transformation.
  141. addTransformationCase( SplitDelta, WrapDelta, ( a, b, isStrong ) => {
  142. // If split is applied at the position between wrapped nodes, we cancel the split as it's results may be unexpected and
  143. // very weird. Even if we do some "magic" we don't know what really are users' expectations.
  144. const operateInSameParent = compareArrays( a.position.getParentPath(), b.range.start.getParentPath() ) === 'SAME';
  145. const splitInsideWrapRange = b.range.start.offset < a.position.offset && b.range.end.offset >= a.position.offset;
  146. if ( operateInSameParent && splitInsideWrapRange ) {
  147. return [ noDelta() ];
  148. } else if ( compareArrays( a.position.getParentPath(), b.range.end.getShiftedBy( -1 ).path ) === 'SAME' ) {
  149. // Split position is directly inside the last node from wrap range.
  150. // If that's the case, we manually change split delta so it will "target" inside the wrapping element.
  151. // By doing so we will be inserting split node right to the original node which feels natural and is a good UX.
  152. const delta = a.clone();
  153. // 1. Fix insert operation position.
  154. // Node to split is the last children of the wrapping element.
  155. // Wrapping element is the element inserted by WrapDelta (re)insert operation.
  156. // It is inserted after the wrapped range, but the wrapped range will be moved inside it.
  157. // Having this in mind, it is correct to use wrapped range start position as the position before wrapping element.
  158. const splitNodePos = Position.createFromPosition( b.range.start );
  159. // Now, `splitNodePos` points before wrapping element.
  160. // To get a position before last children of that element, we expand position's `path` member by proper offset.
  161. splitNodePos.path.push( b.howMany - 1 );
  162. // SplitDelta insert operation position should be right after the node we split.
  163. const insertPos = splitNodePos.getShiftedBy( 1 );
  164. delta._cloneOperation.position = insertPos;
  165. // 2. Fix move operation source position.
  166. // Nodes moved by SplitDelta will be moved from new position, modified by WrapDelta.
  167. // To obtain that new position, `splitNodePos` will be used, as this is the node we are extracting children from.
  168. const sourcePos = Position.createFromPosition( splitNodePos );
  169. // Nothing changed inside split node so it is correct to use the original split position offset.
  170. sourcePos.path.push( a.position.offset );
  171. delta._moveOperation.sourcePosition = sourcePos;
  172. // 3. Fix move operation target position.
  173. // SplitDelta move operation target position should be inside the node inserted by operation above.
  174. // Since the node is empty, we will insert at offset 0.
  175. const targetPos = Position.createFromPosition( insertPos );
  176. targetPos.path.push( 0 );
  177. delta._moveOperation.targetPosition = targetPos;
  178. return [ delta ];
  179. }
  180. return defaultTransform( a, b, isStrong );
  181. } );
  182. // Add special case for UnwrapDelta x SplitDelta transformation.
  183. addTransformationCase( UnwrapDelta, SplitDelta, ( a, b, isStrong ) => {
  184. // If incoming unwrap delta tries to unwrap node that got split we should unwrap the original node and the split copy.
  185. // This can be achieved either by reverting split and applying unwrap to singular node, or creating additional unwrap delta.
  186. if ( compareArrays( a.position.path, b.position.getParentPath() ) === 'SAME' ) {
  187. return [
  188. b.getReversed(),
  189. a.clone()
  190. ];
  191. }
  192. return defaultTransform( a, b, isStrong );
  193. } );
  194. // Add special case for WeakInsertDelta x AttributeDelta transformation.
  195. addTransformationCase( WeakInsertDelta, AttributeDelta, ( a, b, isStrong ) => {
  196. // If nodes are weak-inserted into attribute delta range, we need to apply changes from attribute delta on them.
  197. // So first we do the normal transformation and if this special cases happens, we will add an extra delta.
  198. const deltas = defaultTransform( a, b, isStrong );
  199. if ( b.range.containsPosition( a.position ) ) {
  200. deltas.push( _getComplementaryAttrDelta( a, b ) );
  201. }
  202. return deltas;
  203. } );
  204. // Add special case for WrapDelta x SplitDelta transformation.
  205. addTransformationCase( WrapDelta, SplitDelta, ( a, b, isStrong ) => {
  206. // If incoming wrap delta tries to wrap range that contains split position, we have to cancel the split and apply
  207. // the wrap. Since split was already applied, we have to revert it.
  208. const operateInSameParent = compareArrays( a.range.start.getParentPath(), b.position.getParentPath() ) === 'SAME';
  209. const splitInsideWrapRange = a.range.start.offset < b.position.offset && a.range.end.offset >= b.position.offset;
  210. if ( operateInSameParent && splitInsideWrapRange ) {
  211. return [
  212. b.getReversed(),
  213. a.clone()
  214. ];
  215. } else if ( compareArrays( b.position.getParentPath(), a.range.end.getShiftedBy( -1 ).path ) === 'SAME' ) {
  216. const delta = a.clone();
  217. // Move wrapping element insert position one node further so it is after the split node insertion.
  218. delta._insertOperation.position.offset++;
  219. // Include the split node copy.
  220. delta._moveOperation.howMany++;
  221. // Change the path to wrapping element in move operation.
  222. delta._moveOperation.targetPosition.path[ delta._moveOperation.targetPosition.path.length - 2 ]++;
  223. return [ delta ];
  224. }
  225. return defaultTransform( a, b, isStrong );
  226. } );
  227. // Helper function for `AttributeDelta` class transformations.
  228. // Creates an attribute delta that sets attribute from given `attributeDelta` on nodes from given `weakInsertDelta`.
  229. function _getComplementaryAttrDelta( weakInsertDelta, attributeDelta ) {
  230. const complementaryAttrDelta = new AttributeDelta();
  231. // At the beginning we store the attribute value from the first node on `weakInsertDelta` node list.
  232. let val = weakInsertDelta.nodeList.get( 0 ).getAttribute( attributeDelta.key );
  233. // This stores the last index of `weakInsertDelta` node list where the attribute value was different
  234. // than in the previous node. We need it to create separate `AttributeOperation`s for nodes with different attributes.
  235. let lastIndex = 0;
  236. for ( let i = 0; i < weakInsertDelta.nodeList.length; i++ ) {
  237. const node = weakInsertDelta.nodeList.get( i );
  238. const nodeAttrVal = node.getAttribute( attributeDelta.key );
  239. // If previous node has different attribute value, we will create an operation to the point before current node.
  240. // So all nodes with the same attributes up to this point will be included in one `AttributeOperation`.
  241. if ( nodeAttrVal != val ) {
  242. // New operation is created only when it is needed. If given node already has proper value for this
  243. // attribute we simply skip it without adding a new operation.
  244. if ( val != attributeDelta.value ) {
  245. const range = new Range( weakInsertDelta.position.getShiftedBy( lastIndex ), weakInsertDelta.position.getShiftedBy( i ) );
  246. // We don't care about base version because it will be updated after transformations anyway.
  247. const attrOperation = new AttributeOperation( range, attributeDelta.key, val, attributeDelta.value, 0 );
  248. complementaryAttrDelta.addOperation( attrOperation );
  249. }
  250. val = nodeAttrVal;
  251. lastIndex = i;
  252. }
  253. }
  254. // At the end we have to add additional `AttributeOperation` for the last part of node list. If all nodes on the
  255. // node list had same attributes, this will be the only operation added to the delta.
  256. const range = new Range(
  257. weakInsertDelta.position.getShiftedBy( lastIndex ),
  258. weakInsertDelta.position.getShiftedBy( weakInsertDelta.nodeList.length )
  259. );
  260. complementaryAttrDelta.addOperation( new AttributeOperation( range, attributeDelta.key, val, attributeDelta.value, 0 ) );
  261. return complementaryAttrDelta;
  262. }
  263. // This is "no-op" delta, it has no type and only no-operation, it basically does nothing.
  264. // It is used when we don't want to apply changes but still we need to return a delta.
  265. function noDelta() {
  266. let noDelta = new Delta();
  267. // BaseVersion will be fixed later anyway.
  268. noDelta.addOperation( new NoOperation( 0 ) );
  269. return noDelta;
  270. }