8
0
فهرست منبع

Merge pull request #1175 from ckeditor/t/897

Other: Make `Position` and `Range` immutable in model and view. Closes #897.
Szymon Cofalik 8 سال پیش
والد
کامیت
710f95e6ce
28فایلهای تغییر یافته به همراه808 افزوده شده و 564 حذف شده
  1. 3 2
      packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js
  2. 2 3
      packages/ckeditor5-engine/src/dev-utils/view.js
  3. 25 15
      packages/ckeditor5-engine/src/model/delta/basic-transformations.js
  4. 1 3
      packages/ckeditor5-engine/src/model/documentselection.js
  5. 2 2
      packages/ckeditor5-engine/src/model/liveposition.js
  6. 2 2
      packages/ckeditor5-engine/src/model/liverange.js
  7. 1 1
      packages/ckeditor5-engine/src/model/operation/insertoperation.js
  8. 2 2
      packages/ckeditor5-engine/src/model/operation/renameoperation.js
  9. 38 23
      packages/ckeditor5-engine/src/model/operation/transform.js
  10. 117 69
      packages/ckeditor5-engine/src/model/position.js
  11. 56 24
      packages/ckeditor5-engine/src/model/range.js
  12. 10 12
      packages/ckeditor5-engine/src/model/selection.js
  13. 33 39
      packages/ckeditor5-engine/src/model/treewalker.js
  14. 24 19
      packages/ckeditor5-engine/src/view/position.js
  15. 26 15
      packages/ckeditor5-engine/src/view/range.js
  16. 11 15
      packages/ckeditor5-engine/src/view/selection.js
  17. 41 58
      packages/ckeditor5-engine/src/view/treewalker.js
  18. 29 24
      packages/ckeditor5-engine/src/view/writer.js
  19. 23 16
      packages/ckeditor5-engine/tests/model/delta/transform/_utils/utils.js
  20. 2 2
      packages/ckeditor5-engine/tests/model/delta/transform/movedelta.js
  21. 17 8
      packages/ckeditor5-engine/tests/model/delta/transform/splitdelta.js
  22. 15 5
      packages/ckeditor5-engine/tests/model/liverange.js
  23. 259 136
      packages/ckeditor5-engine/tests/model/operation/transform.js
  24. 0 24
      packages/ckeditor5-engine/tests/model/position.js
  25. 50 25
      packages/ckeditor5-engine/tests/model/range.js
  26. 2 2
      packages/ckeditor5-engine/tests/view/range.js
  27. 5 5
      packages/ckeditor5-engine/tests/view/selection.js
  28. 12 13
      packages/ckeditor5-engine/tests/view/writer/remove.js

+ 3 - 2
packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js

@@ -303,10 +303,11 @@ function extractMarkersFromModelFragment( modelItem ) {
 
 
 		// When marker of given name is not stored it means that we have found the beginning of the range.
 		// When marker of given name is not stored it means that we have found the beginning of the range.
 		if ( !markers.has( markerName ) ) {
 		if ( !markers.has( markerName ) ) {
-			markers.set( markerName, new ModelRange( ModelPosition.createFromPosition( currentPosition ) ) );
+			markers.set( markerName, new ModelRange( currentPosition ) );
 		// Otherwise is means that we have found end of the marker range.
 		// Otherwise is means that we have found end of the marker range.
 		} else {
 		} else {
-			markers.get( markerName ).end = ModelPosition.createFromPosition( currentPosition );
+			const oldMarker = markers.get( markerName );
+			markers.set( markerName, new ModelRange( oldMarker.start, currentPosition ) );
 		}
 		}
 
 
 		// Remove marker element from DocumentFragment.
 		// Remove marker element from DocumentFragment.

+ 2 - 3
packages/ckeditor5-engine/src/dev-utils/view.js

@@ -541,8 +541,7 @@ class RangeParser {
 				throw new Error( `Parse error - end of range was found '${ item.bracket }' but range was not started before.` );
 				throw new Error( `Parse error - end of range was found '${ item.bracket }' but range was not started before.` );
 			}
 			}
 
 
-			// When second start of range is found when one is already opened - selection does not allow intersecting
-			// ranges.
+			// When second start of range is found when one is already opened - selection does not allow intersecting ranges.
 			if ( range && ( item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN ) ) {
 			if ( range && ( item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN ) ) {
 				throw new Error( `Parse error - start of range was found '${ item.bracket }' but one range is already started.` );
 				throw new Error( `Parse error - start of range was found '${ item.bracket }' but one range is already started.` );
 			}
 			}
@@ -550,7 +549,7 @@ class RangeParser {
 			if ( item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN ) {
 			if ( item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN ) {
 				range = new Range( item.position, item.position );
 				range = new Range( item.position, item.position );
 			} else {
 			} else {
-				range.end = item.position;
+				range = new Range( range.start, item.position );
 				ranges.push( range );
 				ranges.push( range );
 				range = null;
 				range = null;
 			}
 			}

+ 25 - 15
packages/ckeditor5-engine/src/model/delta/basic-transformations.js

@@ -73,8 +73,10 @@ addTransformationCase( AttributeDelta, SplitDelta, ( a, b, context ) => {
 			const additionalAttributeDelta = new AttributeDelta();
 			const additionalAttributeDelta = new AttributeDelta();
 
 
 			const rangeStart = splitPosition.getShiftedBy( 1 );
 			const rangeStart = splitPosition.getShiftedBy( 1 );
-			const rangeEnd = Position.createFromPosition( rangeStart );
-			rangeEnd.path.push( 0 );
+
+			const rangeEndPath = rangeStart.path.slice();
+			rangeEndPath.push( 0 );
+			const rangeEnd = new Position( rangeStart.root, rangeEndPath );
 
 
 			const oldValue = b._cloneOperation.nodes.getNode( 0 ).getAttribute( operation.key );
 			const oldValue = b._cloneOperation.nodes.getNode( 0 ).getAttribute( operation.key );
 
 
@@ -236,7 +238,7 @@ addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
 				a._cloneOperation instanceof ReinsertOperation && b._cloneOperation instanceof ReinsertOperation &&
 				a._cloneOperation instanceof ReinsertOperation && b._cloneOperation instanceof ReinsertOperation &&
 				a._cloneOperation.sourcePosition.offset > b._cloneOperation.sourcePosition.offset
 				a._cloneOperation.sourcePosition.offset > b._cloneOperation.sourcePosition.offset
 			) {
 			) {
-				a._cloneOperation.sourcePosition.offset--;
+				a._cloneOperation.sourcePosition = a._cloneOperation.sourcePosition.getShiftedBy( -1 );
 			}
 			}
 
 
 			// `a` splits closer or at same offset.
 			// `a` splits closer or at same offset.
@@ -317,29 +319,33 @@ addTransformationCase( SplitDelta, WrapDelta, ( a, b, context ) => {
 		// Wrapping element is the element inserted by WrapDelta (re)insert operation.
 		// Wrapping element is the element inserted by WrapDelta (re)insert operation.
 		// It is inserted after the wrapped range, but the wrapped range will be moved inside it.
 		// It is inserted after the wrapped range, but the wrapped range will be moved inside it.
 		// Having this in mind, it is correct to use wrapped range start position as the position before wrapping element.
 		// Having this in mind, it is correct to use wrapped range start position as the position before wrapping element.
-		const splitNodePos = Position.createFromPosition( b.range.start );
+
 		// Now, `splitNodePos` points before wrapping element.
 		// Now, `splitNodePos` points before wrapping element.
 		// To get a position before last children of that element, we expand position's `path` member by proper offset.
 		// To get a position before last children of that element, we expand position's `path` member by proper offset.
-		splitNodePos.path.push( b.howMany - 1 );
+		const splitPath = b.range.start.path.slice();
+		splitPath.push( b.howMany - 1 );
+
+		const splitNodePos = new Position( b.range.start.root, splitPath );
 
 
 		// SplitDelta insert operation position should be right after the node we split.
 		// SplitDelta insert operation position should be right after the node we split.
-		const insertPos = splitNodePos.getShiftedBy( 1 );
-		delta._cloneOperation.position = insertPos;
+		delta._cloneOperation.position = splitNodePos.getShiftedBy( 1 );
 
 
 		// 2. Fix move operation source position.
 		// 2. Fix move operation source position.
 		// Nodes moved by SplitDelta will be moved from new position, modified by WrapDelta.
 		// Nodes moved by SplitDelta will be moved from new position, modified by WrapDelta.
 		// To obtain that new position, `splitNodePos` will be used, as this is the node we are extracting children from.
 		// To obtain that new position, `splitNodePos` will be used, as this is the node we are extracting children from.
-		const sourcePos = Position.createFromPosition( splitNodePos );
 		// Nothing changed inside split node so it is correct to use the original split position offset.
 		// Nothing changed inside split node so it is correct to use the original split position offset.
-		sourcePos.path.push( a.position.offset );
-		delta._moveOperation.sourcePosition = sourcePos;
+		const sourcePath = splitNodePos.path.slice();
+		sourcePath.push( a.position.offset );
+
+		delta._moveOperation.sourcePosition = new Position( splitNodePos.root, sourcePath );
 
 
 		// 3. Fix move operation target position.
 		// 3. Fix move operation target position.
 		// SplitDelta move operation target position should be inside the node inserted by operation above.
 		// SplitDelta move operation target position should be inside the node inserted by operation above.
 		// Since the node is empty, we will insert at offset 0.
 		// Since the node is empty, we will insert at offset 0.
-		const targetPos = Position.createFromPosition( insertPos );
-		targetPos.path.push( 0 );
-		delta._moveOperation.targetPosition = targetPos;
+		const targetPath = splitNodePos.getShiftedBy( 1 ).path.slice();
+		targetPath.push( 0 );
+
+		delta._moveOperation.targetPosition = new Position( splitNodePos.root, targetPath );
 
 
 		return [ delta ];
 		return [ delta ];
 	}
 	}
@@ -434,13 +440,17 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
 		const delta = a.clone();
 		const delta = a.clone();
 
 
 		// Move wrapping element insert position one node further so it is after the split node insertion.
 		// Move wrapping element insert position one node further so it is after the split node insertion.
-		delta._insertOperation.position.offset++;
+		delta._insertOperation.position = delta._insertOperation.position.getShiftedBy( 1 );
 
 
 		// Include the split node copy.
 		// Include the split node copy.
 		delta._moveOperation.howMany++;
 		delta._moveOperation.howMany++;
 
 
 		// Change the path to wrapping element in move operation.
 		// Change the path to wrapping element in move operation.
-		delta._moveOperation.targetPosition.path[ delta._moveOperation.targetPosition.path.length - 2 ]++;
+		const index = delta._moveOperation.targetPosition.path.length - 2;
+
+		const path = delta._moveOperation.targetPosition.path.slice();
+		path[ index ] += 1;
+		delta._moveOperation.targetPosition = new Position( delta._moveOperation.targetPosition.root, path );
 
 
 		return [ delta ];
 		return [ delta ];
 	}
 	}

+ 1 - 3
packages/ckeditor5-engine/src/model/documentselection.js

@@ -7,7 +7,6 @@
  * @module engine/model/documentselection
  * @module engine/model/documentselection
  */
  */
 
 
-import Position from './position';
 import Range from './range';
 import Range from './range';
 import LiveRange from './liverange';
 import LiveRange from './liverange';
 import Text from './text';
 import Text from './text';
@@ -666,10 +665,9 @@ export default class DocumentSelection extends Selection {
 	_fixGraveyardSelection( liveRange, removedRangeStart ) {
 	_fixGraveyardSelection( liveRange, removedRangeStart ) {
 		// The start of the removed range is the closest position to the `liveRange` - the original selection range.
 		// The start of the removed range is the closest position to the `liveRange` - the original selection range.
 		// This is a good candidate for a fixed selection range.
 		// This is a good candidate for a fixed selection range.
-		const positionCandidate = Position.createFromPosition( removedRangeStart );
 
 
 		// Find a range that is a correct selection range and is closest to the start of removed range.
 		// Find a range that is a correct selection range and is closest to the start of removed range.
-		const selectionRange = this._document.getNearestSelectionRange( positionCandidate );
+		const selectionRange = this._document.getNearestSelectionRange( removedRangeStart );
 
 
 		// Remove the old selection range before preparing and adding new selection range. This order is important,
 		// Remove the old selection range before preparing and adding new selection range. This order is important,
 		// because new range, in some cases, may intersect with old range (it depends on `getNearestSelectionRange()` result).
 		// because new range, in some cases, may intersect with old range (it depends on `getNearestSelectionRange()` result).

+ 2 - 2
packages/ckeditor5-engine/src/model/liveposition.js

@@ -194,8 +194,8 @@ function transform( type, range, position ) {
 	if ( !this.isEqual( transformed ) ) {
 	if ( !this.isEqual( transformed ) ) {
 		const oldPosition = Position.createFromPosition( this );
 		const oldPosition = Position.createFromPosition( this );
 
 
-		this.path = transformed.path;
-		this.root = transformed.root;
+		this._path = transformed.path;
+		this._root = transformed.root;
 
 
 		this.fire( 'change', oldPosition );
 		this.fire( 'change', oldPosition );
 	}
 	}

+ 2 - 2
packages/ckeditor5-engine/src/model/liverange.js

@@ -178,8 +178,8 @@ function transform( changeType, deltaType, batch, targetRange, sourcePosition )
 		// If range boundaries have changed, fire `change:range` event.
 		// If range boundaries have changed, fire `change:range` event.
 		const oldRange = Range.createFromRange( this );
 		const oldRange = Range.createFromRange( this );
 
 
-		this.start = updated.start;
-		this.end = updated.end;
+		this._start = updated.start;
+		this._end = updated.end;
 
 
 		this.fire( 'change:range', oldRange, {
 		this.fire( 'change:range', oldRange, {
 			type: changeType,
 			type: changeType,

+ 1 - 1
packages/ckeditor5-engine/src/model/operation/insertoperation.js

@@ -37,7 +37,7 @@ export default class InsertOperation extends Operation {
 		 * @readonly
 		 * @readonly
 		 * @member {module:engine/model/position~Position} module:engine/model/operation/insertoperation~InsertOperation#position
 		 * @member {module:engine/model/position~Position} module:engine/model/operation/insertoperation~InsertOperation#position
 		 */
 		 */
-		this.position = Position.createFromPosition( position );
+		this.position = position;
 
 
 		/**
 		/**
 		 * List of nodes to insert.
 		 * List of nodes to insert.

+ 2 - 2
packages/ckeditor5-engine/src/model/operation/renameoperation.js

@@ -66,7 +66,7 @@ export default class RenameOperation extends Operation {
 	 * @returns {module:engine/model/operation/renameoperation~RenameOperation} Clone of this operation.
 	 * @returns {module:engine/model/operation/renameoperation~RenameOperation} Clone of this operation.
 	 */
 	 */
 	clone() {
 	clone() {
-		return new RenameOperation( Position.createFromPosition( this.position ), this.oldName, this.newName, this.baseVersion );
+		return new RenameOperation( this.position, this.oldName, this.newName, this.baseVersion );
 	}
 	}
 
 
 	/**
 	/**
@@ -75,7 +75,7 @@ export default class RenameOperation extends Operation {
 	 * @returns {module:engine/model/operation/renameoperation~RenameOperation}
 	 * @returns {module:engine/model/operation/renameoperation~RenameOperation}
 	 */
 	 */
 	getReversed() {
 	getReversed() {
-		return new RenameOperation( Position.createFromPosition( this.position ), this.newName, this.oldName, this.baseVersion + 1 );
+		return new RenameOperation( this.position, this.newName, this.oldName, this.baseVersion + 1 );
 	}
 	}
 
 
 	/**
 	/**

+ 38 - 23
packages/ckeditor5-engine/src/model/operation/transform.js

@@ -179,25 +179,29 @@ const ot = {
 				// Take the start and the end of the range and transform them by deletion of moved nodes.
 				// Take the start and the end of the range and transform them by deletion of moved nodes.
 				// Note that if rangeB was inside AttributeOperation range, only difference.end will be transformed.
 				// Note that if rangeB was inside AttributeOperation range, only difference.end will be transformed.
 				// This nicely covers the joining simplification we did in the previous step.
 				// This nicely covers the joining simplification we did in the previous step.
-				difference.start = difference.start._getTransformedByDeletion( b.sourcePosition, b.howMany );
-				difference.end = difference.end._getTransformedByDeletion( b.sourcePosition, b.howMany );
+				const differenceTransformed = new Range(
+					difference.start._getTransformedByDeletion( b.sourcePosition, b.howMany ),
+					difference.end._getTransformedByDeletion( b.sourcePosition, b.howMany )
+				);
 
 
 				// MoveOperation pastes nodes into target position. We acknowledge this by proper transformation.
 				// MoveOperation pastes nodes into target position. We acknowledge this by proper transformation.
 				// Note that since we operate on transformed difference range, we should transform by
 				// Note that since we operate on transformed difference range, we should transform by
 				// previously transformed target position.
 				// previously transformed target position.
 				// Note that we do not use Position._getTransformedByMove on range boundaries because we need to
 				// Note that we do not use Position._getTransformedByMove on range boundaries because we need to
 				// transform by insertion a range as a whole, since newTargetPosition might be inside that range.
 				// transform by insertion a range as a whole, since newTargetPosition might be inside that range.
-				ranges = difference._getTransformedByInsertion( b.getMovedRangeStart(), b.howMany, true, false ).reverse();
+				ranges = differenceTransformed._getTransformedByInsertion( b.getMovedRangeStart(), b.howMany, true, false ).reverse();
 			}
 			}
 
 
 			if ( common !== null ) {
 			if ( common !== null ) {
 				// Here we do not need to worry that newTargetPosition is inside moved range, because that
 				// Here we do not need to worry that newTargetPosition is inside moved range, because that
 				// would mean that the MoveOperation targets into itself, and that is incorrect operation.
 				// would mean that the MoveOperation targets into itself, and that is incorrect operation.
 				// Instead, we calculate the new position of that part of original range.
 				// Instead, we calculate the new position of that part of original range.
-				common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
-				common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
+				const commonTransformed = new Range(
+					common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() ),
+					common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() )
+				);
 
 
-				ranges.push( common );
+				ranges.push( commonTransformed );
 			}
 			}
 
 
 			// Map transformed range(s) to operations and return them.
 			// Map transformed range(s) to operations and return them.
@@ -376,7 +380,7 @@ const ot = {
 			// Setting and evaluating some variables that will be used in special cases and default algorithm.
 			// Setting and evaluating some variables that will be used in special cases and default algorithm.
 			//
 			//
 			// Create ranges from `MoveOperations` properties.
 			// Create ranges from `MoveOperations` properties.
-			const rangeA = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
+			let rangeA = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
 			const rangeB = Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
 			const rangeB = Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
 
 
 			// Assign `context.isStrong` to a different variable, because the value may change during execution of
 			// Assign `context.isStrong` to a different variable, because the value may change during execution of
@@ -428,8 +432,10 @@ const ot = {
 			if ( bTargetsToA && rangeA.containsRange( rangeB, true ) ) {
 			if ( bTargetsToA && rangeA.containsRange( rangeB, true ) ) {
 				// There is a mini-special case here, where `rangeB` is on other level than `rangeA`. That's why
 				// There is a mini-special case here, where `rangeB` is on other level than `rangeA`. That's why
 				// we need to transform `a` operation anyway.
 				// we need to transform `a` operation anyway.
-				rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB );
-				rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB );
+				rangeA = new Range(
+					rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB ),
+					rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB )
+				);
 
 
 				return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
 				return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
 			}
 			}
@@ -444,8 +450,10 @@ const ot = {
 			if ( aTargetsToB && rangeB.containsRange( rangeA, true ) ) {
 			if ( aTargetsToB && rangeB.containsRange( rangeA, true ) ) {
 				// `a` operation is "moved together" with `b` operation.
 				// `a` operation is "moved together" with `b` operation.
 				// Here, just move `rangeA` "inside" `rangeB`.
 				// Here, just move `rangeA` "inside" `rangeB`.
-				rangeA.start = rangeA.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
-				rangeA.end = rangeA.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
+				rangeA = new Range(
+					rangeA.start._getCombined( b.sourcePosition, b.getMovedRangeStart() ),
+					rangeA.end._getCombined( b.sourcePosition, b.getMovedRangeStart() )
+				);
 
 
 				return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
 				return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
 			}
 			}
@@ -466,8 +474,10 @@ const ot = {
 				// Transform `rangeA` by `b` operation and make operation out of it, and that's all.
 				// Transform `rangeA` by `b` operation and make operation out of it, and that's all.
 				// Note that this is a simplified version of default case, but here we treat the common part (whole `rangeA`)
 				// Note that this is a simplified version of default case, but here we treat the common part (whole `rangeA`)
 				// like a one difference part.
 				// like a one difference part.
-				rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB );
-				rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB );
+				rangeA = new Range(
+					rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB ),
+					rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB )
+				);
 
 
 				return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
 				return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
 			}
 			}
@@ -500,10 +510,12 @@ const ot = {
 			// This is an array with one or two ranges. Two ranges if `rangeB` is inside `rangeA`.
 			// This is an array with one or two ranges. Two ranges if `rangeB` is inside `rangeA`.
 			const difference = rangeA.getDifference( rangeB );
 			const difference = rangeA.getDifference( rangeB );
 
 
-			for ( const range of difference ) {
+			for ( const rangeInDiff of difference ) {
 				// Transform those ranges by `b` operation. For example if `b` moved range from before those ranges, fix those ranges.
 				// Transform those ranges by `b` operation. For example if `b` moved range from before those ranges, fix those ranges.
-				range.start = range.start._getTransformedByDeletion( b.sourcePosition, b.howMany );
-				range.end = range.end._getTransformedByDeletion( b.sourcePosition, b.howMany );
+				const range = new Range(
+					rangeInDiff.start._getTransformedByDeletion( b.sourcePosition, b.howMany ),
+					rangeInDiff.end._getTransformedByDeletion( b.sourcePosition, b.howMany )
+				);
 
 
 				// If `b` operation targets into `rangeA` on the same level, spread `rangeA` into two ranges.
 				// If `b` operation targets into `rangeA` on the same level, spread `rangeA` into two ranges.
 				const shouldSpread = compareArrays( range.start.getParentPath(), b.getMovedRangeStart().getParentPath() ) == 'same';
 				const shouldSpread = compareArrays( range.start.getParentPath(), b.getMovedRangeStart().getParentPath() ) == 'same';
@@ -513,12 +525,14 @@ const ot = {
 			}
 			}
 
 
 			// Then, we have to manage the "common part" of both move ranges.
 			// Then, we have to manage the "common part" of both move ranges.
-			const common = rangeA.getIntersection( rangeB );
+			const intersectionRange = rangeA.getIntersection( rangeB );
 
 
-			if ( common !== null && isStrong && !bTargetsToA ) {
+			if ( intersectionRange !== null && isStrong && !bTargetsToA ) {
 				// Calculate the new position of that part of original range.
 				// Calculate the new position of that part of original range.
-				common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
-				common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
+				const common = new Range(
+					intersectionRange.start._getCombined( b.sourcePosition, b.getMovedRangeStart() ),
+					intersectionRange.end._getCombined( b.sourcePosition, b.getMovedRangeStart() )
+				);
 
 
 				// Take care of proper range order.
 				// Take care of proper range order.
 				//
 				//
@@ -627,9 +641,10 @@ function joinRanges( ranges ) {
 	} else if ( ranges.length == 1 ) {
 	} else if ( ranges.length == 1 ) {
 		return ranges[ 0 ];
 		return ranges[ 0 ];
 	} else {
 	} else {
-		ranges[ 0 ].end = ranges[ ranges.length - 1 ].end;
-
-		return ranges[ 0 ];
+		return new Range(
+			ranges[ 0 ].start,
+			ranges[ ranges.length - 1 ].end
+		);
 	}
 	}
 }
 }
 
 

+ 117 - 69
packages/ckeditor5-engine/src/model/position.js

@@ -66,47 +66,70 @@ export default class Position {
 
 
 		// Normalize the root and path (if element was passed).
 		// Normalize the root and path (if element was passed).
 		path = root.getPath().concat( path );
 		path = root.getPath().concat( path );
-		root = root.root;
+
+		// Make path immutable
+		Object.freeze( path );
 
 
 		/**
 		/**
 		 * Root of the position path.
 		 * Root of the position path.
 		 *
 		 *
-		 * @readonly
+		 * @protected
 		 * @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
 		 * @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
-		 * module:engine/model/position~Position#root
+		 * module:engine/model/position~Position#_root
 		 */
 		 */
-		this.root = root;
+		this._root = root.root;
 
 
 		/**
 		/**
-		 * Position of the node in the tree. **Path contains offsets, not indexes.**
-		 *
-		 * Position can be placed before, after or in a {@link module:engine/model/node~Node node} if that node has
-		 * {@link module:engine/model/node~Node#offsetSize} greater than `1`. Items in position path are
-		 * {@link module:engine/model/node~Node#startOffset starting offsets} of position ancestors, starting from direct root children,
-		 * down to the position offset in it's parent.
-		 *
-		 *		 ROOT
-		 *		  |- P            before: [ 0 ]         after: [ 1 ]
-		 *		  |- UL           before: [ 1 ]         after: [ 2 ]
-		 *		     |- LI        before: [ 1, 0 ]      after: [ 1, 1 ]
-		 *		     |  |- foo    before: [ 1, 0, 0 ]   after: [ 1, 0, 3 ]
-		 *		     |- LI        before: [ 1, 1 ]      after: [ 1, 2 ]
-		 *		        |- bar    before: [ 1, 1, 0 ]   after: [ 1, 1, 3 ]
+		 * Position of the node in the tree.
 		 *
 		 *
-		 * `foo` and `bar` are representing {@link module:engine/model/text~Text text nodes}. Since text nodes has offset size
-		 * greater than `1` you can place position offset between their start and end:
-		 *
-		 *		 ROOT
-		 *		  |- P
-		 *		  |- UL
-		 *		     |- LI
-		 *		     |  |- f^o|o  ^ has path: [ 1, 0, 1 ]   | has path: [ 1, 0, 2 ]
-		 *		     |- LI
-		 *		        |- b^a|r  ^ has path: [ 1, 1, 1 ]   | has path: [ 1, 1, 2 ]
-		 *
-		 * @member {Array.<Number>} module:engine/model/position~Position#path
+		 * @protected
+		 * @member {Array.<Number>} module:engine/model/position~Position#_path
 		 */
 		 */
-		this.path = path;
+		this._path = path;
+	}
+
+	/**
+	 * Position of the node in the tree. **Path contains offsets, not indexes.**
+	 *
+	 * Position can be placed before, after or in a {@link module:engine/model/node~Node node} if that node has
+	 * {@link module:engine/model/node~Node#offsetSize} greater than `1`. Items in position path are
+	 * {@link module:engine/model/node~Node#startOffset starting offsets} of position ancestors, starting from direct root children,
+	 * down to the position offset in it's parent.
+	 *
+	 *		 ROOT
+	 *		  |- P            before: [ 0 ]         after: [ 1 ]
+	 *		  |- UL           before: [ 1 ]         after: [ 2 ]
+	 *		     |- LI        before: [ 1, 0 ]      after: [ 1, 1 ]
+	 *		     |  |- foo    before: [ 1, 0, 0 ]   after: [ 1, 0, 3 ]
+	 *		     |- LI        before: [ 1, 1 ]      after: [ 1, 2 ]
+	 *		        |- bar    before: [ 1, 1, 0 ]   after: [ 1, 1, 3 ]
+	 *
+	 * `foo` and `bar` are representing {@link module:engine/model/text~Text text nodes}. Since text nodes has offset size
+	 * greater than `1` you can place position offset between their start and end:
+	 *
+	 *		 ROOT
+	 *		  |- P
+	 *		  |- UL
+	 *		     |- LI
+	 *		     |  |- f^o|o  ^ has path: [ 1, 0, 1 ]   | has path: [ 1, 0, 2 ]
+	 *		     |- LI
+	 *		        |- b^a|r  ^ has path: [ 1, 1, 1 ]   | has path: [ 1, 1, 2 ]
+	 *
+	 * @member {Array.<Number>} module:engine/model/position~Position#path
+	 */
+	get path() {
+		return this._path;
+	}
+
+	/**
+	 * Root of the position path.
+	 *
+	 * @readonly
+	 * @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
+	 * module:engine/model/position~Position#root
+	 */
+	get root() {
+		return this._root;
 	}
 	}
 
 
 	/**
 	/**
@@ -119,13 +142,6 @@ export default class Position {
 		return last( this.path );
 		return last( this.path );
 	}
 	}
 
 
-	/**
-	 * @param {Number} newOffset
-	 */
-	set offset( newOffset ) {
-		this.path[ this.path.length - 1 ] = newOffset;
-	}
-
 	/**
 	/**
 	 * Parent element of this position.
 	 * Parent element of this position.
 	 *
 	 *
@@ -340,6 +356,20 @@ export default class Position {
 		return i === 0 ? null : ancestorsA[ i - 1 ];
 		return i === 0 ? null : ancestorsA[ i - 1 ];
 	}
 	}
 
 
+	/**
+	 * Returns a new instance of `Position`, that has same {@link #parent parent} but it's offset
+	 * is set to `offset` value.
+	 *
+	 * @param {Number} offset Position offset. See {@link module:engine/model/position~Position#offset}.
+	 * @returns {module:engine/model/position~Position} Moved position.
+	 */
+	getShiftedTo( offset ) {
+		const path = this.path.slice();
+		path[ path.length - 1 ] = offset;
+
+		return new Position( this.root, path );
+	}
+
 	/**
 	/**
 	 * Returns a new instance of `Position`, that has same {@link #parent parent} but it's offset
 	 * Returns a new instance of `Position`, that has same {@link #parent parent} but it's offset
 	 * is shifted by `shift` value (can be a negative value).
 	 * is shifted by `shift` value (can be a negative value).
@@ -348,12 +378,9 @@ export default class Position {
 	 * @returns {module:engine/model/position~Position} Shifted position.
 	 * @returns {module:engine/model/position~Position} Shifted position.
 	 */
 	 */
 	getShiftedBy( shift ) {
 	getShiftedBy( shift ) {
-		const shifted = Position.createFromPosition( this );
-
-		const offset = shifted.offset + shift;
-		shifted.offset = offset < 0 ? 0 : offset;
+		const newOffset = this.offset + shift;
 
 
-		return shifted;
+		return this.getShiftedTo( newOffset < 0 ? 0 : newOffset );
 	}
 	}
 
 
 	/**
 	/**
@@ -433,13 +460,13 @@ export default class Position {
 				return true;
 				return true;
 
 
 			case 'before':
 			case 'before':
-				left = Position.createFromPosition( this );
-				right = Position.createFromPosition( otherPosition );
+				left = this; // eslint-disable-line consistent-this
+				right = otherPosition;
 				break;
 				break;
 
 
 			case 'after':
 			case 'after':
-				left = Position.createFromPosition( otherPosition );
-				right = Position.createFromPosition( this );
+				left = otherPosition;
+				right = this; // eslint-disable-line consistent-this
 				break;
 				break;
 
 
 			default:
 			default:
@@ -459,19 +486,33 @@ export default class Position {
 					return false;
 					return false;
 				}
 				}
 
 
-				left.path = left.path.slice( 0, -1 );
+				const path = left.getParentPath();
+				path[ path.length - 1 ]++;
+				left = new Position( left.root, path );
+
 				leftParent = leftParent.parent;
 				leftParent = leftParent.parent;
-				left.offset++;
 			} else {
 			} else {
 				if ( right.offset !== 0 ) {
 				if ( right.offset !== 0 ) {
 					return false;
 					return false;
 				}
 				}
 
 
-				right.path = right.path.slice( 0, -1 );
+				right = new Position( right.root, right.getParentPath() );
 			}
 			}
 		}
 		}
 	}
 	}
 
 
+	/**
+	 * Converts `Position` to plain object and returns it.
+	 *
+	 * @returns {Object} `Position` converted to plain object.
+	 */
+	toJSON() {
+		return {
+			root: this.root.toJSON(),
+			path: this.path
+		};
+	}
+
 	/**
 	/**
 	 * Returns a copy of this position that is updated by removing `howMany` nodes starting from `deletePosition`.
 	 * Returns a copy of this position that is updated by removing `howMany` nodes starting from `deletePosition`.
 	 * It may happen that this position is in a removed node. If that is the case, `null` is returned instead.
 	 * It may happen that this position is in a removed node. If that is the case, `null` is returned instead.
@@ -482,14 +523,14 @@ export default class Position {
 	 * @returns {module:engine/model/position~Position|null} Transformed position or `null`.
 	 * @returns {module:engine/model/position~Position|null} Transformed position or `null`.
 	 */
 	 */
 	_getTransformedByDeletion( deletePosition, howMany ) {
 	_getTransformedByDeletion( deletePosition, howMany ) {
-		const transformed = Position.createFromPosition( this );
-
 		// This position can't be affected if deletion was in a different root.
 		// This position can't be affected if deletion was in a different root.
 		if ( this.root != deletePosition.root ) {
 		if ( this.root != deletePosition.root ) {
-			return transformed;
+			return Position.createFromPosition( this );
 		}
 		}
 
 
-		if ( compareArrays( deletePosition.getParentPath(), this.getParentPath() ) == 'same' ) {
+		const comparisonResult = compareArrays( deletePosition.getParentPath(), this.getParentPath() );
+
+		if ( comparisonResult == 'same' ) {
 			// If nodes are removed from the node that is pointed by this position...
 			// If nodes are removed from the node that is pointed by this position...
 			if ( deletePosition.offset < this.offset ) {
 			if ( deletePosition.offset < this.offset ) {
 				// And are removed from before an offset of that position...
 				// And are removed from before an offset of that position...
@@ -497,11 +538,10 @@ export default class Position {
 					// Position is in removed range, it's no longer in the tree.
 					// Position is in removed range, it's no longer in the tree.
 					return null;
 					return null;
 				} else {
 				} else {
-					// Decrement the offset accordingly.
-					transformed.offset -= howMany;
+					return this.getShiftedBy( -howMany );
 				}
 				}
 			}
 			}
-		} else if ( compareArrays( deletePosition.getParentPath(), this.getParentPath() ) == 'prefix' ) {
+		} else if ( comparisonResult == 'prefix' ) {
 			// If nodes are removed from a node that is on a path to this position...
 			// If nodes are removed from a node that is on a path to this position...
 			const i = deletePosition.path.length - 1;
 			const i = deletePosition.path.length - 1;
 
 
@@ -513,12 +553,16 @@ export default class Position {
 					return null;
 					return null;
 				} else {
 				} else {
 					// Otherwise, decrement index on that path.
 					// Otherwise, decrement index on that path.
-					transformed.path[ i ] -= howMany;
+					const path = this.path.slice();
+
+					path[ i ] -= howMany;
+
+					return new Position( this.root, path );
 				}
 				}
 			}
 			}
 		}
 		}
 
 
-		return transformed;
+		return Position.createFromPosition( this );
 	}
 	}
 
 
 	/**
 	/**
@@ -533,11 +577,9 @@ export default class Position {
 	 * @returns {module:engine/model/position~Position} Transformed position.
 	 * @returns {module:engine/model/position~Position} Transformed position.
 	 */
 	 */
 	_getTransformedByInsertion( insertPosition, howMany, insertBefore ) {
 	_getTransformedByInsertion( insertPosition, howMany, insertBefore ) {
-		const transformed = Position.createFromPosition( this );
-
 		// This position can't be affected if insertion was in a different root.
 		// This position can't be affected if insertion was in a different root.
 		if ( this.root != insertPosition.root ) {
 		if ( this.root != insertPosition.root ) {
-			return transformed;
+			return Position.createFromPosition( this );
 		}
 		}
 
 
 		if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'same' ) {
 		if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'same' ) {
@@ -545,7 +587,7 @@ export default class Position {
 			if ( insertPosition.offset < this.offset || ( insertPosition.offset == this.offset && insertBefore ) ) {
 			if ( insertPosition.offset < this.offset || ( insertPosition.offset == this.offset && insertBefore ) ) {
 				// And are inserted before an offset of that position...
 				// And are inserted before an offset of that position...
 				// "Push" this positions offset.
 				// "Push" this positions offset.
-				transformed.offset += howMany;
+				return this.getShiftedBy( howMany );
 			}
 			}
 		} else if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'prefix' ) {
 		} else if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'prefix' ) {
 			// If nodes are inserted in a node that is on a path to this position...
 			// If nodes are inserted in a node that is on a path to this position...
@@ -554,11 +596,15 @@ export default class Position {
 			if ( insertPosition.offset <= this.path[ i ] ) {
 			if ( insertPosition.offset <= this.path[ i ] ) {
 				// And are inserted before next node of that path...
 				// And are inserted before next node of that path...
 				// "Push" the index on that path.
 				// "Push" the index on that path.
-				transformed.path[ i ] += howMany;
+				const path = this.path.slice();
+
+				path[ i ] += howMany;
+
+				return new Position( this.root, path );
 			}
 			}
 		}
 		}
 
 
-		return transformed;
+		return Position.createFromPosition( this );
 	}
 	}
 
 
 	/**
 	/**
@@ -626,18 +672,20 @@ export default class Position {
 		const i = source.path.length - 1;
 		const i = source.path.length - 1;
 
 
 		// The first part of a path to combined position is a path to the place where nodes were moved.
 		// The first part of a path to combined position is a path to the place where nodes were moved.
-		const combined = Position.createFromPosition( target );
+		let combinedPath = target.path.slice();
 
 
 		// Then we have to update the rest of the path.
 		// Then we have to update the rest of the path.
 
 
 		// Fix the offset because this position might be after `from` position and we have to reflect that.
 		// Fix the offset because this position might be after `from` position and we have to reflect that.
-		combined.offset = combined.offset + this.path[ i ] - source.offset;
+		const oldOffset = last( combinedPath );
+		const newOffset = oldOffset + this.path[ i ] - source.offset;
+		combinedPath[ combinedPath.length - 1 ] = newOffset;
 
 
 		// Then, add the rest of the path.
 		// Then, add the rest of the path.
 		// If this position is at the same level as `from` position nothing will get added.
 		// If this position is at the same level as `from` position nothing will get added.
-		combined.path = combined.path.concat( this.path.slice( i + 1 ) );
+		combinedPath = combinedPath.concat( this.path.slice( i + 1 ) );
 
 
-		return combined;
+		return new Position( target.root, combinedPath );
 	}
 	}
 
 
 	/**
 	/**

+ 56 - 24
packages/ckeditor5-engine/src/model/range.js

@@ -27,18 +27,18 @@ export default class Range {
 		/**
 		/**
 		 * Start position.
 		 * Start position.
 		 *
 		 *
-		 * @readonly
+		 * @protected
 		 * @member {module:engine/model/position~Position}
 		 * @member {module:engine/model/position~Position}
 		 */
 		 */
-		this.start = Position.createFromPosition( start );
+		this._start = start;
 
 
 		/**
 		/**
 		 * End position.
 		 * End position.
 		 *
 		 *
-		 * @readonly
+		 * @protected
 		 * @member {module:engine/model/position~Position}
 		 * @member {module:engine/model/position~Position}
 		 */
 		 */
-		this.end = end ? Position.createFromPosition( end ) : Position.createFromPosition( start );
+		this._end = end ? end : start;
 	}
 	}
 
 
 	/**
 	/**
@@ -57,6 +57,26 @@ export default class Range {
 		yield* new TreeWalker( { boundaries: this, ignoreElementEnd: true } );
 		yield* new TreeWalker( { boundaries: this, ignoreElementEnd: true } );
 	}
 	}
 
 
+	/**
+	 * Start position.
+	 *
+	 * @readonly
+	 * @member {module:engine/model/position~Position}
+	 */
+	get start() {
+		return this._start;
+	}
+
+	/**
+	 * End position.
+	 *
+	 * @readonly
+	 * @member {module:engine/model/position~Position}
+	 */
+	get end() {
+		return this._end;
+	}
+
 	/**
 	/**
 	 * Returns whether the range is collapsed, that is if {@link #start} and
 	 * Returns whether the range is collapsed, that is if {@link #start} and
 	 * {@link #end} positions are equal.
 	 * {@link #end} positions are equal.
@@ -280,7 +300,7 @@ export default class Range {
 		const ranges = [];
 		const ranges = [];
 		const diffAt = this.start.getCommonPath( this.end ).length;
 		const diffAt = this.start.getCommonPath( this.end ).length;
 
 
-		const pos = Position.createFromPosition( this.start );
+		let pos = this.start;
 		let posParent = pos.parent;
 		let posParent = pos.parent;
 
 
 		// Go up.
 		// Go up.
@@ -291,8 +311,7 @@ export default class Range {
 				ranges.push( new Range( pos, pos.getShiftedBy( howMany ) ) );
 				ranges.push( new Range( pos, pos.getShiftedBy( howMany ) ) );
 			}
 			}
 
 
-			pos.path = pos.path.slice( 0, -1 );
-			pos.offset++;
+			pos = Position.createAfter( posParent );
 			posParent = posParent.parent;
 			posParent = posParent.parent;
 		}
 		}
 
 
@@ -305,8 +324,11 @@ export default class Range {
 				ranges.push( new Range( pos, pos.getShiftedBy( howMany ) ) );
 				ranges.push( new Range( pos, pos.getShiftedBy( howMany ) ) );
 			}
 			}
 
 
-			pos.offset = offset;
-			pos.path.push( 0 );
+			const path = pos.getParentPath();
+			path.push( offset );
+			path.push( 0 );
+
+			pos = new Position( pos.root, path );
 		}
 		}
 
 
 		return ranges;
 		return ranges;
@@ -466,6 +488,18 @@ export default class Range {
 		return this.start.getCommonAncestor( this.end );
 		return this.start.getCommonAncestor( this.end );
 	}
 	}
 
 
+	/**
+	 * Converts `Range` to plain object and returns it.
+	 *
+	 * @returns {Object} `Range` converted to plain object.
+	 */
+	toJSON() {
+		return {
+			start: this.start.toJSON(),
+			end: this.end.toJSON()
+		};
+	}
+
 	/**
 	/**
 	 * Returns a range that is a result of transforming this range by a change in the model document.
 	 * Returns a range that is a result of transforming this range by a change in the model document.
 	 *
 	 *
@@ -613,15 +647,13 @@ export default class Range {
 				)
 				)
 			];
 			];
 		} else {
 		} else {
-			const range = Range.createFromRange( this );
-
 			const insertBeforeStart = !isSticky;
 			const insertBeforeStart = !isSticky;
-			const insertBeforeEnd = range.isCollapsed ? true : isSticky;
+			const insertBeforeEnd = this.isCollapsed ? true : isSticky;
 
 
-			range.start = range.start._getTransformedByInsertion( insertPosition, howMany, insertBeforeStart );
-			range.end = range.end._getTransformedByInsertion( insertPosition, howMany, insertBeforeEnd );
+			const start = this.start._getTransformedByInsertion( insertPosition, howMany, insertBeforeStart );
+			const end = this.end._getTransformedByInsertion( insertPosition, howMany, insertBeforeEnd );
 
 
-			return [ range ];
+			return [ new Range( start, end ) ];
 		}
 		}
 	}
 	}
 
 
@@ -755,9 +787,8 @@ export default class Range {
 	 */
 	 */
 	static createCollapsedAt( itemOrPosition, offset ) {
 	static createCollapsedAt( itemOrPosition, offset ) {
 		const start = Position.createAt( itemOrPosition, offset );
 		const start = Position.createAt( itemOrPosition, offset );
-		const end = Position.createFromPosition( start );
 
 
-		return new Range( start, end );
+		return new Range( start, start );
 	}
 	}
 
 
 	/**
 	/**
@@ -804,13 +835,14 @@ export default class Range {
 		// 4. At this moment we don't need the original range.
 		// 4. At this moment we don't need the original range.
 		// We are going to modify the result and we need to return a new instance of Range.
 		// We are going to modify the result and we need to return a new instance of Range.
 		// We have to create a copy of the reference range.
 		// We have to create a copy of the reference range.
-		const result = new this( ref.start, ref.end );
+		let start = ref.start;
+		let end = ref.end;
 
 
 		// 5. Ranges should be checked and glued starting from the range that is closest to the reference range.
 		// 5. Ranges should be checked and glued starting from the range that is closest to the reference range.
 		// Since ranges are sorted, start with the range with index that is closest to reference range index.
 		// Since ranges are sorted, start with the range with index that is closest to reference range index.
-		for ( let i = refIndex - 1; i >= 0; i++ ) {
-			if ( ranges[ i ].end.isEqual( result.start ) ) {
-				result.start = Position.createFromPosition( ranges[ i ].start );
+		for ( let i = refIndex - 1; i >= 0; i-- ) {
+			if ( ranges[ i ].end.isEqual( start ) ) {
+				start = ranges[ i ].start;
 			} else {
 			} else {
 				// If ranges are not starting/ending at the same position there is no point in looking further.
 				// If ranges are not starting/ending at the same position there is no point in looking further.
 				break;
 				break;
@@ -820,15 +852,15 @@ export default class Range {
 		// 6. Ranges should be checked and glued starting from the range that is closest to the reference range.
 		// 6. Ranges should be checked and glued starting from the range that is closest to the reference range.
 		// Since ranges are sorted, start with the range with index that is closest to reference range index.
 		// Since ranges are sorted, start with the range with index that is closest to reference range index.
 		for ( let i = refIndex + 1; i < ranges.length; i++ ) {
 		for ( let i = refIndex + 1; i < ranges.length; i++ ) {
-			if ( ranges[ i ].start.isEqual( result.end ) ) {
-				result.end = Position.createFromPosition( ranges[ i ].end );
+			if ( ranges[ i ].start.isEqual( end ) ) {
+				end = ranges[ i ].end;
 			} else {
 			} else {
 				// If ranges are not starting/ending at the same position there is no point in looking further.
 				// If ranges are not starting/ending at the same position there is no point in looking further.
 				break;
 				break;
 			}
 			}
 		}
 		}
 
 
-		return result;
+		return new this( start, end );
 	}
 	}
 
 
 	/**
 	/**

+ 10 - 12
packages/ckeditor5-engine/src/model/selection.js

@@ -174,18 +174,16 @@ export default class Selection {
 	}
 	}
 
 
 	/**
 	/**
-	 * Returns an iterator that iterates over copies of selection ranges.
+	 * Returns an iterator that iterates over selection ranges.
 	 *
 	 *
 	 * @returns {Iterator.<module:engine/model/range~Range>}
 	 * @returns {Iterator.<module:engine/model/range~Range>}
 	 */
 	 */
-	* getRanges() {
-		for ( const range of this._ranges ) {
-			yield Range.createFromRange( range );
-		}
+	getRanges() {
+		return this._ranges[ Symbol.iterator ]();
 	}
 	}
 
 
 	/**
 	/**
-	 * Returns a copy of the first range in the selection.
+	 * Returns first range in the selection.
 	 * First range is the one which {@link module:engine/model/range~Range#start start} position
 	 * First range is the one which {@link module:engine/model/range~Range#start start} position
 	 * {@link module:engine/model/position~Position#isBefore is before} start position of all other ranges
 	 * {@link module:engine/model/position~Position#isBefore is before} start position of all other ranges
 	 * (not to confuse with the first range added to the selection).
 	 * (not to confuse with the first range added to the selection).
@@ -203,11 +201,11 @@ export default class Selection {
 			}
 			}
 		}
 		}
 
 
-		return first ? Range.createFromRange( first ) : null;
+		return first;
 	}
 	}
 
 
 	/**
 	/**
-	 * Returns a copy of the last range in the selection.
+	 * Returns last range in the selection.
 	 * Last range is the one which {@link module:engine/model/range~Range#end end} position
 	 * Last range is the one which {@link module:engine/model/range~Range#end end} position
 	 * {@link module:engine/model/position~Position#isAfter is after} end position of all other ranges (not to confuse with the range most
 	 * {@link module:engine/model/position~Position#isAfter is after} end position of all other ranges (not to confuse with the range most
 	 * recently added to the selection).
 	 * recently added to the selection).
@@ -225,7 +223,7 @@ export default class Selection {
 			}
 			}
 		}
 		}
 
 
-		return last ? Range.createFromRange( last ) : null;
+		return last;
 	}
 	}
 
 
 	/**
 	/**
@@ -238,9 +236,9 @@ export default class Selection {
 	 * @returns {module:engine/model/position~Position|null}
 	 * @returns {module:engine/model/position~Position|null}
 	 */
 	 */
 	getFirstPosition() {
 	getFirstPosition() {
-		const first = this.getFirstRange();
+		const firstRange = this.getFirstRange();
 
 
-		return first ? Position.createFromPosition( first.start ) : null;
+		return firstRange ? firstRange.start : null;
 	}
 	}
 
 
 	/**
 	/**
@@ -255,7 +253,7 @@ export default class Selection {
 	getLastPosition() {
 	getLastPosition() {
 		const lastRange = this.getLastRange();
 		const lastRange = this.getLastRange();
 
 
-		return lastRange ? Position.createFromPosition( lastRange.end ) : null;
+		return lastRange ? lastRange.end : null;
 	}
 	}
 
 
 	/**
 	/**

+ 33 - 39
packages/ckeditor5-engine/src/model/treewalker.js

@@ -85,9 +85,9 @@ export default class TreeWalker {
 		 * @member {module:engine/model/position~Position} module:engine/model/treewalker~TreeWalker#position
 		 * @member {module:engine/model/position~Position} module:engine/model/treewalker~TreeWalker#position
 		 */
 		 */
 		if ( options.startPosition ) {
 		if ( options.startPosition ) {
-			this.position = Position.createFromPosition( options.startPosition );
+			this.position = options.startPosition;
 		} else {
 		} else {
-			this.position = Position.createFromPosition( this.boundaries[ this.direction == 'backward' ? 'end' : 'start' ] );
+			this.position = this.boundaries[ this.direction == 'backward' ? 'end' : 'start' ];
 		}
 		}
 
 
 		/**
 		/**
@@ -204,33 +204,33 @@ export default class TreeWalker {
 	 */
 	 */
 	_next() {
 	_next() {
 		const previousPosition = this.position;
 		const previousPosition = this.position;
-		const position = Position.createFromPosition( this.position );
 		const parent = this._visitedParent;
 		const parent = this._visitedParent;
 
 
 		// We are at the end of the root.
 		// We are at the end of the root.
-		if ( parent.parent === null && position.offset === parent.maxOffset ) {
+		if ( parent.parent === null && this.position.offset === parent.maxOffset ) {
 			return { done: true };
 			return { done: true };
 		}
 		}
 
 
 		// We reached the walker boundary.
 		// We reached the walker boundary.
-		if ( parent === this._boundaryEndParent && position.offset == this.boundaries.end.offset ) {
+		if ( parent === this._boundaryEndParent && this.position.offset == this.boundaries.end.offset ) {
 			return { done: true };
 			return { done: true };
 		}
 		}
 
 
-		const node = position.textNode ? position.textNode : position.nodeAfter;
+		const node = this.position.textNode ? this.position.textNode : this.position.nodeAfter;
 
 
 		if ( node instanceof Element ) {
 		if ( node instanceof Element ) {
 			if ( !this.shallow ) {
 			if ( !this.shallow ) {
 				// Manual operations on path internals for optimization purposes. Here and in the rest of the method.
 				// Manual operations on path internals for optimization purposes. Here and in the rest of the method.
-				position.path.push( 0 );
+				const path = this.position.path.slice();
+				path.push( 0 );
+				this.position = new Position( this.position.root, path );
+
 				this._visitedParent = node;
 				this._visitedParent = node;
 			} else {
 			} else {
-				position.offset++;
+				this.position = this.position.getShiftedBy( 1 );
 			}
 			}
 
 
-			this.position = position;
-
-			return formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
+			return formatReturnValue( 'elementStart', node, previousPosition, this.position, 1 );
 		} else if ( node instanceof Text ) {
 		} else if ( node instanceof Text ) {
 			let charactersCount;
 			let charactersCount;
 
 
@@ -243,27 +243,24 @@ export default class TreeWalker {
 					offset = this.boundaries.end.offset;
 					offset = this.boundaries.end.offset;
 				}
 				}
 
 
-				charactersCount = offset - position.offset;
+				charactersCount = offset - this.position.offset;
 			}
 			}
 
 
-			const offsetInTextNode = position.offset - node.startOffset;
+			const offsetInTextNode = this.position.offset - node.startOffset;
 			const item = new TextProxy( node, offsetInTextNode, charactersCount );
 			const item = new TextProxy( node, offsetInTextNode, charactersCount );
 
 
-			position.offset += charactersCount;
-			this.position = position;
+			this.position = this.position.getShiftedBy( charactersCount );
 
 
-			return formatReturnValue( 'text', item, previousPosition, position, charactersCount );
+			return formatReturnValue( 'text', item, previousPosition, this.position, charactersCount );
 		} else {
 		} else {
 			// `node` is not set, we reached the end of current `parent`.
 			// `node` is not set, we reached the end of current `parent`.
-			position.path.pop();
-			position.offset++;
-			this.position = position;
+			this.position = Position.createAfter( parent );
 			this._visitedParent = parent.parent;
 			this._visitedParent = parent.parent;
 
 
 			if ( this.ignoreElementEnd ) {
 			if ( this.ignoreElementEnd ) {
 				return this._next();
 				return this._next();
 			} else {
 			} else {
-				return formatReturnValue( 'elementEnd', parent, previousPosition, position );
+				return formatReturnValue( 'elementEnd', parent, previousPosition, this.position );
 			}
 			}
 		}
 		}
 	}
 	}
@@ -278,39 +275,38 @@ export default class TreeWalker {
 	 */
 	 */
 	_previous() {
 	_previous() {
 		const previousPosition = this.position;
 		const previousPosition = this.position;
-		const position = Position.createFromPosition( this.position );
 		const parent = this._visitedParent;
 		const parent = this._visitedParent;
 
 
 		// We are at the beginning of the root.
 		// We are at the beginning of the root.
-		if ( parent.parent === null && position.offset === 0 ) {
+		if ( parent.parent === null && this.position.offset === 0 ) {
 			return { done: true };
 			return { done: true };
 		}
 		}
 
 
 		// We reached the walker boundary.
 		// We reached the walker boundary.
-		if ( parent == this._boundaryStartParent && position.offset == this.boundaries.start.offset ) {
+		if ( parent == this._boundaryStartParent && this.position.offset == this.boundaries.start.offset ) {
 			return { done: true };
 			return { done: true };
 		}
 		}
 
 
 		// Get node just before current position
 		// Get node just before current position
-		const node = position.textNode ? position.textNode : position.nodeBefore;
+		const node = this.position.textNode ? this.position.textNode : this.position.nodeBefore;
 
 
 		if ( node instanceof Element ) {
 		if ( node instanceof Element ) {
-			position.offset--;
+			this.position = this.position.getShiftedBy( -1 );
 
 
 			if ( !this.shallow ) {
 			if ( !this.shallow ) {
-				position.path.push( node.maxOffset );
-				this.position = position;
+				const path = this.position.path.slice();
+				path.push( node.maxOffset );
+
+				this.position = new Position( this.position.root, path );
 				this._visitedParent = node;
 				this._visitedParent = node;
 
 
 				if ( this.ignoreElementEnd ) {
 				if ( this.ignoreElementEnd ) {
 					return this._previous();
 					return this._previous();
 				} else {
 				} else {
-					return formatReturnValue( 'elementEnd', node, previousPosition, position );
+					return formatReturnValue( 'elementEnd', node, previousPosition, this.position );
 				}
 				}
 			} else {
 			} else {
-				this.position = position;
-
-				return formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
+				return formatReturnValue( 'elementStart', node, previousPosition, this.position, 1 );
 			}
 			}
 		} else if ( node instanceof Text ) {
 		} else if ( node instanceof Text ) {
 			let charactersCount;
 			let charactersCount;
@@ -324,23 +320,21 @@ export default class TreeWalker {
 					offset = this.boundaries.start.offset;
 					offset = this.boundaries.start.offset;
 				}
 				}
 
 
-				charactersCount = position.offset - offset;
+				charactersCount = this.position.offset - offset;
 			}
 			}
 
 
-			const offsetInTextNode = position.offset - node.startOffset;
+			const offsetInTextNode = this.position.offset - node.startOffset;
 			const item = new TextProxy( node, offsetInTextNode - charactersCount, charactersCount );
 			const item = new TextProxy( node, offsetInTextNode - charactersCount, charactersCount );
 
 
-			position.offset -= charactersCount;
-			this.position = position;
+			this.position = this.position.getShiftedBy( -charactersCount );
 
 
-			return formatReturnValue( 'text', item, previousPosition, position, charactersCount );
+			return formatReturnValue( 'text', item, previousPosition, this.position, charactersCount );
 		} else {
 		} else {
 			// `node` is not set, we reached the beginning of current `parent`.
 			// `node` is not set, we reached the beginning of current `parent`.
-			position.path.pop();
-			this.position = position;
+			this.position = Position.createBefore( parent );
 			this._visitedParent = parent.parent;
 			this._visitedParent = parent.parent;
 
 
-			return formatReturnValue( 'elementStart', parent, previousPosition, position, 1 );
+			return formatReturnValue( 'elementStart', parent, previousPosition, this.position, 1 );
 		}
 		}
 	}
 	}
 }
 }

+ 24 - 19
packages/ckeditor5-engine/src/view/position.js

@@ -24,20 +24,28 @@ export default class Position {
 	 * @param {Number} offset Position offset.
 	 * @param {Number} offset Position offset.
 	 */
 	 */
 	constructor( parent, offset ) {
 	constructor( parent, offset ) {
-		/**
-		 * Position parent.
-		 *
-		 * @member {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment}
-		 * module:engine/view/position~Position#parent
-		 */
-		this.parent = parent;
-
-		/**
-		 * Position offset.
-		 *
-		 * @member {Number} module:engine/view/position~Position#offset
-		 */
-		this.offset = offset;
+		this._parent = parent;
+		this._offset = offset;
+	}
+
+	/**
+	 * Position parent.
+	 *
+	 * @readonly
+	 * @type {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment}
+	 */
+	get parent() {
+		return this._parent;
+	}
+
+	/**
+	 * Position offset.
+	 *
+	 * @readonly
+	 * @type {Number}
+	 */
+	get offset() {
+		return this._offset;
 	}
 	}
 
 
 	/**
 	/**
@@ -129,12 +137,9 @@ export default class Position {
 	 * @returns {module:engine/view/position~Position} Shifted position.
 	 * @returns {module:engine/view/position~Position} Shifted position.
 	 */
 	 */
 	getShiftedBy( shift ) {
 	getShiftedBy( shift ) {
-		const shifted = Position.createFromPosition( this );
-
-		const offset = shifted.offset + shift;
-		shifted.offset = offset < 0 ? 0 : offset;
+		const offset = this.offset + shift;
 
 
-		return shifted;
+		return new Position( this.parent, offset < 0 ? 0 : offset );
 	}
 	}
 
 
 	/**
 	/**

+ 26 - 15
packages/ckeditor5-engine/src/view/range.js

@@ -23,19 +23,8 @@ export default class Range {
 	 * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
 	 * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
 	 */
 	 */
 	constructor( start, end = null ) {
 	constructor( start, end = null ) {
-		/**
-		 * Start position.
-		 *
-		 * @member {module:engine/view/position~Position}
-		 */
-		this.start = Position.createFromPosition( start );
-
-		/**
-		 * End position.
-		 *
-		 * @member {module:engine/view/position~Position}
-		 */
-		this.end = end ? Position.createFromPosition( end ) : Position.createFromPosition( start );
+		this._start = start;
+		this._end = end ? end : start;
 	}
 	}
 
 
 	/**
 	/**
@@ -53,9 +42,30 @@ export default class Range {
 		yield* new TreeWalker( { boundaries: this, ignoreElementEnd: true } );
 		yield* new TreeWalker( { boundaries: this, ignoreElementEnd: true } );
 	}
 	}
 
 
+	/**
+	 * Start position.
+	 *
+	 * @readonly
+	 * @type {module:engine/view/position~Position}
+	 */
+	get start() {
+		return this._start;
+	}
+
+	/**
+	 * End position.
+	 *
+	 * @readonly
+	 * @type {module:engine/view/position~Position}
+	 */
+	get end() {
+		return this._end;
+	}
+
 	/**
 	/**
 	 * Returns whether the range is collapsed, that is it start and end positions are equal.
 	 * Returns whether the range is collapsed, that is it start and end positions are equal.
 	 *
 	 *
+	 * @readonly
 	 * @type {Boolean}
 	 * @type {Boolean}
 	 */
 	 */
 	get isCollapsed() {
 	get isCollapsed() {
@@ -66,6 +76,7 @@ export default class Range {
 	 * Returns whether this range is flat, that is if {@link module:engine/view/range~Range#start start} position and
 	 * Returns whether this range is flat, that is if {@link module:engine/view/range~Range#start start} position and
 	 * {@link module:engine/view/range~Range#end end} position are in the same {@link module:engine/view/position~Position#parent parent}.
 	 * {@link module:engine/view/range~Range#end end} position are in the same {@link module:engine/view/position~Position#parent parent}.
 	 *
 	 *
+	 * @readonly
 	 * @type {Boolean}
 	 * @type {Boolean}
 	 */
 	 */
 	get isFlat() {
 	get isFlat() {
@@ -75,6 +86,7 @@ export default class Range {
 	/**
 	/**
 	 * Range root element.
 	 * Range root element.
 	 *
 	 *
+	 * @readonly
 	 * @type {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment}
 	 * @type {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment}
 	 */
 	 */
 	get root() {
 	get root() {
@@ -451,9 +463,8 @@ export default class Range {
 	 */
 	 */
 	static createCollapsedAt( itemOrPosition, offset ) {
 	static createCollapsedAt( itemOrPosition, offset ) {
 		const start = Position.createAt( itemOrPosition, offset );
 		const start = Position.createAt( itemOrPosition, offset );
-		const end = Position.createFromPosition( start );
 
 
-		return new Range( start, end );
+		return new Range( start, start );
 	}
 	}
 }
 }
 
 

+ 11 - 15
packages/ckeditor5-engine/src/view/selection.js

@@ -132,9 +132,8 @@ export default class Selection {
 			return null;
 			return null;
 		}
 		}
 		const range = this._ranges[ this._ranges.length - 1 ];
 		const range = this._ranges[ this._ranges.length - 1 ];
-		const anchor = this._lastRangeBackward ? range.end : range.start;
 
 
-		return Position.createFromPosition( anchor );
+		return this._lastRangeBackward ? range.end : range.start;
 	}
 	}
 
 
 	/**
 	/**
@@ -148,9 +147,8 @@ export default class Selection {
 			return null;
 			return null;
 		}
 		}
 		const range = this._ranges[ this._ranges.length - 1 ];
 		const range = this._ranges[ this._ranges.length - 1 ];
-		const focus = this._lastRangeBackward ? range.start : range.end;
 
 
-		return Position.createFromPosition( focus );
+		return this._lastRangeBackward ? range.start : range.end;
 	}
 	}
 
 
 	/**
 	/**
@@ -222,18 +220,16 @@ export default class Selection {
 	}
 	}
 
 
 	/**
 	/**
-	 * Returns an iterator that contains copies of all ranges added to the selection.
+	 * Returns an iterator that contains all ranges added to the selection.
 	 *
 	 *
 	 * @returns {Iterator.<module:engine/view/range~Range>}
 	 * @returns {Iterator.<module:engine/view/range~Range>}
 	 */
 	 */
-	* getRanges() {
-		for ( const range of this._ranges ) {
-			yield Range.createFromRange( range );
-		}
+	getRanges() {
+		return this._ranges[ Symbol.iterator ]();
 	}
 	}
 
 
 	/**
 	/**
-	 * Returns copy of the first range in the selection. First range is the one which
+	 * Returns first range in the selection. First range is the one which
 	 * {@link module:engine/view/range~Range#start start} position {@link module:engine/view/position~Position#isBefore is before} start
 	 * {@link module:engine/view/range~Range#start start} position {@link module:engine/view/position~Position#isBefore is before} start
 	 * position of all other ranges (not to confuse with the first range added to the selection).
 	 * position of all other ranges (not to confuse with the first range added to the selection).
 	 * Returns `null` if no ranges are added to selection.
 	 * Returns `null` if no ranges are added to selection.
@@ -249,11 +245,11 @@ export default class Selection {
 			}
 			}
 		}
 		}
 
 
-		return first ? Range.createFromRange( first ) : null;
+		return first;
 	}
 	}
 
 
 	/**
 	/**
-	 * Returns copy of the last range in the selection. Last range is the one which {@link module:engine/view/range~Range#end end}
+	 * Returns last range in the selection. Last range is the one which {@link module:engine/view/range~Range#end end}
 	 * position {@link module:engine/view/position~Position#isAfter is after} end position of all other ranges (not to confuse
 	 * position {@link module:engine/view/position~Position#isAfter is after} end position of all other ranges (not to confuse
 	 * with the last range added to the selection). Returns `null` if no ranges are added to selection.
 	 * with the last range added to the selection). Returns `null` if no ranges are added to selection.
 	 *
 	 *
@@ -268,7 +264,7 @@ export default class Selection {
 			}
 			}
 		}
 		}
 
 
-		return last ? Range.createFromRange( last ) : null;
+		return last;
 	}
 	}
 
 
 	/**
 	/**
@@ -281,7 +277,7 @@ export default class Selection {
 	getFirstPosition() {
 	getFirstPosition() {
 		const firstRange = this.getFirstRange();
 		const firstRange = this.getFirstRange();
 
 
-		return firstRange ? Position.createFromPosition( firstRange.start ) : null;
+		return firstRange ? firstRange.start : null;
 	}
 	}
 
 
 	/**
 	/**
@@ -294,7 +290,7 @@ export default class Selection {
 	getLastPosition() {
 	getLastPosition() {
 		const lastRange = this.getLastRange();
 		const lastRange = this.getLastRange();
 
 
-		return lastRange ? Position.createFromPosition( lastRange.end ) : null;
+		return lastRange ? lastRange.end : null;
 	}
 	}
 
 
 	/**
 	/**

+ 41 - 58
packages/ckeditor5-engine/src/view/treewalker.js

@@ -73,9 +73,9 @@ export default class TreeWalker {
 		 * @member {module:engine/view/position~Position} module:engine/view/treewalker~TreeWalker#position
 		 * @member {module:engine/view/position~Position} module:engine/view/treewalker~TreeWalker#position
 		 */
 		 */
 		if ( options.startPosition ) {
 		if ( options.startPosition ) {
-			this.position = Position.createFromPosition( options.startPosition );
+			this.position = options.startPosition;
 		} else {
 		} else {
-			this.position = Position.createFromPosition( options.boundaries[ options.direction == 'backward' ? 'end' : 'start' ] );
+			this.position = options.boundaries[ options.direction == 'backward' ? 'end' : 'start' ];
 		}
 		}
 
 
 		/**
 		/**
@@ -187,17 +187,16 @@ export default class TreeWalker {
 	 * @returns {module:engine/view/treewalker~TreeWalkerValue} return.value Information about taken step.
 	 * @returns {module:engine/view/treewalker~TreeWalkerValue} return.value Information about taken step.
 	 */
 	 */
 	_next() {
 	_next() {
-		let position = Position.createFromPosition( this.position );
 		const previousPosition = this.position;
 		const previousPosition = this.position;
-		const parent = position.parent;
+		const parent = this.position.parent;
 
 
 		// We are at the end of the root.
 		// We are at the end of the root.
-		if ( parent.parent === null && position.offset === parent.childCount ) {
+		if ( parent.parent === null && this.position.offset === parent.childCount ) {
 			return { done: true };
 			return { done: true };
 		}
 		}
 
 
 		// We reached the walker boundary.
 		// We reached the walker boundary.
-		if ( parent === this._boundaryEndParent && position.offset == this.boundaries.end.offset ) {
+		if ( parent === this._boundaryEndParent && this.position.offset == this.boundaries.end.offset ) {
 			return { done: true };
 			return { done: true };
 		}
 		}
 
 
@@ -206,32 +205,29 @@ export default class TreeWalker {
 
 
 		// Text is a specific parent because it contains string instead of child nodes.
 		// Text is a specific parent because it contains string instead of child nodes.
 		if ( parent instanceof Text ) {
 		if ( parent instanceof Text ) {
-			if ( position.isAtEnd ) {
+			if ( this.position.isAtEnd ) {
 				// Prevent returning "elementEnd" for Text node. Skip that value and return the next walker step.
 				// Prevent returning "elementEnd" for Text node. Skip that value and return the next walker step.
 				this.position = Position.createAfter( parent );
 				this.position = Position.createAfter( parent );
 
 
 				return this._next();
 				return this._next();
 			}
 			}
 
 
-			node = parent.data[ position.offset ];
+			node = parent.data[ this.position.offset ];
 		} else {
 		} else {
-			node = parent.getChild( position.offset );
+			node = parent.getChild( this.position.offset );
 		}
 		}
 
 
 		if ( node instanceof Element ) {
 		if ( node instanceof Element ) {
 			if ( !this.shallow ) {
 			if ( !this.shallow ) {
-				position = new Position( node, 0 );
+				this.position = new Position( node, 0 );
 			} else {
 			} else {
-				position.offset++;
+				this.position = this.position.getShiftedBy( 1 );
 			}
 			}
 
 
-			this.position = position;
-
-			return this._formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
+			return this._formatReturnValue( 'elementStart', node, previousPosition, this.position, 1 );
 		} else if ( node instanceof Text ) {
 		} else if ( node instanceof Text ) {
 			if ( this.singleCharacters ) {
 			if ( this.singleCharacters ) {
-				position = new Position( node, 0 );
-				this.position = position;
+				this.position = new Position( node, 0 );
 
 
 				return this._next();
 				return this._next();
 			} else {
 			} else {
@@ -242,15 +238,13 @@ export default class TreeWalker {
 				if ( node == this._boundaryEndParent ) {
 				if ( node == this._boundaryEndParent ) {
 					charactersCount = this.boundaries.end.offset;
 					charactersCount = this.boundaries.end.offset;
 					item = new TextProxy( node, 0, charactersCount );
 					item = new TextProxy( node, 0, charactersCount );
-					position = Position.createAfter( item );
+					this.position = Position.createAfter( item );
 				} else {
 				} else {
 					// If not just keep moving forward.
 					// If not just keep moving forward.
-					position.offset++;
+					this.position = this.position.getShiftedBy( 1 );
 				}
 				}
 
 
-				this.position = position;
-
-				return this._formatReturnValue( 'text', item, previousPosition, position, charactersCount );
+				return this._formatReturnValue( 'text', item, previousPosition, this.position, charactersCount );
 			}
 			}
 		} else if ( typeof node == 'string' ) {
 		} else if ( typeof node == 'string' ) {
 			let textLength;
 			let textLength;
@@ -261,24 +255,22 @@ export default class TreeWalker {
 				// Check if text stick out of walker range.
 				// Check if text stick out of walker range.
 				const endOffset = parent === this._boundaryEndParent ? this.boundaries.end.offset : parent.data.length;
 				const endOffset = parent === this._boundaryEndParent ? this.boundaries.end.offset : parent.data.length;
 
 
-				textLength = endOffset - position.offset;
+				textLength = endOffset - this.position.offset;
 			}
 			}
 
 
-			const textProxy = new TextProxy( parent, position.offset, textLength );
+			const textProxy = new TextProxy( parent, this.position.offset, textLength );
 
 
-			position.offset += textLength;
-			this.position = position;
+			this.position = this.position.getShiftedBy( textLength );
 
 
-			return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength );
+			return this._formatReturnValue( 'text', textProxy, previousPosition, this.position, textLength );
 		} else {
 		} else {
 			// `node` is not set, we reached the end of current `parent`.
 			// `node` is not set, we reached the end of current `parent`.
-			position = Position.createAfter( parent );
-			this.position = position;
+			this.position = Position.createAfter( parent );
 
 
 			if ( this.ignoreElementEnd ) {
 			if ( this.ignoreElementEnd ) {
 				return this._next();
 				return this._next();
 			} else {
 			} else {
-				return this._formatReturnValue( 'elementEnd', parent, previousPosition, position );
+				return this._formatReturnValue( 'elementEnd', parent, previousPosition, this.position );
 			}
 			}
 		}
 		}
 	}
 	}
@@ -292,17 +284,16 @@ export default class TreeWalker {
 	 * @returns {module:engine/view/treewalker~TreeWalkerValue} return.value Information about taken step.
 	 * @returns {module:engine/view/treewalker~TreeWalkerValue} return.value Information about taken step.
 	 */
 	 */
 	_previous() {
 	_previous() {
-		let position = Position.createFromPosition( this.position );
 		const previousPosition = this.position;
 		const previousPosition = this.position;
-		const parent = position.parent;
+		const parent = this.position.parent;
 
 
 		// We are at the beginning of the root.
 		// We are at the beginning of the root.
-		if ( parent.parent === null && position.offset === 0 ) {
+		if ( parent.parent === null && this.position.offset === 0 ) {
 			return { done: true };
 			return { done: true };
 		}
 		}
 
 
 		// We reached the walker boundary.
 		// We reached the walker boundary.
-		if ( parent == this._boundaryStartParent && position.offset == this.boundaries.start.offset ) {
+		if ( parent == this._boundaryStartParent && this.position.offset == this.boundaries.start.offset ) {
 			return { done: true };
 			return { done: true };
 		}
 		}
 
 
@@ -311,38 +302,35 @@ export default class TreeWalker {
 
 
 		// Text {@link module:engine/view/text~Text} element is a specific parent because contains string instead of child nodes.
 		// Text {@link module:engine/view/text~Text} element is a specific parent because contains string instead of child nodes.
 		if ( parent instanceof Text ) {
 		if ( parent instanceof Text ) {
-			if ( position.isAtStart ) {
+			if ( this.position.isAtStart ) {
 				// Prevent returning "elementStart" for Text node. Skip that value and return the next walker step.
 				// Prevent returning "elementStart" for Text node. Skip that value and return the next walker step.
 				this.position = Position.createBefore( parent );
 				this.position = Position.createBefore( parent );
 
 
 				return this._previous();
 				return this._previous();
 			}
 			}
 
 
-			node = parent.data[ position.offset - 1 ];
+			node = parent.data[ this.position.offset - 1 ];
 		} else {
 		} else {
-			node = parent.getChild( position.offset - 1 );
+			node = parent.getChild( this.position.offset - 1 );
 		}
 		}
 
 
 		if ( node instanceof Element ) {
 		if ( node instanceof Element ) {
 			if ( !this.shallow ) {
 			if ( !this.shallow ) {
-				position = new Position( node, node.childCount );
-				this.position = position;
+				this.position = new Position( node, node.childCount );
 
 
 				if ( this.ignoreElementEnd ) {
 				if ( this.ignoreElementEnd ) {
 					return this._previous();
 					return this._previous();
 				} else {
 				} else {
-					return this._formatReturnValue( 'elementEnd', node, previousPosition, position );
+					return this._formatReturnValue( 'elementEnd', node, previousPosition, this.position );
 				}
 				}
 			} else {
 			} else {
-				position.offset--;
-				this.position = position;
+				this.position = this.position.getShiftedBy( -1 );
 
 
-				return this._formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
+				return this._formatReturnValue( 'elementStart', node, previousPosition, this.position, 1 );
 			}
 			}
 		} else if ( node instanceof Text ) {
 		} else if ( node instanceof Text ) {
 			if ( this.singleCharacters ) {
 			if ( this.singleCharacters ) {
-				position = new Position( node, node.data.length );
-				this.position = position;
+				this.position = new Position( node, node.data.length );
 
 
 				return this._previous();
 				return this._previous();
 			} else {
 			} else {
@@ -355,15 +343,13 @@ export default class TreeWalker {
 
 
 					item = new TextProxy( node, offset, node.data.length - offset );
 					item = new TextProxy( node, offset, node.data.length - offset );
 					charactersCount = item.data.length;
 					charactersCount = item.data.length;
-					position = Position.createBefore( item );
+					this.position = Position.createBefore( item );
 				} else {
 				} else {
 					// If not just keep moving backward.
 					// If not just keep moving backward.
-					position.offset--;
+					this.position = this.position.getShiftedBy( -1 );
 				}
 				}
 
 
-				this.position = position;
-
-				return this._formatReturnValue( 'text', item, previousPosition, position, charactersCount );
+				return this._formatReturnValue( 'text', item, previousPosition, this.position, charactersCount );
 			}
 			}
 		} else if ( typeof node == 'string' ) {
 		} else if ( typeof node == 'string' ) {
 			let textLength;
 			let textLength;
@@ -372,24 +358,21 @@ export default class TreeWalker {
 				// Check if text stick out of walker range.
 				// Check if text stick out of walker range.
 				const startOffset = parent === this._boundaryStartParent ? this.boundaries.start.offset : 0;
 				const startOffset = parent === this._boundaryStartParent ? this.boundaries.start.offset : 0;
 
 
-				textLength = position.offset - startOffset;
+				textLength = this.position.offset - startOffset;
 			} else {
 			} else {
 				textLength = 1;
 				textLength = 1;
 			}
 			}
 
 
-			position.offset -= textLength;
-
-			const textProxy = new TextProxy( parent, position.offset, textLength );
+			this.position = this.position.getShiftedBy( -textLength );
 
 
-			this.position = position;
+			const textProxy = new TextProxy( parent, this.position.offset, textLength );
 
 
-			return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength );
+			return this._formatReturnValue( 'text', textProxy, previousPosition, this.position, textLength );
 		} else {
 		} else {
 			// `node` is not set, we reached the beginning of current `parent`.
 			// `node` is not set, we reached the beginning of current `parent`.
-			position = Position.createBefore( parent );
-			this.position = position;
+			this.position = Position.createBefore( parent );
 
 
-			return this._formatReturnValue( 'elementStart', parent, previousPosition, position, 1 );
+			return this._formatReturnValue( 'elementStart', parent, previousPosition, this.position, 1 );
 		}
 		}
 	}
 	}
 
 

+ 29 - 24
packages/ckeditor5-engine/src/view/writer.js

@@ -305,7 +305,7 @@ export function insert( position, nodes ) {
 	const insertionPosition = _breakAttributes( position, true );
 	const insertionPosition = _breakAttributes( position, true );
 
 
 	const length = container.insertChildren( insertionPosition.offset, nodes );
 	const length = container.insertChildren( insertionPosition.offset, nodes );
-	const endPosition = insertionPosition.getShiftedBy( length );
+	let endPosition = insertionPosition.getShiftedBy( length );
 	const start = mergeAttributes( insertionPosition );
 	const start = mergeAttributes( insertionPosition );
 
 
 	// When no nodes were inserted - return collapsed range.
 	// When no nodes were inserted - return collapsed range.
@@ -314,7 +314,7 @@ export function insert( position, nodes ) {
 	} else {
 	} else {
 		// If start position was merged - move end position.
 		// If start position was merged - move end position.
 		if ( !start.isEqual( insertionPosition ) ) {
 		if ( !start.isEqual( insertionPosition ) ) {
-			endPosition.offset--;
+			endPosition = endPosition.getShiftedBy( -1 );
 		}
 		}
 
 
 		const end = mergeAttributes( endPosition );
 		const end = mergeAttributes( endPosition );
@@ -331,8 +331,7 @@ export function insert( position, nodes ) {
  * same parent container.
  * same parent container.
  *
  *
  * @function module:engine/view/writer~writer.remove
  * @function module:engine/view/writer~writer.remove
- * @param {module:engine/view/range~Range} range Range to remove from container. After removing, it will be updated
- * to a collapsed range showing the new position.
+ * @param {module:engine/view/range~Range} range Range to remove from container.
  * @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
  * @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
  */
  */
 export function remove( range ) {
 export function remove( range ) {
@@ -353,9 +352,7 @@ export function remove( range ) {
 	const removed = parentContainer.removeChildren( breakStart.offset, count );
 	const removed = parentContainer.removeChildren( breakStart.offset, count );
 
 
 	// Merge after removing.
 	// Merge after removing.
-	const mergePosition = mergeAttributes( breakStart );
-	range.start = mergePosition;
-	range.end = Position.createFromPosition( mergePosition );
+	mergeAttributes( breakStart );
 
 
 	// Return removed nodes.
 	// Return removed nodes.
 	return new DocumentFragment( removed );
 	return new DocumentFragment( removed );
@@ -406,17 +403,20 @@ export function clear( range, element ) {
 
 
 		// If we have found element to remove.
 		// If we have found element to remove.
 		if ( rangeToRemove ) {
 		if ( rangeToRemove ) {
+			let rangeEnd = rangeToRemove.end;
+			let rangeStart = rangeToRemove.start;
+
 			// We need to check if element range stick out of the given range and truncate if it is.
 			// We need to check if element range stick out of the given range and truncate if it is.
 			if ( rangeToRemove.end.isAfter( range.end ) ) {
 			if ( rangeToRemove.end.isAfter( range.end ) ) {
-				rangeToRemove.end = range.end;
+				rangeEnd = range.end;
 			}
 			}
 
 
 			if ( rangeToRemove.start.isBefore( range.start ) ) {
 			if ( rangeToRemove.start.isBefore( range.start ) ) {
-				rangeToRemove.start = range.start;
+				rangeStart = range.start;
 			}
 			}
 
 
 			// At the end we remove range with found element.
 			// At the end we remove range with found element.
-			remove( rangeToRemove );
+			remove( new Range( rangeStart, rangeEnd ) );
 		}
 		}
 	}
 	}
 }
 }
@@ -447,7 +447,7 @@ export function move( sourceRange, targetPosition ) {
 
 
 		nodes = remove( sourceRange );
 		nodes = remove( sourceRange );
 
 
-		targetPosition.offset += ( parent.childCount - countBefore );
+		targetPosition = targetPosition.getShiftedBy( parent.childCount - countBefore );
 	} else {
 	} else {
 		nodes = remove( sourceRange );
 		nodes = remove( sourceRange );
 	}
 	}
@@ -511,10 +511,13 @@ export function wrap( range, attribute ) {
 	const start = mergeAttributes( newRange.start );
 	const start = mergeAttributes( newRange.start );
 
 
 	// If start position was merged - move end position back.
 	// If start position was merged - move end position back.
+	let rangeEnd = newRange.end;
+
 	if ( !start.isEqual( newRange.start ) ) {
 	if ( !start.isEqual( newRange.start ) ) {
-		newRange.end.offset--;
+		rangeEnd = rangeEnd.getShiftedBy( -1 );
 	}
 	}
-	const end = mergeAttributes( newRange.end );
+
+	const end = mergeAttributes( rangeEnd );
 
 
 	return new Range( start, end );
 	return new Range( start, end );
 }
 }
@@ -537,7 +540,7 @@ export function wrapPosition( position, attribute ) {
 
 
 	// Return same position when trying to wrap with attribute similar to position parent.
 	// Return same position when trying to wrap with attribute similar to position parent.
 	if ( attribute.isSimilar( position.parent ) ) {
 	if ( attribute.isSimilar( position.parent ) ) {
-		return movePositionToTextNode( Position.createFromPosition( position ) );
+		return movePositionToTextNode( position );
 	}
 	}
 
 
 	// When position is inside text node - break it and place new position between two text nodes.
 	// When position is inside text node - break it and place new position between two text nodes.
@@ -625,10 +628,12 @@ export function unwrap( range, attribute ) {
 	const start = mergeAttributes( newRange.start );
 	const start = mergeAttributes( newRange.start );
 
 
 	// If start position was merged - move end position back.
 	// If start position was merged - move end position back.
+	let rangeEnd = newRange.end;
+
 	if ( !start.isEqual( newRange.start ) ) {
 	if ( !start.isEqual( newRange.start ) ) {
-		newRange.end.offset--;
+		rangeEnd = rangeEnd.getShiftedBy( -1 );
 	}
 	}
-	const end = mergeAttributes( newRange.end );
+	const end = mergeAttributes( rangeEnd );
 
 
 	return new Range( start, end );
 	return new Range( start, end );
 }
 }
@@ -702,12 +707,12 @@ function _breakAttributesRange( range, forceSplitText = false ) {
 		return new Range( position, position );
 		return new Range( position, position );
 	}
 	}
 
 
-	const breakEnd = _breakAttributes( rangeEnd, forceSplitText );
+	let breakEnd = _breakAttributes( rangeEnd, forceSplitText );
 	const count = breakEnd.parent.childCount;
 	const count = breakEnd.parent.childCount;
 	const breakStart = _breakAttributes( rangeStart, forceSplitText );
 	const breakStart = _breakAttributes( rangeStart, forceSplitText );
 
 
 	// Calculate new break end offset.
 	// Calculate new break end offset.
-	breakEnd.offset += breakEnd.parent.childCount - count;
+	breakEnd = breakEnd.getShiftedBy( breakEnd.parent.childCount - count );
 
 
 	return new Range( breakStart, breakEnd );
 	return new Range( breakStart, breakEnd );
 }
 }
@@ -752,12 +757,12 @@ function _breakAttributes( position, forceSplitText = false ) {
 
 
 	// There are no attributes to break and text nodes breaking is not forced.
 	// There are no attributes to break and text nodes breaking is not forced.
 	if ( !forceSplitText && positionParent.is( 'text' ) && isContainerOrFragment( positionParent.parent ) ) {
 	if ( !forceSplitText && positionParent.is( 'text' ) && isContainerOrFragment( positionParent.parent ) ) {
-		return Position.createFromPosition( position );
+		return position;
 	}
 	}
 
 
 	// Position's parent is container, so no attributes to break.
 	// Position's parent is container, so no attributes to break.
 	if ( isContainerOrFragment( positionParent ) ) {
 	if ( isContainerOrFragment( positionParent ) ) {
-		return Position.createFromPosition( position );
+		return position;
 	}
 	}
 
 
 	// Break text and start again in new position.
 	// Break text and start again in new position.
@@ -857,8 +862,8 @@ function unwrapChildren( parent, startOffset, endOffset, attribute ) {
 	// Merge at each unwrap.
 	// Merge at each unwrap.
 	let offsetChange = 0;
 	let offsetChange = 0;
 
 
-	for ( const position of unwrapPositions ) {
-		position.offset -= offsetChange;
+	for ( let position of unwrapPositions ) {
+		position = position.getShiftedBy( -offsetChange );
 
 
 		// Do not merge with elements outside selected children.
 		// Do not merge with elements outside selected children.
 		if ( position.offset == startOffset || position.offset == endOffset ) {
 		if ( position.offset == startOffset || position.offset == endOffset ) {
@@ -918,8 +923,8 @@ function wrapChildren( parent, startOffset, endOffset, attribute ) {
 	// Merge at each wrap.
 	// Merge at each wrap.
 	let offsetChange = 0;
 	let offsetChange = 0;
 
 
-	for ( const position of wrapPositions ) {
-		position.offset -= offsetChange;
+	for ( let position of wrapPositions ) {
+		position = position.getShiftedBy( -offsetChange );
 
 
 		// Do not merge with elements outside selected children.
 		// Do not merge with elements outside selected children.
 		if ( position.offset == startOffset ) {
 		if ( position.offset == startOffset ) {

+ 23 - 16
packages/ckeditor5-engine/tests/model/delta/transform/_utils/utils.js

@@ -68,12 +68,14 @@ export function getMarkerDelta( name, oldRange, newRange, version ) {
 export function getMergeDelta( position, howManyInPrev, howManyInNext, version ) {
 export function getMergeDelta( position, howManyInPrev, howManyInNext, version ) {
 	const delta = new MergeDelta();
 	const delta = new MergeDelta();
 
 
-	const sourcePosition = Position.createFromPosition( position );
-	sourcePosition.path.push( 0 );
+	const sourcePath = position.path.slice();
+	sourcePath.push( 0 );
+	const sourcePosition = new Position( position.root, sourcePath );
 
 
-	const targetPosition = Position.createFromPosition( position );
-	targetPosition.offset--;
-	targetPosition.path.push( howManyInPrev );
+	const targetPath = position.getShiftedBy( -1 ).path.slice();
+	targetPath.push( howManyInPrev );
+
+	const targetPosition = new Position( position.root, targetPath );
 
 
 	const move = new MoveOperation( sourcePosition, howManyInNext, targetPosition, version );
 	const move = new MoveOperation( sourcePosition, howManyInNext, targetPosition, version );
 	move.isSticky = true;
 	move.isSticky = true;
@@ -129,12 +131,15 @@ export function getRenameDelta( position, oldName, newName, baseVersion ) {
 export function getSplitDelta( position, nodeCopy, howManyMove, version ) {
 export function getSplitDelta( position, nodeCopy, howManyMove, version ) {
 	const delta = new SplitDelta();
 	const delta = new SplitDelta();
 
 
-	const insertPosition = Position.createFromPosition( position );
-	insertPosition.path = insertPosition.getParentPath();
-	insertPosition.offset++;
+	const insertPath = position.getParentPath();
+	insertPath[ insertPath.length - 1 ]++;
+
+	const insertPosition = new Position( position.root, insertPath );
 
 
-	const targetPosition = Position.createFromPosition( insertPosition );
-	targetPosition.path.push( 0 );
+	const targetPath = insertPosition.path.slice();
+	targetPath.push( 0 );
+
+	const targetPosition = new Position( insertPosition.root, targetPath );
 
 
 	delta.addOperation( new InsertOperation( insertPosition, [ nodeCopy ], version ) );
 	delta.addOperation( new InsertOperation( insertPosition, [ nodeCopy ], version ) );
 
 
@@ -153,8 +158,10 @@ export function getWrapDelta( range, element, version ) {
 
 
 	const insert = new InsertOperation( range.end, element, version );
 	const insert = new InsertOperation( range.end, element, version );
 
 
-	const targetPosition = Position.createFromPosition( range.end );
-	targetPosition.path.push( 0 );
+	const targetPath = range.end.path.slice();
+	targetPath.push( 0 );
+	const targetPosition = new Position( range.end.root, targetPath );
+
 	const move = new MoveOperation( range.start, range.end.offset - range.start.offset, targetPosition, version + 1 );
 	const move = new MoveOperation( range.start, range.end.offset - range.start.offset, targetPosition, version + 1 );
 
 
 	delta.addOperation( insert );
 	delta.addOperation( insert );
@@ -168,14 +175,14 @@ export function getWrapDelta( range, element, version ) {
 export function getUnwrapDelta( positionBefore, howManyChildren, version ) {
 export function getUnwrapDelta( positionBefore, howManyChildren, version ) {
 	const delta = new UnwrapDelta();
 	const delta = new UnwrapDelta();
 
 
-	const sourcePosition = Position.createFromPosition( positionBefore );
-	sourcePosition.path.push( 0 );
+	const sourcePath = positionBefore.path.slice();
+	sourcePath.push( 0 );
+	const sourcePosition = new Position( positionBefore.root, sourcePath );
 
 
 	const move = new MoveOperation( sourcePosition, howManyChildren, positionBefore, version );
 	const move = new MoveOperation( sourcePosition, howManyChildren, positionBefore, version );
 	move.isSticky = true;
 	move.isSticky = true;
 
 
-	const removePosition = Position.createFromPosition( positionBefore );
-	removePosition.offset += howManyChildren;
+	const removePosition = positionBefore.getShiftedBy( howManyChildren );
 
 
 	const gy = sourcePosition.root.document.graveyard;
 	const gy = sourcePosition.root.document.graveyard;
 	const gyPos = Position.createAt( gy, 0 );
 	const gyPos = Position.createAt( gy, 0 );

+ 2 - 2
packages/ckeditor5-engine/tests/model/delta/transform/movedelta.js

@@ -135,8 +135,8 @@ describe( 'transform', () => {
 			} );
 			} );
 
 
 			it( 'move range in merged node #2', () => {
 			it( 'move range in merged node #2', () => {
-				moveDelta._moveOperation.sourcePosition.path = [ 3, 3, 1 ];
-				moveDelta._moveOperation.targetPosition.path = [ 3, 3, 4 ];
+				moveDelta._moveOperation.sourcePosition = new Position( root, [ 3, 3, 1 ] );
+				moveDelta._moveOperation.targetPosition = new Position( root, [ 3, 3, 4 ] );
 
 
 				const mergePosition = new Position( root, [ 3, 3 ] );
 				const mergePosition = new Position( root, [ 3, 3 ] );
 				const mergeDelta = getMergeDelta( mergePosition, 1, 4, baseVersion );
 				const mergeDelta = getMergeDelta( mergePosition, 1, 4, baseVersion );

+ 17 - 8
packages/ckeditor5-engine/tests/model/delta/transform/splitdelta.js

@@ -6,6 +6,7 @@
 import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
 import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
 
 
 import deltaTransform from '../../../../src/model/delta/transform';
 import deltaTransform from '../../../../src/model/delta/transform';
+
 const transform = deltaTransform.transform;
 const transform = deltaTransform.transform;
 
 
 import Element from '../../../../src/model/element';
 import Element from '../../../../src/model/element';
@@ -967,10 +968,15 @@ describe( 'transform', () => {
 				baseVersion = removeDelta.operations.length;
 				baseVersion = removeDelta.operations.length;
 
 
 				const newInsertPosition = removeOperation.targetPosition.getShiftedBy( 2 );
 				const newInsertPosition = removeOperation.targetPosition.getShiftedBy( 2 );
-				const newMoveSourcePosition = removeOperation.targetPosition.getShiftedBy( 1 );
-				newMoveSourcePosition.path.push( 2 );
-				const newMoveTargetPosition = Position.createAt( newInsertPosition );
-				newMoveTargetPosition.path.push( 0 );
+
+				const newSourcePath = removeOperation.targetPosition.getShiftedBy( 1 ).path.slice();
+				newSourcePath.push( 2 );
+				const newMoveSourcePosition = new Position( removeOperation.targetPosition.root, newSourcePath );
+
+				const newMoveTargetPath = newInsertPosition.path.slice();
+				newMoveTargetPath.push( 0 );
+
+				const newMoveTargetPosition = new Position( newInsertPosition.root, newMoveTargetPath );
 
 
 				expectDelta( transformed[ 0 ], {
 				expectDelta( transformed[ 0 ], {
 					type: SplitDelta,
 					type: SplitDelta,
@@ -1003,10 +1009,13 @@ describe( 'transform', () => {
 				baseVersion = removeDelta.operations.length;
 				baseVersion = removeDelta.operations.length;
 
 
 				const newInsertPosition = removeOperation.targetPosition.getShiftedBy( 2 );
 				const newInsertPosition = removeOperation.targetPosition.getShiftedBy( 2 );
-				const newMoveSourcePosition = removeOperation.targetPosition.getShiftedBy( 1 );
-				newMoveSourcePosition.path.push( 3 );
-				const newMoveTargetPosition = Position.createAt( newInsertPosition );
-				newMoveTargetPosition.path.push( 0 );
+				const newMoveSourcePath = removeOperation.targetPosition.getShiftedBy( 1 ).path.slice();
+				newMoveSourcePath.push( 3 );
+				const newMoveSourcePosition = new Position( removeOperation.targetPosition.root, newMoveSourcePath );
+
+				const newMoveTargetPath = newInsertPosition.path.slice();
+				newMoveTargetPath.push( 0 );
+				const newMoveTargetPosition = new Position( newInsertPosition.root, newMoveTargetPath );
 
 
 				expectDelta( transformed[ 0 ], {
 				expectDelta( transformed[ 0 ], {
 					type: SplitDelta,
 					type: SplitDelta,

+ 15 - 5
packages/ckeditor5-engine/tests/model/liverange.js

@@ -208,7 +208,9 @@ describe( 'LiveRange', () => {
 			} );
 			} );
 
 
 			it( 'is at the live range start position and live range is collapsed', () => {
 			it( 'is at the live range start position and live range is collapsed', () => {
-				live.end.path = [ 0, 1, 4 ];
+				live = new LiveRange( live.start, new Position( live.end.root, [ 0, 1, 4 ] ) );
+				spy = sinon.spy();
+				live.on( 'change:range', spy );
 
 
 				const insertRange = new Range( new Position( root, [ 0, 1, 4 ] ), new Position( root, [ 0, 1, 8 ] ) );
 				const insertRange = new Range( new Position( root, [ 0, 1, 4 ] ), new Position( root, [ 0, 1, 8 ] ) );
 
 
@@ -372,7 +374,9 @@ describe( 'LiveRange', () => {
 			} );
 			} );
 
 
 			it( 'is equal to live range', () => {
 			it( 'is equal to live range', () => {
-				live.end.path = [ 0, 1, 7 ];
+				live = new LiveRange( live.start, new Position( live.end.root, [ 0, 1, 7 ] ) );
+				spy = sinon.spy();
+				live.on( 'change:range', spy );
 
 
 				const moveSource = new Position( root, [ 0, 1, 4 ] );
 				const moveSource = new Position( root, [ 0, 1, 4 ] );
 				const moveRange = new Range( new Position( root, [ 0, 3, 0 ] ), new Position( root, [ 0, 3, 3 ] ) );
 				const moveRange = new Range( new Position( root, [ 0, 3, 0 ] ), new Position( root, [ 0, 3, 3 ] ) );
@@ -389,7 +393,9 @@ describe( 'LiveRange', () => {
 			} );
 			} );
 
 
 			it( 'contains live range', () => {
 			it( 'contains live range', () => {
-				live.end.path = [ 0, 1, 7 ];
+				live = new LiveRange( live.start, new Position( live.end.root, [ 0, 1, 7 ] ) );
+				spy = sinon.spy();
+				live.on( 'change:range', spy );
 
 
 				const moveSource = new Position( root, [ 0, 1, 3 ] );
 				const moveSource = new Position( root, [ 0, 1, 3 ] );
 				const moveRange = new Range( new Position( root, [ 0, 3, 0 ] ), new Position( root, [ 0, 3, 9 ] ) );
 				const moveRange = new Range( new Position( root, [ 0, 3, 0 ] ), new Position( root, [ 0, 3, 9 ] ) );
@@ -406,7 +412,9 @@ describe( 'LiveRange', () => {
 			} );
 			} );
 
 
 			it( 'is intersecting with live range and points to live range', () => {
 			it( 'is intersecting with live range and points to live range', () => {
-				live.end.path = [ 0, 1, 12 ];
+				live = new LiveRange( live.start, new Position( live.end.root, [ 0, 1, 12 ] ) );
+				spy = sinon.spy();
+				live.on( 'change:range', spy );
 
 
 				const moveSource = new Position( root, [ 0, 1, 2 ] );
 				const moveSource = new Position( root, [ 0, 1, 2 ] );
 				const moveRange = new Range( new Position( root, [ 0, 1, 7 ] ), new Position( root, [ 0, 1, 10 ] ) );
 				const moveRange = new Range( new Position( root, [ 0, 1, 7 ] ), new Position( root, [ 0, 1, 10 ] ) );
@@ -656,7 +664,9 @@ describe( 'LiveRange', () => {
 			} );
 			} );
 
 
 			it( 'from the range to the range', () => {
 			it( 'from the range to the range', () => {
-				live.end.path = [ 0, 1, 12 ];
+				live = new LiveRange( live.start, new Position( live.end.root, [ 0, 1, 12 ] ) );
+				spy = sinon.spy();
+				live.on( 'change:content', spy );
 
 
 				const moveSource = new Position( root, [ 0, 1, 6 ] );
 				const moveSource = new Position( root, [ 0, 1, 6 ] );
 				const moveRange = new Range( new Position( root, [ 0, 1, 8 ] ), new Position( root, [ 0, 1, 10 ] ) );
 				const moveRange = new Range( new Position( root, [ 0, 1, 8 ] ), new Position( root, [ 0, 1, 10 ] ) );

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 259 - 136
packages/ckeditor5-engine/tests/model/operation/transform.js


+ 0 - 24
packages/ckeditor5-engine/tests/model/position.js

@@ -291,14 +291,6 @@ describe( 'Position', () => {
 		expect( new Position( root, [ 1, 0, 3 ] ) ).to.have.property( 'index' ).that.equals( 1 );
 		expect( new Position( root, [ 1, 0, 3 ] ) ).to.have.property( 'index' ).that.equals( 1 );
 	} );
 	} );
 
 
-	it( 'should be able to set offset', () => {
-		const position = new Position( root, [ 1, 0, 2 ] );
-		position.offset = 4;
-
-		expect( position.offset ).to.equal( 4 );
-		expect( position.path ).to.deep.equal( [ 1, 0, 4 ] );
-	} );
-
 	it( 'should have nodeBefore if it is not inside a text node', () => {
 	it( 'should have nodeBefore if it is not inside a text node', () => {
 		expect( new Position( root, [ 0 ] ).nodeBefore ).to.be.null;
 		expect( new Position( root, [ 0 ] ).nodeBefore ).to.be.null;
 		expect( new Position( root, [ 1 ] ).nodeBefore ).to.equal( p );
 		expect( new Position( root, [ 1 ] ).nodeBefore ).to.equal( p );
@@ -605,14 +597,6 @@ describe( 'Position', () => {
 	} );
 	} );
 
 
 	describe( '_getTransformedByInsertion()', () => {
 	describe( '_getTransformedByInsertion()', () => {
-		it( 'should return a new Position instance', () => {
-			const position = new Position( root, [ 0 ] );
-			const transformed = position._getTransformedByInsertion( new Position( root, [ 2 ] ), 4, false );
-
-			expect( transformed ).not.to.equal( position );
-			expect( transformed ).to.be.instanceof( Position );
-		} );
-
 		it( 'should increment offset if insertion is in the same parent and closer offset', () => {
 		it( 'should increment offset if insertion is in the same parent and closer offset', () => {
 			const position = new Position( root, [ 1, 2, 3 ] );
 			const position = new Position( root, [ 1, 2, 3 ] );
 			const transformed = position._getTransformedByInsertion( new Position( root, [ 1, 2, 2 ] ), 2, false );
 			const transformed = position._getTransformedByInsertion( new Position( root, [ 1, 2, 2 ] ), 2, false );
@@ -665,14 +649,6 @@ describe( 'Position', () => {
 	} );
 	} );
 
 
 	describe( '_getTransformedByDeletion()', () => {
 	describe( '_getTransformedByDeletion()', () => {
-		it( 'should return a new Position instance', () => {
-			const position = new Position( root, [ 0 ] );
-			const transformed = position._getTransformedByDeletion( new Position( root, [ 2 ] ), 4 );
-
-			expect( transformed ).not.to.equal( position );
-			expect( transformed ).to.be.instanceof( Position );
-		} );
-
 		it( 'should return null if original position is inside one of removed nodes', () => {
 		it( 'should return null if original position is inside one of removed nodes', () => {
 			const position = new Position( root, [ 1, 2 ] );
 			const position = new Position( root, [ 1, 2 ] );
 			const transformed = position._getTransformedByDeletion( new Position( root, [ 0 ] ), 2 );
 			const transformed = position._getTransformedByDeletion( new Position( root, [ 0 ] ), 2 );

+ 50 - 25
packages/ckeditor5-engine/tests/model/range.js

@@ -855,7 +855,8 @@ describe( 'Range', () => {
 			} );
 			} );
 
 
 			it( 'move inside the range', () => {
 			it( 'move inside the range', () => {
-				range.end.offset = 6;
+				range = new Range( range.start, range.end.getShiftedTo( 6 ) );
+
 				const start = new Position( root, [ 3 ] );
 				const start = new Position( root, [ 3 ] );
 				const target = new Position( root, [ 5 ] );
 				const target = new Position( root, [ 5 ] );
 				const delta = getMoveDelta( start, 1, target, 1 );
 				const delta = getMoveDelta( start, 1, target, 1 );
@@ -977,8 +978,10 @@ describe( 'Range', () => {
 
 
 		describe( 'by SplitDelta', () => {
 		describe( 'by SplitDelta', () => {
 			it( 'split inside range', () => {
 			it( 'split inside range', () => {
-				range.start = new Position( root, [ 0, 2 ] );
-				range.end = new Position( root, [ 0, 4 ] );
+				range = new Range(
+					new Position( root, [ 0, 2 ] ),
+					new Position( root, [ 0, 4 ] )
+				);
 
 
 				const delta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, 1 );
 				const delta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, 1 );
 
 
@@ -990,8 +993,10 @@ describe( 'Range', () => {
 			} );
 			} );
 
 
 			it( 'split at the beginning of multi-element range', () => {
 			it( 'split at the beginning of multi-element range', () => {
-				range.start = new Position( root, [ 0, 4 ] );
-				range.end = new Position( root, [ 1, 2 ] );
+				range = new Range(
+					new Position( root, [ 0, 4 ] ),
+					new Position( root, [ 1, 2 ] )
+				);
 
 
 				const delta = getSplitDelta( new Position( root, [ 0, 4 ] ), new Element( 'p' ), 3, 1 );
 				const delta = getSplitDelta( new Position( root, [ 0, 4 ] ), new Element( 'p' ), 3, 1 );
 
 
@@ -1003,8 +1008,10 @@ describe( 'Range', () => {
 			} );
 			} );
 
 
 			it( 'split inside range which starts at the beginning of split element', () => {
 			it( 'split inside range which starts at the beginning of split element', () => {
-				range.start = new Position( root, [ 0, 0 ] );
-				range.end = new Position( root, [ 0, 4 ] );
+				range = new Range(
+					new Position( root, [ 0, 0 ] ),
+					new Position( root, [ 0, 4 ] )
+				);
 
 
 				const delta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, 1 );
 				const delta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, 1 );
 
 
@@ -1016,8 +1023,10 @@ describe( 'Range', () => {
 			} );
 			} );
 
 
 			it( 'split inside range which end is at the end of split element', () => {
 			it( 'split inside range which end is at the end of split element', () => {
-				range.start = new Position( root, [ 0, 3 ] );
-				range.end = new Position( root, [ 0, 6 ] );
+				range = new Range(
+					new Position( root, [ 0, 3 ] ),
+					new Position( root, [ 0, 6 ] )
+				);
 
 
 				const delta = getSplitDelta( new Position( root, [ 0, 4 ] ), new Element( 'p' ), 2, 1 );
 				const delta = getSplitDelta( new Position( root, [ 0, 4 ] ), new Element( 'p' ), 2, 1 );
 
 
@@ -1029,8 +1038,10 @@ describe( 'Range', () => {
 			} );
 			} );
 
 
 			it( 'split element which has collapsed range at the end', () => {
 			it( 'split element which has collapsed range at the end', () => {
-				range.start = new Position( root, [ 0, 6 ] );
-				range.end = new Position( root, [ 0, 6 ] );
+				range = new Range(
+					new Position( root, [ 0, 6 ] ),
+					new Position( root, [ 0, 6 ] )
+				);
 
 
 				const delta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, 1 );
 				const delta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, 1 );
 
 
@@ -1044,8 +1055,10 @@ describe( 'Range', () => {
 
 
 		describe( 'by MergeDelta', () => {
 		describe( 'by MergeDelta', () => {
 			it( 'merge element with collapsed range', () => {
 			it( 'merge element with collapsed range', () => {
-				range.start = new Position( root, [ 1, 0 ] );
-				range.end = new Position( root, [ 1, 0 ] );
+				range = new Range(
+					new Position( root, [ 1, 0 ] ),
+					new Position( root, [ 1, 0 ] )
+				);
 
 
 				const delta = getMergeDelta( new Position( root, [ 1 ] ), 3, 3, 1 );
 				const delta = getMergeDelta( new Position( root, [ 1 ] ), 3, 3, 1 );
 
 
@@ -1106,8 +1119,10 @@ describe( 'Range', () => {
 		describe( 'by WrapDelta', () => {
 		describe( 'by WrapDelta', () => {
 			it( 'maintans start position when wrapping element in which the range starts and ends', () => {
 			it( 'maintans start position when wrapping element in which the range starts and ends', () => {
 				// <p>f[o]o</p><p>bar</p>
 				// <p>f[o]o</p><p>bar</p>
-				range.start = new Position( root, [ 0, 1 ] );
-				range.end = new Position( root, [ 0, 2 ] );
+				range = new Range(
+					new Position( root, [ 0, 1 ] ),
+					new Position( root, [ 0, 2 ] )
+				);
 
 
 				const wrapRange = new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
 				const wrapRange = new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
 				const wrapElement = new Element( 'w' );
 				const wrapElement = new Element( 'w' );
@@ -1123,8 +1138,10 @@ describe( 'Range', () => {
 
 
 			it( 'maintans start position when wrapping element in which the range starts but not ends', () => {
 			it( 'maintans start position when wrapping element in which the range starts but not ends', () => {
 				// <p>f[oo</p><p>b]ar</p>
 				// <p>f[oo</p><p>b]ar</p>
-				range.start = new Position( root, [ 0, 1 ] );
-				range.end = new Position( root, [ 1, 1 ] );
+				range = new Range(
+					new Position( root, [ 0, 1 ] ),
+					new Position( root, [ 1, 1 ] )
+				);
 
 
 				const wrapRange = new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
 				const wrapRange = new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
 				const wrapElement = new Element( 'w' );
 				const wrapElement = new Element( 'w' );
@@ -1140,8 +1157,10 @@ describe( 'Range', () => {
 
 
 			it( 'maintans end position when wrapping element in which the range ends but not starts', () => {
 			it( 'maintans end position when wrapping element in which the range ends but not starts', () => {
 				// <p>f[oo</p><p>b]ar</p>
 				// <p>f[oo</p><p>b]ar</p>
-				range.start = new Position( root, [ 0, 1 ] );
-				range.end = new Position( root, [ 1, 1 ] );
+				range = new Range(
+					new Position( root, [ 0, 1 ] ),
+					new Position( root, [ 1, 1 ] )
+				);
 
 
 				const wrapRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
 				const wrapRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
 				const wrapElement = new Element( 'w' );
 				const wrapElement = new Element( 'w' );
@@ -1159,8 +1178,10 @@ describe( 'Range', () => {
 		describe( 'by UnwrapDelta', () => {
 		describe( 'by UnwrapDelta', () => {
 			it( 'maintans start position when wrapping element in which the range starts and ends', () => {
 			it( 'maintans start position when wrapping element in which the range starts and ends', () => {
 				// <w><p>f[o]o</p></w><p>bar</p>
 				// <w><p>f[o]o</p></w><p>bar</p>
-				range.start = new Position( root, [ 0, 0, 1 ] );
-				range.end = new Position( root, [ 0, 0, 2 ] );
+				range = new Range(
+					new Position( root, [ 0, 0, 1 ] ),
+					new Position( root, [ 0, 0, 2 ] )
+				);
 
 
 				const unwrapPosition = new Position( root, [ 0 ] );
 				const unwrapPosition = new Position( root, [ 0 ] );
 				const delta = getUnwrapDelta( unwrapPosition, 1, 1 );
 				const delta = getUnwrapDelta( unwrapPosition, 1, 1 );
@@ -1175,8 +1196,10 @@ describe( 'Range', () => {
 
 
 			it( 'maintans start position when wrapping element in which the range starts but not ends', () => {
 			it( 'maintans start position when wrapping element in which the range starts but not ends', () => {
 				// <w><p>f[oo</p></w><p>b]ar</p>
 				// <w><p>f[oo</p></w><p>b]ar</p>
-				range.start = new Position( root, [ 0, 0, 1 ] );
-				range.end = new Position( root, [ 1, 1 ] );
+				range = new Range(
+					new Position( root, [ 0, 0, 1 ] ),
+					new Position( root, [ 1, 1 ] )
+				);
 
 
 				const unwrapPosition = new Position( root, [ 0 ] );
 				const unwrapPosition = new Position( root, [ 0 ] );
 				const delta = getUnwrapDelta( unwrapPosition, 1, 1 );
 				const delta = getUnwrapDelta( unwrapPosition, 1, 1 );
@@ -1194,8 +1217,10 @@ describe( 'Range', () => {
 
 
 			it( 'maintans end position when wrapping element in which the range ends but not starts', () => {
 			it( 'maintans end position when wrapping element in which the range ends but not starts', () => {
 				// <p>f[oo</p><w><p>b]ar</p></w>
 				// <p>f[oo</p><w><p>b]ar</p></w>
-				range.start = new Position( root, [ 0, 1 ] );
-				range.end = new Position( root, [ 1, 0, 1 ] );
+				range = new Range(
+					new Position( root, [ 0, 1 ] ),
+					new Position( root, [ 1, 0, 1 ] )
+				);
 
 
 				const unwrapPosition = new Position( root, [ 1 ] );
 				const unwrapPosition = new Position( root, [ 1 ] );
 				const delta = getUnwrapDelta( unwrapPosition, 1, 1 );
 				const delta = getUnwrapDelta( unwrapPosition, 1, 1 );

+ 2 - 2
packages/ckeditor5-engine/tests/view/range.js

@@ -25,8 +25,8 @@ describe( 'Range', () => {
 			const range = new Range( start, end );
 			const range = new Range( start, end );
 
 
 			expect( range ).to.be.an.instanceof( Range );
 			expect( range ).to.be.an.instanceof( Range );
-			expect( range ).to.have.property( 'start' ).that.not.equals( start );
-			expect( range ).to.have.property( 'end' ).that.not.equals( end );
+			expect( range ).to.have.property( 'start' ).that.equals( start );
+			expect( range ).to.have.property( 'end' ).that.equals( end );
 			expect( range.start.parent ).to.equal( start.parent );
 			expect( range.start.parent ).to.equal( start.parent );
 			expect( range.end.parent ).to.equal( end.parent );
 			expect( range.end.parent ).to.equal( end.parent );
 			expect( range.start.offset ).to.equal( start.offset );
 			expect( range.start.offset ).to.equal( start.offset );

+ 5 - 5
packages/ckeditor5-engine/tests/view/selection.js

@@ -59,7 +59,7 @@ describe( 'Selection', () => {
 			const anchor = selection.anchor;
 			const anchor = selection.anchor;
 
 
 			expect( anchor.isEqual( range1.start ) ).to.be.true;
 			expect( anchor.isEqual( range1.start ) ).to.be.true;
-			expect( anchor ).to.not.equal( range1.start );
+			expect( anchor ).to.equal( range1.start );
 		} );
 		} );
 
 
 		it( 'should return end of single range in selection when added as backward', () => {
 		it( 'should return end of single range in selection when added as backward', () => {
@@ -67,7 +67,7 @@ describe( 'Selection', () => {
 			const anchor = selection.anchor;
 			const anchor = selection.anchor;
 
 
 			expect( anchor.isEqual( range1.end ) ).to.be.true;
 			expect( anchor.isEqual( range1.end ) ).to.be.true;
-			expect( anchor ).to.not.equal( range1.end );
+			expect( anchor ).to.equal( range1.end );
 		} );
 		} );
 
 
 		it( 'should get anchor from last inserted range', () => {
 		it( 'should get anchor from last inserted range', () => {
@@ -95,7 +95,7 @@ describe( 'Selection', () => {
 			const focus = selection.focus;
 			const focus = selection.focus;
 
 
 			expect( focus.isEqual( range1.start ) ).to.be.true;
 			expect( focus.isEqual( range1.start ) ).to.be.true;
-			expect( focus ).to.not.equal( range1.start );
+			expect( focus ).to.equal( range1.start );
 		} );
 		} );
 
 
 		it( 'should get focus from last inserted range', () => {
 		it( 'should get focus from last inserted range', () => {
@@ -410,7 +410,7 @@ describe( 'Selection', () => {
 			const position = selection.getFirstPosition();
 			const position = selection.getFirstPosition();
 
 
 			expect( position.isEqual( range2.start ) ).to.be.true;
 			expect( position.isEqual( range2.start ) ).to.be.true;
-			expect( position ).to.not.equal( range2.start );
+			expect( position ).to.equal( range2.start );
 		} );
 		} );
 
 
 		it( 'should return null if no ranges are present', () => {
 		it( 'should return null if no ranges are present', () => {
@@ -427,7 +427,7 @@ describe( 'Selection', () => {
 			const position = selection.getLastPosition();
 			const position = selection.getLastPosition();
 
 
 			expect( position.isEqual( range3.end ) ).to.be.true;
 			expect( position.isEqual( range3.end ) ).to.be.true;
-			expect( position ).to.not.equal( range3.end );
+			expect( position ).to.equal( range3.end );
 		} );
 		} );
 
 
 		it( 'should return null if no ranges are present', () => {
 		it( 'should return null if no ranges are present', () => {

+ 12 - 13
packages/ckeditor5-engine/tests/view/writer/remove.js

@@ -15,8 +15,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 
 describe( 'writer', () => {
 describe( 'writer', () => {
 	/**
 	/**
-	 * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
-	 * test ranges.
+	 * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create ranges.
 	 *
 	 *
 	 * @param {String} input
 	 * @param {String} input
 	 * @param {String} expectedResult
 	 * @param {String} expectedResult
@@ -27,7 +26,7 @@ describe( 'writer', () => {
 
 
 		const range = selection.getFirstRange();
 		const range = selection.getFirstRange();
 		const removed = remove( range );
 		const removed = remove( range );
-		expect( stringify( view, range, { showType: true, showPriority: true } ) ).to.equal( expectedResult );
+		expect( stringify( view, null, { showType: true, showPriority: true } ) ).to.equal( expectedResult );
 		expect( stringify( removed, null, { showType: true, showPriority: true } ) ).to.equal( expectedRemoved );
 		expect( stringify( removed, null, { showType: true, showPriority: true } ) ).to.equal( expectedRemoved );
 	}
 	}
 
 
@@ -60,21 +59,21 @@ describe( 'writer', () => {
 		} );
 		} );
 
 
 		it( 'should remove single text node', () => {
 		it( 'should remove single text node', () => {
-			test( '<container:p>[foobar]</container:p>', '<container:p>[]</container:p>', 'foobar' );
+			test( '<container:p>[foobar]</container:p>', '<container:p></container:p>', 'foobar' );
 		} );
 		} );
 
 
 		it( 'should not leave empty text nodes', () => {
 		it( 'should not leave empty text nodes', () => {
-			test( '<container:p>{foobar}</container:p>', '<container:p>[]</container:p>', 'foobar' );
+			test( '<container:p>{foobar}</container:p>', '<container:p></container:p>', 'foobar' );
 		} );
 		} );
 
 
 		it( 'should remove part of the text node', () => {
 		it( 'should remove part of the text node', () => {
-			test( '<container:p>f{oob}ar</container:p>', '<container:p>f{}ar</container:p>', 'oob' );
+			test( '<container:p>f{oob}ar</container:p>', '<container:p>far</container:p>', 'oob' );
 		} );
 		} );
 
 
 		it( 'should remove parts of nodes #1', () => {
 		it( 'should remove parts of nodes #1', () => {
 			test(
 			test(
 				'<container:p>f{oo<attribute:b view-priority="10">ba}r</attribute:b></container:p>',
 				'<container:p>f{oo<attribute:b view-priority="10">ba}r</attribute:b></container:p>',
-				'<container:p>f[]<attribute:b view-priority="10">r</attribute:b></container:p>',
+				'<container:p>f<attribute:b view-priority="10">r</attribute:b></container:p>',
 				'oo<attribute:b view-priority="10">ba</attribute:b>'
 				'oo<attribute:b view-priority="10">ba</attribute:b>'
 			);
 			);
 		} );
 		} );
@@ -82,7 +81,7 @@ describe( 'writer', () => {
 		it( 'should support unicode', () => {
 		it( 'should support unicode', () => {
 			test(
 			test(
 				'<container:p>நி{லை<attribute:b view-priority="10">க்}கு</attribute:b></container:p>',
 				'<container:p>நி{லை<attribute:b view-priority="10">க்}கு</attribute:b></container:p>',
-				'<container:p>நி[]<attribute:b view-priority="10">கு</attribute:b></container:p>',
+				'<container:p>நி<attribute:b view-priority="10">கு</attribute:b></container:p>',
 				'லை<attribute:b view-priority="10">க்</attribute:b>'
 				'லை<attribute:b view-priority="10">க்</attribute:b>'
 			);
 			);
 		} );
 		} );
@@ -92,7 +91,7 @@ describe( 'writer', () => {
 				'<container:p>' +
 				'<container:p>' +
 					'<attribute:b view-priority="1">foo</attribute:b>[bar]<attribute:b view-priority="1">bazqux</attribute:b>' +
 					'<attribute:b view-priority="1">foo</attribute:b>[bar]<attribute:b view-priority="1">bazqux</attribute:b>' +
 				'</container:p>',
 				'</container:p>',
-				'<container:p><attribute:b view-priority="1">foo{}bazqux</attribute:b></container:p>',
+				'<container:p><attribute:b view-priority="1">foobazqux</attribute:b></container:p>',
 				'bar'
 				'bar'
 			);
 			);
 		} );
 		} );
@@ -102,19 +101,19 @@ describe( 'writer', () => {
 				'<container:p>' +
 				'<container:p>' +
 					'<attribute:b view-priority="1">fo{o</attribute:b>bar<attribute:b view-priority="1">ba}zqux</attribute:b>' +
 					'<attribute:b view-priority="1">fo{o</attribute:b>bar<attribute:b view-priority="1">ba}zqux</attribute:b>' +
 				'</container:p>',
 				'</container:p>',
-				'<container:p><attribute:b view-priority="1">fo{}zqux</attribute:b></container:p>',
+				'<container:p><attribute:b view-priority="1">fozqux</attribute:b></container:p>',
 				'<attribute:b view-priority="1">o</attribute:b>bar<attribute:b view-priority="1">ba</attribute:b>'
 				'<attribute:b view-priority="1">o</attribute:b>bar<attribute:b view-priority="1">ba</attribute:b>'
 			);
 			);
 		} );
 		} );
 
 
 		it( 'should remove part of the text node in document fragment', () => {
 		it( 'should remove part of the text node in document fragment', () => {
-			test( 'fo{ob}ar', 'fo{}ar', 'ob' );
+			test( 'fo{ob}ar', 'foar', 'ob' );
 		} );
 		} );
 
 
 		it( 'should remove EmptyElement', () => {
 		it( 'should remove EmptyElement', () => {
 			test(
 			test(
 				'<container:p>foo[<empty:img></empty:img>]bar</container:p>',
 				'<container:p>foo[<empty:img></empty:img>]bar</container:p>',
-				'<container:p>foo{}bar</container:p>',
+				'<container:p>foobar</container:p>',
 				'<empty:img></empty:img>'
 				'<empty:img></empty:img>'
 			);
 			);
 		} );
 		} );
@@ -133,7 +132,7 @@ describe( 'writer', () => {
 		it( 'should remove UIElement', () => {
 		it( 'should remove UIElement', () => {
 			test(
 			test(
 				'<container:p>foo[<ui:span></ui:span>]bar</container:p>',
 				'<container:p>foo[<ui:span></ui:span>]bar</container:p>',
-				'<container:p>foo{}bar</container:p>',
+				'<container:p>foobar</container:p>',
 				'<ui:span></ui:span>'
 				'<ui:span></ui:span>'
 			);
 			);
 		} );
 		} );

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است