Procházet zdrojové kódy

Merge branch 'master' into t/455

Szymon Kupś před 9 roky
rodič
revize
569c780575
100 změnil soubory, kde provedl 2563 přidání a 2269 odebrání
  1. 88 60
      packages/ckeditor5-engine/.jscsrc
  2. 10 11
      packages/ckeditor5-engine/.jshintrc
  3. 10 11
      packages/ckeditor5-engine/dev/.jshintrc
  4. 1 1
      packages/ckeditor5-engine/gulpfile.js
  5. 0 0
      packages/ckeditor5-engine/src/conversion/conversion.jsdoc
  6. 10 12
      packages/ckeditor5-engine/src/conversion/mapper.js
  7. 7 9
      packages/ckeditor5-engine/src/conversion/model-converter-builder.js
  8. 14 15
      packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js
  9. 2 4
      packages/ckeditor5-engine/src/conversion/model-to-view-converters.js
  10. 18 26
      packages/ckeditor5-engine/src/conversion/modelconsumable.js
  11. 11 13
      packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js
  12. 9 8
      packages/ckeditor5-engine/src/conversion/view-converter-builder.js
  13. 9 12
      packages/ckeditor5-engine/src/conversion/view-selection-to-model-converters.js
  14. 6 7
      packages/ckeditor5-engine/src/conversion/view-to-model-converters.js
  15. 0 2
      packages/ckeditor5-engine/src/conversion/viewconsumable.js
  16. 0 2
      packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js
  17. 0 2
      packages/ckeditor5-engine/src/datacontroller.js
  18. 0 2
      packages/ckeditor5-engine/src/dataprocessor/basichtmlwriter.js
  19. 0 2
      packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js
  20. 1 22
      packages/ckeditor5-engine/src/editingcontroller.js
  21. 7 10
      packages/ckeditor5-engine/src/model/batch.js
  22. 0 134
      packages/ckeditor5-engine/src/model/characterproxy.js
  23. 0 2
      packages/ckeditor5-engine/src/model/composer/composer.js
  24. 0 2
      packages/ckeditor5-engine/src/model/composer/deletecontents.js
  25. 10 12
      packages/ckeditor5-engine/src/model/composer/modifyselection.js
  26. 33 29
      packages/ckeditor5-engine/src/model/delta/attributedelta.js
  27. 0 2
      packages/ckeditor5-engine/src/model/delta/basic-deltas.js
  28. 31 27
      packages/ckeditor5-engine/src/model/delta/basic-transformations.js
  29. 0 3
      packages/ckeditor5-engine/src/model/delta/delta.js
  30. 0 2
      packages/ckeditor5-engine/src/model/delta/deltafactory.js
  31. 11 4
      packages/ckeditor5-engine/src/model/delta/insertdelta.js
  32. 12 8
      packages/ckeditor5-engine/src/model/delta/mergedelta.js
  33. 16 11
      packages/ckeditor5-engine/src/model/delta/movedelta.js
  34. 6 8
      packages/ckeditor5-engine/src/model/delta/removedelta.js
  35. 17 5
      packages/ckeditor5-engine/src/model/delta/renamedelta.js
  36. 20 12
      packages/ckeditor5-engine/src/model/delta/splitdelta.js
  37. 1 3
      packages/ckeditor5-engine/src/model/delta/transform.js
  38. 8 4
      packages/ckeditor5-engine/src/model/delta/unwrapdelta.js
  39. 10 12
      packages/ckeditor5-engine/src/model/delta/weakinsertdelta.js
  40. 12 5
      packages/ckeditor5-engine/src/model/delta/wrapdelta.js
  41. 6 7
      packages/ckeditor5-engine/src/model/document.js
  42. 145 44
      packages/ckeditor5-engine/src/model/documentfragment.js
  43. 128 114
      packages/ckeditor5-engine/src/model/element.js
  44. 3 5
      packages/ckeditor5-engine/src/model/history.js
  45. 17 16
      packages/ckeditor5-engine/src/model/liveposition.js
  46. 13 16
      packages/ckeditor5-engine/src/model/liverange.js
  47. 21 20
      packages/ckeditor5-engine/src/model/liveselection.js
  48. 199 50
      packages/ckeditor5-engine/src/model/node.js
  49. 106 400
      packages/ckeditor5-engine/src/model/nodelist.js
  50. 30 24
      packages/ckeditor5-engine/src/model/operation/attributeoperation.js
  51. 40 15
      packages/ckeditor5-engine/src/model/operation/insertoperation.js
  52. 24 26
      packages/ckeditor5-engine/src/model/operation/moveoperation.js
  53. 7 5
      packages/ckeditor5-engine/src/model/operation/nooperation.js
  54. 0 2
      packages/ckeditor5-engine/src/model/operation/operation.js
  55. 1 3
      packages/ckeditor5-engine/src/model/operation/operationfactory.js
  56. 11 11
      packages/ckeditor5-engine/src/model/operation/reinsertoperation.js
  57. 14 10
      packages/ckeditor5-engine/src/model/operation/removeoperation.js
  58. 0 2
      packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js
  59. 18 20
      packages/ckeditor5-engine/src/model/operation/transform.js
  60. 308 264
      packages/ckeditor5-engine/src/model/position.js
  61. 109 100
      packages/ckeditor5-engine/src/model/range.js
  62. 22 13
      packages/ckeditor5-engine/src/model/rootelement.js
  63. 5 7
      packages/ckeditor5-engine/src/model/schema.js
  64. 252 139
      packages/ckeditor5-engine/src/model/selection.js
  65. 35 84
      packages/ckeditor5-engine/src/model/text.js
  66. 106 91
      packages/ckeditor5-engine/src/model/textproxy.js
  67. 64 64
      packages/ckeditor5-engine/src/model/treewalker.js
  68. 316 0
      packages/ckeditor5-engine/src/model/writer.js
  69. 0 2
      packages/ckeditor5-engine/src/view/attributeelement.js
  70. 0 2
      packages/ckeditor5-engine/src/view/containerelement.js
  71. 17 3
      packages/ckeditor5-engine/src/view/document.js
  72. 1 3
      packages/ckeditor5-engine/src/view/documentfragment.js
  73. 0 2
      packages/ckeditor5-engine/src/view/domconverter.js
  74. 0 2
      packages/ckeditor5-engine/src/view/editableelement.js
  75. 0 2
      packages/ckeditor5-engine/src/view/element.js
  76. 0 2
      packages/ckeditor5-engine/src/view/filler.js
  77. 0 2
      packages/ckeditor5-engine/src/view/matcher.js
  78. 0 2
      packages/ckeditor5-engine/src/view/node.js
  79. 0 2
      packages/ckeditor5-engine/src/view/observer/domeventdata.js
  80. 0 2
      packages/ckeditor5-engine/src/view/observer/domeventobserver.js
  81. 3 5
      packages/ckeditor5-engine/src/view/observer/focusobserver.js
  82. 2 4
      packages/ckeditor5-engine/src/view/observer/keyobserver.js
  83. 2 4
      packages/ckeditor5-engine/src/view/observer/mutationobserver.js
  84. 0 2
      packages/ckeditor5-engine/src/view/observer/observer.js
  85. 2 4
      packages/ckeditor5-engine/src/view/observer/selectionobserver.js
  86. 12 14
      packages/ckeditor5-engine/src/view/position.js
  87. 0 2
      packages/ckeditor5-engine/src/view/range.js
  88. 4 6
      packages/ckeditor5-engine/src/view/renderer.js
  89. 0 2
      packages/ckeditor5-engine/src/view/rooteditableelement.js
  90. 12 6
      packages/ckeditor5-engine/src/view/selection.js
  91. 0 2
      packages/ckeditor5-engine/src/view/text.js
  92. 0 2
      packages/ckeditor5-engine/src/view/textproxy.js
  93. 37 39
      packages/ckeditor5-engine/src/view/treewalker.js
  94. 0 2
      packages/ckeditor5-engine/src/view/writer.js
  95. 21 23
      packages/ckeditor5-engine/tests/.jshintrc
  96. 30 18
      packages/ckeditor5-engine/tests/_utils-tests/model.js
  97. 0 2
      packages/ckeditor5-engine/tests/_utils-tests/view.js
  98. 13 14
      packages/ckeditor5-engine/tests/_utils/model.js
  99. 0 2
      packages/ckeditor5-engine/tests/_utils/view.js
  100. 17 22
      packages/ckeditor5-engine/tests/conversion/advanced-converters.js

+ 88 - 60
packages/ckeditor5-engine/.jscsrc

@@ -1,62 +1,90 @@
 {
-	"esnext": true,
-
-	"requireCurlyBraces": [
-		"if", "else", "for", "while", "do", "switch", "try", "catch"
-	],
-	"requireSpaceBeforeKeywords": true,
-	"requireSpaceAfterKeywords": [
-		"do", "for", "if", "else", "switch", "case", "try", "catch", "void", "while", "with", "return", "typeof"
-	],
-	"requireSpaceBeforeBlockStatements": true,
-	"requireParenthesesAroundIIFE": true,
-	"requireSpacesInConditionalExpression": {
-		"afterTest": true,
-		"beforeConsequent": true,
-		"afterConsequent": true,
-		"beforeAlternate": true
-	},
-	"requireSpacesInFunction": {
-		"beforeOpeningCurlyBrace": true
-	},
-	"disallowSpacesInFunction": {
-		"beforeOpeningRoundBrace": true
-	},
-	"disallowSpacesInCallExpression": true,
-	"disallowMultipleVarDecl": "exceptUndefined",
-	"requireBlocksOnNewline": true,
-	"disallowPaddingNewlinesInBlocks": true,
-	"requirePaddingNewlinesBeforeKeywords": [
-		"do", "for", "if", "switch", "case", "try", "while", "with", "return"
-	],
-	"requireSpacesInsideObjectBrackets": "all",
-	"requireSpacesInsideArrayBrackets": "all",
-	"requireSpacesInsideParentheses": "all",
-	"disallowSpaceAfterObjectKeys": true,
-	"requireSpaceBeforeObjectValues": true,
-	"requireCommaBeforeLineBreak": true,
-	"requireOperatorBeforeLineBreak": true,
-	"disallowSpaceAfterPrefixUnaryOperators": true,
-	"disallowSpaceBeforePostfixUnaryOperators": true,
-	"requireSpaceBeforeBinaryOperators": true,
-	"disallowImplicitTypeConversion": [
-		"numeric", "binary", "string"
-	],
-	"requireCamelCaseOrUpperCaseIdentifiers": true,
-	"requireSpaceAfterBinaryOperators": true,
-	"disallowKeywords": [
-		"with"
-	],
-	"disallowMultipleLineStrings": true,
-	"disallowMultipleLineBreaks": true,
-	"disallowMixedSpacesAndTabs": true,
-	"disallowTrailingWhitespace": true,
-	"maximumLineLength": 140,
-	"requireCapitalizedConstructors": true,
-	"requireDotNotation": true,
-	"disallowYodaConditions": true,
-	"disallowNewlineBeforeBlockStatements": true,
-	"validateLineBreaks": "LF",
-	"validateQuoteMarks": "'",
-	"validateIndentation": "\t"
+  "esnext": true,
+  "requireCurlyBraces": [
+    "if",
+    "else",
+    "for",
+    "while",
+    "do",
+    "switch",
+    "try",
+    "catch"
+  ],
+  "requireSpaceBeforeKeywords": true,
+  "requireSpaceAfterKeywords": [
+    "do",
+    "for",
+    "if",
+    "else",
+    "switch",
+    "case",
+    "try",
+    "catch",
+    "void",
+    "while",
+    "with",
+    "return",
+    "typeof"
+  ],
+  "requireSpaceBeforeBlockStatements": true,
+  "requireParenthesesAroundIIFE": true,
+  "requireSpacesInConditionalExpression": {
+    "afterTest": true,
+    "beforeConsequent": true,
+    "afterConsequent": true,
+    "beforeAlternate": true
+  },
+  "requireSpacesInFunction": {
+    "beforeOpeningCurlyBrace": true
+  },
+  "disallowSpacesInFunction": {
+    "beforeOpeningRoundBrace": true
+  },
+  "disallowSpacesInCallExpression": true,
+  "disallowMultipleVarDecl": "exceptUndefined",
+  "requireBlocksOnNewline": true,
+  "disallowPaddingNewlinesInBlocks": true,
+  "requirePaddingNewlinesBeforeKeywords": [
+    "do",
+    "for",
+    "if",
+    "switch",
+    "case",
+    "try",
+    "while",
+    "with",
+    "return"
+  ],
+  "requireSpacesInsideObjectBrackets": "all",
+  "requireSpacesInsideArrayBrackets": "all",
+  "requireSpacesInsideParentheses": "all",
+  "disallowSpaceAfterObjectKeys": true,
+  "requireSpaceBeforeObjectValues": true,
+  "requireCommaBeforeLineBreak": true,
+  "requireOperatorBeforeLineBreak": true,
+  "disallowSpaceAfterPrefixUnaryOperators": true,
+  "disallowSpaceBeforePostfixUnaryOperators": true,
+  "requireSpaceBeforeBinaryOperators": true,
+  "disallowImplicitTypeConversion": [
+    "numeric",
+    "binary",
+    "string"
+  ],
+  "requireCamelCaseOrUpperCaseIdentifiers": true,
+  "requireSpaceAfterBinaryOperators": true,
+  "disallowKeywords": [
+    "with"
+  ],
+  "disallowMultipleLineStrings": true,
+  "disallowMultipleLineBreaks": true,
+  "disallowMixedSpacesAndTabs": true,
+  "disallowTrailingWhitespace": true,
+  "maximumLineLength": 140,
+  "requireCapitalizedConstructors": true,
+  "requireDotNotation": true,
+  "disallowYodaConditions": true,
+  "disallowNewlineBeforeBlockStatements": true,
+  "validateLineBreaks": "LF",
+  "validateQuoteMarks": "'",
+  "validateIndentation": "\t"
 }

+ 10 - 11
packages/ckeditor5-engine/.jshintrc

@@ -1,13 +1,12 @@
 {
-	"browser": true,
-	"esnext": true,
-
-	"immed": true,
-	"loopfunc": true,
-	"noarg": true,
-	"nonbsp": true,
-	"undef": true,
-	"unused": true,
-	"strict": "global",
-	"varstmt": true
+  "browser": true,
+  "esnext": true,
+  "immed": true,
+  "loopfunc": true,
+  "noarg": true,
+  "nonbsp": true,
+  "undef": true,
+  "unused": true,
+  "strict": "implied",
+  "varstmt": true
 }

+ 10 - 11
packages/ckeditor5-engine/dev/.jshintrc

@@ -1,13 +1,12 @@
 {
-	"node": true,
-	"esnext": true,
-
-	"immed": true,
-	"loopfunc": true,
-	"noarg": true,
-	"nonbsp": true,
-	"undef": true,
-	"unused": true,
-	"strict": true,
-	"varstmt": true
+  "node": true,
+  "esnext": true,
+  "immed": true,
+  "loopfunc": true,
+  "noarg": true,
+  "nonbsp": true,
+  "undef": true,
+  "unused": true,
+  "strict": true,
+  "varstmt": true
 }

+ 1 - 1
packages/ckeditor5-engine/gulpfile.js

@@ -1,4 +1,4 @@
-/* jshint node: true */
+/* jshint browser: false, node: true, strict: true */
 
 'use strict';
 

+ 0 - 0
packages/ckeditor5-engine/src/conversion/treecontroller.jsdoc → packages/ckeditor5-engine/src/conversion/conversion.jsdoc


+ 10 - 12
packages/ckeditor5-engine/src/conversion/mapper.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import ModelPosition from '../model/position.js';
 import ViewPosition from '../view/position.js';
 import ModelRange from '../model/range.js';
@@ -14,8 +12,8 @@ import ViewText from '../view/text.js';
 /**
  * Maps elements and positions between {@link engine.view.Document view} and {@link engine.model model}.
  *
- * Mapper use binded elements to find corresponding elements and positions, so, to get proper results,
- * all Tree Model elements should be {@link engine.conversion.Mapper#bindElements binded}.
+ * Mapper use bound elements to find corresponding elements and positions, so, to get proper results,
+ * all model elements should be {@link engine.conversion.Mapper#bindElements bound}.
  *
  * @memberOf engine.conversion
  */
@@ -25,7 +23,7 @@ export default class Mapper {
 	 */
 	constructor() {
 		/**
-		 * Model element to View element mapping.
+		 * Model element to view element mapping.
 		 *
 		 * @private
 		 * @member {WeakMap} engine.conversion.Mapper#_modelToViewMapping
@@ -33,7 +31,7 @@ export default class Mapper {
 		this._modelToViewMapping = new WeakMap();
 
 		/**
-		 * View element to Model element mapping.
+		 * View element to model element mapping.
 		 *
 		 * @private
 		 * @member {WeakMap} engine.conversion.Mapper#_viewToModelMapping
@@ -67,7 +65,7 @@ export default class Mapper {
 	 * Gets the corresponding model element.
 	 *
 	 * @param {engine.view.Element} viewElement View element.
-	 * @returns {engine.model.Element|null} Corresponding model element or `null` if not found.
+	 * @returns {engine.model.Element|undefined} Corresponding model element or `undefined` if not found.
 	 */
 	toModelElement( viewElement ) {
 		return this._viewToModelMapping.get( viewElement );
@@ -77,7 +75,7 @@ export default class Mapper {
 	 * Gets the corresponding view element.
 	 *
 	 * @param {engine.model.Element} modelElement Model element.
-	 * @returns {engine.view.Element|null} Corresponding view element or `null` if not found.
+	 * @returns {engine.view.Element|undefined} Corresponding view element or `undefined` if not found.
 	 */
 	toViewElement( modelElement ) {
 		return this._modelToViewMapping.get( modelElement );
@@ -156,10 +154,10 @@ export default class Mapper {
 	_toModelOffset( viewParent, viewOffset, viewBlock ) {
 		if ( viewBlock != viewParent ) {
 			// See example.
-			const offsetToParentBegging = this._toModelOffset( viewParent.parent, viewParent.getIndex(), viewBlock );
+			const offsetToParentStart = this._toModelOffset( viewParent.parent, viewParent.getIndex(), viewBlock );
 			const offsetInParent = this._toModelOffset( viewParent, viewOffset, viewParent );
 
-			return offsetToParentBegging + offsetInParent;
+			return offsetToParentStart + offsetInParent;
 		}
 
 		// viewBlock == viewParent, so we need to calculate the offset in the parent element.
@@ -185,8 +183,8 @@ export default class Mapper {
 	 * Examples:
 	 *
 	 *		foo          -> 3 // Length of the text is the length of data.
-	 *		<b>foo</b>   -> 3 // Length the element which has no corresponding model element is a length of its children.
-	 *		<p>foo</p>   -> 1 // Length the element which has corresponding model element is always 1.
+	 *		<b>foo</b>   -> 3 // Length of the element which has no corresponding model element is a length of its children.
+	 *		<p>foo</p>   -> 1 // Length of the element which has corresponding model element is always 1.
 	 *
 	 * @private
 	 * @param {engine.view.Element} viewNode View node.

+ 7 - 9
packages/ckeditor5-engine/src/conversion/model-converter-builder.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import {
 	insertElement,
 	setAttribute,
@@ -42,9 +40,9 @@ import ViewContainerElement from '../view/containerelement.js';
  *		BuildModelConverterFor( dispatcher ).fromAttribute( 'src' ).toAttribute();
  *
  * 3. Model attribute to view element converter. This is a converter that takes model attributes and represents them
- * as view elements. Those view elements are wrapping view elements are node that correspond to model elements and
- * nodes which had converter attribute. It is suitable for attributes like `bold`, where `bold` attribute set on model
- * text nodes is converter to `strong` view element.
+ * as view elements. Elements created by this kind of converter are wrapping other view elements. Wrapped view nodes
+ * correspond to model nodes had converter attribute. It is suitable for attributes like `bold`, where `bold` attribute
+ * set on model text nodes is converter to `strong` view element.
  *
  *		BuildModelConverterFor( dispatcher ).fromAttribute( 'bold' ).toElement( 'strong' );
  *
@@ -52,8 +50,8 @@ import ViewContainerElement from '../view/containerelement.js';
  * and {@link engine.conversion.ModelConverterBuilder#toAttribute} methods. See their descriptions to learn more.
  *
  * It is also possible to {@link engine.conversion.ModelConverterBuilder#withPriority change default priority}
- * of created converters to decide which converter should be fired earlier and which later. This is useful if you provide
- * a general converter but want to provide different converter for a specific-case (i.e. given model element is converted
+ * of created converters to decide which converter should be fired earlier and which later. This is useful if you have
+ * a general converter but also want to provide different special-case converters (i.e. given model element is converted
  * always to given view element, but if it has given attribute it is converter to other view element). For this,
  * use {@link engine.conversion.ModelConverterBuilder#withPriority withPriority} right after `from...` method.
  *
@@ -127,8 +125,8 @@ class ModelConverterBuilder {
 	 * Default priority is `10`.
 	 *
 	 * **Note:** Keep in mind that event priority, that is set by this modifier, is used for attribute priority
-	 * when {@link engine.view.writer} is used. This changes how model attributes converter to view elements are
-	 * ordered, i.e.: `<strong><em>foo</em></strong>` vs `<em><strong>foo</strong></em>`. Using priority you can also
+	 * when {@link engine.view.writer} is used. This changes how view elements are ordered,
+	 * i.e.: `<strong><em>foo</em></strong>` vs `<em><strong>foo</strong></em>`. Using priority you can also
 	 * prevent node merging, i.e.: `<span class="bold"><span class="theme">foo</span><span>` vs `<span class="bold theme">foo</span>`.
 	 * If you want to prevent merging, just set different priority for both converters.
 	 *

+ 14 - 15
packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import ViewElement from '../view/element.js';
 import ViewRange from '../view/range.js';
 import viewWriter from '../view/writer.js';
@@ -58,15 +56,15 @@ export function convertRangeSelection() {
  *
  * Example of view state before and after converting collapsed selection:
  *
- *		<p><strong>f^oo<strong>bar</p>
+ *		   <p><strong>f^oo<strong>bar</p>
  *		-> <p><strong>f</strong>^<strong>oo</strong>bar</p>
  *
- * By breaking attribute elements like `<strong>` selection is in correct elements. See also complementary
+ * By breaking attribute elements like `<strong>`, selection is in correct element. See also complementary
  * {@link engine.conversion.modelSelectionToView.convertSelectionAttribute attribute converter} for selection attributes,
  * which wraps collapsed selection into view elements. Those converters together ensure, that selection ends up in
- * appropriate elements.
+ * appropriate attribute elements.
  *
- * See also {@link engine.conversion.modelSelectionToView.clearAttributes} which do the clean-up by merging attributes.
+ * See also {@link engine.conversion.modelSelectionToView.clearAttributes} which does a clean-up by merging attributes.
  *
  * @external engine.conversion.modelSelectionToView
  * @function engine.conversion.modelSelectionToView.convertCollapsedSelection
@@ -118,20 +116,21 @@ export function convertCollapsedSelection() {
  * **Note:** You can use the same `elementCreator` function for this converter factory and {@link engine.conversion.modelToView.wrap}
  * model to view converter, as long as the `elementCreator` function uses only the first parameter (attribute value).
  *
- * Example of view state after converting collapsed selection. The scenario is: selection is inside bold text (`<strong>` element)
- * but it does not have bold attribute itself, but has italic attribute instead (let's assume that user turned off bold and turned
- * on italic with selection collapsed):
- *
  *		modelDispatcher.on( 'selection', convertCollapsedSelection() );
  *		modelDispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
+ *		modelDispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'strong' ) ) );
  *
  * Example of view states before and after converting collapsed selection:
  *
- *		<p><em>f^oo</em>bar</p>
+ *		   <p><em>f^oo</em>bar</p>
  *		-> <p><em>f</em>^<em>oo</em>bar</p>
  *		-> <p><em>f^oo</em>bar</p>
  *
- *		<p><strong>f^oo<strong>bar</p>
+ * Example of view state after converting collapsed selection. The scenario is: selection is inside bold text (`<strong>` element)
+ * but it does not have bold attribute itself and has italic attribute instead (let's assume that user turned off bold and turned
+ * on italic with selection collapsed):
+ *
+ *		   <p><strong>f^oo<strong>bar</p>
  *		-> <p><strong>f</strong>^<strong>oo<strong>bar</p>
  *		-> <p><strong>f</strong><em>^</em><strong>oo</strong>bar</p>
  *
@@ -176,13 +175,13 @@ export function convertSelectionAttribute( elementCreator ) {
  * {@link engine.view.AttributeElement view attribute elements} and merge sibling attributes at all start and end
  * positions of all ranges.
  *
- *		<p><strong>^</strong></p>
+ *		   <p><strong>^</strong></p>
  *		-> <p>^</p>
  *
- *		<p><strong>foo</strong>^<strong>bar</strong>bar</p>
+ *		   <p><strong>foo</strong>^<strong>bar</strong>bar</p>
  *		-> <p><strong>foo^bar<strong>bar</p>
  *
- *		<p><strong>foo</strong><em>^</em><strong>bar</strong>bar</p>
+ *		   <p><strong>foo</strong><em>^</em><strong>bar</strong>bar</p>
  *		-> <p><strong>foo^bar<strong>bar</p>
  *
  * This listener should be assigned before any converter for the new selection:

+ 2 - 4
packages/ckeditor5-engine/src/conversion/model-to-view-converters.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import ModelTreeWalker from '../model/treewalker.js';
 import ModelRange from '../model/range.js';
 
@@ -81,7 +79,7 @@ export function insertText() {
 		consumable.consume( data.item, 'insert' );
 
 		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
-		const viewText = new ViewText( data.item.text );
+		const viewText = new ViewText( data.item.data );
 
 		viewWriter.insert( viewPosition, viewText );
 
@@ -185,7 +183,7 @@ export function removeAttribute( attributeCreator ) {
 /**
  * Function factory, creates a converter that converts set/change attribute changes from the model to the view. In this case,
  * model attributes are converted to a view element that will be wrapping view nodes which corresponding model nodes had
- * the attribute set. This is useful for attributes like `bold`, which may be set on a text nodes in model but are
+ * the attribute set. This is useful for attributes like `bold`, which may be set on text nodes in model but are
  * represented as an element in the view:
  *
  *		[paragraph]              MODEL ====> VIEW        <p>

+ 18 - 26
packages/ckeditor5-engine/src/conversion/modelconsumable.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import TextProxy from '../model/textproxy.js';
 
 /**
@@ -41,9 +39,7 @@ import TextProxy from '../model/textproxy.js';
  *		//
  *		// [image]
  *		//   └─ [caption]
- *		//       ├─ f
- *		//       ├─ o
- *		//       └─ o
+ *		//       └─ foo
  *		//
  *		// View:
  *		//
@@ -243,22 +239,18 @@ export default class ModelConsumable {
 	_getSymbolForTextProxy( textProxy ) {
 		let symbol = null;
 
-		const startIndex = textProxy.first.getIndex();
-		const endIndex = startIndex + textProxy.text.length;
-		const parent = textProxy.commonParent;
-
-		const startIndexMap = this._textProxyRegistry.get( startIndex );
+		const startMap = this._textProxyRegistry.get( textProxy.startOffset );
 
-		if ( startIndexMap ) {
-			const endIndexMap = startIndexMap.get( endIndex );
+		if ( startMap ) {
+			const endMap = startMap.get( textProxy.endOffset );
 
-			if ( endIndexMap ) {
-				symbol = endIndexMap.get( parent );
+			if ( endMap ) {
+				symbol = endMap.get( textProxy.parent );
 			}
 		}
 
 		if ( !symbol ) {
-			symbol = this._addSymbolForTextProxy( startIndex, endIndex, parent );
+			symbol = this._addSymbolForTextProxy( textProxy.startOffset, textProxy.endOffset, textProxy.parent );
 		}
 
 		return symbol;
@@ -275,25 +267,25 @@ export default class ModelConsumable {
 	 * @param {engine.model.Element} parent Text proxy parent.
 	 * @returns {Symbol} Symbol generated for given properties.
 	 */
-	_addSymbolForTextProxy( startIndex, endIndex, parent ) {
+	_addSymbolForTextProxy( start, end, parent ) {
 		const symbol = Symbol();
-		let startIndexMap, endIndexMap;
+		let startMap, endMap;
 
-		startIndexMap = this._textProxyRegistry.get( startIndex );
+		startMap = this._textProxyRegistry.get( start );
 
-		if ( !startIndexMap ) {
-			startIndexMap = new Map();
-			this._textProxyRegistry.set( startIndex, startIndexMap );
+		if ( !startMap ) {
+			startMap = new Map();
+			this._textProxyRegistry.set( start, startMap );
 		}
 
-		endIndexMap = startIndexMap.get( endIndex );
+		endMap = startMap.get( end );
 
-		if ( !endIndexMap ) {
-			endIndexMap = new Map();
-			startIndexMap.set( endIndex, endIndexMap );
+		if ( !endMap ) {
+			endMap = new Map();
+			startMap.set( end, endMap );
 		}
 
-		endIndexMap.set( parent, symbol );
+		endMap.set( parent, symbol );
 
 		return symbol;
 	}

+ 11 - 13
packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Consumable from './modelconsumable.js';
 import Range from '../model/range.js';
 import TextProxy from '../model/textproxy.js';
@@ -179,12 +177,12 @@ export default class ModelConversionDispatcher {
 			// Fire a separate addAttribute event for each attribute that was set on inserted items.
 			// This is important because most attributes converters will listen only to add/change/removeAttribute events.
 			// If we would not add this part, attributes on inserted nodes would not be converted.
-			for ( let attr of item.getAttributes() ) {
-				data.attributeKey = attr[ 0 ];
+			for ( let key of item.getAttributeKeys() ) {
+				data.attributeKey = key;
 				data.attributeOldValue = null;
-				data.attributeNewValue = attr[ 1 ];
+				data.attributeNewValue = item.getAttribute( key );
 
-				this._testAndFire( 'addAttribute:' + attr[ 0 ], data, consumable );
+				this._testAndFire( 'addAttribute:' + key, data, consumable );
 			}
 		}
 	}
@@ -268,9 +266,9 @@ export default class ModelConversionDispatcher {
 
 		this.fire( 'selection', data, consumable, this.conversionApi );
 
-		for ( let attr of selection.getAttributes() ) {
-			data.key = attr[ 0 ];
-			data.value = attr[ 1 ];
+		for ( let key of selection.getAttributeKeys() ) {
+			data.key = key;
+			data.value = selection.getAttribute( key );
 
 			// Do not fire event if the attribute has been consumed.
 			if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) {
@@ -295,8 +293,8 @@ export default class ModelConversionDispatcher {
 
 			consumable.add( item, 'insert' );
 
-			for ( let attr of item.getAttributes() ) {
-				consumable.add( item, 'addAttribute:' + attr[ 0 ] );
+			for ( let key of item.getAttributeKeys() ) {
+				consumable.add( item, 'addAttribute:' + key );
 			}
 		}
 
@@ -337,8 +335,8 @@ export default class ModelConversionDispatcher {
 
 		consumable.add( selection, 'selection' );
 
-		for ( let attr of selection.getAttributes() ) {
-			consumable.add( selection, 'selectionAttribute:' + attr[ 0 ] );
+		for ( let key of selection.getAttributeKeys() ) {
+			consumable.add( selection, 'selectionAttribute:' + key );
 		}
 
 		return consumable;

+ 9 - 8
packages/ckeditor5-engine/src/conversion/view-converter-builder.js

@@ -3,10 +3,10 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Matcher from '../view/matcher.js';
 import ModelElement from '../model/element.js';
+import ModelPosition from '../model/position.js';
+import modelWriter from '../model/writer.js';
 import isIterable from '../../utils/isiterable.js';
 
 /**
@@ -255,8 +255,7 @@ class ViewConverterBuilder {
 					const modelElement = element instanceof Function ? element( data.input ) : new ModelElement( element );
 
 					// Check whether generated structure is okay with `Schema`.
-					// TODO: Make it more sane after .getAttributeKeys() is available for ModelElement.
-					const keys = Array.from( modelElement.getAttributes() ).map( ( attribute ) => attribute[ 0 ] );
+					const keys = Array.from( modelElement.getAttributeKeys() );
 
 					if ( !conversionApi.schema.check( { name: modelElement.name, attributes: keys, inside: data.context } ) ) {
 						continue;
@@ -272,7 +271,9 @@ class ViewConverterBuilder {
 					data.context.push( modelElement );
 
 					// Convert children of converted view element and append them to `modelElement`.
-					modelElement.appendChildren( conversionApi.convertChildren( data.input, consumable, data ) );
+					const modelChildren = conversionApi.convertChildren( data.input, consumable, data );
+					const insertPosition = ModelPosition.createAt( modelElement, 'end' );
+					modelWriter.insert( insertPosition, modelChildren );
 
 					// Remove created `modelElement` from the parents stack.
 					data.context.pop();
@@ -370,7 +371,7 @@ class ViewConverterBuilder {
 	}
 }
 
-// Helper function that sets given attributes on given `engine.model.Item` or `engine.model.DocumentFragment`.
+// Helper function that sets given attributes on given `engine.model.Node` or `engine.model.DocumentFragment`.
 function setAttributeOn( toChange, attribute, data, conversionApi ) {
 	if ( isIterable( toChange ) ) {
 		for ( let node of toChange ) {
@@ -380,8 +381,8 @@ function setAttributeOn( toChange, attribute, data, conversionApi ) {
 		return;
 	}
 
-	// TODO: Make it more sane after .getAttributeKeys() is available for ModelElement.
-	const keys = Array.from( toChange.getAttributes() ).map( ( attribute ) => attribute[ 0 ] ).concat( attribute.key );
+	const keys = Array.from( toChange.getAttributeKeys() );
+	keys.push( attribute.key );
 
 	const schemaQuery = {
 		name: toChange.name || '$text',

+ 9 - 12
packages/ckeditor5-engine/src/conversion/view-selection-to-model-converters.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 /**
  * Contains {@link engine.view.Selection view selection} to {@link engine.model.Selection model selection} conversion
  * helper.
@@ -14,29 +12,28 @@
 
 /**
  * Function factory, creates a callback function which converts a {@link engine.view.Selection view selection} taken
- * from the {@link engine.view.Document#selectionChange} event and set in on the
- * {@link engine.model.Document#selection model}.
+ * from the {@link engine.view.Document#selectionChange} event and sets in on the {@link engine.model.Document#selection model}.
  *
- * Note that because there is not view selection change dispatcher nor any other advance view selection to model
- * conversion mechanism, this method is simple event listener.
+ * **Note**: because there is no view selection change dispatcher nor any other advanced view selection to model
+ * conversion mechanism, the callback should be set directly on view document.
  *
- *		view.document.on( 'selectionChange', convertSelectionChange( model, mapper ) );
+ *		view.document.on( 'selectionChange', convertSelectionChange( modelDocument, mapper ) );
  *
  * @function engine.conversion.viewSelectionToModel.convertSelectionChange
- * @param {engine.model.Document} model Document model on which selection should be updated.
+ * @param {engine.model.Document} modelDocument Model document on which selection should be updated.
  * @param {engine.conversion.Mapper} mapper Conversion mapper.
  * @returns {Function} {@link engine.view.Document#selectionChange} callback function.
  */
-export function convertSelectionChange( model, mapper ) {
+export function convertSelectionChange( modelDocument, mapper ) {
 	return ( evt, data ) => {
-		model.enqueueChanges( () => {
+		modelDocument.enqueueChanges( () => {
 			const viewSelection = data.newSelection;
 
-			model.selection.removeAllRanges();
+			modelDocument.selection.removeAllRanges();
 
 			for ( let viewRange of viewSelection.getRanges() ) {
 				const modelRange = mapper.toModelRange( viewRange );
-				model.selection.addRange( modelRange, viewSelection.isBackward );
+				modelDocument.selection.addRange( modelRange, viewSelection.isBackward );
 			}
 		} );
 	};

+ 6 - 7
packages/ckeditor5-engine/src/conversion/view-to-model-converters.js

@@ -3,10 +3,9 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import ModelDocumentFragment from '../model/documentfragment.js';
 import ModelText from '../model/text.js';
+import { normalizeNodes } from '../model/writer.js';
 
 /**
  * Contains {@link engine.view view} to {@link engine.model model} converters for
@@ -18,12 +17,12 @@ import ModelText from '../model/text.js';
 /**
  * Function factory, creates a converter that converts {@link engine.view.DocumentFragment view document fragment} or
  * all children of {@link engine.view.Element} into {@link engine.model.DocumentFragment model document fragment}.
- * This is the "entry-point" converter for view to model conversion. This converter starts the conversion of all "children"
- * of passed view document fragment. Those "children" {@link engine.view.Node view nodes} are then handled by other converters.
+ * This is the "entry-point" converter for view to model conversion. This converter starts the conversion of all children
+ * of passed view document fragment. Those children {@link engine.view.Node view nodes} are then handled by other converters.
  *
  * This also a "default", last resort converter for all view elements that has not been converted by other converters.
- * When a view element is converted to the model and it does not have it's converter specified, all of that elements
- * children will be converted to {@link engine.model.DocumentFragment} and returned.
+ * When a view element is being converted to the model but it does not have converter specified, that view element
+ * will be converted to {@link engine.model.DocumentFragment model document fragment} and returned.
  *
  * @external engine.conversion.viewToModel
  * @function engine.conversion.viewToModel.convertToModelFragment
@@ -37,7 +36,7 @@ export function convertToModelFragment() {
 		if ( !data.output && consumable.test( data.input, { name: true } ) ) {
 			const convertedChildren = conversionApi.convertChildren( data.input, consumable, data );
 
-			data.output = new ModelDocumentFragment( convertedChildren );
+			data.output = new ModelDocumentFragment( normalizeNodes( convertedChildren ) );
 		}
 	};
 }

+ 0 - 2
packages/ckeditor5-engine/src/conversion/viewconsumable.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import isArray from '../../utils/lib/lodash/isArray.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 import ViewElement from '../view/element.js';

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

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import ViewConsumable from './viewconsumable.js';
 import ViewElement from '../view/element.js';
 import ViewText from '../view/text.js';

+ 0 - 2
packages/ckeditor5-engine/src/datacontroller.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Mapper from './conversion/mapper.js';
 
 import ModelConversionDispatcher from './conversion/modelconversiondispatcher.js';

+ 0 - 2
packages/ckeditor5-engine/src/dataprocessor/basichtmlwriter.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 /**
  * Basic HTML writer, it uses the native `innerHTML` property for basic conversion
  * from DocumentFragment to an HTML string.

+ 0 - 2
packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import BasicHtmlWriter from './basichtmlwriter.js';
 
 /**

+ 1 - 22
packages/ckeditor5-engine/src/editingcontroller.js

@@ -3,14 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import ViewDocument from './view/document.js';
-import MutationObserver from './view/observer/mutationobserver.js';
-import SelectionObserver from './view/observer/selectionobserver.js';
-import FocusObserver from './view/observer/focusobserver.js';
-import KeyObserver from './view/observer/keyobserver.js';
-
 import Mapper from './conversion/mapper.js';
 import ModelConversionDispatcher from './conversion/modelconversiondispatcher.js';
 import { insertText, remove, move } from './conversion/model-to-view-converters.js';
@@ -26,15 +19,7 @@ import EmitterMixin from '../utils/emittermixin.js';
 /**
  * Controller for the editing pipeline. The editing pipeline controls {@link engine.EditingController#model model} rendering,
  * including selection handling. It also creates {@link engine.EditingController#view view document} which build a
- * browser-independent virtualization over the DOM elements. Editing controller also attach default converters and
- * observers.
- *
- * Note that the following observers are attached by the controller and are always available:
- *
- * * {@link view.observer.MutationObserver},
- * * {@link view.observer.SelectionObserver},
- * * {@link view.observer.FocusObserver},
- * * {@link view.observer.KeyObserver}.
+ * browser-independent virtualization over the DOM elements. Editing controller also attach default converters.
  *
  * @memberOf engine
  */
@@ -61,12 +46,6 @@ export default class EditingController {
 		 */
 		this.view = new ViewDocument();
 
-		// Attach default observers.
-		this.view.addObserver( MutationObserver );
-		this.view.addObserver( SelectionObserver );
-		this.view.addObserver( FocusObserver );
-		this.view.addObserver( KeyObserver );
-
 		/**
 		 * Mapper which describes model-view binding.
 		 *

+ 7 - 10
packages/ckeditor5-engine/src/model/batch.js

@@ -3,15 +3,12 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import CKEditorError from '../../utils/ckeditorerror.js';
 
 /**
- * The Batch class groups document changes (deltas). All deltas grouped in a single Batch can be
- * reverted together, so you can think about the Batch as a single undo step. If you want to extend one
- * undo step you can call another method on the same Batch object. If you want to create a separate undo step
- * you can create a new Batch.
+ * `Batch` instance groups document changes ({@link engine.model.Delta deltas}). All deltas grouped in a single `Batch` can be
+ * reverted together, so you can think about `Batch` as of a single undo step. If you want to extend given undo step you
+ * can call another method on the same `Batch` object. If you want to create a separate undo step you can create a new `Batch`.
  *
  * For example to create two separate undo steps you can call:
  *
@@ -32,7 +29,7 @@ import CKEditorError from '../../utils/ckeditorerror.js';
  */
 export default class Batch {
 	/**
-	 * Creates Batch instance. Not recommended to use directly, use {@link engine.model.Document#batch} instead.
+	 * Creates `Batch` instance. Not recommended to use directly, use {@link engine.model.Document#batch} instead.
 	 *
 	 * @param {engine.model.Document} document Document which this Batch changes.
 	 * @param {'transparent'|'default'} [type='default'] Type of the batch.
@@ -105,7 +102,7 @@ export default class Batch {
 }
 
 /**
- * Function to register batch methods. To make code scalable Batch do not have modification
+ * Function to register batch methods. To make code scalable `Batch` do not have modification
  * methods built in. They can be registered using this method.
  *
  * This method checks if there is no naming collision and throws `batch-register-taken` if the method name
@@ -116,10 +113,10 @@ export default class Batch {
  * For example:
  *
  *		Batch.register( 'insert', function( position, nodes ) {
- *			// You can use a class inherit from Delta if that class should handle OT in the special way.
+ *			// You can use a class inheriting from `Delta` if that class should handle OT in a special way.
  *			const delta = new Delta();
  *
- *			// Add delta to the Batch instance. It is important to add delta to batch before applying any operation.
+ *			// Add delta to the Batch instance. It is important to add a delta to the batch before applying any operation.
  *			this.addDelta( delta );
  *
  *			// Create operations which should be components of this delta.

+ 0 - 134
packages/ckeditor5-engine/src/model/characterproxy.js

@@ -1,134 +0,0 @@
-/**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-'use strict';
-
-import Node from './node.js';
-import toMap from '../../utils/tomap.js';
-
-/**
- * A proxy object representing one character stored in the tree data model. It looks and behaves like
- * normal node, but is a read-only structure. This is a representation of the data. Manipulating it won't affect
- * the actual nodes in tree model.
- *
- * Keep in mind that CharacterProxy is static, meaning that it won't change when tree model changes. For example, if you
- * have a {engine.model.Element element} `myEl` containing text `foobar` and then assign `let b = myEl.getChild( 3 )`
- * and then remove all nodes from `myEl`, `b` will still have character `b`, parent `myEl` and offset `3`.
- *
- * CharacterProxy is created on the fly basing on tree model. It is not an explicit node in a tree model but
- * rather represents it. Because of this, it is not advised to store or compare instances of CharacterProxy class.
- * If you want to keep live reference to a point in a text, you should use {@link engine.model.LivePosition}.
- *
- * You should never create an instance of this class by your own. When passing parameters to constructors,
- * use string literals or {@link engine.model.Text} instead.
- *
- * @memberOf engine.model
- * @extends engine.model.Node
- */
-export default class CharacterProxy extends Node {
-	/**
-	 * Creates character node proxy.
-	 *
-	 * @protected
-	 * @param {engine.model.NodeListText} nodeListText Reference to a text object in a node list containing this character.
-	 * @param {Number} index Index of the character in `nodeListText`.
-	 */
-	constructor( nodeListText, index ) {
-		super( nodeListText._attrs );
-
-		/**
-		 * Character represented by this proxy.
-		 *
-		 * @readonly
-		 * @member {String} engine.model.CharacterProxy#character
-		 */
-		this.character = nodeListText.text.substr( index, 1 );
-
-		/**
-		 * @inheritdoc
-		 */
-		this.parent = nodeListText.parent;
-
-		/**
-		 * Reference to a text object in a node list containing this character.
-		 *
-		 * @protected
-		 * @readonly
-		 * @member {engine.model.NodeListText} engine.model.CharacterProxy#_nodeListText
-		 */
-		this._nodeListText = nodeListText;
-
-		/**
-		 * Index of the character in `nodeListText`.
-		 *
-		 * @protected
-		 * @readonly
-		 * @member {Number} engine.model.CharacterProxy#_index
-		 */
-		this._index = index;
-	}
-
-	/**
-	 * Sets attribute on the text fragment. If attribute with the same key already is set, it overwrites its values.
-	 *
-	 * **Note:** Changing attributes of text fragment affects document state. This TextProxy instance properties
-	 * will be refreshed, but other may get invalidated. It is highly unrecommended to store references to TextProxy instances.
-	 *
-	 * @param {String} key Key of attribute to set.
-	 * @param {*} value Attribute value.
-	 */
-	setAttribute( key, value ) {
-		let index = this.getIndex();
-
-		this.parent._children.setAttribute( index, 1, key, value );
-		this._attrs.set( key, value );
-	}
-
-	/**
-	 * Removes all attributes from the character proxy and sets given attributes.
-	 *
-	 * **Note:** Changing attributes of character proxy affects document state. This `CharacterProxy` instance properties
-	 * will be refreshed, but other instances of `CharacterProxy` and `TextProxy` may get invalidated.
-	 * It is highly unrecommended to store references to `CharacterProxy` instances.
-	 *
-	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set. See
-	 * {@link engine.model.CharacterProxy#getAttributes}.
-	 */
-	setAttributesTo( attrs ) {
-		let attrsMap = toMap( attrs );
-
-		this.clearAttributes();
-
-		for ( let attr of attrsMap ) {
-			this.setAttribute( attr[ 0 ], attr[ 1 ] );
-		}
-	}
-
-	/**
-	 * Removes an attribute with given key from the character proxy.
-	 *
-	 * **Note:** Changing attributes of character proxy affects document state. This `CharacterProxy` instance properties
-	 * will be refreshed, but other instances of `CharacterProxy` and `TextProxy` may get invalidated.
-	 * It is highly unrecommended to store references to `CharacterProxy` instances.
-	 *
-	 * @param {String} key Key of attribute to remove.
-	 */
-	removeAttribute( key ) {
-		this.setAttribute( key, null );
-	}
-
-	/**
-	 * Removes all attributes from the character proxy.
-	 *
-	 * **Note:** Changing attributes of character proxy affects document state. This `CharacterProxy` instance properties
-	 * will be refreshed, but other instances of `CharacterProxy` and `TextProxy` may get invalidated.
-	 * It is highly unrecommended to store references to `CharacterProxy` instances.
-	 */
-	clearAttributes() {
-		for ( let attr of this.getAttributes() ) {
-			this.removeAttribute( attr[ 0 ] );
-		}
-	}
-}

+ 0 - 2
packages/ckeditor5-engine/src/model/composer/composer.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import mix from '../../../utils/mix.js';
 import EmitterMixin from '../../../utils/emittermixin.js';
 import deleteContents from './deletecontents.js';

+ 0 - 2
packages/ckeditor5-engine/src/model/composer/deletecontents.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import LivePosition from '../liveposition.js';
 import Position from '../position.js';
 import compareArrays from '../../../utils/comparearrays.js';

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

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Position from '../position.js';
 import TreeWalker from '../treewalker.js';
 import Range from '../range.js';
@@ -13,22 +11,22 @@ import Range from '../range.js';
  * Modifies the selection. Currently the supported modifications are:
  *
  * * Extending. The selection focus is moved in the specified `options.direction` with a step specified in `options.unit`
- * (defaults to `'CHARACTER'`, other values are not not yet supported).
+ * (defaults to `'character'`, other values are not not yet supported).
  * Note: if you extend a forward selection in a backward direction you will in fact shrink it.
  *
  * @method engine.model.composer.modifySelection
  * @param {engine.model.Selection} The selection to modify.
  * @param {Object} [options]
- * @param {'FORWARD'|'BACKWARD'} [options.direction='FORWARD'] The direction in which the selection should be modified.
+ * @param {'forward'|'backward'} [options.direction='forward'] The direction in which the selection should be modified.
  */
 export default function modifySelection( selection, options = {} ) {
-	const isForward = options.direction != 'BACKWARD';
+	const isForward = options.direction != 'backward';
 
 	const focus = selection.focus;
 	const walker = new TreeWalker( {
 		boundaries: getSearchRange( focus, isForward ),
 		singleCharacters: true,
-		direction: isForward ? 'FORWARD' : 'BACKWARD'
+		direction: isForward ? 'forward' : 'backward'
 	} );
 
 	let next = walker.next();
@@ -41,15 +39,15 @@ export default function modifySelection( selection, options = {} ) {
 	let value = next.value;
 
 	// 2. Consume next character.
-	if ( value.type == 'CHARACTER' ) {
+	if ( value.type == 'text' ) {
 		selection.setFocus( value.nextPosition );
 
 		return;
 	}
 
 	// 3. We're entering an element, so let's consume it fully.
-	if ( value.type == ( isForward ? 'ELEMENT_START' : 'ELEMENT_END' ) ) {
-		selection.setFocus( value.item, isForward ? 'AFTER' : 'BEFORE' );
+	if ( value.type == ( isForward ? 'elementStart' : 'elementEnd' ) ) {
+		selection.setFocus( value.item, isForward ? 'after' : 'before' );
 
 		return;
 	}
@@ -67,18 +65,18 @@ export default function modifySelection( selection, options = {} ) {
 
 	// 4.2. Character found after element end. Not really a valid case in our data model, but let's
 	// do something sensible and put the selection focus before that character.
-	if ( value.type == 'CHARACTER' ) {
+	if ( value.type == 'text' ) {
 		selection.setFocus( value.previousPosition );
 	}
 	// 4.3. OK, we're entering a new element. So let's place there the focus.
 	else {
-		selection.setFocus( value.item, isForward ? 0 : 'END' );
+		selection.setFocus( value.item, isForward ? 0 : 'end' );
 	}
 }
 
 function getSearchRange( start, isForward ) {
 	const root = start.root;
-	const searchEnd = Position.createAt( root, isForward ? 'END' : 0 );
+	const searchEnd = Position.createAt( root, isForward ? 'end' : 0 );
 
 	if ( isForward ) {
 		return new Range( start, searchEnd );

+ 33 - 29
packages/ckeditor5-engine/src/model/delta/attributedelta.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Delta from './delta.js';
 import DeltaFactory from './deltafactory.js';
 import { register } from '../batch.js';
@@ -16,7 +14,7 @@ import RootElement from '../rootelement.js';
 import Element from '../element.js';
 
 /**
- * To provide specific OT behavior and better collisions solving, change methods ({@link engine.model.Batch#setAttr}
+ * To provide specific OT behavior and better collisions solving, methods to change attributes ({@link engine.model.Batch#setAttr}
  * and {@link engine.model.Batch#removeAttr}) use `AttributeDelta` class which inherits from the `Delta` class and may
  * overwrite some methods.
  *
@@ -27,6 +25,7 @@ export default class AttributeDelta extends Delta {
 	/**
 	 * The attribute key that is changed by the delta or `null` if the delta has no operations.
 	 *
+	 * @readonly
 	 * @type {String|null}
 	 */
 	get key() {
@@ -36,6 +35,7 @@ export default class AttributeDelta extends Delta {
 	/**
 	 * The attribute value that is set by the delta or `null` if the delta has no operations.
 	 *
+	 * @readonly
 	 * @type {*|null}
 	 */
 	get value() {
@@ -45,6 +45,7 @@ export default class AttributeDelta extends Delta {
 	/**
 	 * The range on which delta operates or `null` if the delta has no operations.
 	 *
+	 * @readonly
 	 * @type {engine.model.Range|null}
 	 */
 	get range() {
@@ -77,13 +78,16 @@ export default class AttributeDelta extends Delta {
 		return 'engine.model.delta.AttributeDelta';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	static get _priority() {
 		return 20;
 	}
 }
 
 /**
- * To provide specific OT behavior and better collisions solving, change methods ({@link engine.model.Batch#setAttr}
+ * To provide specific OT behavior and better collisions solving, methods to change attributes ({@link engine.model.Batch#setAttr}
  * and {@link engine.model.Batch#removeAttr}) use `RootAttributeDelta` class which inherits from the `Delta` class and may
  * overwrite some methods.
  *
@@ -100,62 +104,63 @@ export class RootAttributeDelta extends Delta {
 }
 
 /**
- * Sets the value of the attribute of the node or on the range.
+ * Sets value of the attribute with given key on a {@link engine.model.Item model item} or on a {@link engine.model.Range range}.
  *
  * @chainable
  * @method engine.model.Batch#setAttr
  * @param {String} key Attribute key.
  * @param {*} value Attribute new value.
- * @param {engine.model.Node|engine.model.Range} nodeOrRange Node or range on which the attribute will be set.
+ * @param {engine.model.Item|engine.model.Range} itemOrRange Model item or range on which the attribute will be set.
  */
-register( 'setAttr', function( key, value, nodeOrRange ) {
-	attribute( this, key, value, nodeOrRange );
+register( 'setAttr', function( key, value, itemOrRange ) {
+	attribute( this, key, value, itemOrRange );
 
 	return this;
 } );
 
 /**
- * Removes an attribute from the range.
+ * Removes an attribute with given key from a {@link engine model.Item model item} or from a {@link engine.model.Range range}.
  *
  * @chainable
  * @method engine.model.Batch#removeAttr
  * @param {String} key Attribute key.
- * @param {engine.model.Node|engine.model.Range} nodeOrRange Node or range on which the attribute will be removed.
+ * @param {engine.model.Item|engine.model.Range} itemOrRange Model item or range from which the attribute will be removed.
  */
-register( 'removeAttr', function( key, nodeOrRange ) {
-	attribute( this, key, null, nodeOrRange );
+register( 'removeAttr', function( key, itemOrRange ) {
+	attribute( this, key, null, itemOrRange );
 
 	return this;
 } );
 
-function attribute( batch, key, value, nodeOrRange ) {
-	if ( nodeOrRange instanceof Range ) {
-		changeRange( batch, batch.document, key, value, nodeOrRange );
+function attribute( batch, key, value, itemOrRange ) {
+	if ( itemOrRange instanceof Range ) {
+		changeRange( batch, batch.document, key, value, itemOrRange );
 	} else {
-		changeNode( batch, batch.document, key, value, nodeOrRange );
+		changeItem( batch, batch.document, key, value, itemOrRange );
 	}
 }
 
-function changeNode( batch, doc, key, value, node ) {
-	const previousValue = node.getAttribute( key );
+function changeItem( batch, doc, key, value, item ) {
+	const previousValue = item.getAttribute( key );
 	let range, operation;
 
-	const delta = node instanceof RootElement ? new RootAttributeDelta() : new AttributeDelta();
+	const delta = item instanceof RootElement ? new RootAttributeDelta() : new AttributeDelta();
 	batch.addDelta( delta );
 
 	if ( previousValue != value ) {
-		if ( node instanceof RootElement ) {
+		if ( item instanceof RootElement ) {
 			// If we change attributes of root element, we have to use `RootAttributeOperation`.
-			operation = new RootAttributeOperation( node, key, previousValue, value, doc.version );
+			operation = new RootAttributeOperation( item, key, previousValue, value, doc.version );
 		} else {
-			if ( node instanceof Element ) {
+			if ( item instanceof Element ) {
 				// If we change the attribute of the element, we do not want to change attributes of its children, so
-				// the end on the range can not be put after the closing tag, it should be inside that element with the
-				// offset 0, so the range will contains only the opening tag...
-				range = new Range( Position.createBefore( node ), Position.createFromParentAndOffset( node, 0 ) );
+				// the end of the range cannot be after the closing tag, it should be inside that element, before any of
+				// it's children, so the range will contain only the opening tag.
+				range = new Range( Position.createBefore( item ), Position.createFromParentAndOffset( item, 0 ) );
 			} else {
-				// ...but for characters we can not put the range inside it, so we end the range after that character.
-				range = new Range( Position.createBefore( node ), Position.createAfter( node ) );
+				// If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change
+				// all characters represented by it.
+				range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
 			}
 
 			operation = new AttributeOperation( range, key, previousValue || null, value || null, doc.version );
@@ -166,7 +171,7 @@ function changeNode( batch, doc, key, value, node ) {
 	}
 }
 
-// Because attribute operation needs to have the same attribute value on the whole range, this function split the range
+// Because attribute operation needs to have the same attribute value on the whole range, this function splits the range
 // into smaller parts.
 function changeRange( batch, doc, attributeKey, attributeValue, range ) {
 	const delta = new AttributeDelta();
@@ -217,5 +222,4 @@ function changeRange( batch, doc, attributeKey, attributeValue, range ) {
 }
 
 DeltaFactory.register( AttributeDelta );
-
 DeltaFactory.register( RootAttributeDelta );

+ 0 - 2
packages/ckeditor5-engine/src/model/delta/basic-deltas.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 // Deltas require `register` method that require `Batch` class and is defined in batch-base.js.
 // We would like to group all deltas files in one place, so we would only have to include batch.js
 // which would already have all default deltas registered.

+ 31 - 27
packages/ckeditor5-engine/src/model/delta/basic-transformations.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import { addTransformationCase, defaultTransform } from './transform.js';
 
 import Range from '../range.js';
@@ -64,7 +62,7 @@ addTransformationCase( MoveDelta, MergeDelta, ( a, b, isStrong ) => {
 
 	const operateInSameParent =
 		a.sourcePosition.root == b.position.root &&
-		compareArrays( a.sourcePosition.getParentPath(), b.position.getParentPath() ) === 'SAME';
+		compareArrays( a.sourcePosition.getParentPath(), b.position.getParentPath() ) === 'same';
 
 	const mergeInsideMoveRange = a.sourcePosition.offset <= b.position.offset && a.sourcePosition.offset + a.howMany > b.position.offset;
 
@@ -96,7 +94,7 @@ addTransformationCase( MergeDelta, MoveDelta, ( a, b, isStrong ) => {
 
 	const operateInSameParent =
 		a.position.root == b.sourcePosition.root &&
-		compareArrays( a.position.getParentPath(), b.sourcePosition.getParentPath() ) === 'SAME';
+		compareArrays( a.position.getParentPath(), b.sourcePosition.getParentPath() ) === 'same';
 
 	const mergeInsideMoveRange = b.sourcePosition.offset <= a.position.offset && b.sourcePosition.offset + b.howMany > a.position.offset;
 
@@ -113,7 +111,7 @@ addTransformationCase( SplitDelta, SplitDelta, ( a, b, isStrong ) => {
 	const pathB = b.position.getParentPath();
 
 	// The special case is for splits inside the same parent.
-	if ( compareArrays( pathA, pathB ) == 'SAME' ) {
+	if ( compareArrays( pathA, pathB ) == 'same' ) {
 		if ( a.position.offset == b.position.offset ) {
 			// We are applying split at the position where split already happened. Additional split is not needed.
 			return [ noDelta() ];
@@ -167,7 +165,7 @@ addTransformationCase( SplitDelta, SplitDelta, ( a, b, isStrong ) => {
 addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, isStrong ) => {
 	// If incoming split delta tries to split a node that just got unwrapped, there is actually nothing to split,
 	// so we discard that delta.
-	if ( compareArrays( b.position.path, a.position.getParentPath() ) === 'SAME' ) {
+	if ( compareArrays( b.position.path, a.position.getParentPath() ) === 'same' ) {
 		return [ noDelta() ];
 	}
 
@@ -179,12 +177,12 @@ addTransformationCase( SplitDelta, WrapDelta, ( a, b, isStrong ) => {
 	// If split is applied at the position between wrapped nodes, we cancel the split as it's results may be unexpected and
 	// very weird. Even if we do some "magic" we don't know what really are users' expectations.
 
-	const operateInSameParent = compareArrays( a.position.getParentPath(), b.range.start.getParentPath() ) === 'SAME';
+	const operateInSameParent = compareArrays( a.position.getParentPath(), b.range.start.getParentPath() ) === 'same';
 	const splitInsideWrapRange = b.range.start.offset < a.position.offset && b.range.end.offset >= a.position.offset;
 
 	if ( operateInSameParent && splitInsideWrapRange ) {
 		return [ noDelta() ];
-	} else if ( compareArrays( a.position.getParentPath(), b.range.end.getShiftedBy( -1 ).path ) === 'SAME' ) {
+	} else if ( compareArrays( a.position.getParentPath(), b.range.end.getShiftedBy( -1 ).path ) === 'same' ) {
 		// Split position is directly inside the last node from wrap range.
 		// If that's the case, we manually change split delta so it will "target" inside the wrapping element.
 		// By doing so we will be inserting split node right to the original node which feels natural and is a good UX.
@@ -229,7 +227,7 @@ addTransformationCase( SplitDelta, WrapDelta, ( a, b, isStrong ) => {
 addTransformationCase( UnwrapDelta, SplitDelta, ( a, b, isStrong ) => {
 	// If incoming unwrap delta tries to unwrap node that got split we should unwrap the original node and the split copy.
 	// This can be achieved either by reverting split and applying unwrap to singular node, or creating additional unwrap delta.
-	if ( compareArrays( a.position.path, b.position.getParentPath() ) === 'SAME' ) {
+	if ( compareArrays( a.position.path, b.position.getParentPath() ) === 'same' ) {
 		const transformed = [
 			b.getReversed(),
 			a.clone()
@@ -266,7 +264,7 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, isStrong ) => {
 	// If incoming wrap delta tries to wrap range that contains split position, we have to cancel the split and apply
 	// the wrap. Since split was already applied, we have to revert it.
 
-	const operateInSameParent = compareArrays( a.range.start.getParentPath(), b.position.getParentPath() ) === 'SAME';
+	const operateInSameParent = compareArrays( a.range.start.getParentPath(), b.position.getParentPath() ) === 'same';
 	const splitInsideWrapRange = a.range.start.offset < b.position.offset && a.range.end.offset >= b.position.offset;
 
 	if ( operateInSameParent && splitInsideWrapRange ) {
@@ -274,7 +272,7 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, isStrong ) => {
 			b.getReversed(),
 			a.clone()
 		];
-	} else if ( compareArrays( b.position.getParentPath(), a.range.end.getShiftedBy( -1 ).path ) === 'SAME' ) {
+	} else if ( compareArrays( b.position.getParentPath(), a.range.end.getShiftedBy( -1 ).path ) === 'same' ) {
 		const delta = a.clone();
 
 		// Move wrapping element insert position one node further so it is after the split node insertion.
@@ -296,16 +294,19 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, isStrong ) => {
 // Creates an attribute delta that sets attribute from given `attributeDelta` on nodes from given `weakInsertDelta`.
 function _getComplementaryAttrDelta( weakInsertDelta, attributeDelta ) {
 	const complementaryAttrDelta = new AttributeDelta();
+	const nodes = weakInsertDelta.nodes;
 
 	// At the beginning we store the attribute value from the first node on `weakInsertDelta` node list.
-	let val = weakInsertDelta.nodeList.get( 0 ).getAttribute( attributeDelta.key );
+	let val = nodes.getNode( 0 ).getAttribute( attributeDelta.key );
 
 	// This stores the last index of `weakInsertDelta` node list where the attribute value was different
 	// than in the previous node. We need it to create separate `AttributeOperation`s for nodes with different attributes.
-	let lastIndex = 0;
+	let lastOffset = 0;
+	// Sum of offsets of already processed nodes.
+	let offsetSum = nodes.getNode( 0 ).offsetSize;
 
-	for ( let i = 0; i < weakInsertDelta.nodeList.length; i++ ) {
-		const node = weakInsertDelta.nodeList.get( i );
+	for ( let i = 1; i < nodes.length; i++ ) {
+		const node = nodes.getNode( i );
 		const nodeAttrVal = node.getAttribute( attributeDelta.key );
 
 		// If previous node has different attribute value, we will create an operation to the point before current node.
@@ -314,28 +315,31 @@ function _getComplementaryAttrDelta( weakInsertDelta, attributeDelta ) {
 			// New operation is created only when it is needed. If given node already has proper value for this
 			// attribute we simply skip it without adding a new operation.
 			if ( val != attributeDelta.value ) {
-				const range = new Range( weakInsertDelta.position.getShiftedBy( lastIndex ), weakInsertDelta.position.getShiftedBy( i ) );
-
-				// We don't care about base version because it will be updated after transformations anyway.
-				const attrOperation = new AttributeOperation( range, attributeDelta.key, val, attributeDelta.value, 0 );
-				complementaryAttrDelta.addOperation( attrOperation );
+				addOperation();
 			}
 
 			val = nodeAttrVal;
-			lastIndex = i;
+			lastOffset = offsetSum;
 		}
+
+		offsetSum = offsetSum + node.offsetSize;
 	}
 
 	// At the end we have to add additional `AttributeOperation` for the last part of node list. If all nodes on the
 	// node list had same attributes, this will be the only operation added to the delta.
-	const range = new Range(
-		weakInsertDelta.position.getShiftedBy( lastIndex ),
-		weakInsertDelta.position.getShiftedBy( weakInsertDelta.nodeList.length )
-	);
-
-	complementaryAttrDelta.addOperation( new AttributeOperation( range, attributeDelta.key, val, attributeDelta.value, 0 ) );
+	addOperation();
 
 	return complementaryAttrDelta;
+
+	function addOperation() {
+		const range = new Range(
+			weakInsertDelta.position.getShiftedBy( lastOffset ),
+			weakInsertDelta.position.getShiftedBy( offsetSum )
+		);
+
+		const attrOperation = new AttributeOperation( range, attributeDelta.key, val, attributeDelta.value, 0 );
+		complementaryAttrDelta.addOperation( attrOperation );
+	}
 }
 
 // This is "no-op" delta, it has no type and only no-operation, it basically does nothing.

+ 0 - 3
packages/ckeditor5-engine/src/model/delta/delta.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import clone from '../../../utils/lib/lodash/clone.js';
 import DeltaFactory from './deltafactory.js';
 
@@ -57,7 +55,6 @@ export default class Delta {
 	}
 
 	/**
-	 * @protected
 	 * @param {Number} baseVersion
 	 */
 	set baseVersion( baseVersion ) {

+ 0 - 2
packages/ckeditor5-engine/src/model/delta/deltafactory.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import CKEditorError from '../../../utils/ckeditorerror.js';
 
 import OperationFactory from '../operation/operationfactory.js';

+ 11 - 4
packages/ckeditor5-engine/src/model/delta/insertdelta.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Delta from './delta.js';
 import DeltaFactory from './deltafactory.js';
 import RemoveDelta from './removedelta.js';
@@ -22,6 +20,7 @@ export default class InsertDelta extends Delta {
 	/**
 	 * Position where the delta inserts nodes or `null` if there are no operations in the delta.
 	 *
+	 * @readonly
 	 * @type {engine.model.Position|null}
 	 */
 	get position() {
@@ -31,15 +30,17 @@ export default class InsertDelta extends Delta {
 	/**
 	 * Node list containing all the nodes inserted by the delta or `null` if there are no operations in the delta.
 	 *
+	 * @readonly
 	 * @type {engine.model.NodeList|null}
 	 */
-	get nodeList() {
-		return this._insertOperation ? this._insertOperation.nodeList : null;
+	get nodes() {
+		return this._insertOperation ? this._insertOperation.nodes : null;
 	}
 
 	/**
 	 * Insert operation that is saved in this delta or `null` if there are no operations in the delta.
 	 *
+	 * @readonly
 	 * @protected
 	 * @type {engine.model.operation.InsertOperation|null}
 	 */
@@ -47,6 +48,9 @@ export default class InsertDelta extends Delta {
 		return this.operations[ 0 ] || null;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	get _reverseDeltaClass() {
 		return RemoveDelta;
 	}
@@ -58,6 +62,9 @@ export default class InsertDelta extends Delta {
 		return 'engine.model.delta.InsertDelta';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	static get _priority() {
 		return 20;
 	}

+ 12 - 8
packages/ckeditor5-engine/src/model/delta/mergedelta.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Delta from './delta.js';
 import DeltaFactory from './deltafactory.js';
 import SplitDelta from './splitdelta.js';
@@ -26,12 +24,16 @@ export default class MergeDelta extends Delta {
 	/**
 	 * Position between to merged nodes or `null` if the delta has no operations.
 	 *
+	 * @readonly
 	 * @type {engine.model.Position|null}
 	 */
 	get position() {
 		return this._removeOperation ? this._removeOperation.sourcePosition : null;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	getReversed() {
 		let delta = super.getReversed();
 
@@ -48,6 +50,7 @@ export default class MergeDelta extends Delta {
 	 * this might be an instance of {@link engine.model.operation.MoveOperation} instead of
 	 * {@link engine.model.operation.RemoveOperation}.
 	 *
+	 * @readonly
 	 * @protected
 	 * @type {engine.model.operation.MoveOperation|null}
 	 */
@@ -55,6 +58,9 @@ export default class MergeDelta extends Delta {
 		return this.operations[ 1 ] || null;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	get _reverseDeltaClass() {
 		return SplitDelta;
 	}
@@ -90,8 +96,7 @@ register( 'merge', function( position ) {
 		 *
 		 * @error batch-merge-no-element-before
 		 */
-		throw new CKEditorError(
-			'batch-merge-no-element-before: Node before merge position must be an element.' );
+		throw new CKEditorError( 'batch-merge-no-element-before: Node before merge position must be an element.' );
 	}
 
 	if ( !( nodeAfter instanceof Element ) ) {
@@ -100,14 +105,13 @@ register( 'merge', function( position ) {
 		 *
 		 * @error batch-merge-no-element-after
 		 */
-		throw new CKEditorError(
-			'batch-merge-no-element-after: Node after merge position must be an element.' );
+		throw new CKEditorError( 'batch-merge-no-element-after: Node after merge position must be an element.' );
 	}
 
 	const positionAfter = Position.createFromParentAndOffset( nodeAfter, 0 );
-	const positionBefore = Position.createFromParentAndOffset( nodeBefore, nodeBefore.getChildCount() );
+	const positionBefore = Position.createFromParentAndOffset( nodeBefore, nodeBefore.getMaxOffset() );
 
-	const move = new MoveOperation( positionAfter, nodeAfter.getChildCount(), positionBefore, this.document.version );
+	const move = new MoveOperation( positionAfter, nodeAfter.getMaxOffset(), positionBefore, this.document.version );
 	move.isSticky = true;
 	delta.addOperation( move );
 	this.document.applyOperation( move );

+ 16 - 11
packages/ckeditor5-engine/src/model/delta/movedelta.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Delta from './delta.js';
 import DeltaFactory from './deltafactory.js';
 import { register } from '../batch.js';
@@ -22,7 +20,7 @@ import CKEditorError from '../../../utils/ckeditorerror.js';
  */
 export default class MoveDelta extends Delta {
 	/**
-	 * How many nodes are moved by the delta or `null` if there are no operations in the delta.
+	 * Offset size of moved range or `null` if there are no operations in the delta.
 	 *
 	 * @type {Number|null}
 	 */
@@ -53,7 +51,8 @@ export default class MoveDelta extends Delta {
 	}
 
 	/**
-	 * Move operation that is saved in this delta or `null` if there are no operations in the delta.
+	 * {@link engine.model.delta.MoveDelta#_moveOperation Move operation} that is saved in this delta or `null`
+	 * if there are no operations in the delta.
 	 *
 	 * @protected
 	 * @type {engine.model.operation.MoveOperation|null}
@@ -62,6 +61,9 @@ export default class MoveDelta extends Delta {
 		return this.operations[ 0 ] || null;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	get _reverseDeltaClass() {
 		return MoveDelta;
 	}
@@ -73,6 +75,9 @@ export default class MoveDelta extends Delta {
 		return 'engine.model.delta.MoveDelta';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	static get _priority() {
 		return 20;
 	}
@@ -85,19 +90,19 @@ function addMoveOperation( batch, delta, sourcePosition, howMany, targetPosition
 }
 
 /**
- * Moves given node or given range of nodes to target position.
+ * Moves given {@link engine.model.Item model item} or given range to target position.
  *
  * @chainable
  * @method engine.model.Batch#move
- * @param {engine.model.Node|engine.model.Range} nodeOrRange Node or range of nodes to move.
+ * @param {engine.model.Item|engine.model.Range} itemOrRange Model item or range of nodes to move.
  * @param {engine.model.Position} targetPosition Position where moved nodes will be inserted.
  */
-register( 'move', function( nodeOrRange, targetPosition ) {
+register( 'move', function( itemOrRange, targetPosition ) {
 	const delta = new MoveDelta();
 	this.addDelta( delta );
 
-	if ( nodeOrRange instanceof Range ) {
-		if ( !nodeOrRange.isFlat ) {
+	if ( itemOrRange instanceof Range ) {
+		if ( !itemOrRange.isFlat ) {
 			/**
 			 * Range to move is not flat.
 			 *
@@ -106,9 +111,9 @@ register( 'move', function( nodeOrRange, targetPosition ) {
 			throw new CKEditorError( 'batch-move-range-not-flat: Range to move is not flat.' );
 		}
 
-		addMoveOperation( this, delta, nodeOrRange.start, nodeOrRange.end.offset - nodeOrRange.start.offset, targetPosition );
+		addMoveOperation( this, delta, itemOrRange.start, itemOrRange.end.offset - itemOrRange.start.offset, targetPosition );
 	} else {
-		addMoveOperation( this, delta, Position.createBefore( nodeOrRange ), 1, targetPosition );
+		addMoveOperation( this, delta, Position.createBefore( itemOrRange ), 1, targetPosition );
 	}
 
 	return this;

+ 6 - 8
packages/ckeditor5-engine/src/model/delta/removedelta.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import MoveDelta from './movedelta.js';
 import { register } from '../batch.js';
 import DeltaFactory from './deltafactory.js';
@@ -35,25 +33,25 @@ function addRemoveOperation( batch, delta, position, howMany ) {
 }
 
 /**
- * Removes given node or range of nodes.
+ * Removes given {@link engine.model.Item model item} or given range.
  *
  * @chainable
  * @method engine.model.Batch#remove
- * @param {engine.model.Node|engine.model.Range} nodeOrRange Node or range of nodes to remove.
+ * @param {engine.model.Item|engine.model.Range} itemOrRange Model item or range to remove.
  */
-register( 'remove', function( nodeOrRange ) {
+register( 'remove', function( itemOrRange ) {
 	const delta = new RemoveDelta();
 	this.addDelta( delta );
 
-	if ( nodeOrRange instanceof Range ) {
+	if ( itemOrRange instanceof Range ) {
 		// The array is reversed, so the ranges are correct and do not have to be updated.
-		let ranges = nodeOrRange.getMinimalFlatRanges().reverse();
+		let ranges = itemOrRange.getMinimalFlatRanges().reverse();
 
 		for ( let flat of ranges ) {
 			addRemoveOperation( this, delta, flat.start, flat.end.offset - flat.start.offset );
 		}
 	} else {
-		addRemoveOperation( this, delta, Position.createBefore( nodeOrRange ), 1 );
+		addRemoveOperation( this, delta, Position.createBefore( itemOrRange ), 1 );
 	}
 
 	return this;

+ 17 - 5
packages/ckeditor5-engine/src/model/delta/renamedelta.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Delta from './delta.js';
 import DeltaFactory from './deltafactory.js';
 import { register } from '../batch.js';
@@ -13,6 +11,7 @@ import RemoveOperation from '../operation/removeoperation.js';
 import MoveOperation from '../operation/moveoperation.js';
 import Element from '../element.js';
 import Position from '../position.js';
+import CKEditorError from '../../../utils/ckeditorerror.js';
 
 /**
  * To provide specific OT behavior and better collisions solving, the {@link engine.model.Batch#rename Batch#rename} method
@@ -21,6 +20,9 @@ import Position from '../position.js';
  * @memberOf engine.model.delta
  */
 export default class RenameDelta extends Delta {
+	/**
+	 * @inheritDoc
+	 */
 	get _reverseDeltaClass() {
 		return RenameDelta;
 	}
@@ -34,13 +36,12 @@ export default class RenameDelta extends Delta {
 }
 
 function apply( batch, delta, operation ) {
-	batch.addDelta( delta );
 	delta.addOperation( operation );
 	batch.document.applyOperation( operation );
 }
 
 /**
- * Renames the given element.
+ * Renames given element.
  *
  * @chainable
  * @method engine.model.Batch#rename
@@ -48,7 +49,18 @@ function apply( batch, delta, operation ) {
  * @param {engine.model.Element} element The element to rename.
  */
 register( 'rename', function( newName, element ) {
+	if ( !( element instanceof Element ) ) {
+		/**
+		 * Trying to rename an object which is not an instance of Element.
+		 *
+		 * @error batch-rename-not-element-instance
+		 */
+		throw new CKEditorError( 'batch-rename-not-element-instance: Trying to rename an object which is not an instance of Element.' );
+	}
+
 	const delta = new RenameDelta();
+	this.addDelta( delta );
+
 	const newElement = new Element( newName );
 
 	apply(
@@ -58,7 +70,7 @@ register( 'rename', function( newName, element ) {
 
 	apply(
 		this, delta,
-		new MoveOperation( Position.createAt( element ), element.getChildCount(), Position.createAt( newElement ), this.document.version )
+		new MoveOperation( Position.createAt( element ), element.getMaxOffset(), Position.createAt( newElement ), this.document.version )
 	);
 
 	apply(

+ 20 - 12
packages/ckeditor5-engine/src/model/delta/splitdelta.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Delta from './delta.js';
 import DeltaFactory from './deltafactory.js';
 import { register } from '../batch.js';
@@ -32,6 +30,9 @@ export default class SplitDelta extends Delta {
 		return this._moveOperation ? this._moveOperation.sourcePosition : null;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	getReversed() {
 		let delta = super.getReversed();
 
@@ -43,12 +44,13 @@ export default class SplitDelta extends Delta {
 	}
 
 	/**
-	 * Operation in the delta that adds a node to the tree model where split elements will be moved to or `null` if
+	 * Operation in the delta that adds to model an element into which split nodes will be moved, or `null` if
 	 * there are no operations in the delta.
 	 *
-	 * Most commonly this will be insert operation, as `SplitDelta` has to create a new node. If `SplitDelta` was created
-	 * through {@link engine.model.delta.MergeDelta MergeDelta} {@link engine.model.delta.Delta#getReversed reversing},
-	 * this will be a reinsert operation, as we will want to "insert-back" the node that was removed by `MergeDelta`.
+	 * Most commonly this will be {@link engine.model.operation.InsertOperation an insert operation}, as `SplitDelta`
+	 * has to create a new node. If `SplitDelta` was created through {@link engine.model.delta.Delta#getReversed reversing}
+	 * a {@link engine.model.delta.MergeDelta merge delta}, this will be a {@link engine.model.operation.ReinsertOperation reinsert operation},
+	 * as we will want to re-insert the exact element that was removed by that merge delta.
 	 *
 	 * @protected
 	 * @type {engine.model.operation.InsertOpertaion|engine.model.operation.ReinsertOperation|null}
@@ -58,8 +60,8 @@ export default class SplitDelta extends Delta {
 	}
 
 	/**
-	 * Operation in the delta that moves nodes from after split position to their new parent
-	 * or `null` if there are no operations in the delta.
+	 * Operation in the delta that moves model items, that are after split position, to their new parent or `null`
+	 * if there are no operations in the delta.
 	 *
 	 * @protected
 	 * @type {engine.model.operation.MoveOperation|null}
@@ -68,6 +70,9 @@ export default class SplitDelta extends Delta {
 		return this.operations[ 1 ] || null;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	get _reverseDeltaClass() {
 		return MergeDelta;
 	}
@@ -79,15 +84,18 @@ export default class SplitDelta extends Delta {
 		return 'engine.model.delta.SplitDelta';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	static get _priority() {
 		return 5;
 	}
 }
 
 /**
- * Splits a node at the given position.
+ * Splits an element at the given position.
  *
- * This cannot be a position inside the root element. The `batch-split-root` error will be thrown if
+ * The element cannot be a root element, as root element cannot be split. The `batch-split-root` error will be thrown if
  * you try to split the root element.
  *
  * @chainable
@@ -109,7 +117,7 @@ register( 'split', function( position ) {
 		throw new CKEditorError( 'batch-split-root: Root element can not be split.' );
 	}
 
-	const copy = new Element( splitElement.name, splitElement._attrs );
+	const copy = new Element( splitElement.name, splitElement.getAttributes() );
 
 	const insert = new InsertOperation( Position.createAfter( splitElement ), copy, this.document.version );
 
@@ -118,7 +126,7 @@ register( 'split', function( position ) {
 
 	const move = new MoveOperation(
 		position,
-		splitElement.getChildCount() - position.offset,
+		splitElement.getMaxOffset() - position.offset,
 		Position.createFromParentAndOffset( copy, 0 ),
 		this.document.version
 	);

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

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import operationTransform from '../operation/transform.js';
 import arrayUtils from '../../../utils/lib/lodash/array.js';
 
@@ -62,7 +60,7 @@ function updateBaseVersion( baseVersion, deltas ) {
 /**
  * The default delta transformation function. It is used for those deltas that are not in special case conflict.
  *
- * This algorithm is similar to popular `dOPT` algorithm used in operational transformation, as we are in fact
+ * This algorithm is similar to a popular `dOPT` algorithm used in operational transformation, as we are in fact
  * transforming two sets of operations by each other.
  *
  * @external engine.model.delta.transform

+ 8 - 4
packages/ckeditor5-engine/src/model/delta/unwrapdelta.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Delta from './delta.js';
 import DeltaFactory from './deltafactory.js';
 import WrapDelta from './wrapdelta.js';
@@ -41,6 +39,9 @@ export default class UnwrapDelta extends Delta {
 		return this.operations[ 0 ] || null;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	get _reverseDeltaClass() {
 		return WrapDelta;
 	}
@@ -52,13 +53,16 @@ export default class UnwrapDelta extends Delta {
 		return 'engine.model.delta.UnwrapDelta';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	static get _priority() {
 		return 10;
 	}
 }
 
 /**
- * Unwraps specified element, that is moves all it's children before it and then removes it. Throws
+ * Unwraps given element, that is moves all it's children before it and then removes it. Throws
  * error if you try to unwrap an element that does not have a parent.
  *
  * @chainable
@@ -80,7 +84,7 @@ register( 'unwrap', function( element ) {
 
 	let sourcePosition = Position.createFromParentAndOffset( element, 0 );
 
-	const move = new MoveOperation( sourcePosition, element.getChildCount(), Position.createBefore( element ), this.document.version );
+	const move = new MoveOperation( sourcePosition, element.getMaxOffset(), Position.createBefore( element ), this.document.version );
 	move.isSticky = true;
 	delta.addOperation( move );
 	this.document.applyOperation( move );

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

@@ -3,13 +3,11 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import InsertDelta from './insertdelta.js';
 import { register } from '../batch.js';
 import DeltaFactory from './deltafactory.js';
 import InsertOperation from '../operation/insertoperation.js';
-import NodeList from '../nodelist.js';
+import { normalizeNodes } from './../writer.js';
 
 /**
  * @classdesc
@@ -28,18 +26,18 @@ export default class WeakInsertDelta extends InsertDelta {
 }
 
 /**
- * Inserts a node or nodes at the given position. {@link engine.model.Batch#weakInsert weakInsert} is commonly used for actions
+ * Inserts a node or nodes at given position. {@link engine.model.Batch#weakInsert weakInsert} is commonly used for actions
  * like typing or plain-text paste (without formatting). There are two differences between
  * {@link engine.model.Batch#insert insert} and {@link engine.model.Batch#weakInsert weakInsert}:
  *
  * * When using `weakInsert`, inserted nodes will have same attributes as the current attributes of
  * {@link engine.model.Document#selection document selection}.
- * * Normal behavior is that inserting inside range changed by
- * {@link engine.model.operation.AttributeOperation AttributeOperation} splits
- * the operation into two operations, which "omit" the inserted nodes. The correct behavior for `WeakInsertDelta` is that
+ * * If {@link engine.model.operation.InsertOperation insert operation} position is inside a range changed by
+ * {@link engine.model.operation.AttributeOperation attribute operation}, the attribute operation is split into two operations.
+ * Thanks to this, attribute change "omits" the inserted nodes. The correct behavior for `WeakInsertDelta` is that
  * {@link engine.model.operation.AttributeOperation AttributeOperation} does not "break" and also
- * applies attributes for inserted nodes.
- * The above has to be reflected during {@link engine.model.operation.transform operational transformation}.
+ * applies attributes for inserted nodes. This behavior has to be reflected during
+ * {@link engine.model.delta.transform delta transformation}.
  *
  * @chainable
  * @method engine.model.Batch#weakInsert
@@ -50,10 +48,10 @@ register( 'weakInsert', function( position, nodes ) {
 	const delta = new WeakInsertDelta();
 	this.addDelta( delta );
 
-	nodes = new NodeList( nodes );
+	nodes = normalizeNodes( nodes );
 
-	for ( let node of nodes._nodes ) {
-		node._attrs = new Map( this.document.selection.getAttributes() );
+	for ( let node of nodes ) {
+		node.setAttributesTo( this.document.selection.getAttributes() );
 	}
 
 	const operation = new InsertOperation( position, nodes, this.document.version );

+ 12 - 5
packages/ckeditor5-engine/src/model/delta/wrapdelta.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Delta from './delta.js';
 import DeltaFactory from './deltafactory.js';
 import UnwrapDelta from './unwrapdelta.js';
@@ -36,7 +34,7 @@ export default class WrapDelta extends Delta {
 	}
 
 	/**
-	 * How many nodes is wrapped by the delta or `null` if there are no operations in delta.
+	 * Offset size of range to wrap by the delta or `null` if there are no operations in delta.
 	 *
 	 * @type {Number}
 	 */
@@ -66,22 +64,31 @@ export default class WrapDelta extends Delta {
 		return this.operations[ 1 ] || null;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	get _reverseDeltaClass() {
 		return UnwrapDelta;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	static get className() {
 		return 'engine.model.delta.WrapDelta';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	static get _priority() {
 		return 10;
 	}
 }
 
 /**
- * Wraps given range with given element or with a new element of specified name if string has been passed.
- * **Note:** given range should be a "flat range" (see {@link engine.model.Range#isFlat}). If not, error will be thrown.
+ * Wraps given range with given element or with a new element with specified name, if string has been passed.
+ * **Note:** range to wrap should be a "flat range" (see {@link engine.model.Range#isFlat}). If not, error will be thrown.
  *
  * @chainable
  * @method engine.model.Batch#wrap

+ 6 - 7
packages/ckeditor5-engine/src/model/document.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 // Load all basic deltas and transformations, they register themselves, but they need to be imported somewhere.
 import deltas from './delta/basic-deltas.js'; // jshint ignore:line
 import transformations from './delta/basic-transformations.js'; // jshint ignore:line
@@ -13,12 +11,12 @@ import RootElement from './rootelement.js';
 import Batch from './batch.js';
 import History from './history.js';
 import LiveSelection from './liveselection.js';
-import EmitterMixin from '../../utils/emittermixin.js';
-import CKEditorError from '../../utils/ckeditorerror.js';
-import mix from '../../utils/mix.js';
 import Schema from './schema.js';
 import Composer from './composer/composer.js';
 import clone from '../../utils/lib/lodash/clone.js';
+import EmitterMixin from '../../utils/emittermixin.js';
+import CKEditorError from '../../utils/ckeditorerror.js';
+import mix from '../../utils/mix.js';
 
 const graveyardName = '$graveyard';
 
@@ -28,7 +26,7 @@ const graveyardName = '$graveyard';
  * represented by the separate root.
  *
  * All changes in the document are done by {@link engine.model.operation.Operation operations}. To create operations in
- * the simple way use use the {@link engine.model.Batch} API, for example:
+ * a simple way, use the {@link engine.model.Batch} API, for example:
  *
  *		doc.batch().insert( position, nodes ).split( otherPosition );
  *
@@ -38,7 +36,8 @@ const graveyardName = '$graveyard';
  */
 export default class Document {
 	/**
-	 * Creates an empty document instance with no {@link engine.model.Document#roots} (other than graveyard).
+	 * Creates an empty document instance with no {@link engine.model.Document#roots} (other than
+	 * a {@link engine.model.Document#graveyard graveyard root}).
 	 */
 	constructor() {
 		/**

+ 145 - 44
packages/ckeditor5-engine/src/model/documentfragment.js

@@ -3,28 +3,29 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import NodeList from './nodelist.js';
+import Element from './element.js';
+import Text from './text.js';
+import isIterable from '../../utils/isiterable.js';
 
 /**
- * DocumentFragment represents a part of Tree Model which does not have a common root but it's top level nodes
- * can be seen as siblings.
+ * DocumentFragment represents a part of model which does not have a common root but it's top-level nodes
+ * can be seen as siblings. In other words, it is a detached part of model tree, without a root.
  *
  * @memberOf engine.model
  */
 export default class DocumentFragment {
 	/**
-	 * Creates empty DocumentFragment.
+	 * Creates an empty `DocumentFragment`.
 	 *
-	 * @param {engine.model.NodeSet} children List of nodes contained inside the DocumentFragment.
+	 * @param {engine.model.Node|Iterable.<engine.model.Node>} children Nodes to be contained inside the `DocumentFragment`.
 	 */
 	constructor( children ) {
 		/**
-		 * List of nodes contained inside the DocumentFragment.
+		 * List of nodes contained inside the document fragment.
 		 *
-		 * @protected
-		 * @member {engine.model.NodeSet} engine.model.DocumentFragment#_children
+		 * @private
+		 * @member {engine.model.NodeList} engine.model.DocumentFragment#_children
 		 */
 		this._children = new NodeList();
 
@@ -34,14 +35,16 @@ export default class DocumentFragment {
 	}
 
 	/**
-	 * `DocumentFragment` iterator. Returns {@link engine.model.Node nodes} that are added to the `DocumentFragment`.
+	 * Returns an iterator that iterates over all nodes contained inside this document fragment.
+	 *
+	 * @returns {Iterator.<engine.model.Node>}
 	 */
 	[ Symbol.iterator ]() {
-		return this._children[ Symbol.iterator ]();
+		return this.getChildren();
 	}
 
 	/**
-	 * The root of `DocumentFragment`. Returns itself. Added for compatibility reasons with {@link engine.model.Element}.
+	 * Artificial root of `DocumentFragment`. Returns itself. Added for compatibility reasons.
 	 *
 	 * @readonly
 	 * @type {engine.model.DocumentFragment}
@@ -51,88 +54,186 @@ export default class DocumentFragment {
 	}
 
 	/**
-	 * Returns path to the `DocumentFragment` This is always equal to empty array and is added for compatibility reasons
-	 * with {@link engine.model.Element}.
+	 * Returns path to a `DocumentFragment`, which is an empty array. Added for compatibility reasons.
 	 *
-	 * @returns {Array} The path.
+	 * @returns {Array}
 	 */
 	getPath() {
 		return [];
 	}
 
 	/**
-	 * Gets child at the given index.
+	 * Returns `true` if there are no nodes inside this document fragment, `false` otherwise.
+	 *
+	 * @returns {Boolean}
+	 */
+	isEmpty() {
+		return this.getChildCount() === 0;
+	}
+
+	/**
+	 * Gets the child at the given index. Returns `null` if incorrect index was passed.
 	 *
 	 * @param {Number} index Index of child.
-	 * @returns {engine.model.Node} Child node.
+	 * @returns {engine.model.Node|null} Child node.
 	 */
 	getChild( index ) {
-		return this._children.get( index );
+		return this._children.getNode( index );
+	}
+
+	/**
+	 * Returns an index of the given child node. Returns `null` if given node is not a child of this document fragment.
+	 *
+	 * @param {engine.model.Node} node Child node to look for.
+	 * @returns {Number|null} Child node's index.
+	 */
+	getChildIndex( node ) {
+		return this._children.getNodeIndex( node );
+	}
+
+	/**
+	 * Returns an iterator that iterates over all of this document fragment's children.
+	 *
+	 * @returns {Iterable.<engine.model.Node>}
+	 */
+	getChildren() {
+		return this._children[ Symbol.iterator ]();
 	}
 
 	/**
-	 * Gets the number of top-level elements of DocumentFragment.
+	 * Returns the number of this document fragment's children.
 	 *
-	 * @returns {Number} The number of top-level elements.
+	 * @returns {Number}
 	 */
 	getChildCount() {
 		return this._children.length;
 	}
 
 	/**
-	 * Gets index of the given child node.
+	 * Returns the starting offset of given child. Starting offset is equal to the sum of
+	 * {engine.model.Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if
+	 * given node is not a child of this document fragment.
 	 *
-	 * @param {engine.model.Node} node Child node.
-	 * @returns {Number} Index of the child node.
+	 * @param {engine.model.Node} node Child node to look for.
+	 * @returns {Number|null} Child node's starting offset.
 	 */
-	getChildIndex( node ) {
-		return this._children.indexOf( node );
+	getChildStartOffset( node ) {
+		return this._children.getNodeStartOffset( node );
 	}
 
 	/**
-	 * Inserts a child node or a list of child nodes at the end of this DocumentFragment.
+	 * Returns the sum of {engine.model.Node#offsetSize offset sizes} of all of this document fragment's children.
 	 *
-	 * @param {engine.model.NodeSet} nodes The list of nodes to be inserted.
+	 * @returns {Number}
+	 */
+	getMaxOffset() {
+		return this._children.totalOffset;
+	}
+
+	/**
+	 * Converts offset "position" to index "position".
+	 *
+	 * Returns index of a node that occupies given offset. If given offset is too low, returns `0`. If given offset is
+	 * too high, returns {@link engine.model.DocumentFragment#getChildCount index after last child}.
+	 *
+	 *		const textNode = new Text( 'foo' );
+	 *		const pElement = new Element( 'p' );
+	 *		const docFrag = new DocumentFragment( [ textNode, pElement ] );
+	 *		docFrag.offsetToIndex( -1 ); // Returns 0, because offset is too low.
+	 *		docFrag.offsetToIndex( 0 ); // Returns 0, because offset 0 is taken by `textNode` which is at index 0.
+	 *		docFrag.offsetToIndex( 1 ); // Returns 0, because `textNode` has `offsetSize` equal to 3, so it occupies offset 1 too.
+	 *		docFrag.offsetToIndex( 2 ); // Returns 0.
+	 *		docFrag.offsetToIndex( 3 ); // Returns 1.
+	 *		docFrag.offsetToIndex( 4 ); // Returns 2. There are no nodes at offset 4, so last available index is returned.
+	 *
+	 * @param {Number} offset Offset to look for.
+	 * @returns {Number} Index of a node that occupies given offset.
+	 */
+	offsetToIndex( offset ) {
+		return this._children.offsetToIndex( offset );
+	}
+
+	/**
+	 * {@link engine.model.DocumentFragment#insertChildren Inserts} one or more nodes at the end of this document fragment.
+	 *
+	 * @param {engine.model.Node|Iterable.<engine.model.Node>} nodes Nodes to be inserted.
 	 */
 	appendChildren( nodes ) {
 		this.insertChildren( this.getChildCount(), nodes );
 	}
 
 	/**
-	 * Inserts a list of child nodes on the given index and sets the parent of these nodes to this DocumentFragment.
+	 * Inserts one or more nodes at the given index and sets {@link engine.model.Node#parent parent} of these nodes
+	 * to this document fragment.
 	 *
-	 * @param {Number} index Position where nodes should be inserted.
-	 * @param {engine.model.NodeSet} nodes The list of nodes to be inserted.
+	 * @param {Number} index Index at which nodes should be inserted.
+	 * @param {engine.model.Node|Iterable.<engine.model.Node>} nodes Nodes to be inserted.
 	 */
 	insertChildren( index, nodes ) {
-		let nodeList = new NodeList( nodes );
-
-		for ( let node of nodeList._nodes ) {
-			node.parent = this;
+		if ( !isIterable( nodes ) ) {
+			nodes = [ nodes ];
 		}
 
-		// Clean original DocumentFragment so it won't contain nodes that were added somewhere else.
-		if ( nodes instanceof DocumentFragment ) {
-			nodes._children = new NodeList();
+		for ( let node of nodes ) {
+			node.parent = this;
 		}
 
-		this._children.insert( index, nodeList );
+		this._children.insertNodes( index, nodes );
 	}
 
 	/**
-	 * Removes number of child nodes starting at the given index and set the parent of these nodes to `null`.
+	 * Removes one or more nodes starting at the given index and sets {@link engine.model.Node#parent parent} of these nodes to `null`.
 	 *
-	 * @param {Number} index Position of the first node to remove.
+	 * @param {Number} index Index of the first node to remove.
 	 * @param {Number} [howMany=1] Number of nodes to remove.
-	 * @returns {engine.model.NodeList} The list of removed nodes.
+	 * @returns {Array.<engine.model.Node>} Array containing removed nodes.
 	 */
 	removeChildren( index, howMany = 1 ) {
-		let nodeList = this._children.remove( index, howMany );
+		const nodes = this._children.removeNodes( index, howMany );
 
-		for ( let node of nodeList._nodes ) {
+		for ( let node of nodes ) {
 			node.parent = null;
 		}
 
-		return nodeList;
+		return nodes;
+	}
+
+	/**
+	 * Converts `DocumentFragment` instance to plain object and returns it.
+	 * Takes care of converting all of this document fragment's children.
+	 *
+	 * @returns {Object} `DocumentFragment` instance converted to plain object.
+	 */
+	toJSON() {
+		let json = [];
+
+		for ( let node of this._children ) {
+			json.push( node.toJSON() );
+		}
+
+		return json;
+	}
+
+	/**
+	 * Creates a `DocumentFragment` instance from given plain object (i.e. parsed JSON string).
+	 * Converts `DocumentFragment` children to proper nodes.
+	 *
+	 * @param {Object} json Plain object to be converted to `DocumentFragment`.
+	 * @returns {engine.model.DocumentFragment} `DocumentFragment` instance created using given plain object.
+	 */
+	static fromJSON( json ) {
+		let children = [];
+
+		for ( let child of json ) {
+			if ( child.name ) {
+				// If child has name property, it is an Element.
+				children.push( Element.fromJSON( child ) );
+			} else {
+				// Otherwise, it is a Text node.
+				children.push( Text.fromJSON( child ) );
+			}
+		}
+
+		return new DocumentFragment( children );
 	}
 }

+ 128 - 114
packages/ckeditor5-engine/src/model/element.js

@@ -3,27 +3,27 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Node from './node.js';
 import NodeList from './nodelist.js';
-import DocumentFragment from './documentfragment.js';
-import Range from './range.js';
-import toMap from '../../utils/tomap.js';
+import Text from './text.js';
+import isIterable from '../../utils/isiterable.js';
 
 /**
- * Tree data model element.
+ * Model element. Type of {@link engine.model.Node node} that has a {@link engine.model.Element#name name} and
+ * {@link engine.model.Element#getChildren child nodes}.
+ *
+ * **Important**: see {@link engine.model.Node} to read about restrictions using `Element` and `Node` API.
  *
  * @memberOf engine.model
  */
 export default class Element extends Node {
 	/**
-	 * Creates a tree data model element.
+	 * Creates a model element.
 	 *
-	 * @param {String} name Node name.
-	 * @param {Iterable} [attrs] Iterable collection of attributes.
-	 * @param {engine.model.NodeSet} [children] List of nodes to be inserted.
-	 * into created element. List of nodes can be of any type accepted by the {@link engine.model.NodeList} constructor.
+	 * @param {String} name Element's name.
+	 * @param {Object} [attrs] Element's attributes. See {@link utils.toMap} for a list of accepted values.
+	 * @param {engine.model.Node|Iterable.<engine.model.Node>} [children] One or more nodes to be inserted as children of
+	 * created element.
 	 */
 	constructor( name, attrs, children ) {
 		super( attrs );
@@ -39,7 +39,7 @@ export default class Element extends Node {
 		/**
 		 * List of children nodes.
 		 *
-		 * @protected
+		 * @private
 		 * @member {engine.model.NodeList} engine.model.Element#_children
 		 */
 		this._children = new NodeList();
@@ -50,185 +50,199 @@ export default class Element extends Node {
 	}
 
 	/**
-	 * Gets child at the given index.
+	 * Creates a copy of this element and returns it. Created element has same name and attributes as original element.
+	 * If clone is not deep, children of copied element are references to the same nodes as in original element.
+	 * If clone is deep, original element's children are also cloned.
 	 *
-	 * @param {Number} index Index of child.
-	 * @returns {engine.model.Node} Child node.
+	 * @param {Boolean} [deep=false] Decides whether children of this element should also be cloned (`true`) or not (`false`).
 	 */
-	getChild( index ) {
-		return this._children.get( index );
+	clone( deep = false ) {
+		const children = deep ?
+			Array.from( this._children ).map( ( node ) => node.clone() ) :
+			Array.from( this._children );
+
+		return new Element( this.name, this.getAttributes(), children );
 	}
 
 	/**
-	 * Gets the number of element's children.
+	 * Returns `true` if there are no nodes inside this element, `false` otherwise.
 	 *
-	 * @returns {Number} The number of element's children.
+	 * @returns {Boolean}
 	 */
-	getChildCount() {
-		return this._children.length;
+	isEmpty() {
+		return this.getChildCount() === 0;
 	}
 
 	/**
-	 * Gets index of the given child node.
+	 * Gets the child at the given index.
 	 *
-	 * @param {engine.model.Node} node Child node.
-	 * @returns {Number} Index of the child node.
+	 * @param {Number} index Index of child.
+	 * @returns {engine.model.Node} Child node.
 	 */
-	getChildIndex( node ) {
-		return this._children.indexOf( node );
+	getChild( index ) {
+		return this._children.getNode( index );
 	}
 
 	/**
-	 * {@link engine.model.Element#insert Inserts} a child node or a list of child nodes at the end of this node and sets
-	 * the parent of these nodes to this element.
-	 *
-	 * Note that the list of children can be modified only in elements not yet attached to the document.
-	 * All attached nodes should be modified using the {@link engine.model.operation.InsertOperation}.
+	 * Returns an index of the given child node. Returns `null` if given node is not a child of this element.
 	 *
-	 * @param {engine.model.NodeSet} nodes The list of nodes to be inserted.
+	 * @param {engine.model.Node} node Child node to look for.
+	 * @returns {Number} Child node's index in this element.
 	 */
-	appendChildren( nodes ) {
-		this.insertChildren( this.getChildCount(), nodes );
+	getChildIndex( node ) {
+		return this._children.getNodeIndex( node );
 	}
 
 	/**
-	 * Inserts a list of child nodes on the given index and sets the parent of these nodes to this element.
-	 *
-	 * Note that the list of children can be modified only in elements not yet attached to the document.
-	 * All attached nodes should be modified using the {@link engine.model.operation.InsertOperation}.
+	 * Returns an iterator that iterates over all of this element's children.
 	 *
-	 * @param {Number} index Position where nodes should be inserted.
-	 * @param {engine.model.NodeSet} nodes The list of nodes to be inserted.
+	 * @returns {Iterable.<engine.model.Node>}
 	 */
-	insertChildren( index, nodes ) {
-		let nodeList = new NodeList( nodes );
-
-		for ( let node of nodeList._nodes ) {
-			node.parent = this;
-		}
-
-		// Clean original DocumentFragment so it won't contain nodes that were added somewhere else.
-		if ( nodes instanceof DocumentFragment ) {
-			nodes._children = new NodeList();
-		}
-
-		this._children.insert( index, nodeList );
+	getChildren() {
+		return this._children[ Symbol.iterator ]();
 	}
 
 	/**
-	 * Removes number of child nodes starting at the given index and set the parent of these nodes to `null`.
-	 *
-	 * Note that the list of children can be modified only in elements not yet attached to the document.
-	 * All attached nodes should be modified using the {@link engine.model.operation.RemoveOperation}.
+	 * Returns the number of this element's children.
 	 *
-	 * @param {Number} index Position of the first node to remove.
-	 * @param {Number} [howMany=1] Number of nodes to remove.
-	 * @returns {engine.model.NodeList} The list of removed nodes.
+	 * @returns {Number}
 	 */
-	removeChildren( index, howMany = 1 ) {
-		let nodeList = this._children.remove( index, howMany );
-
-		for ( let node of nodeList._nodes ) {
-			node.parent = null;
-		}
-
-		return nodeList;
+	getChildCount() {
+		return this._children.length;
 	}
 
 	/**
-	 * Sets attribute on the element. If attribute with the same key already is set, it overwrites its value.
+	 * Returns the starting offset of given child. Starting offset is equal to the sum of
+	 * {engine.model.Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if
+	 * given node is not a child of this element.
 	 *
-	 * @param {String} key Key of attribute to set.
-	 * @param {*} value Attribute value.
+	 * @param {engine.model.Node} node Child node to look for.
+	 * @returns {Number} Child node's starting offset.
 	 */
-	setAttribute( key, value ) {
-		this._attrs.set( key, value );
+	getChildStartOffset( node ) {
+		return this._children.getNodeStartOffset( node );
 	}
 
 	/**
-	 * Removes all attributes from the element and sets given attributes.
+	 * Returns the sum of {engine.model.Node#offsetSize offset sizes} of all of this element's children.
 	 *
-	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set. See {@link engine.model.Node#getAttributes}.
+	 * @returns {Number}
 	 */
-	setAttributesTo( attrs ) {
-		this._attrs = toMap( attrs );
+	getMaxOffset() {
+		return this._children.totalOffset;
 	}
 
 	/**
-	 * Removes an attribute with given key from the element.
-	 *
-	 * @param {String} key Key of attribute to remove.
-	 * @returns {Boolean} `true` if the attribute was set on the element, `false` otherwise.
+	 * Returns index of a node that occupies given offset. If given offset is too low, returns `0`. If given offset is
+	 * too high, returns {@link engine.model.Element#getChildCount index after last child}.
+	 *
+	 *		const textNode = new Text( 'foo' );
+	 *		const pElement = new Element( 'p' );
+	 *		const divElement = new Element( [ textNode, pElement ] );
+	 *		divElement.offsetToIndex( -1 ); // Returns 0, because offset is too low.
+	 *		divElement.offsetToIndex( 0 ); // Returns 0, because offset 0 is taken by `textNode` which is at index 0.
+	 *		divElement.offsetToIndex( 1 ); // Returns 0, because `textNode` has `offsetSize` equal to 3, so it occupies offset 1 too.
+	 *		divElement.offsetToIndex( 2 ); // Returns 0.
+	 *		divElement.offsetToIndex( 3 ); // Returns 1.
+	 *		divElement.offsetToIndex( 4 ); // Returns 2. There are no nodes at offset 4, so last available index is returned.
+	 *
+	 * @param {Number} offset Offset to look for.
+	 * @returns {Number}
 	 */
-	removeAttribute( key ) {
-		return this._attrs.delete( key );
+	offsetToIndex( offset ) {
+		return this._children.offsetToIndex( offset );
 	}
 
 	/**
-	 * Removes all attributes from the element.
+	 * {@link engine.model.Element#insertChildren Inserts} one or more nodes at the end of this element.
+	 *
+	 * @param {engine.model.Node|Iterable.<engine.model.Node>} nodes Nodes to be inserted.
 	 */
-	clearAttributes() {
-		this._attrs.clear();
+	appendChildren( nodes ) {
+		this.insertChildren( this.getChildCount(), nodes );
 	}
 
 	/**
-	 * Checks whether element is empty (has no children).
+	 * Inserts one or more nodes at the given index and sets {@link engine.model.Node#parent parent} of these nodes
+	 * to this element.
 	 *
-	 * @returns {Boolean}
+	 * @param {Number} index Index at which nodes should be inserted.
+	 * @param {engine.model.Node|Iterable.<engine.model.Node>} nodes Nodes to be inserted.
 	 */
-	isEmpty() {
-		return this.getChildCount() === 0;
+	insertChildren( index, nodes ) {
+		if ( !isIterable( nodes ) ) {
+			nodes = [ nodes ];
+		}
+
+		for ( let node of nodes ) {
+			node.parent = this;
+		}
+
+		this._children.insertNodes( index, nodes );
 	}
 
 	/**
-	 * Gets the text content of the element. The return value is created by concatenating all
-	 * text nodes in this element and its descendants.
+	 * Removes one or more nodes starting at the given index and sets {@link engine.model.Node#parent parent} of these nodes to `null`.
 	 *
-	 * @returns {String}
+	 * @param {Number} index Index of the first node to remove.
+	 * @param {Number} [howMany=1] Number of nodes to remove.
+	 * @returns {Array.<engine.model.Node>} Array containing removed nodes.
 	 */
-	getText() {
-		let text = '';
+	removeChildren( index, howMany = 1 ) {
+		const nodes = this._children.removeNodes( index, howMany );
 
-		for ( let value of Range.createFromElement( this ) ) {
-			if ( value.type == 'TEXT' ) {
-				text += value.item.text;
-			}
+		for ( let node of nodes ) {
+			node.parent = null;
 		}
 
-		return text;
+		return nodes;
 	}
 
 	/**
-	 * Custom toJSON method to solve child-parent circular dependencies.
+	 * Converts `Element` instance to plain object and returns it. Takes care of converting all of this element's children.
 	 *
-	 * @returns {Object} Clone of this object with the parent property replaced with its name.
+	 * @returns {Object} `Element` instance converted to plain object.
 	 */
 	toJSON() {
 		let json = super.toJSON();
 
-		if ( this._children.length ) {
-			json.children = this._children.toJSON();
-		}
-
 		json.name = this.name;
 
+		if ( this._children.length > 0 ) {
+			json.children = [];
+
+			for ( let node of this._children ) {
+				json.children.push( node.toJSON() );
+			}
+		}
+
 		return json;
 	}
 
 	/**
-	 * Creates Element object from deserilized object, ie. from parsed JSON string.
+	 * Creates an `Element` instance from given plain object (i.e. parsed JSON string).
+	 * Converts `Element` children to proper nodes.
 	 *
-	 *		let deserialized = JSON.parse( JSON.stringify( someElementObject ) );
-	 *		let element = NodeList.fromJSON( deserialized );
-	 *
-	 * @param {Object} json
-	 * @returns {engine.model.Element}
+	 * @param {Object} json Plain object to be converted to `Element`.
+	 * @returns {engine.model.Element} `Element` instance created using given plain object.
 	 */
 	static fromJSON( json ) {
+		let children = null;
+
 		if ( json.children ) {
-			return new Element( json.name, json.attributes, NodeList.fromJSON( json.children ) );
+			children = [];
+
+			for ( let child of json.children ) {
+				if ( child.name ) {
+					// If child has name property, it is an Element.
+					children.push( Element.fromJSON( child ) );
+				} else {
+					// Otherwise, it is a Text node.
+					children.push( Text.fromJSON( child ) );
+				}
+			}
 		}
 
-		return new Element( json.name, json.attributes );
+		return new Element( json.name, json.attributes, children );
 	}
 }

+ 3 - 5
packages/ckeditor5-engine/src/model/history.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import CKEditorError from '../../utils/ckeditorerror.js';
 
 /**
@@ -14,7 +12,7 @@ import CKEditorError from '../../utils/ckeditorerror.js';
  *
  * **Note:** deltas kept in `History` should be used only to transform deltas. It's not advised to use `History` to get
  * original delta basing on it's {@link engine.model.delta.Delta#baseVersion baseVersion}. Also, after transforming a
- * delta by deltas from `History`, fix it's base version accordingly (set to {@link engine.model.Document#version}.
+ * delta by deltas from `History`, fix it's base version accordingly (set it to {@link engine.model.Document#version document version}).
  *
  * @memberOf engine.model
  */
@@ -58,7 +56,7 @@ export default class History {
 	/**
 	 * Returns deltas added to the history.
 	 *
-	 * @param {Number} [from=0] Base version from which deltas should be returned (inclusive). Defaults to `0` which means
+	 * @param {Number} [from=0] Base version from which deltas should be returned (inclusive). Defaults to `0`, which means
 	 * that deltas from the first one will be returned.
 	 * @param {Number} [to=Number.POSITIVE_INFINITY] Base version up to which deltas should be returned (exclusive).
 	 * Defaults to `Number.POSITIVE_INFINITY` which means that deltas up to the last one will be returned.
@@ -145,7 +143,7 @@ export default class History {
 	 * **Note:** if delta with `baseVersion` was already updated by multiple deltas, all updated deltas will be removed
 	 * and new deltas will be inserted at their position.
 	 *
-	 * **Note:** delta marked as reversed won't get updated.
+	 * **Note:** removed delta won't get updated.
 	 *
 	 * @param {Number} baseVersion Base version of a delta to update.
 	 * @param {Iterable.<engine.model.delta.Delta>} updatedDeltas Deltas to be inserted in place of updated delta.

+ 17 - 16
packages/ckeditor5-engine/src/model/liveposition.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import RootElement from './rootelement.js';
 import Position from './position.js';
 import Range from './range.js';
@@ -13,12 +11,14 @@ import mix from '../../utils/mix.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 
 /**
- * LivePosition is a position in {@link engine.model.Document Document} that updates itself as Document is changed
- * through operations. It may be used as a bookmark in the Document.
- * **Note:** Contrary to {@link engine.model.Position}, LivePosition works only in roots that are
+ * `LivePosition` is a type of {@link engine.model.Position Position} that updates itself as {@link engine.model.Document document}
+ * is changed through operations. It may be used as a bookmark.
+ *
+ * **Note:** Contrary to {@link engine.model.Position}, `LivePosition` works only in roots that are
  * {@link engine.model.RootElement}. If {@link engine.model.DocumentFragment} is passed, error will be thrown.
- * **Note:** Be very careful when dealing with LivePosition. Each LivePosition instance bind events that might
- * have to be unbound. Use {@link engine.model.LivePosition#detach} whenever you don't need LivePosition anymore.
+ *
+ * **Note:** Be very careful when dealing with `LivePosition`. Each `LivePosition` instance bind events that might
+ * have to be unbound. Use {@link engine.model.LivePosition#detach} whenever you don't need `LivePosition` anymore.
  *
  * @memberOf engine.model
  * @extends engine.model.Position
@@ -46,10 +46,10 @@ export default class LivePosition extends Position {
 		super( root, path );
 
 		/**
-		 * Flag representing LivePosition stickiness. LivePosition might be sticking to previous node or next node.
-		 * Whenever some nodes are inserted at the same position as LivePosition, `stickiness` is checked to decide if
+		 * Flag representing `LivePosition` stickiness. `LivePosition` might be sticking to previous node or next node.
+		 * Whenever some nodes are inserted at the same position as `LivePosition`, `stickiness` is checked to decide if
 		 * LivePosition should be moved. Similar applies when a range of nodes is moved and one of it's boundary
-		 * position is same as LivePosition.
+		 * position is same as `LivePosition`.
 		 *
 		 * Examples:
 		 *
@@ -71,7 +71,7 @@ export default class LivePosition extends Position {
 	}
 
 	/**
-	 * Unbinds all events previously bound by LivePosition. Use it whenever you don't need LivePosition instance
+	 * Unbinds all events previously bound by `LivePosition`. Use it whenever you don't need `LivePosition` instance
 	 * anymore (i.e. when leaving scope in which it was declared or before re-assigning variable that was
 	 * referring to it).
 	 */
@@ -114,7 +114,8 @@ export default class LivePosition extends Position {
 }
 
 /**
- * Binds this LivePosition to the {@link engine.model.Document} that owns this position {@link engine.model.RootElement root}.
+ * Binds this `LivePosition` to the {@link engine.model.Document document} that owns
+ * this position's {@link engine.model.Position#root root}.
  *
  * @ignore
  * @private
@@ -134,7 +135,7 @@ function bindWithDocument() {
 }
 
 /**
- * Updates this position accordingly to the updates applied to the Tree Model. Bases on change events.
+ * Updates this position accordingly to the updates applied to the model. Bases on change events.
  *
  * @ignore
  * @private
@@ -152,7 +153,7 @@ function transform( type, range, position ) {
 	switch ( type ) {
 		case 'insert':
 			let insertBefore = this.stickiness == 'STICKS_TO_NEXT';
-			transformed = this.getTransformedByInsertion( range.start, howMany, insertBefore );
+			transformed = this._getTransformedByInsertion( range.start, howMany, insertBefore );
 			break;
 
 		case 'move':
@@ -164,12 +165,12 @@ function transform( type, range, position ) {
 				( originalRange.start.isEqual( this ) && this.stickiness == 'STICKS_TO_NEXT' ) ||
 				( originalRange.end.isEqual( this ) && this.stickiness == 'STICKS_TO_PREVIOUS' );
 
-			// We can't use .getTransformedByMove() because we have a different if-condition.
+			// We can't use ._getTransformedByMove() because we have a different if-condition.
 			if ( gotMoved ) {
 				transformed = this._getCombined( position, range.start );
 			} else {
 				let insertBefore = this.stickiness == 'STICKS_TO_NEXT';
-				transformed = this.getTransformedByMove( position, range.start, howMany, insertBefore );
+				transformed = this._getTransformedByMove( position, range.start, howMany, insertBefore );
 			}
 			break;
 		default:

+ 13 - 16
packages/ckeditor5-engine/src/model/liverange.js

@@ -3,14 +3,13 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Range from './range.js';
 import EmitterMixin from '../../utils/emittermixin.js';
 import mix from '../../utils/mix.js';
 
 /**
- * LiveRange is a Range in the Tree Model that updates itself as the tree changes. It may be used as a bookmark.
+ * `LiveRange` is a type of {@link engine.model.Range Range} that updates itself as {@link engine.model.Document document}
+ * is changed through operations. It may be used as a bookmark.
  *
  * **Note:** Be very careful when dealing with `LiveRange`. Each `LiveRange` instance bind events that might
  * have to be unbound. Use {@link engine.model.LiveRange#detach detach} whenever you don't need `LiveRange` anymore.
@@ -30,7 +29,7 @@ export default class LiveRange extends Range {
 	}
 
 	/**
-	 * Unbinds all events previously bound by LiveRange. Use it whenever you don't need LiveRange instance
+	 * Unbinds all events previously bound by `LiveRange`. Use it whenever you don't need `LiveRange` instance
 	 * anymore (i.e. when leaving scope in which it was declared or before re-assigning variable that was
 	 * referring to it).
 	 */
@@ -76,7 +75,7 @@ export default class LiveRange extends Range {
 }
 
 /**
- * Binds this LiveRange to the {@link engine.model.Document} that owns this range.
+ * Binds this `LiveRange` to the {@link engine.model.Document document} that owns this range's {@link engine.model.Range#root root}.
  *
  * @ignore
  * @private
@@ -89,32 +88,30 @@ function bindWithDocument() {
 		this.root.document,
 		'change',
 		( event, type, changes ) => {
-			fixBoundaries.call( this, type, changes.range, changes.sourcePosition );
+			transform.call( this, type, changes.range, changes.sourcePosition );
 		},
 		this
 	);
 }
 
 /**
- * LiveRange boundaries are instances of {@link engine.model.LivePosition}, so it is updated thanks to them. This method
- * additionally fixes the results of updating live positions taking into account that those live positions
- * are boundaries of a range. An example case for fixing live positions is end boundary is moved before start boundary.
+ * Updates this range accordingly to the updates applied to the model. Bases on change events.
  *
  * @ignore
  * @private
- * @method fixBoundaries
+ * @method transform
  * @param {String} type Type of changes applied to the Tree Model.
  * @param {engine.model.Range} range Range containing the result of applied change.
  * @param {engine.model.Position} [position] Additional position parameter provided by some change events.
  */
-function fixBoundaries( type, range, position ) {
+function transform( type, range, position ) {
 	/* jshint validthis: true */
 	let updated;
 	const howMany = range.end.offset - range.start.offset;
 
 	switch ( type ) {
 		case 'insert':
-			updated = this.getTransformedByInsertion( range.start, howMany, false, true )[ 0 ];
+			updated = this._getTransformedByInsertion( range.start, howMany, false, true )[ 0 ];
 			break;
 
 		case 'move':
@@ -122,12 +119,12 @@ function fixBoundaries( type, range, position ) {
 		case 'reinsert':
 			const sourcePosition = position;
 
-			// Range.getTransformedByMove is expecting `targetPosition` to be "before" move
+			// Range._getTransformedByMove is expecting `targetPosition` to be "before" move
 			// (before transformation). `range.start` is already after the move happened.
 			// We have to revert `range.start` to the state before the move.
-			const targetPosition = range.start.getTransformedByInsertion( sourcePosition, howMany );
+			const targetPosition = range.start._getTransformedByInsertion( sourcePosition, howMany );
 
-			const result = this.getTransformedByMove( sourcePosition, targetPosition, howMany, false, true );
+			const result = this._getTransformedByMove( sourcePosition, targetPosition, howMany, false, true );
 
 			// First item in the array is the "difference" part, so a part of the range
 			// that did not get moved. We use it as reference range and expand if possible.
@@ -135,7 +132,7 @@ function fixBoundaries( type, range, position ) {
 
 			// We will check if there is other range and if it is touching the reference range.
 			// If it does, we will expand the reference range (at the beginning or at the end).
-			// Keep in mind that without settings `spread` flag, `getTransformedByMove` may
+			// Keep in mind that without settings `spread` flag, `_getTransformedByMove` may
 			// return maximum two ranges.
 			if ( result.length > 1 ) {
 				let otherRange = result[ 1 ];

+ 21 - 20
packages/ckeditor5-engine/src/model/liveselection.js

@@ -3,12 +3,11 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import LiveRange from './liverange.js';
 import Range from './range.js';
 import Position from './position.js';
-import CharacterProxy from './characterproxy.js';
+import Text from './text.js';
+import TextProxy from './textproxy.js';
 import toMap from '../../utils/tomap.js';
 
 import Selection from './selection.js';
@@ -16,21 +15,24 @@ import Selection from './selection.js';
 const storePrefix = 'selection:';
 
 /**
- * `LiveSelection` is a special type of {@link engine.model.Selection selection} that listens to changes on a
+ * `LiveSelection` is a type of {@link engine.model.Selection selection} that listens to changes on a
  * {@link engine.model.Document document} and has it ranges updated accordingly. Internal implementation of this
  * mechanism bases on {@link engine.model.LiveRange live ranges}.
  *
- * Differences between {@link engine.model.Selection} and `LiveSelection` are three:
- * * there is always a range in `LiveSelection`, even if no ranges were added - in this case, there is a
- * "default range" in selection which is a collapsed range set at the beginning of the {@link engine.model.Document document},
- * * ranges added to this selection updates automatically when the document changes,
- * * live selection may have attributes.
+ * Differences between {@link engine.model.Selection} and `LiveSelection` are two:
+ * * there is always a range in `LiveSelection` - even if no ranges were added there is a
+ * {@link engine.model.LiveSelection#_getDefaultRange "default range"} present in the selection,
+ * * ranges added to this selection updates automatically when the document changes.
+ *
+ * Since `LiveSelection` uses {@link engine.model.LiveRange live ranges} and is updated when {@link engine.model.Document document}
+ * changes, it cannot be set on {@link engine.model.Node nodes} that are inside {@link engine.model.DocumentFragment document fragment}.
+ * If you need to represent a selection in document fragment, use {@link engine.model.Selection "normal" selection} instead.
  *
  * @memberOf engine.model
  */
 export default class LiveSelection extends Selection {
 	/**
-	 * Creates an empty document selection for given {@link engine.model.Document}.
+	 * Creates an empty live selection for given {@link engine.model.Document}.
 	 *
 	 * @param {engine.model.Document} document Document which owns this selection.
 	 */
@@ -168,7 +170,7 @@ export default class LiveSelection extends Selection {
 
 	/**
 	 * Returns a default range for this selection. The default range is a collapsed range that starts and ends
-	 * at the beginning of this selection's document {@link engine.model.Document#_getDefaultRoot default root}.
+	 * at the beginning of this selection's document's {@link engine.model.Document#_getDefaultRoot default root}.
 	 * This "artificial" range is important for algorithms that base on selection, so they won't break or need
 	 * special logic if there are no real ranges in the selection.
 	 *
@@ -199,11 +201,11 @@ export default class LiveSelection extends Selection {
 		const selectionParent = this.getFirstPosition().parent;
 
 		if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
-			for ( let attr of selectionParent.getAttributes() ) {
-				if ( attr[ 0 ].indexOf( storePrefix ) === 0 ) {
-					const realKey = attr[ 0 ].substr( storePrefix.length );
+			for ( let key of selectionParent.getAttributeKeys() ) {
+				if ( key.indexOf( storePrefix ) === 0 ) {
+					const realKey = key.substr( storePrefix.length );
 
-					yield [ realKey, attr[ 1 ] ];
+					yield [ realKey, selectionParent.getAttribute( key ) ];
 				}
 			}
 		}
@@ -283,7 +285,6 @@ export default class LiveSelection extends Selection {
 	 */
 	_updateAttributes() {
 		const position = this.getFirstPosition();
-		const positionParent = position.parent;
 
 		let attrs = null;
 
@@ -295,15 +296,15 @@ export default class LiveSelection extends Selection {
 			for ( let item of range ) {
 				// This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454.
 				// It can be done better by using `break;` instead of checking `attrs === null`.
-				if ( item.type == 'TEXT' && attrs === null ) {
+				if ( item.type == 'text' && attrs === null ) {
 					attrs = item.item.getAttributes();
 				}
 			}
 		} else {
 			// 2. If the selection is a caret or the range does not contain a character node...
 
-			const nodeBefore = positionParent.getChild( position.offset - 1 );
-			const nodeAfter = positionParent.getChild( position.offset );
+			const nodeBefore = position.textNode ? position.textNode : position.nodeBefore;
+			const nodeAfter = position.textNode ? position.textNode : position.nodeAfter;
 
 			// ...look at the node before caret and take attributes from it if it is a character node.
 			attrs = getAttrsIfCharacter( nodeBefore );
@@ -346,7 +347,7 @@ export default class LiveSelection extends Selection {
 		}
 
 		function getAttrsIfCharacter( node ) {
-			if ( node instanceof CharacterProxy ) {
+			if ( node instanceof TextProxy || node instanceof Text ) {
 				return node.getAttributes();
 			}
 

+ 199 - 50
packages/ckeditor5-engine/src/model/node.js

@@ -3,28 +3,51 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import toMap from '../../utils/tomap.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 
 /**
- * Tree model node. This is an abstract class for other classes representing different nodes in Tree Model.
+ * Model node. Most basic structure of model tree.
+ *
+ * This is an abstract class that is a base for other classes representing different nodes in model.
+ *
+ * **Note:** If a node is detached from the model tree, you can manipulate it using it's API.
+ * However, it is **very important** that nodes already attached to model tree should be only changed through
+ * {@link engine.model.Document#batch Batch API}.
+ *
+ * Changes done by `Node` methods, like {@link engine.model.Node#insertChildren insertChildren} or
+ * {@link engine.model.Node#setAttribute setAttribute} do not generate {@link engine.model.operation.Operation operations}
+ * which are essential for correct editor work if you modify nodes in {@link engine.model.Document document} root.
+ *
+ * The flow of working on `Node` (and classes that inherits from it) is as such:
+ * 1. You can create a `Node` instance, modify it using it's API.
+ * 2. Add `Node` to the model using `Batch` API.
+ * 3. Change `Node` that was already added to the model using `Batch` API.
+ *
+ * Similarly, you cannot use `Batch` API on a node that has not been added to the model tree, with the exception
+ * of {@link engine.model.Batch#insert inserting} that node to the model tree.
+ *
+ * Be aware that using {@link engine.model.Batch#remove remove from Batch API} does not allow to use `Node` API because
+ * the information about `Node` is still kept in model document.
+ *
+ * In case of {@link engine.model.Element element node}, adding and removing children also counts as changing a node and
+ * follows same rules.
  *
  * @memberOf engine.model
  */
 export default class Node {
 	/**
-	 * Creates a tree node.
+	 * Creates a model node.
 	 *
 	 * This is an abstract class, so this constructor should not be used directly.
 	 *
 	 * @abstract
-	 * @param {Iterable|Object} [attrs] Iterable collection of attributes.
+	 * @param {Object} [attrs] Node's attributes. See {@link utils.toMap} for a list of accepted values.
 	 */
 	constructor( attrs ) {
 		/**
-		 * Element or DocumentFragment that is a parent of this node.
+		 * Parent of this node. It could be {@link engine.model.Element} or {@link engine.model.DocumentFragment}.
+		 * Equals to `null` if the node has no parent.
 		 *
 		 * @readonly
 		 * @member {engine.model.Element|engine.model.DocumentFragment|null} engine.model.Node#parent
@@ -32,39 +55,16 @@ export default class Node {
 		this.parent = null;
 
 		/**
-		 * List of attributes set on this node.
-		 *
-		 * **Note:** It is **important** that attributes of nodes already attached to the document must be changed
-		 * only by an {@link engine.model.operation.AttributeOperation}. Do not set attributes of such nodes
-		 * using {@link engine.model.Node} methods.
+		 * Attributes set on this node.
 		 *
-		 * @protected
+		 * @private
 		 * @member {Map} engine.model.Node#_attrs
 		 */
 		this._attrs = toMap( attrs );
 	}
 
 	/**
-	 * Depth of the node, which equals to total number of its parents.
-	 *
-	 * @readonly
-	 * @type {Number}
-	 */
-	get depth() {
-		let depth = 0;
-		let parent = this.parent;
-
-		while ( parent ) {
-			depth++;
-
-			parent = parent.parent;
-		}
-
-		return depth;
-	}
-
-	/**
-	 * Nodes next sibling or `null` if it is the last child.
+	 * Node's next sibling or `null` if the node is a last child of it's parent or if the node has no parent.
 	 *
 	 * @readonly
 	 * @type {engine.model.Node|null}
@@ -76,7 +76,7 @@ export default class Node {
 	}
 
 	/**
-	 * Nodes previous sibling or null if it is the last child.
+	 * Node's previous sibling or `null` if the node is a first child of it's parent or if the node has no parent.
 	 *
 	 * @readonly
 	 * @type {engine.model.Node|null}
@@ -88,10 +88,11 @@ export default class Node {
 	}
 
 	/**
-	 * The top parent for the node. If node has no parent it is the root itself.
+	 * The top-most ancestor of the node. If node has no parent it is the root itself. If the node is a part
+	 * of {@link engine.model.DocumentFragment}, it's `root` is equal to that `DocumentFragment`.
 	 *
 	 * @readonly
-	 * @type {engine.model.Element}
+	 * @type {engine.model.Node|engine.model.DocumentFragment}
 	 */
 	get root() {
 		let root = this;
@@ -104,11 +105,94 @@ export default class Node {
 	}
 
 	/**
-	 * Index of the node in the parent element or null if the node has no parent.
+	 * {@link engine.model.Document Document} that owns this node or `null` if the node has no parent or is inside
+	 * a {@link engine.model.DocumentFragment DocumentFragment}.
+	 *
+	 * @readonly
+	 * @type {engine.model.Document|null}
+	 */
+	get document() {
+		// This is a top element of a sub-tree.
+		if ( this.root == this ) {
+			return null;
+		}
+
+		// Root may be `DocumentFragment` which does not have document property.
+		return this.root.document || null;
+	}
+
+	/**
+	 * Offset at which this node starts in it's parent. It is equal to the sum of {@link engine.model.Node#offsetSize offsetSize}
+	 * of all it's previous siblings. Equals to `null` if node has no parent.
+	 *
+	 * Throws error if the parent element does not contain this node. This means that model tree got broken.
+	 *
+	 * @readonly
+	 * @type {Number|Null}
+	 */
+	get startOffset() {
+		let pos;
+
+		if ( !this.parent ) {
+			return null;
+		}
+
+		if ( ( pos = this.parent.getChildStartOffset( this ) ) === null ) {
+			/**
+			 * The node's parent does not contain this node.
+			 *
+			 * @error node-not-found-in-parent
+			 */
+			throw new CKEditorError( 'node-not-found-in-parent: The node\'s parent does not contain this node.' );
+		}
+
+		return pos;
+	}
+
+	/**
+	 * Offset size of this node. Represents how much "offset space" is occupied by the node in it's parent.
+	 * It is important for {@link engine.model.Position position}. When node has `offsetSize` greater than `1`, position
+	 * can be placed between that node start and end. `offsetSize` greater than `1` is for nodes that represents more
+	 * than one entity, i.e. {@link engine.model.Text text node}.
+	 *
+	 * @readonly
+	 * @type {Number}
+	 */
+	get offsetSize() {
+		return 1;
+	}
+
+	/**
+	 * Offset at which this node ends in it's parent. It is equal to the sum of this node's
+	 * {@link engine.model.Node#startOffset start offset} and {@link engine.model.Node#offsetSize offset size}. Equals
+	 * to `null` if the node has no parent.
+	 *
+	 * @readonly
+	 * @type {Number|null}
+	 */
+	get endOffset() {
+		if ( !this.parent ) {
+			return null;
+		}
+
+		return this.startOffset + this.offsetSize;
+	}
+
+	/**
+	 * Creates a copy of this node, that is a node with exactly same attributes, and returns it.
 	 *
-	 * Throws error if the parent element does not contain this node.
+	 * @returns {engine.model.Node} Node with same attributes as this node.
+	 */
+	clone() {
+		return new Node( this._attrs );
+	}
+
+	/**
+	 * Index of this node in it's parent or `null` if the node has no parent.
 	 *
-	 * @returns {Number|Null} Index of the node in the parent element or null if the node has not parent.
+	 * Throws error if the parent element does not contain this node. This means that model tree got broken.
+	 *
+	 * @returns {Number|null}
 	 */
 	getIndex() {
 		let pos;
@@ -117,7 +201,7 @@ export default class Node {
 			return null;
 		}
 
-		if ( ( pos = this.parent.getChildIndex( this ) ) == -1 ) {
+		if ( ( pos = this.parent.getChildIndex( this ) ) === null ) {
 			/**
 			 * The node's parent does not contain this node.
 			 *
@@ -130,17 +214,27 @@ export default class Node {
 	}
 
 	/**
-	 * Gets path to the node. For example if the node is the second child of the first child of the root then the path
-	 * will be `[ 1, 2 ]`. This path can be used as a parameter of {@link engine.model.Position}.
+	 * Gets path to the node. The path is an array containing starting offsets of consecutive ancestors of this node,
+	 * beginning from {@link engine.model.Node#root root}, down to this node's starting offset. The path can be used to
+	 * create {@link engine.model.Position Position} instance.
+	 *
+	 *		const abc = new Text( 'abc' );
+	 *		const foo = new Text( 'foo' );
+	 *		const h1 = new Element( 'h1', null, new Text( 'header' ) );
+	 *		const p = new Element( 'p', null, [ abc, foo ] );
+	 *		const div = new Element( 'div', null, [ h1, p ] );
+	 *		foo.getPath(); // Returns [ 1, 3 ]. `foo` is in `p` which is in `div`. `p` starts at offset 1, while `foo` at 3.
+	 *		h1.getPath(); // Returns [ 0 ].
+	 *		div.getPath(); // Returns [].
 	 *
-	 * @returns {Number[]} The path.
+	 * @returns {Array.<Number>} The path.
 	 */
 	getPath() {
 		const path = [];
 		let node = this;
 
 		while ( node.parent ) {
-			path.unshift( node.getIndex() );
+			path.unshift( node.startOffset );
 			node = node.parent;
 		}
 
@@ -148,7 +242,14 @@ export default class Node {
 	}
 
 	/**
-	 * Checks if the node has an attribute for given key.
+	 * Removes this node from it's parent.
+	 */
+	remove() {
+		this.parent.removeChildren( this.getIndex() );
+	}
+
+	/**
+	 * Checks if the node has an attribute with given key.
 	 *
 	 * @param {String} key Key of attribute to check.
 	 * @returns {Boolean} `true` if attribute with given key is set on node, `false` otherwise.
@@ -158,28 +259,76 @@ export default class Node {
 	}
 
 	/**
-	 * Gets an attribute value for given key or undefined if that attribute is not set on node.
+	 * Gets an attribute value for given key or `undefined` if that attribute is not set on node.
 	 *
 	 * @param {String} key Key of attribute to look for.
-	 * @returns {*} Attribute value or null.
+	 * @returns {*} Attribute value or `undefined`.
 	 */
 	getAttribute( key ) {
 		return this._attrs.get( key );
 	}
 
 	/**
-	 * Returns iterator that iterates over this node attributes.
+	 * Returns iterator that iterates over this node's attributes.
+	 *
+	 * Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
+	 * This format is accepted by native `Map` object and also can be passed in `Node` constructor.
 	 *
 	 * @returns {Iterable.<*>}
 	 */
 	getAttributes() {
-		return this._attrs[ Symbol.iterator ]();
+		return this._attrs.entries();
+	}
+
+	/**
+	 * Returns iterator that iterates over this node's attribute keys.
+	 *
+	 * @returns {Iterator.<String>}
+	 */
+	getAttributeKeys() {
+		return this._attrs.keys();
+	}
+
+	/**
+	 * Sets attribute on the node. If attribute with the same key already is set, it's value is overwritten.
+	 *
+	 * @param {String} key Key of attribute to set.
+	 * @param {*} value Attribute value.
+	 */
+	setAttribute( key, value ) {
+		this._attrs.set( key, value );
+	}
+
+	/**
+	 * Removes all attributes from the node and sets given attributes.
+	 *
+	 * @param {Object} [attrs] Attributes to set. See {@link utils.toMap} for a list of accepted values.
+	 */
+	setAttributesTo( attrs ) {
+		this._attrs = toMap( attrs );
+	}
+
+	/**
+	 * Removes an attribute with given key from the node.
+	 *
+	 * @param {String} key Key of attribute to remove.
+	 * @returns {Boolean} `true` if the attribute was set on the element, `false` otherwise.
+	 */
+	removeAttribute( key ) {
+		return this._attrs.delete( key );
+	}
+
+	/**
+	 * Removes all attributes from the node.
+	 */
+	clearAttributes() {
+		this._attrs.clear();
 	}
 
 	/**
-	 * Custom toJSON method to solve child-parent circular dependencies.
+	 * Converts `Node` to plain object and returns it.
 	 *
-	 * @returns {Object} Clone of this object with the parent property replaced with its name.
+	 * @returns {Object} `Node` converted to plain object.
 	 */
 	toJSON() {
 		let json = {};

+ 106 - 400
packages/ckeditor5-engine/src/model/nodelist.js

@@ -3,498 +3,204 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
-import CharacterProxy from './characterproxy.js';
-import Text from './text.js';
-import Element from './element.js';
-import DocumentFragment from './documentfragment.js';
-import mapsEqual from '../../utils/mapsequal.js';
-import isIterable from '../../utils/isiterable.js';
+import Node from './node.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 
 /**
- * This is a private helper-class for {@link engine.model.NodeList} text compression utility.
- *
- * @protected
- * @memberOf engine.model
- * @extends engine.model.Text
- */
-class NodeListText extends Text {
-	/**
-	 * @see engine.model.Text#constructor
-	 * @protected
-	 * @constructor
-	 */
-	constructor( text, attrs ) {
-		super( text, attrs );
-
-		/**
-		 * Element that contains character nodes represented by this NodeListText.
-		 *
-		 * @type {engine.model.Element|engine.model.DocumentFragment|null}
-		 */
-		this.parent = null;
-	}
-
-	/**
-	 * Gets a character at given index and creates a {@link engine.model.CharacterProxy} out of it.
-	 *
-	 * @param {Number} index Character index.
-	 * @returns {engine.model.CharacterProxy}
-	 */
-	getCharAt( index ) {
-		index = index && index >= 0 ? index : 0;
-
-		return new CharacterProxy( this, index );
-	}
-}
-
-/**
- * List of nodes. It is used to represent multiple nodes with a given order, for example children of
- * {@link engine.model.Element} object or nodes inserted using {@link engine.model.operation.InsertOperation}.
- *
- * Thanks to the constructor, which accepts various arguments, this class lets you easily create desired list of nodes.
- *
- * Parameters passed to constructor are converted and internally kept as an array of {@link engine.model.Node}
- * and {@link engine.model.Text} instances.
- *
- * @memberOf engine.model
+ * Provides an interface to operate on a list of {@link engine.model.Node nodes}. `NodeList` is used internally
+ * in classes like {@link engine.model.Element Element} or {@link engine.model.DocumentFragment DocumentFragment}.
  */
 export default class NodeList {
 	/**
-	 * Constructor lets you create a list of nodes in many ways. See examples:
-	 *
-	 *		let nodeList = new NodeList( [ new Element( p1 ), new Element( p1 ) ] );
-	 *		nodeList.length; // 2
-	 *
-	 *		let nodeList = new NodeList( new Element( p ) );
-	 *		nodeList.length; // 1
+	 * Creates an empty node list.
 	 *
-	 *		let nodeList = new NodeList( [ 'foo', new Element( p ), 'bar' ] );
-	 *		nodeList.length; // 7
-	 *
-	 *		let nodeList = new NodeList( 'foo' );
-	 *		nodeList.length; // 3
-	 *
-	 *		let nodeList = new NodeList( new Text( 'foo', { bar: 'bom' } ) );
-	 *		nodeList.length; // 3
-	 *		nodeList.get( 0 ).getAttribute( 'bar' ); // 'bom'
-	 *		nodeList.get( 1 ).getAttribute( 'bar' ); // 'bom'
-	 *		nodeList.get( 2 ).getAttribute( 'bar' ); // 'bom'
-	 *
-	 *		let nodeListA = new NodeList( 'foo' );
-	 *		let nodeListB = new NodeList( nodeListA );
-	 *		nodeListA === nodeListB // true
-	 *		nodeListB.length // 3
-	 *
-	 * @see engine.model.NodeSet
-	 *
-	 * @param {engine.model.NodeSet} nodes List of nodes.
-	 * @constructor
+	 * @param {Iterable.<engine.model.Node>} nodes Nodes contained in this node list.
 	 */
 	constructor( nodes ) {
-		if ( nodes instanceof NodeList ) {
-			// We do not clone anything.
-			return nodes;
-		} else if ( nodes instanceof DocumentFragment ) {
-			return nodes._children;
-		}
-
 		/**
-		 * Internal array to store nodes.
+		 * Nodes contained in this node list.
 		 *
-		 * @protected
-		 * @member {Array} engine.model.NodeList#_nodes
+		 * @private
+		 * @member {Array.<engine.model.Node>} engine.model.NodeList#_nodes
 		 */
 		this._nodes = [];
 
 		/**
-		 * Internal array where each index is mapped to correct node from `_nodes` array. This is introduced
-		 * to easily refer `_nodes` by index, since some of elements in `_nodes` may contain multiple characters,
-		 * which occupy multiple slots in `_indexMap`.
+		 * Represents which node occupies given offset.
 		 *
 		 * @private
-		 * @member {Array} engine.model.NodeList#_indexMap
+		 * @member {Array.<engine.model.Node>} engine.model.NodeList#_nodeAtOffset
 		 */
-		this._indexMap = [];
+		this._nodeAtOffset = [];
 
 		if ( nodes ) {
-			if ( typeof nodes == 'string' || !isIterable( nodes ) ) {
-				nodes = [ nodes ];
-			}
-
-			for ( let node of getNodes( nodes ) ) {
-				let indexInNodes = this._nodes.length;
-				let mergedWithPrev = false;
-				let length = 1;
-
-				if ( node instanceof CharacterProxy ) {
-					node = new NodeListText( node.character, node._attrs );
-				} else if ( node instanceof Text ) {
-					node = new NodeListText( node.text, node._attrs );
-				} else if ( typeof node == 'string' ) {
-					node = new NodeListText( node, [] );
-				}
-
-				if ( node instanceof NodeListText ) {
-					length = node.text.length;
-
-					let prev = this._nodes[ this._nodes.length - 1 ];
-
-					if ( prev instanceof NodeListText && mapsEqual( prev._attrs, node._attrs ) ) {
-						// If previously added text has same attributes, merge this text with it.
-						prev.text += node.text;
-						mergedWithPrev = true;
-						indexInNodes--;
-					} else if ( node.text.length === 0 ) {
-						// If this is an empty text just omit it.
-						continue;
-					}
-				}
-
-				if ( !mergedWithPrev ) {
-					this._nodes.push( node );
-				}
-
-				for ( let i = 0; i < length; i++ ) {
-					this._indexMap.push( indexInNodes );
-				}
-			}
+			this.insertNodes( 0, nodes );
 		}
 	}
 
 	/**
-	 * Number of nodes in the node list.
+	 * Returns an iterator that iterates over all nodes contained inside this node list.
 	 *
-	 * @readonly
-	 * @type {Number}
+	 * @returns {Iterator.<engine.model.Node>}
 	 */
-	get length() {
-		return this._indexMap.length;
+	[ Symbol.iterator ]() {
+		return this._nodes[ Symbol.iterator ]();
 	}
 
 	/**
-	 * Node list iterator.
-	 */
-	*[ Symbol.iterator ]() {
-		for ( let i = 0; i < this.length; i++ ) {
-			yield this.get( i );
-		}
-	}
-
-	/**
-	 * Returns node at the given index.
+	 * Returns the number of nodes contained inside this node list.
 	 *
-	 * @param {Number} index Node index.
-	 * @returns {engine.model.Node} Node at given index.
+	 * @returns {Number}
 	 */
-	get( index ) {
-		let realIndex = this._indexMap[ index ];
-		let node = this._nodes[ realIndex ];
-
-		if ( node instanceof NodeListText ) {
-			return node.getCharAt( this._getCharIndex( index ) );
-		} else {
-			return node;
-		}
+	get length() {
+		return this._nodes.length;
 	}
 
 	/**
-	 * Search for the element in the node list.
+	 * Returns the sum of {engine.model.Node#offsetSize offset sizes} of all nodes contained inside this node list.
 	 *
-	 * @param {engine.model.Node} node Node to find.
-	 * @returns {Number} Position of the element in the list or -1 if not found.
+	 * @returns {Number}
 	 */
-	indexOf( node ) {
-		if ( node instanceof CharacterProxy ) {
-			let baseIndex = this.indexOf( node._nodeListText );
-
-			return baseIndex == -1 ? -1 : baseIndex + node._index;
-		}
-
-		let realIndex = this._nodes.indexOf( node );
-
-		return this._indexMap.indexOf( realIndex );
+	get totalOffset() {
+		return this._nodeAtOffset.length;
 	}
 
 	/**
-	 * Inserts nodes from the given node list into this node list at the given index.
+	 * Gets the node at the given index. Returns `null` if incorrect index was passed.
 	 *
-	 * @param {Number} index Position where nodes should be inserted.
-	 * @param {engine.model.NodeList} nodeList List of nodes to insert.
+	 * @param {Number} index Index of node.
+	 * @returns {engine.model.Node|null} Node at given index.
 	 */
-	insert( index, nodeList ) {
-		if ( this._nodes.length === 0 ) {
-			this._nodes = nodeList._nodes.slice();
-			this._indexMap = nodeList._indexMap.slice();
-
-			return;
-		}
-
-		// If we are inserting into a text, splitting may be needed.
-		this._splitNodeAt( index );
-
-		// If `index` is too high to be found in `_indexMap` it means that we insert at the end of node list.
-		let realIndex = index >= this._indexMap.length ? this._nodes.length : this._indexMap[ index ];
-
-		// Splice arrays from inserted nodeList into this nodeList.
-		this._indexMap.splice.apply( this._indexMap, [ index, 0 ].concat( nodeList._indexMap ) );
-		this._nodes.splice.apply( this._nodes, [ realIndex, 0 ].concat( nodeList._nodes ) );
-
-		// Fix indexes in index map.
-		// From the beginning of spliced-in array to the end of spliced-in array.
-		for ( let i = index; i < index + nodeList._indexMap.length; i++ ) {
-			this._indexMap[ i ] += realIndex;
-		}
-
-		// From the end of spliced-in array to the end of original array.
-		for ( let i = index + nodeList._indexMap.length; i < this._indexMap.length; i++ ) {
-			this._indexMap[ i ] += nodeList._nodes.length;
-		}
-
-		this._mergeNodeAt( index );
-		this._mergeNodeAt( index + nodeList.length );
+	getNode( index ) {
+		return this._nodes[ index ] || null;
 	}
 
 	/**
-	 * Removes number of nodes starting at the given index.
+	 * Returns an index of the given node. Returns `null` if given node is not inside this node list.
 	 *
-	 * @param {Number} index Position of the first node to remove.
-	 * @param {Number} number Number of nodes to remove.
-	 * @returns {engine.model.NodeList} List of removed nodes.
+	 * @param {engine.model.Node} node Child node to look for.
+	 * @returns {Number|null} Child node's index.
 	 */
-	remove( index, number ) {
-		if ( this._nodes.length === 0 ) {
-			return new NodeList();
-		}
-
-		// Removed "range" may start in NodeListText or end in NodeListText. Some splitting may be needed.
-		this._splitNodeAt( index );
-		this._splitNodeAt( index + number );
-
-		// If given index is too high to be found in `_indexMap` it means that we remove to the end of node list.
-		let realIndexEnd = ( index + number ) >= this._indexMap.length ? this._nodes.length : this._indexMap[ index + number ];
-		let realIndexStart = this._indexMap[ index ];
-		let removed = this._nodes.splice( realIndexStart, realIndexEnd - realIndexStart );
-
-		this._indexMap.splice( index, number );
+	getNodeIndex( node ) {
+		const index = this._nodes.indexOf( node );
 
-		for ( let i = index; i < this._indexMap.length; i++ ) {
-			this._indexMap[ i ] -= removed.length;
-		}
-
-		this._mergeNodeAt( index );
-
-		return new NodeList( removed );
+		return index == -1 ? null : index;
 	}
 
 	/**
-	 * Sets or removes given attribute on a range of nodes in the node list.
+	 * Returns the starting offset of given node. Starting offset is equal to the sum of
+	 * {engine.model.Node#offsetSize offset sizes} of all nodes that are before this node in this node list.
 	 *
-	 * @param {Number} index Position of the first node to change.
-	 * @param {Number} number Number of nodes to change.
-	 * @param {String} key Attribute key to change.
-	 * @param {*} [attribute] Attribute value or null if attribute with given key should be removed.
+	 * @param {engine.model.Node} node Node to look for.
+	 * @returns {Number|null} Node's starting offset.
 	 */
-	setAttribute( index, number, key, value ) {
-		if ( index < 0 || index + number > this.length ) {
-			/**
-			 * Trying to set attribute on non-existing node list items.
-			 *
-			 * @error nodelist-setattribute-out-of-bounds
-			 * @param root
-			 */
-			throw new CKEditorError( 'nodelist-setattribute-out-of-bounds: Trying to set attribute on non-existing node list items.' );
-		}
-
-		// "Range" of nodes to remove attributes may start in NodeListText or end in NodeListText. Some splitting may be needed.
-		this._splitNodeAt( index );
-		this._splitNodeAt( index + number );
-
-		let i = index;
-
-		while ( i < index + number ) {
-			let node = this._nodes[ this._indexMap[ i ] ];
-
-			if ( node instanceof NodeListText ) {
-				if ( value !== null ) {
-					node._attrs.set( key, value );
-				} else {
-					node._attrs.delete( key );
-				}
-
-				this._mergeNodeAt( i );
-				i += node.text.length;
-			} else {
-				if ( value !== null ) {
-					node.setAttribute( key, value );
-				} else {
-					node.removeAttribute( key );
-				}
-
-				i++;
-			}
-		}
+	getNodeStartOffset( node ) {
+		const offset = this._nodeAtOffset.indexOf( node );
 
-		this._mergeNodeAt( index + number );
+		return offset == -1 ? null : offset;
 	}
 
 	/**
-	 * Custom toJSON method to solve child-parent circular dependencies.
+	 * Converts index "position" to offset "position".
 	 *
-	 * @returns {Object} Clone of this object with the parent property replaced with its name.
+	 * Returns starting offset of a node that is at given index. If given index is too low, `0` is returned. If
+	 * given index is too high, {@link engine.model.NodeList#totalOffset last available offset} is returned.
+	 *
+	 * @param {Number} index Node's index.
+	 * @returns {Number} Node's starting offset.
 	 */
-	toJSON() {
-		if ( !this._nodes.length ) {
-			return {};
+	indexToOffset( index ) {
+		if ( index < 0 ) {
+			return 0;
+		} else if ( index >= this._nodes.length ) {
+			return this.totalOffset;
 		}
 
-		let json = { nodes: [] };
-
-		for ( let node of this._nodes ) {
-			json.nodes.push( node.toJSON() );
-		}
+		const node = this._nodes[ index ];
 
-		return json;
+		return this.getNodeStartOffset( node );
 	}
 
 	/**
-	 * Creates NodeList object from deserilized object, ie. from parsed JSON string.
+	 * Converts offset "position" to index "position".
 	 *
-	 *		let deserialized = JSON.parse( JSON.stringify( someNodeList ) );
-	 *		let nodeList = NodeList.fromJSON( deserialized );
+	 * Returns index of a node that occupies given offset. If given offset is too low, `0` is returned. If
+	 * given offset is too high, {@link engine.model.NodeList#length last available index} is returned.
 	 *
-	 * @param {Object} json Deserialized JSON object.
-	 * @returns {engine.model.NodeList}
+	 * @param {Number} offset Offset to look for.
+	 * @returns {Number} Index of a node that occupies given offset.
 	 */
-	static fromJSON( json ) {
-		let nodes = [];
-
-		if ( json.nodes ) {
-			for ( let node of json.nodes ) {
-				if ( node.text ) {
-					nodes.push( new Text( node.text, node.attributes ) );
-				} else {
-					nodes.push( Element.fromJSON( node ) );
-				}
-			}
+	offsetToIndex( offset ) {
+		if ( offset < 0 ) {
+			return 0;
+		} else if ( offset >= this._nodeAtOffset.length ) {
+			return this.length;
 		}
 
-		return new NodeList( nodes );
+		const node = this._nodeAtOffset[ offset ];
+
+		return this.getNodeIndex( node );
 	}
 
 	/**
-	 * Checks whether given index is inside a text and if so, splits that text node. This method should be used
-	 * to split text objects whenever there are some changes made on a part of text object (i.e. removing part of text,
-	 * inserting between text object, changing attributes of part of a text object).
+	 * Inserts given nodes at given index.
 	 *
-	 * @protected
-	 * @param {Number} index Index in the node list at which node should be broken.
+	 * @param {Number} index Index at which nodes should be inserted.
+	 * @param {Iterable.<engine.model.Node>} nodes Nodes to be inserted.
 	 */
-	_splitNodeAt( index ) {
-		if ( this._indexMap[ index ] != this._indexMap[ index - 1 ] || this._indexMap.length === 0 ) {
-			// Node before and node after splitting point are already different.
-			// Or the node list is empty.
-			// No splitting is needed.
-			return;
+	insertNodes( index, nodes ) {
+		// Validation.
+		for ( let node of nodes ) {
+			if ( !( node instanceof Node ) ) {
+				/**
+				 * Trying to insert an object which is not a Node instance.
+				 *
+				 * @error nodelist-insertNodes-not-node
+				 */
+				throw new CKEditorError( 'nodelist-insertNodes-not-node: Trying to insert an object which is not a Node instance.' );
+			}
 		}
 
-		let realIndex = this._indexMap[ index ];
-		let node = this._nodes[ realIndex ];
-
-		// Get position in the text node where the text should be split.
-		let charIndex = this._getCharIndex( index );
-
-		// Get text part before and after split point.
-		let textBefore = node.text.substr( 0, charIndex );
-		let textAfter = node.text.substr( charIndex );
+		const offset = this.indexToOffset( index );
 
-		// "Remove" part after split point from current text node.
-		node.text = textBefore;
+		this._nodes.splice( index, 0, ...nodes );
 
-		// Create a new text node with the "removed" part and splice it after original node.
-		let newText = new NodeListText( textAfter, node._attrs );
-		newText.parent = node.parent;
-		this._nodes.splice.call( this._nodes, realIndex + 1, 0, newText );
+		const offsetsArray = [];
 
-		// We added new element in the middle of _nodes what invalidated _indexMap. We have to fix it.
-		for ( let i = index; i < this._indexMap.length; i++ ) {
-			this._indexMap[ i ]++;
+		for ( let node of nodes ) {
+			for ( let i = 0; i < node.offsetSize; i++ ) {
+				offsetsArray.push( node );
+			}
 		}
+
+		this._nodeAtOffset.splice( offset, 0, ...offsetsArray );
 	}
 
 	/**
-	 * Checks whether given index is between two text nodes that have same attributes and if so, merges them
-	 * together into one node. Used to compress characters into large text objects and use less memory. This method
-	 * should be used whenever there are some changed done to the node list to check whether it is possible to merge
-	 * text objects.
+	 * Removes one or more nodes starting at the given index.
 	 *
-	 * @param {Number} index Index in the node list at which node should be merged.
-	 * @protected
+	 * @param {Number} indexStart Index of the first node to remove.
+	 * @param {Number} [howMany=1] Number of nodes to remove.
+	 * @returns {Array.<engine.model.Node>} Array containing removed nodes.
 	 */
-	_mergeNodeAt( index ) {
-		if ( this._indexMap[ index ] == this._indexMap[ index - 1 ] || this._indexMap.length === 0 ) {
-			// Node before and node after splitting point are already same.
-			// Or the node list is empty.
-			// No splitting is needed.
-			return;
-		}
+	removeNodes( indexStart, howMany = 1 ) {
+		const indexEnd = indexStart + howMany;
 
-		// Get the node before and after given index.
-		let realIndexBefore = this._indexMap[ index - 1 ];
-		let realIndexAfter = this._indexMap[ index ];
+		const offsetStart = this.indexToOffset( indexStart );
+		const offsetEnd = this.indexToOffset( indexEnd );
 
-		let nodeBefore = this._nodes[ realIndexBefore ];
-		let nodeAfter = this._nodes[ realIndexAfter ];
+		this._nodeAtOffset.splice( offsetStart, offsetEnd - offsetStart );
 
-		// Check if both of those nodes are text objects with same attributes.
-		if ( nodeBefore instanceof NodeListText && nodeAfter instanceof NodeListText && mapsEqual( nodeBefore._attrs, nodeAfter._attrs ) ) {
-			// Append text of text node after index to the before one.
-			nodeBefore.text += nodeAfter.text;
-
-			// Remove text node after index.
-			this._nodes.splice( realIndexAfter, 1 );
-
-			for ( let i = index; i < this._indexMap.length; i++ ) {
-				this._indexMap[ i ]--;
-			}
-		}
+		return this._nodes.splice( indexStart, howMany );
 	}
 
 	/**
-	 * Helper function that takes an index in a node list that is inside a text node and returns the offset of that
-	 * index from the beginning of that text node. If index
+	 * Converts `NodeList` instance to an array containing nodes that were inserted in the node list. Nodes
+	 * are also converted to their plain object representation.
 	 *
-	 * @param index
-	 * @returns {Number} Offset of given index from the beginning of the text node.
-	 * @private
+	 * @returns {Array.<engine.model.Node>} `NodeList` instance converted to `Array`.
 	 */
-	_getCharIndex( index ) {
-		return index - this._indexMap.indexOf( this._indexMap[ index ] );
-	}
-}
-
-/**
- * Value that is convertible to an item kept in {@link engine.model.NodeList} or an iterable collection of such items.
- * In other words, this is anything that {@link engine.model.NodeList#constructor} is able to take and convert to node:
- * * {@link engine.model.Node} will be left as is
- * * {@link engine.model.Text} and {String} will be converted to a set of {@link engine.model.CharacterProxy}
- * * {@link engine.model.NodeList} will clone a node list (but not the nodes inside, so the new and passed list will
- * point to the same nodes.
- * * Iterable collection of above items will be iterated over and all items will be added to the node list.
- *
- * @typedef {engine.model.Node|engine.model.Text|String|engine.model.NodeList|engine.model.DocumentFragment|Iterable}
- * engine.model.NodeSet
- */
-
-// Helper function that "flattens" `engine.model.DocumentFragment`.
-function* getNodes( nodes ) {
-	for ( let node of nodes ) {
-		if ( node instanceof DocumentFragment ) {
-			yield* node;
-		} else {
-			yield node;
-		}
+	toJSON() {
+		return this._nodes.map( ( node ) => node.toJSON() );
 	}
 }

+ 30 - 24
packages/ckeditor5-engine/src/model/operation/attributeoperation.js

@@ -3,14 +3,15 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Operation from './operation.js';
 import Range from '../range.js';
 import CKEditorError from '../../../utils/ckeditorerror.js';
+import writer from '../writer.js';
 
 /**
- * Operation to change nodes' attribute. Using this class you can add, remove or change value of the attribute.
+ * Operation to change nodes' attribute.
+ *
+ * Using this class you can add, remove or change value of the attribute.
  *
  * @memberOf engine.model.operation
  * @extends engine.model.operation.Operation
@@ -19,19 +20,19 @@ export default class AttributeOperation extends Operation {
 	/**
 	 * Creates an operation that changes, removes or adds attributes.
 	 *
-	 * If only the new attribute is set, then it will be inserted. Note that in all nodes in ranges there must be
-	 * no attributes with the same key as the new attribute.
+	 * If only `newValue` is set, attribute will be added on a node. Note that all nodes in operation's range must not
+	 * have an attribute with the same key as the added attribute.
 	 *
-	 * If only the old attribute is set, then it will be removed. Note that this attribute must be present in all nodes in
-	 * ranges.
+	 * If only `oldValue` is set, then attribute with given key will be removed. Note that all nodes in operation's range
+	 * must have an attribute with that key added.
 	 *
-	 * If both new and old attributes are set, then the operation will change the attribute value. Note that both new and
-	 * old attributes have to have the same key and the old attribute must be present in all nodes in ranges.
+	 * If both `newValue` and `oldValue` are set, then the operation will change the attribute value. Note that all nodes in
+	 * operation's ranges must already have an attribute with given key and `oldValue` as value
 	 *
 	 * @param {engine.model.Range} range Range on which the operation should be applied.
 	 * @param {String} key Key of an attribute to change or remove.
-	 * @param {*} oldValue Old value of the attribute with given key or `null` if adding a new attribute.
-	 * @param {*} newValue New value to set for the attribute. If `null`, then the operation just removes the attribute.
+	 * @param {*} oldValue Old value of the attribute with given key or `null`, if attribute was not set before.
+	 * @param {*} newValue New value of the attribute with given key or `null`, if operation should remove attribute.
 	 * @param {Number} baseVersion {@link engine.model.Document#version} on which the operation can be applied.
 	 */
 	constructor( range, key, oldValue, newValue, baseVersion ) {
@@ -54,7 +55,7 @@ export default class AttributeOperation extends Operation {
 		this.key = key;
 
 		/**
-		 * Old value of the attribute with given key or `null` if adding a new attribute.
+		 * Old value of the attribute with given key or `null`, if attribute was not set before.
 		 *
 		 * @readonly
 		 * @member {*} engine.model.operation.AttributeOperation#oldValue
@@ -62,7 +63,7 @@ export default class AttributeOperation extends Operation {
 		this.oldValue = oldValue;
 
 		/**
-		 * New value to set for the attribute. If `null`, then the operation just removes the attribute.
+		 * New value of the attribute with given key or `null`, if operation should remove attribute.
 		 *
 		 * @readonly
 		 * @member {*} engine.model.operation.AttributeOperation#newValue
@@ -70,6 +71,9 @@ export default class AttributeOperation extends Operation {
 		this.newValue = newValue;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	get type() {
 		if ( this.oldValue === null ) {
 			return 'addAttribute';
@@ -81,6 +85,7 @@ export default class AttributeOperation extends Operation {
 	}
 
 	/**
+	 * @inheritDoc
 	 * @returns {engine.model.operation.AttributeOperation}
 	 */
 	clone() {
@@ -88,25 +93,29 @@ export default class AttributeOperation extends Operation {
 	}
 
 	/**
+	 * @inheritDoc
 	 * @returns {engine.model.operation.AttributeOperation}
 	 */
 	getReversed() {
 		return new AttributeOperation( this.range, this.key, this.newValue, this.oldValue, this.baseVersion + 1 );
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	_execute() {
+		// Validation.
 		for ( let item of this.range.getItems() ) {
 			if ( this.oldValue !== null && item.getAttribute( this.key ) !== this.oldValue ) {
 				/**
-				 * The attribute which should be removed does not exists for the given node.
+				 * Changed node has different attribute value than operation's old attribute value.
 				 *
-				 * @error operation-attribute-no-attr-to-remove
+				 * @error operation-attribute-wrong-old-value
 				 * @param {engine.model.Item} item
 				 * @param {String} key
-				 * @param {*} value
 				 */
 				throw new CKEditorError(
-					'operation-attribute-no-attr-to-remove: The attribute which should be removed does not exists for given node.',
+					'operation-attribute-wrong-old-value: Changed node has different attribute value than operation\'s old attribute value.',
 					{ item: item, key: this.key }
 				);
 			}
@@ -124,14 +133,11 @@ export default class AttributeOperation extends Operation {
 					{ node: item, key: this.key }
 				);
 			}
-
-			if ( this.newValue !== null ) {
-				item.setAttribute( this.key, this.newValue );
-			} else {
-				item.removeAttribute( this.key );
-			}
 		}
 
+		// Execution.
+		writer.setAttribute( this.range, this.key, this.newValue );
+
 		return { range: this.range, key: this.key, oldValue: this.oldValue, newValue: this.newValue };
 	}
 
@@ -143,7 +149,7 @@ export default class AttributeOperation extends Operation {
 	}
 
 	/**
-	 * Creates AttributeOperation object from deserilized object, i.e. from parsed JSON string.
+	 * Creates `AttributeOperation` object from deserilized object, i.e. from parsed JSON string.
 	 *
 	 * @param {Object} json Deserialized JSON object.
 	 * @param {engine.model.Document} document Document on which this operation will be applied.

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

@@ -3,16 +3,17 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Operation from './operation.js';
-import NodeList from '../nodelist.js';
 import Position from '../position.js';
-import Range from '../range.js';
+import NodeList from '../nodelist.js';
 import RemoveOperation from './removeoperation.js';
+import writer from './../writer.js';
+import { normalizeNodes } from './../writer.js';
+import Text from '../text.js';
+import Element from '../element.js';
 
 /**
- * Operation to insert list of nodes on the given position in the tree data model.
+ * Operation to insert one or more nodes at given position in the model.
  *
  * @memberOf engine.model.operation
  * @extends engine.model.operation.Operation
@@ -23,7 +24,6 @@ export default class InsertOperation extends Operation {
 	 *
 	 * @param {engine.model.Position} position Position of insertion.
 	 * @param {engine.model.NodeSet} nodes The list of nodes to be inserted.
-	 * List of nodes can be any type accepted by the {@link engine.model.NodeList} constructor.
 	 * @param {Number} baseVersion {@link engine.model.Document#version} on which operation can be applied.
 	 */
 	constructor( position, nodes, baseVersion ) {
@@ -43,33 +43,46 @@ export default class InsertOperation extends Operation {
 		 * @readonly
 		 * @member {engine.model.NodeList} engine.model.operation.InsertOperation#nodeList
 		 */
-		this.nodeList = new NodeList( nodes );
+		this.nodes = new NodeList( normalizeNodes( nodes ) );
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	get type() {
 		return 'insert';
 	}
 
 	/**
+	 * @inheritDoc
 	 * @returns {engine.model.operation.InsertOperation}
 	 */
 	clone() {
-		return new InsertOperation( this.position, this.nodeList, this.baseVersion );
+		return new InsertOperation( this.position, this.nodes, this.baseVersion );
 	}
 
 	/**
+	 * @inheritDoc
 	 * @returns {engine.model.operation.RemoveOperation}
 	 */
 	getReversed() {
-		return new RemoveOperation( this.position, this.nodeList.length, this.baseVersion + 1 );
+		return new RemoveOperation( this.position, this.nodes.totalOffset, this.baseVersion + 1 );
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	_execute() {
-		this.position.parent.insertChildren( this.position.offset, this.nodeList );
+		// What happens here is that we want original nodes be passed to writer because we want original nodes
+		// to be inserted to the model. But in InsertOperation, we want to keep those nodes as they were added
+		// to the operation, not modified. For example, text nodes can get merged or cropped while Elements can
+		// get children. It is important that InsertOperation has the copy of original nodes in intact state.
+		const originalNodes = this.nodes;
+		this.nodes = new NodeList( [ ...originalNodes ].map( ( node ) => node.clone( true ) ) );
 
-		return {
-			range: Range.createFromPositionAndShift( this.position, this.nodeList.length )
-		};
+		const range = writer.insert( this.position, originalNodes );
+
+		return { range };
 	}
 
 	/**
@@ -80,13 +93,25 @@ export default class InsertOperation extends Operation {
 	}
 
 	/**
-	 * Creates InsertOperation object from deserilized object, i.e. from parsed JSON string.
+	 * Creates `InsertOperation` object from deserilized object, i.e. from parsed JSON string.
 	 *
 	 * @param {Object} json Deserialized JSON object.
 	 * @param {engine.model.Document} document Document on which this operation will be applied.
 	 * @returns {engine.model.operation.InsertOperation}
 	 */
 	static fromJSON( json, document ) {
-		return new InsertOperation( Position.fromJSON( json.position, document ), NodeList.fromJSON( json.nodeList ), json.baseVersion );
+		let children = [];
+
+		for ( let child of json.nodes ) {
+			if ( child.name ) {
+				// If child has name property, it is an Element.
+				children.push( Element.fromJSON( child ) );
+			} else {
+				// Otherwise, it is a Text node.
+				children.push( Text.fromJSON( child ) );
+			}
+		}
+
+		return new InsertOperation( Position.fromJSON( json.position, document ), children, json.baseVersion );
 	}
 }

+ 24 - 26
packages/ckeditor5-engine/src/model/operation/moveoperation.js

@@ -3,48 +3,47 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Operation from './operation.js';
 import Position from '../position.js';
 import Range from '../range.js';
 import CKEditorError from '../../../utils/ckeditorerror.js';
 import compareArrays from '../../../utils/comparearrays.js';
+import writer from './../writer.js';
 
 /**
- * Operation to move list of subsequent nodes from one position in the document to another.
+ * Operation to move a range of {@link engine.model.Item model items} to given {@link engine.model.Position target position}.
  *
  * @memberOf engine.model.operation
- * @extends engine.model.operation.Operation
  */
 export default class MoveOperation extends Operation {
 	/**
 	 * Creates a move operation.
 	 *
-	 * @param {engine.model.Position} sourcePosition Position before the first node to move.
-	 * @param {Number} howMany How many consecutive nodes to move, starting from `sourcePosition`.
-	 * @param {engine.model.Position} targetPosition Position where moved nodes will be inserted.
+	 * @param {engine.model.Position} sourcePosition Position before the first {@link engine.model.Item model item} to move.
+	 * @param {Number} howMany Offset size of moved range. Moved range will start from `sourcePosition` and end at
+	 * `sourcePosition` with offset shifted by `howMany`.
+	 * @param {engine.model.Position} targetPosition Position at which moved nodes will be inserted.
 	 * @param {Number} baseVersion {@link engine.model.Document#version} on which operation can be applied.
 	 */
 	constructor( sourcePosition, howMany, targetPosition, baseVersion ) {
 		super( baseVersion );
 
 		/**
-		 * Source move position.
+		 * Position before the first {@link engine.model.Item model item} to move.
 		 *
 		 * @member {engine.model.Position} engine.model.operation.MoveOperation#sourcePosition
 		 */
 		this.sourcePosition = Position.createFromPosition( sourcePosition );
 
 		/**
-		 * How many nodes to move.
+		 * Offset size of moved range.
 		 *
 		 * @member {Number} engine.model.operation.MoveOperation#howMany
 		 */
 		this.howMany = howMany;
 
 		/**
-		 * Target move position.
+		 * Position at which moved nodes will be inserted.
 		 *
 		 * @member {engine.model.Position} engine.model.operation.MoveOperation#targetPosition
 		 */
@@ -64,7 +63,7 @@ export default class MoveOperation extends Operation {
 		 *
 		 * @member {engine.model.Position} engine.model.operation.MoveOperation#movedRangeStart
 		 */
-		this.movedRangeStart = this.targetPosition.getTransformedByDeletion( this.sourcePosition, this.howMany );
+		this.movedRangeStart = this.targetPosition._getTransformedByDeletion( this.sourcePosition, this.howMany );
 
 		/**
 		 * Defines whether `MoveOperation` is sticky. If `MoveOperation` is sticky, during
@@ -77,11 +76,15 @@ export default class MoveOperation extends Operation {
 		this.isSticky = false;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	get type() {
 		return 'move';
 	}
 
 	/**
+	 * @inheritDoc
 	 * @returns {engine.model.operation.MoveOperation}
 	 */
 	clone() {
@@ -92,10 +95,11 @@ export default class MoveOperation extends Operation {
 	}
 
 	/**
+	 * @inheritDoc
 	 * @returns {engine.model.operation.MoveOperation}
 	 */
 	getReversed() {
-		let newTargetPosition = this.sourcePosition.getTransformedByInsertion( this.targetPosition, this.howMany );
+		let newTargetPosition = this.sourcePosition._getTransformedByInsertion( this.targetPosition, this.howMany );
 
 		const op = new this.constructor( this.movedRangeStart, this.howMany, newTargetPosition, this.baseVersion + 1 );
 		op.isSticky = this.isSticky;
@@ -103,6 +107,9 @@ export default class MoveOperation extends Operation {
 		return op;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	_execute() {
 		let sourceElement = this.sourcePosition.parent;
 		let targetElement = this.targetPosition.parent;
@@ -121,7 +128,7 @@ export default class MoveOperation extends Operation {
 			throw new CKEditorError(
 				'operation-move-position-invalid: Source position or target position is invalid.'
 			);
-		} else if ( sourceOffset + this.howMany > sourceElement.getChildCount() ) {
+		} else if ( sourceOffset + this.howMany > sourceElement.getMaxOffset() ) {
 			/**
 			 * The nodes which should be moved do not exist.
 			 *
@@ -140,7 +147,7 @@ export default class MoveOperation extends Operation {
 				'operation-move-range-into-itself: Trying to move a range of nodes to the inside of that range.'
 			);
 		} else if ( this.sourcePosition.root == this.targetPosition.root ) {
-			if ( compareArrays( this.sourcePosition.getParentPath(), this.targetPosition.getParentPath() ) == 'PREFIX' ) {
+			if ( compareArrays( this.sourcePosition.getParentPath(), this.targetPosition.getParentPath() ) == 'prefix' ) {
 				let i = this.sourcePosition.path.length - 1;
 
 				if ( this.targetPosition.path[ i ] >= sourceOffset && this.targetPosition.path[ i ] < sourceOffset + this.howMany ) {
@@ -155,21 +162,12 @@ export default class MoveOperation extends Operation {
 				}
 			}
 		}
-		// End of validation.
-
-		// If we move children in the same element and we remove elements on the position before the target we
-		// need to update a target offset.
-		if ( sourceElement === targetElement && sourceOffset < targetOffset ) {
-			targetOffset -= this.howMany;
-		}
-
-		const removedNodes = sourceElement.removeChildren( sourceOffset, this.howMany );
 
-		targetElement.insertChildren( targetOffset, removedNodes );
+		const range = writer.move( Range.createFromPositionAndShift( this.sourcePosition, this.howMany ), this.targetPosition );
 
 		return {
 			sourcePosition: this.sourcePosition,
-			range: Range.createFromPositionAndShift( this.movedRangeStart, this.howMany )
+			range: range
 		};
 	}
 
@@ -181,7 +179,7 @@ export default class MoveOperation extends Operation {
 	}
 
 	/**
-	 * Creates MoveOperation object from deserilized object, i.e. from parsed JSON string.
+	 * Creates `MoveOperation` object from deserilized object, i.e. from parsed JSON string.
 	 *
 	 * @param {Object} json Deserialized JSON object.
 	 * @param {engine.model.Document} document Document on which this operation will be applied.

+ 7 - 5
packages/ckeditor5-engine/src/model/operation/nooperation.js

@@ -3,14 +3,11 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Operation from './operation.js';
 
 /**
- * Operation which is doing nothing ("empty operation", "do-nothing operation", "noop").
- * This is an operation, which when executed does not change the tree model.
- * It still has some parameters defined for transformation purposes.
+ * Operation which is doing nothing ("empty operation", "do-nothing operation", "noop"). This is an operation,
+ * which when executed does not change the tree model. It still has some parameters defined for transformation purposes.
  *
  * In most cases this operation is a result of transforming operations. When transformation returns
  * {@link engine.model.operation.NoOperation} it means that changes done by the transformed operation
@@ -21,6 +18,7 @@ import Operation from './operation.js';
  */
 export default class NoOperation extends Operation {
 	/**
+	 * @inheritDoc
 	 * @returns {engine.model.operation.NoOperation}
 	 */
 	clone() {
@@ -28,12 +26,16 @@ export default class NoOperation extends Operation {
 	}
 
 	/**
+	 * @inheritDoc
 	 * @returns {engine.model.operation.NoOperation}
 	 */
 	getReversed() {
 		return new NoOperation( this.baseVersion + 1 );
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	_execute() {
 		// Do nothing.
 	}

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

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import clone from '../../../utils/lib/lodash/clone.js';
 
 /**

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

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import AttributeOperation from '../operation/attributeoperation.js';
 import InsertOperation from '../operation/insertoperation.js';
 import MoveOperation from '../operation/moveoperation.js';
@@ -32,7 +30,7 @@ operations[ RootAttributeOperation.className ] = RootAttributeOperation;
  */
 export default class OperationFactory {
 	/**
-	 * Creates concrete Operation object from deserilized object, i.e. from parsed JSON string.
+	 * Creates concrete `Operation` object from deserilized object, i.e. from parsed JSON string.
 	 *
 	 * @param {Object} json Deserialized JSON object.
 	 * @param {engine.model.Document} document Document on which this operation will be applied.

+ 11 - 11
packages/ckeditor5-engine/src/model/operation/reinsertoperation.js

@@ -3,26 +3,19 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import MoveOperation from './moveoperation.js';
 import RemoveOperation from './removeoperation.js';
 
 /**
- * Operation to reinsert previously removed nodes back to the non-graveyard root.
- * This is basically {@link engine.model.operation.MoveOperation} but it returns
- * {@link engine.model.operation.RemoveOperation} when reversed.
- *
- * With this class, we achieve two goals: by having separate classes it's easier to distinguish whether move
- * operation is actually a remove/reinsert operation and fire proper events. Also it
- * will be easier to expand if we need to change operation's behavior if it is remove/reinsert.
+ * Operation to reinsert previously removed nodes back to the non-graveyard root. This operation acts like
+ * {@link engine.model.operation.MoveOperation} but it returns {@link engine.model.operation.RemoveOperation} when reversed
+ * and fires different change event.
  *
  * @memberOf engine.model.operation
- * @extends engine.model.operation.Operation
  */
 export default class ReinsertOperation extends MoveOperation {
 	/**
-	 * Position where re-inserted node will be inserted.
+	 * Position where nodes will be re-inserted.
 	 *
 	 * @type {engine.model.Position}
 	 */
@@ -30,15 +23,22 @@ export default class ReinsertOperation extends MoveOperation {
 		return this.targetPosition;
 	}
 
+	/**
+	 * @param {engine.model.Position} pos
+	 */
 	set position( pos ) {
 		this.targetPosition = pos;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	get type() {
 		return 'reinsert';
 	}
 
 	/**
+	 * @inheritDoc
 	 * @returns {engine.model.operation.RemoveOperation}
 	 */
 	getReversed() {

+ 14 - 10
packages/ckeditor5-engine/src/model/operation/removeoperation.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import MoveOperation from './moveoperation.js';
 import Position from '../position.js';
 import Element from '../element.js';
@@ -14,21 +12,21 @@ import ReinsertOperation from './reinsertoperation.js';
  * Operation to remove a range of nodes.
  *
  * @memberOf engine.model.operation
- * @extends engine.model.operation.Operation
  */
 export default class RemoveOperation extends MoveOperation {
 	/**
-	 *
 	 * Creates a remove operation.
 	 *
-	 * @param {engine.model.Position} position Position before the first node to remove.
-	 * @param {Number} howMany How many nodes to remove.
+	 * @param {engine.model.Position} position Position before the first {@link engine.model.Item model item} to remove.
+	 * @param {Number} howMany Offset size of removed range. {@link engine.model.Item Model items} will be removed starting
+	 * from `sourcePosition`, up to a `sourcePosition` with offset shifted by `howMany`.
 	 * @param {Number} baseVersion {@link engine.model.Document#version} on which operation can be applied.
 	 */
 	constructor( position, howMany, baseVersion ) {
 		const graveyard = position.root.document.graveyard;
+		const graveyardPosition = new Position( graveyard, [ graveyard.getMaxOffset(), 0 ] );
 
-		super( position, howMany, new Position( graveyard, [ graveyard.getChildCount(), 0 ] ), baseVersion );
+		super( position, howMany, graveyardPosition, baseVersion );
 	}
 
 	/**
@@ -59,9 +57,11 @@ export default class RemoveOperation extends MoveOperation {
 	}
 
 	/**
-	 * Flag informing whether this operation should insert "holder" element (`true`) or should remove nodes
-	 * into existing "holder" element (`false`). It is `true` for each `RemoveOperation` that is the first `RemoveOperation`
-	 * in it's delta which points to given holder element.
+	 * Flag informing whether this operation should insert "holder" element (`true`) or should move removed nodes
+	 * into existing "holder" element (`false`).
+	 *
+	 * It is `true` for each `RemoveOperation` that is the first `RemoveOperation` in it's delta that points to given holder element.
+	 * This way only one `RemoveOperation` in given delta will insert "holder" element.
 	 *
 	 * @protected
 	 * @type {Boolean}
@@ -90,6 +90,7 @@ export default class RemoveOperation extends MoveOperation {
 	}
 
 	/**
+	 * @inheritDoc
 	 * @returns {engine.model.operation.ReinsertOperation}
 	 */
 	getReversed() {
@@ -97,6 +98,7 @@ export default class RemoveOperation extends MoveOperation {
 	}
 
 	/**
+	 * @inheritDoc
 	 * @returns {engine.model.operation.RemoveOperation}
 	 */
 	clone() {
@@ -111,6 +113,7 @@ export default class RemoveOperation extends MoveOperation {
 	 * @inheritDoc
 	 */
 	_execute() {
+		// Insert "holder" element in graveyard root, if the operation needs it.
 		if ( this._needsHolderElement ) {
 			const graveyard = this.targetPosition.root;
 			const holderElement = new Element( '$graveyardHolder' );
@@ -118,6 +121,7 @@ export default class RemoveOperation extends MoveOperation {
 			graveyard.insertChildren( this.targetPosition.path[ 0 ], holderElement );
 		}
 
+		// Then, execute as a move operation.
 		return super._execute();
 	}
 

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

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Operation from './operation.js';
 import CKEditorError from '../../../utils/ckeditorerror.js';
 

+ 18 - 20
packages/ckeditor5-engine/src/model/operation/transform.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import InsertOperation from './insertoperation.js';
 import AttributeOperation from './attributeoperation.js';
 import RootAttributeOperation from './rootattributeoperation.js';
@@ -16,10 +14,8 @@ import isEqual from '../../../utils/lib/lodash/isEqual.js';
 import compareArrays from '../../../utils/comparearrays.js';
 
 /**
- * Transforms given {@link engine.model.operation.Operation operation} by another
- * {@link engine.model.operation.Operation operation} and
- * returns the result of that transformation as an array containing one or more
- * {@link engine.model.operation.Operation operation} elements.
+ * Transforms given {@link engine.model.operation.Operation operation} by another {@link engine.model.operation.Operation operation}
+ * and returns the result of that transformation as an array containing one or more {@link engine.model.operation.Operation operations}.
  *
  * Operations work on specified positions, passed to them when they are created. Whenever {@link engine.model.Document document}
  * changes, we have to reflect those modifications by updating or "transforming" operations which are not yet applied.
@@ -67,7 +63,7 @@ const ot = {
 			const transformed = a.clone();
 
 			// Transform insert position by the other operation position.
-			transformed.position = transformed.position.getTransformedByInsertion( b.position, b.nodeList.length, !isStrong );
+			transformed.position = transformed.position._getTransformedByInsertion( b.position, b.nodes.totalOffset, !isStrong );
 
 			return [ transformed ];
 		},
@@ -82,7 +78,7 @@ const ot = {
 			const transformed = a.clone();
 
 			// Transform insert position by the other operation parameters.
-			transformed.position = a.position.getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !isStrong, b.isSticky );
+			transformed.position = a.position._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !isStrong, b.isSticky );
 
 			return [ transformed ];
 		}
@@ -92,7 +88,7 @@ const ot = {
 		// Transforms AttributeOperation `a` by InsertOperation `b`. Returns results as an array of operations.
 		InsertOperation( a, b ) {
 			// Transform this operation's range.
-			const ranges = a.range.getTransformedByInsertion( b.position, b.nodeList.length, true, false );
+			const ranges = a.range._getTransformedByInsertion( b.position, b.nodes.totalOffset, true, false );
 
 			// Map transformed range(s) to operations and return them.
 			return ranges.reverse().map( ( range ) => {
@@ -171,15 +167,15 @@ const ot = {
 				// 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.
 				// 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 );
+				difference.start = difference.start._getTransformedByDeletion( b.sourcePosition, b.howMany );
+				difference.end = difference.end._getTransformedByDeletion( b.sourcePosition, b.howMany );
 
 				// 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
 				// 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.
-				ranges = difference.getTransformedByInsertion( b.movedRangeStart, b.howMany, true, false ).reverse();
+				ranges = difference._getTransformedByInsertion( b.movedRangeStart, b.howMany, true, false ).reverse();
 			}
 
 			if ( common !== null ) {
@@ -225,12 +221,14 @@ const ot = {
 		InsertOperation( a, b, isStrong ) {
 			// Create range from MoveOperation properties and transform it by insertion.
 			let range = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
-			range = range.getTransformedByInsertion( b.position, b.nodeList.length, false, a.isSticky )[ 0 ];
+			range = range._getTransformedByInsertion( b.position, b.nodes.totalOffset, false, a.isSticky )[ 0 ];
 
 			let result = new a.constructor(
 				range.start,
 				range.end.offset - range.start.offset,
-				a instanceof RemoveOperation ? a.baseVersion : a.targetPosition.getTransformedByInsertion( b.position, b.nodeList.length, !isStrong ),
+				a instanceof RemoveOperation ?
+					a.baseVersion :
+					a.targetPosition._getTransformedByInsertion( b.position, b.nodes.totalOffset, !isStrong ),
 				a instanceof RemoveOperation ? undefined : a.baseVersion
 			);
 
@@ -289,8 +287,8 @@ const ot = {
 			let difference = joinRanges( rangeA.getDifference( rangeB ) );
 
 			if ( difference ) {
-				difference.start = difference.start.getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !a.isSticky, false );
-				difference.end = difference.end.getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, a.isSticky, false );
+				difference.start = difference.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !a.isSticky, false );
+				difference.end = difference.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, a.isSticky, false );
 
 				ranges.push( difference );
 			}
@@ -322,7 +320,7 @@ const ot = {
 			let aIsInside = rangeB.containsRange( rangeA ) &&
 				( rangeB.containsPosition( a.targetPosition ) || rangeB.start.isEqual( a.targetPosition ) || rangeB.end.isEqual( a.targetPosition ) );
 
-			if ( common !== null && ( aCompB === 'EXTENSION' || ( aCompB === 'SAME' && isStrong ) || aIsInside ) && !bTargetsToA ) {
+			if ( common !== null && ( aCompB === 'extension' || ( aCompB === 'same' && isStrong ) || aIsInside ) && !bTargetsToA ) {
 				// 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.
 				// Instead, we calculate the new position of that part of original range.
@@ -344,7 +342,7 @@ const ot = {
 			}
 
 			// Target position also could be affected by the other MoveOperation. We will transform it.
-			let newTargetPosition = a.targetPosition.getTransformedByMove(
+			let newTargetPosition = a.targetPosition._getTransformedByMove(
 				b.sourcePosition,
 				b.targetPosition,
 				b.howMany,
@@ -424,7 +422,7 @@ function updateBaseVersions( baseVersion, operations ) {
 
 // Checks whether MoveOperation targetPosition is inside a node from the moved range of the other MoveOperation.
 function moveTargetIntoMovedRange( a, b ) {
-	return a.targetPosition.getTransformedByDeletion( b.sourcePosition, b.howMany ) === null;
+	return a.targetPosition._getTransformedByDeletion( b.sourcePosition, b.howMany ) === null;
 }
 
 // Gets an array of Ranges and produces one Range out of it. The root of a new range will be same as

+ 308 - 264
packages/ckeditor5-engine/src/model/position.js

@@ -3,17 +3,29 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import DocumentFragment from './documentfragment.js';
 import Element from './element.js';
 import last from '../../utils/lib/lodash/last.js';
 import compareArrays from '../../utils/comparearrays';
 import CKEditorError from '../../utils/ckeditorerror.js';
+import Text from './text.js';
 
 /**
- * Position in the tree. Position is always located before or after a node.
- * See {@link #path} property for more information.
+ * Represents a position in the model tree.
+ *
+ * Since position in a model is represented by a {@link engine.model.Position#root position root} and
+ * {@link engine.model.Position#path position path} it is possible to create positions placed in non-existing elements.
+ * This requirement is important for {@link engine.model.operation.transfrom operational transformation}.
+ *
+ * Also, {@link engine.model.operation.Operation operations} kept in {@link engine.model.Document#history document history}
+ * are storing positions (and ranges) which were correct when those operations were applied, but may not be correct
+ * after document got changed.
+ *
+ * When changes are applied to model, it may also happen that {@link engine.model.Position#parent position parent} will change
+ * even if position path has not changed. Keep in mind, that if a position leads to non-existing element,
+ * {@link engine.model.Position#parent} and some other properties and methods will throw errors.
+ *
+ * In most cases, position with wrong path is caused by an error in code, but it is sometimes needed, as described above.
  *
  * @memberOf engine.model
  */
@@ -21,9 +33,8 @@ export default class Position {
 	/**
 	 * Creates a position.
 	 *
-	 * @param {engine.model.Element|engine.model.DocumentFragment} root
-	 * Root of the position path. Element (most often a {@link engine.model.RootElement}) or a document fragment.
-	 * @param {Array.<Number>} path Position path. See {@link engine.model.Position#path} property for more information.
+	 * @param {engine.model.Element|engine.model.DocumentFragment} root Root of the position.
+	 * @param {Array.<Number>} path Position path. See {@link engine.model.Position#path}.
 	 */
 	constructor( root, path ) {
 		if ( !( root instanceof Element ) && !( root instanceof DocumentFragment ) ) {
@@ -58,19 +69,31 @@ export default class Position {
 		this.root = root;
 
 		/**
-		 * Position of the node it the tree. Must contain at least one item. For example:
+		 * Position of the node it the tree.
 		 *
-		 *		 root
-		 *		  |- p         Before: [ 0 ]       After: [ 1 ]
-		 *		  |- ul        Before: [ 1 ]       After: [ 2 ]
-		 *		     |- li     Before: [ 1, 0 ]    After: [ 1, 1 ]
-		 *		     |  |- f   Before: [ 1, 0, 0 ] After: [ 1, 0, 1 ]
-		 *		     |  |- o   Before: [ 1, 0, 1 ] After: [ 1, 0, 2 ]
-		 *		     |  |- o   Before: [ 1, 0, 2 ] After: [ 1, 0, 3 ]
-		 *		     |- li     Before: [ 1, 1 ]    After: [ 1, 2 ]
-		 *		        |- b   Before: [ 1, 1, 0 ] After: [ 1, 1, 1 ]
-		 *		        |- a   Before: [ 1, 1, 1 ] After: [ 1, 1, 2 ]
-		 *		        |- r   Before: [ 1, 1, 2 ] After: [ 1, 1, 3 ]
+		 * Position can be placed before, after or in a {@link engine.model.Node node} if that node has
+		 * {@link engine.model.Node#offsetSize} greater than `1`. Items in position path are
+		 * {@link engine.model.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 engine.model.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>} engine.model.Position#path
 		 */
@@ -78,29 +101,54 @@ export default class Position {
 	}
 
 	/**
-	 * Node directly after the position.
+	 * Position {@link engine.model.Position#offset offset} converted to an index in position's parent node. It is
+	 * equal to the {@link engine.model.Node#getIndex index} of a node after this position. If position is placed
+	 * in text node, position index is equal to the index of that text node.
+	 *
+	 * @readonly
+	 * @type {Number}
+	 */
+	get index() {
+		return this.parent.offsetToIndex( this.offset );
+	}
+
+	/**
+	 * Returns {@link engine.model.Text text node} instance in which this position is placed or `null` if this
+	 * position is not in a text node.
 	 *
 	 * @readonly
-	 * @type {engine.model.Node}
+	 * @type {engine.model.Text|null}
+	 */
+	get textNode() {
+		let node = this.parent.getChild( this.index );
+
+		return ( node instanceof Text && node.startOffset < this.offset ) ? node : null;
+	}
+
+	/**
+	 * Node directly after this position or `null` if this position is in text node.
+	 *
+	 * @readonly
+	 * @type {engine.model.Node|null}
 	 */
 	get nodeAfter() {
-		return this.parent.getChild( this.offset ) || null;
+		return this.textNode === null ? this.parent.getChild( this.index ) : null;
 	}
 
 	/**
-	 * Node directly before the position.
+	 * Node directly before this position or `null` if this position is in text node.
 	 *
 	 * @readonly
 	 * @type {Node}
 	 */
 	get nodeBefore() {
-		return this.parent.getChild( this.offset - 1 ) || null;
+		return this.textNode === null ? this.parent.getChild( this.index - 1 ) : null;
 	}
 
 	/**
-	 * Offset at which the position is located in the {@link #parent}.
+	 * Offset at which this position is located in it's {@link engine.model.Position#parent parent}. It is equal
+	 * to the last item in position {@link engine.model.Position#path path}.
 	 *
-	 * @readonly
 	 * @type {Number}
 	 */
 	get offset() {
@@ -108,8 +156,6 @@ export default class Position {
 	}
 
 	/**
-	 * Sets offset in the parent, which is the last element of the path.
-	 *
 	 * @param {Number} newOffset
 	 */
 	set offset( newOffset ) {
@@ -117,7 +163,12 @@ export default class Position {
 	}
 
 	/**
-	 * Parent element of the position. The position is located at {@link #offset} in this element.
+	 * Parent element of this position.
+	 *
+	 * Keep in mind that `parent` value is calculated when the property is accessed. If {@link engine.model.Position#path position path}
+	 * leads to a non-existing element, `parent` property will throw error.
+	 *
+	 * Also it is a good idea to cache `parent` property if it is used frequently in an algorithm (i.e. in a long loop).
 	 *
 	 * @readonly
 	 * @type {engine.model.Element}
@@ -125,10 +176,8 @@ export default class Position {
 	get parent() {
 		let parent = this.root;
 
-		let i, len;
-
-		for ( i = 0, len = this.path.length - 1; i < len; i++ ) {
-			parent = parent.getChild( this.path[ i ] );
+		for ( let i = 0; i < this.path.length - 1; i++ ) {
+			parent = parent.getChild( parent.offsetToIndex( this.path[ i ] ) );
 		}
 
 		return parent;
@@ -142,45 +191,47 @@ export default class Position {
 	 */
 	compareWith( otherPosition ) {
 		if ( this.root != otherPosition.root ) {
-			return 'DIFFERENT';
+			return 'different';
 		}
 
 		const result = compareArrays( this.path, otherPosition.path );
 
 		switch ( result ) {
-			case 'SAME':
-				return 'SAME';
+			case 'same':
+				return 'same';
 
-			case 'PREFIX':
-				return 'BEFORE';
+			case 'prefix':
+				return 'before';
 
-			case 'EXTENSION':
-				return 'AFTER';
+			case 'extension':
+				return 'after';
 
 			default:
 				if ( this.path[ result ] < otherPosition.path[ result ] ) {
-					return 'BEFORE';
+					return 'before';
 				} else {
-					return 'AFTER';
+					return 'after';
 				}
 		}
 	}
 
 	/**
-	 * Returns the path to the parent, which is the {@link engine.model.Position#path} without the last element.
+	 * Returns a path to this position's parent. Parent path is equal to position {@link engine.model.Position#path path}
+	 * but without the last item.
 	 *
 	 * This method returns the parent path even if the parent does not exists.
 	 *
-	 * @returns {Number[]} Path to the parent.
+	 * @returns {Array.<Number>} Path to the parent.
 	 */
 	getParentPath() {
 		return this.path.slice( 0, -1 );
 	}
 
 	/**
-	 * Returns a new instance of Position with offset incremented by `shift` value.
+	 * Returns a new instance of `Position`, that has same {@link engine.model.Position#parent parent} but it's offset
+	 * is shifted by `shift` value (can be a negative value).
 	 *
-	 * @param {Number} shift How position offset should get changed. Accepts negative values.
+	 * @param {Number} shift Offset shift. Can be a negative value.
 	 * @returns {engine.model.Position} Shifted position.
 	 */
 	getShiftedBy( shift ) {
@@ -193,7 +244,139 @@ export default class Position {
 	}
 
 	/**
-	 * Returns this position after being updated by removing `howMany` nodes starting from `deletePosition`.
+	 * Checks whether this position is after given position.
+	 *
+	 * @see engine.model.Position#isBefore
+	 *
+	 * @param {engine.model.Position} otherPosition Position to compare with.
+	 * @returns {Boolean} True if this position is after given position.
+	 */
+	isAfter( otherPosition ) {
+		return this.compareWith( otherPosition ) == 'after';
+	}
+
+	/**
+	 * Checks whether this position is before given position.
+	 *
+	 * **Note:** watch out when using negation of the value returned by this method, because the negation will also
+	 * be `true` if positions are in different roots and you might not expect this. You should probably use
+	 * `a.isAfter( b ) || a.isEqual( b )` or `!a.isBefore( p ) && a.root == b.root` in most scenarios. If your
+	 * condition uses multiple `isAfter` and `isBefore` checks, build them so they do not use negated values, i.e.:
+	 *
+	 *		if ( a.isBefore( b ) && c.isAfter( d ) ) {
+	 *			// do A.
+	 *		} else {
+	 *			// do B.
+	 *		}
+	 *
+	 * or, if you have only one if-branch:
+	 *
+	 *		if ( !( a.isBefore( b ) && c.isAfter( d ) ) {
+	 *			// do B.
+	 *		}
+	 *
+	 * rather than:
+	 *
+	 *		if ( !a.isBefore( b ) || && !c.isAfter( d ) ) {
+	 *			// do B.
+	 *		} else {
+	 *			// do A.
+	 *		}
+	 *
+	 * @param {engine.model.Position} otherPosition Position to compare with.
+	 * @returns {Boolean} True if this position is before given position.
+	 */
+	isBefore( otherPosition ) {
+		return this.compareWith( otherPosition ) == 'before';
+	}
+
+	/**
+	 * Checks whether this position is equal to given position.
+	 *
+	 * @param {engine.model.Position} otherPosition Position to compare with.
+	 * @returns {Boolean} True if positions are same.
+	 */
+	isEqual( otherPosition ) {
+		return this.compareWith( otherPosition ) == 'same';
+	}
+
+	/**
+	 * Checks whether this position is touching given position. Positions touch when there are no text nodes
+	 * or empty nodes in a range between them. Technically, those positions are not equal but in many cases
+	 * they are very similar or even indistinguishable.
+	 *
+	 * @param {engine.model.Position} otherPosition Position to compare with.
+	 * @returns {Boolean} True if positions touch.
+	 */
+	isTouching( otherPosition ) {
+		let left = null;
+		let right = null;
+		let compare = this.compareWith( otherPosition );
+
+		switch ( compare ) {
+			case 'same':
+				return true;
+
+			case 'before':
+				left = Position.createFromPosition( this );
+				right = Position.createFromPosition( otherPosition );
+				break;
+
+			case 'after':
+				left = Position.createFromPosition( otherPosition );
+				right = Position.createFromPosition( this );
+				break;
+
+			default:
+				return false;
+		}
+
+		// Cached for optimization purposes.
+		let leftParent = left.parent;
+
+		while ( left.path.length + right.path.length ) {
+			if ( left.isEqual( right ) ) {
+				return true;
+			}
+
+			if ( left.path.length > right.path.length ) {
+				if ( left.offset !== leftParent.getMaxOffset() ) {
+					return false;
+				}
+
+				left.path = left.path.slice( 0, -1 );
+				leftParent = leftParent.parent;
+				left.offset++;
+			} else {
+				if ( right.offset !== 0 ) {
+					return false;
+				}
+
+				right.path = right.path.slice( 0, -1 );
+			}
+		}
+	}
+
+	/**
+	 * Returns `true` if position is at the beginning of its {@link engine.model.Position#parent parent}, `false` otherwise.
+	 *
+	 * @returns {Boolean}
+	 */
+	isAtStart() {
+		return this.offset === 0;
+	}
+
+	/**
+	 * Returns `true` if position is at the end of its {@link engine.model.Position#parent parent}, `false` otherwise.
+	 *
+	 * @returns {Boolean}
+	 */
+	isAtEnd() {
+		return this.offset == this.parent.getMaxOffset();
+	}
+
+	/**
+	 * 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.
 	 *
 	 * @protected
@@ -201,7 +384,7 @@ export default class Position {
 	 * @param {Number} howMany How many nodes are removed.
 	 * @returns {engine.model.Position|null} Transformed position or `null`.
 	 */
-	getTransformedByDeletion( deletePosition, howMany ) {
+	_getTransformedByDeletion( deletePosition, howMany ) {
 		let transformed = Position.createFromPosition( this );
 
 		// This position can't be affected if deletion was in a different root.
@@ -209,7 +392,7 @@ export default class Position {
 			return transformed;
 		}
 
-		if ( compareArrays( deletePosition.getParentPath(), this.getParentPath() ) == 'SAME' ) {
+		if ( compareArrays( deletePosition.getParentPath(), this.getParentPath() ) == 'same' ) {
 			// If nodes are removed from the node that is pointed by this position...
 			if ( deletePosition.offset < this.offset ) {
 				// And are removed from before an offset of that position...
@@ -221,7 +404,7 @@ export default class Position {
 					transformed.offset -= howMany;
 				}
 			}
-		} else if ( compareArrays( deletePosition.getParentPath(), this.getParentPath() ) == 'PREFIX' ) {
+		} else if ( compareArrays( deletePosition.getParentPath(), this.getParentPath() ) == 'prefix' ) {
 			// If nodes are removed from a node that is on a path to this position...
 			const i = deletePosition.path.length - 1;
 
@@ -242,7 +425,7 @@ export default class Position {
 	}
 
 	/**
-	 * Returns this position after being updated by inserting `howMany` nodes at `insertPosition`.
+	 * Returns a copy of this position that is updated by inserting `howMany` nodes at `insertPosition`.
 	 *
 	 * @protected
 	 * @param {engine.model.Position} insertPosition Position where nodes are inserted.
@@ -252,7 +435,7 @@ export default class Position {
 	 * set to `true`, this position will get transformed. If the flag is set to `false`, it won't.
 	 * @returns {engine.model.Position} Transformed position.
 	 */
-	getTransformedByInsertion( insertPosition, howMany, insertBefore ) {
+	_getTransformedByInsertion( insertPosition, howMany, insertBefore ) {
 		let transformed = Position.createFromPosition( this );
 
 		// This position can't be affected if insertion was in a different root.
@@ -260,14 +443,14 @@ export default class Position {
 			return transformed;
 		}
 
-		if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'SAME' ) {
+		if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'same' ) {
 			// If nodes are inserted in the node that is pointed by this position...
 			if ( insertPosition.offset < this.offset || ( insertPosition.offset == this.offset && insertBefore ) ) {
 				// And are inserted before an offset of that position...
 				// "Push" this positions offset.
 				transformed.offset += 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...
 			const i = insertPosition.path.length - 1;
 
@@ -282,7 +465,7 @@ export default class Position {
 	}
 
 	/**
-	 * Returns this position after being updated by moving `howMany` nodes from `sourcePosition` to `targetPosition`.
+	 * Returns a copy of this position that is updated by moving `howMany` nodes from `sourcePosition` to `targetPosition`.
 	 *
 	 * @protected
 	 * @param {engine.model.Position} sourcePosition Position before the first element to move.
@@ -295,12 +478,12 @@ export default class Position {
 	 * with the moved range if it is equal to one of range's boundaries.
 	 * @returns {engine.model.Position} Transformed position.
 	 */
-	getTransformedByMove( sourcePosition, targetPosition, howMany, insertBefore, sticky ) {
+	_getTransformedByMove( sourcePosition, targetPosition, howMany, insertBefore, sticky ) {
 		// Moving a range removes nodes from their original position. We acknowledge this by proper transformation.
-		let transformed = this.getTransformedByDeletion( sourcePosition, howMany );
+		let transformed = this._getTransformedByDeletion( sourcePosition, howMany );
 
 		// Then we update target position, as it could be affected by nodes removal too.
-		targetPosition = targetPosition.getTransformedByDeletion( sourcePosition, howMany );
+		targetPosition = targetPosition._getTransformedByDeletion( sourcePosition, howMany );
 
 		if ( transformed === null || ( sticky && transformed.isEqual( sourcePosition ) ) ) {
 			// This position is inside moved range (or sticks to it).
@@ -309,138 +492,55 @@ export default class Position {
 		} else {
 			// This position is not inside a removed range.
 			// In next step, we simply reflect inserting `howMany` nodes, which might further affect the position.
-			transformed = transformed.getTransformedByInsertion( targetPosition, howMany, insertBefore );
+			transformed = transformed._getTransformedByInsertion( targetPosition, howMany, insertBefore );
 		}
 
 		return transformed;
 	}
 
 	/**
-	 * Checks whether this position is after given position.
-	 *
-	 * @see engine.model.Position#isBefore
-	 *
-	 * @param {engine.model.Position} otherPosition Position to compare with.
-	 * @returns {Boolean} True if this position is after given position.
-	 */
-	isAfter( otherPosition ) {
-		return this.compareWith( otherPosition ) == 'AFTER';
-	}
-
-	/**
-	 * Checks whether this position is before given position.
-	 *
-	 * **Note:** watch out when using negation of the value returned by this method, because the negation will also
-	 * be `true` if positions are in different roots and you might not expect this. You should probably use
-	 * `a.isAfter( b ) || a.isEqual( b )` or `!a.isBefore( p ) && a.root == b.root` in most scenarios. If your
-	 * condition uses multiple `isAfter` and `isBefore` checks, build them so they do not use negated values, i.e.:
-	 *
-	 *		if ( a.isBefore( b ) && c.isAfter( d ) ) {
-	 *			// do A.
-	 *		} else {
-	 *			// do B.
-	 *		}
+	 * Returns a new position that is a combination of this position and given positions.
 	 *
-	 * or, if you have only one if-branch:
+	 * The combined position is a copy of this position transformed by moving a range starting at `source` position
+	 * to the `target` position. It is expected that this position is inside the moved range.
 	 *
-	 *		if ( !( a.isBefore( b ) && c.isAfter( d ) ) {
-	 *			// do B.
-	 *		}
+	 * Example:
 	 *
-	 * rather than:
+	 *		let original = new Position( root, [ 2, 3, 1 ] );
+	 *		let source = new Position( root, [ 2, 2 ] );
+	 *		let target = new Position( otherRoot, [ 1, 1, 3 ] );
+	 *		original._getCombined( source, target ); // path is [ 1, 1, 4, 1 ], root is `otherRoot`
 	 *
-	 *		if ( !a.isBefore( b ) || && !c.isAfter( d ) ) {
-	 *			// do B.
-	 *		} else {
-	 *			// do A.
-	 *		}
-	 *
-	 * @param {engine.model.Position} otherPosition Position to compare with.
-	 * @returns {Boolean} True if this position is before given position.
-	 */
-	isBefore( otherPosition ) {
-		return this.compareWith( otherPosition ) == 'BEFORE';
-	}
-
-	/**
-	 * Checks whether this position equals given position.
+	 * Explanation:
 	 *
-	 * @param {engine.model.Position} otherPosition Position to compare with.
-	 * @returns {Boolean} True if positions are same.
-	 */
-	isEqual( otherPosition ) {
-		return this.compareWith( otherPosition ) == 'SAME';
-	}
-
-	/**
-	 * Checks whether this position is touching given position. Positions touch when there are no characters
-	 * or empty nodes in a range between them. Technically, those positions are not equal but in many cases
-	 * they are very similar or even indistinguishable when they touch.
+	 * We have a position `[ 2, 3, 1 ]` and move some nodes from `[ 2, 2 ]` to `[ 1, 1, 3 ]`. The original position
+	 * was inside moved nodes and now should point to the new place. The moved nodes will be after
+	 * positions `[ 1, 1, 3 ]`, `[ 1, 1, 4 ]`, `[ 1, 1, 5 ]`. Since our position was in the second moved node,
+	 * the transformed position will be in a sub-tree of a node at `[ 1, 1, 4 ]`. Looking at original path, we
+	 * took care of `[ 2, 3 ]` part of it. Now we have to add the rest of the original path to the transformed path.
+	 * Finally, the transformed position will point to `[ 1, 1, 4, 1 ]`.
 	 *
-	 * @param {engine.model.Position} otherPosition Position to compare with.
-	 * @returns {Boolean} True if positions touch.
+	 * @protected
+	 * @param {engine.model.Position} source Beginning of the moved range.
+	 * @param {engine.model.Position} target Position where the range is moved.
+	 * @returns {engine.model.Position} Combined position.
 	 */
-	isTouching( otherPosition ) {
-		let left = null;
-		let right = null;
-		let compare = this.compareWith( otherPosition );
-
-		switch ( compare ) {
-			case 'SAME':
-				return true;
-
-			case 'BEFORE':
-				left = Position.createFromPosition( this );
-				right = Position.createFromPosition( otherPosition );
-				break;
-
-			case 'AFTER':
-				left = Position.createFromPosition( otherPosition );
-				right = Position.createFromPosition( this );
-				break;
-
-			default:
-				return false;
-		}
-
-		while ( left.path.length + right.path.length ) {
-			if ( left.isEqual( right ) ) {
-				return true;
-			}
+	_getCombined( source, target ) {
+		const i = source.path.length - 1;
 
-			if ( left.path.length > right.path.length ) {
-				if ( left.nodeAfter !== null ) {
-					return false;
-				}
+		// The first part of a path to combined position is a path to the place where nodes were moved.
+		let combined = Position.createFromPosition( target );
 
-				left.path = left.path.slice( 0, -1 );
-				left.offset++;
-			} else {
-				if ( right.nodeBefore !== null ) {
-					return false;
-				}
+		// Then we have to update the rest of the path.
 
-				right.path = right.path.slice( 0, -1 );
-			}
-		}
-	}
+		// 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;
 
-	/**
-	 * Whether position is at the beginning of its {@link engine.model.Position#parent}.
-	 *
-	 * @returns {Boolean}
-	 */
-	isAtStart() {
-		return this.offset === 0;
-	}
+		// Then, add the rest of the path.
+		// 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 ) );
 
-	/**
-	 * Whether position is at the end of its {@link engine.model.Position#parent}.
-	 *
-	 * @returns {Boolean}
-	 */
-	isAtEnd() {
-		return this.offset == this.parent.getChildCount();
+		return combined;
 	}
 
 	/**
@@ -448,8 +548,8 @@ export default class Position {
 	 *
 	 * * a {@link engine.model.Position position},
 	 * * parent element and offset (offset defaults to `0`),
-	 * * parent element and `'END'` (sets selection at the end of that element),
-	 * * node and `'BEFORE'` or `'AFTER'` (sets selection before or after the given node).
+	 * * parent element and `'end'` (sets position at the end of that element),
+	 * * {@link engine.model.Item model item} and `'before'` or `'after'` (sets position before or after given model item).
 	 *
 	 * This method is a shortcut to other constructors such as:
 	 *
@@ -458,23 +558,21 @@ export default class Position {
 	 * * {@link engine.model.Position.createFromParentAndOffset},
 	 * * {@link engine.model.Position.createFromPosition}.
 	 *
-	 * @param {engine.model.Node|engine.model.Position} nodeOrPosition
-	 * @param {Number|'END'|'BEFORE'|'AFTER'} [offset=0] Offset or one of the flags. Used only when
-	 * first parameter is a node.
+	 * @param {engine.model.Item|engine.model.Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * first parameter is a {@link engine.model.Item model item}.
 	 */
-	static createAt( nodeOrPosition, offset ) {
-		let node;
-
-		if ( nodeOrPosition instanceof Position ) {
-			return this.createFromPosition( nodeOrPosition );
+	static createAt( itemOrPosition, offset ) {
+		if ( itemOrPosition instanceof Position ) {
+			return this.createFromPosition( itemOrPosition );
 		} else {
-			node = nodeOrPosition;
+			const node = itemOrPosition;
 
-			if ( offset == 'END' ) {
-				offset = node.getChildCount();
-			} else if ( offset == 'BEFORE' ) {
+			if ( offset == 'end' ) {
+				offset = node.getMaxOffset();
+			} else if ( offset == 'before' ) {
 				return this.createBefore( node );
-			} else if ( offset == 'AFTER' ) {
+			} else if ( offset == 'after' ) {
 				return this.createAfter( node );
 			} else if ( !offset ) {
 				offset = 0;
@@ -485,54 +583,49 @@ export default class Position {
 	}
 
 	/**
-	 * Creates a new position after given node.
-	 *
-	 * @see {@link engine.model.TreeWalkerValue}
+	 * Creates a new position, after given {@link engine.model.Item model item}.
 	 *
-	 * @param {engine.model.Node} node Node the position should be directly after.
+	 * @param {engine.model.Item} item Item after which the position should be placed.
 	 * @returns {engine.model.Position}
 	 */
-	static createAfter( node ) {
-		if ( !node.parent ) {
+	static createAfter( item ) {
+		if ( !item.parent ) {
 			/**
 			 * You can not make position after root.
 			 *
 			 * @error position-after-root
-			 * @param {engine.model.Node} root
+			 * @param {engine.model.Item} root
 			 */
-			throw new CKEditorError( 'position-after-root: You can not make position after root.', { root: node } );
+			throw new CKEditorError( 'position-after-root: You can not make position after root.', { root: item } );
 		}
 
-		return this.createFromParentAndOffset( node.parent, node.getIndex() + 1 );
+		return this.createFromParentAndOffset( item.parent, item.endOffset );
 	}
 
 	/**
-	 * Creates a new position before the given node.
+	 * Creates a new position, before the given {@link engine.model.Item model item}.
 	 *
-	 * @see {@link engine.model.TreeWalkerValue}
-	 *
-	 * @param {engine.model.node} node Node the position should be directly before.
+	 * @param {engine.model.Item} item Item before which the position should be placed.
 	 * @returns {engine.model.Position}
 	 */
-	static createBefore( node ) {
-		if ( !node.parent ) {
+	static createBefore( item ) {
+		if ( !item.parent ) {
 			/**
 			 * You can not make position before root.
 			 *
 			 * @error position-before-root
-			 * @param {engine.model.Node} root
+			 * @param {engine.model.Item} root
 			 */
-			throw new CKEditorError( 'position-before-root: You can not make position before root.', { root: node } );
+			throw new CKEditorError( 'position-before-root: You can not make position before root.', { root: item } );
 		}
 
-		return this.createFromParentAndOffset( node.parent, node.getIndex() );
+		return this.createFromParentAndOffset( item.parent, item.startOffset );
 	}
 
 	/**
-	 * Creates a new position from the parent element and the offset in that element.
+	 * Creates a new position from the parent element and an offset in that element.
 	 *
-	 * @param {engine.model.Element|engine.model.DocumentFragment} parent Position's parent element or
-	 * document fragment.
+	 * @param {engine.model.Element|engine.model.DocumentFragment} parent Position's parent.
 	 * @param {Number} offset Position's offset.
 	 * @returns {engine.model.Position}
 	 */
@@ -543,7 +636,7 @@ export default class Position {
 			 *
 			 * @error position-parent-incorrect
 			 */
-			throw new CKEditorError( 'position-parent-incorrect: Position parent have to be a model element or model document fragment.' );
+			throw new CKEditorError( 'position-parent-incorrect: Position parent have to be a element or document fragment.' );
 		}
 
 		const path = parent.getPath();
@@ -554,7 +647,7 @@ export default class Position {
 	}
 
 	/**
-	 * Creates and returns a new instance of Position, which is equal to passed position.
+	 * Creates a new position, which is equal to passed position.
 	 *
 	 * @param {engine.model.Position} position Position to be cloned.
 	 * @returns {engine.model.Position}
@@ -564,11 +657,10 @@ export default class Position {
 	}
 
 	/**
-	 * Creates Element object from deserilized object, ie. from parsed JSON string.
+	 * Creates a `Position` instance from given plain object (i.e. parsed JSON string).
 	 *
-	 * @param {Object} json Deserialized JSON object.
-	 * @param {engine.model.Document} doc Document on which this operation will be applied.
-	 * @returns {engine.model.Position}
+	 * @param {Object} json Plain object to be converted to `Position`.
+	 * @returns {engine.model.Position} `Position` instance created using given plain object.
 	 */
 	static fromJSON( json, doc ) {
 		if ( json.root === '$graveyard' ) {
@@ -590,59 +682,11 @@ export default class Position {
 
 		return new Position( doc.getRoot( json.root ), json.path );
 	}
-
-	/**
-	 * Returns a new position that is a combination of this position and given positions. The combined
-	 * position is this position transformed by moving a range starting at `from` to `to` position.
-	 * It is expected that this position is inside the moved range.
-	 *
-	 * In other words, this method in a smart way "cuts out" `source` path from this position and
-	 * injects `target` path in it's place, while doing necessary fixes in order to get a correct path.
-	 *
-	 * Example:
-	 *
-	 * 	let original = new Position( root, [ 2, 3, 1 ] );
-	 * 	let source = new Position( root, [ 2, 2 ] );
-	 * 	let target = new Position( otherRoot, [ 1, 1, 3 ] );
-	 * 	let combined = original.getCombined( source, target );
-	 * 	// combined.path is [ 1, 1, 4, 1 ], combined.root is otherRoot
-	 *
-	 * Explanation:
-	 *
-	 * We have a position `[ 2, 3, 1 ]` and move some nodes from `[ 2, 2 ]` to `[ 1, 1, 3 ]`. The original position
-	 * was inside moved nodes and now should point to the new place. The moved nodes will be after
-	 * positions `[ 1, 1, 3 ]`, `[ 1, 1, 4 ]`, `[ 1, 1, 5 ]`. Since our position was in the second moved node,
-	 * the transformed position will be in a sub-tree of a node at `[ 1, 1, 4 ]`. Looking at original path, we
-	 * took care of `[ 2, 3 ]` part of it. Now we have to add the rest of the original path to the transformed path.
-	 * Finally, the transformed position will point to `[ 1, 1, 4, 1 ]`.
-	 *
-	 * @protected
-	 * @param {engine.model.Position} source Beginning of the moved range.
-	 * @param {engine.model.Position} target Position where the range is moved.
-	 * @returns {engine.model.Position} Combined position.
-	 */
-	_getCombined( source, target ) {
-		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.
-		let combined = Position.createFromPosition( target );
-
-		// 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.
-		combined.offset = combined.offset + this.path[ i ] - source.offset;
-
-		// Then, add the rest of the path.
-		// 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 ) );
-
-		return combined;
-	}
 }
 
 /**
- * A flag indicating whether this position is `'BEFORE'` or `'AFTER'` or `'SAME'` as given position.
- * If positions are in different roots `'DIFFERENT'` flag is returned.
+ * A flag indicating whether this position is `'before'` or `'after'` or `'same'` as given position.
+ * If positions are in different roots `'different'` flag is returned.
  *
  * @typedef {String} engine.model.PositionRelation
  */

+ 109 - 100
packages/ckeditor5-engine/src/model/range.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Position from './position.js';
 import TreeWalker from './treewalker.js';
 import compareArrays from '../../utils/comparearrays.js';
@@ -17,7 +15,8 @@ import compareArrays from '../../utils/comparearrays.js';
 export default class Range {
 	/**
 	 * Creates a range spanning from `start` position to `end` position.
-	 * **Note:** Constructor creates it's own {@link engine.model.Position} instances basing on passed values.
+	 *
+	 * **Note:** Constructor creates it's own {@link engine.model.Position Position} instances basing on passed values.
 	 *
 	 * @param {engine.model.Position} start Start position.
 	 * @param {engine.model.Position} end End position.
@@ -43,20 +42,12 @@ export default class Range {
 	/**
 	 * Returns an iterator that iterates over all {@link engine.model.Item items} that are in this range and returns
 	 * them together with additional information like length or {@link engine.model.Position positions},
-	 * grouped as {@link engine.model.TreeWalkerValue}. It iterates over all {@link engine.model.TextProxy texts}
-	 * that are inside the range and all the {@link engine.model.Element}s we enter into when iterating over this
-	 * range.
+	 * grouped as {@link engine.model.TreeWalkerValue}. It iterates over all {@link engine.model.TextProxy text contents}
+	 * that are inside the range and all the {@link engine.model.Element}s that are entered into when iterating over this range.
 	 *
-	 * **Note:** iterator will not return a parent node of start position. This is in contrary to
-	 * {@link engine.model.TreeWalker} which will return that node with `'ELEMENT_END'` type. Iterator also
-	 * returns each {@link engine.model.Element} once, while simply used {@link engine.model.TreeWalker} might
-	 * return it twice: for `'ELEMENT_START'` and `'ELEMENT_END'`.
+	 * This iterator uses {@link engine.model.TreeWalker} with `boundaries` set to this range and `ignoreElementEnd` option
+	 * set to `true`.
 	 *
-	 * **Note:** because iterator does not return {@link engine.model.TreeWalkerValue values} with the type of
-	 * `'ELEMENT_END'`, you can use {@link engine.model.TreeWalkerValue.previousPosition} as a position before the
-	 * item.
-	 *
-	 * @see engine.model.TreeWalker
 	 * @returns {Iterable.<engine.model.TreeWalkerValue>}
 	 */
 	*[ Symbol.iterator ]() {
@@ -64,7 +55,8 @@ export default class Range {
 	}
 
 	/**
-	 * Returns whether the range is collapsed, that is it start and end positions are equal.
+	 * Returns whether the range is collapsed, that is if {@link engine.model.Range#start start} and
+	 * {@link engine.model.Range#end end} positions are equal.
 	 *
 	 * @type {Boolean}
 	 */
@@ -73,7 +65,8 @@ export default class Range {
 	}
 
 	/**
-	 * Returns whether this range is flat, that is if start position and end position are in the same parent.
+	 * Returns whether this range is flat, that is if {@link engine.model.Range#start start} position and
+	 * {@link engine.model.Range#end end} position are in the same {@link engine.model.Position#parent parent}.
 	 *
 	 * @type {Boolean}
 	 */
@@ -82,7 +75,10 @@ export default class Range {
 	}
 
 	/**
-	 * Returns whether this range has any nodes in it.
+	 * Returns whether this range has no nodes in it, that is if {@link engine.model.Range#start start} position and
+	 * {@link engine.model.Range#end end} position are {@link engine.model.Position#isTouching touching}.
+	 *
+	 * **Note:** A range may be empty, but not {@link engine.model.Range#isCollapsed collapsed}.
 	 *
 	 * @type {Boolean}
 	 */
@@ -93,19 +89,17 @@ export default class Range {
 	/**
 	 * Range root element.
 	 *
-	 * Equals to the root of start position (which should be same as root of end position).
-	 *
-	 * @type {engine.model.RootElement|engine.model.DocumentFragment}
+	 * @type {engine.model.Element|engine.model.DocumentFragment}
 	 */
 	get root() {
 		return this.start.root;
 	}
 
 	/**
-	 * Checks whether this contains given {@link engine.model.Position position}.
+	 * Checks whether this range contains given {@link engine.model.Position position}.
 	 *
 	 * @param {engine.model.Position} position Position to check.
-	 * @returns {Boolean} True if given {@link engine.model.Position position} is contained.
+	 * @returns {Boolean} `true` if given {@link engine.model.Position position} is contained in this range, `false` otherwise.
 	 */
 	containsPosition( position ) {
 		return position.isAfter( this.start ) && position.isBefore( this.end );
@@ -115,15 +109,36 @@ export default class Range {
 	 * Checks whether this range contains given {@link engine.model.Range range}.
 	 *
 	 * @param {engine.model.Range} otherRange Range to check.
-	 * @returns {Boolean} True if given {@link engine.model.Range range} boundaries are contained by this range.
+	 * @returns {Boolean} `true` if given {@link engine.model.Range range} boundaries are contained by this range, `false` otherwise.
 	 */
 	containsRange( otherRange ) {
 		return this.containsPosition( otherRange.start ) && this.containsPosition( otherRange.end );
 	}
 
 	/**
-	 * Gets a part of this {@link engine.model.Range range} which is not a part of given {@link engine.model.Range range}. Returned
-	 * array contains zero, one or two {@link engine.model.Range ranges}.
+	 * Two ranges are equal if their {@link engine.model.Range#start start} and
+	 * {@link engine.model.Range#end end} positions are equal.
+	 *
+	 * @param {engine.model.Range} otherRange Range to compare with.
+	 * @returns {Boolean} `true` if ranges are equal, `false` otherwise.
+	 */
+	isEqual( otherRange ) {
+		return this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end );
+	}
+
+	/**
+	 * Checks and returns whether this range intersects with given range.
+	 *
+	 * @param {engine.model.Range} otherRange Range to compare with.
+	 * @returns {Boolean} `true` if ranges intersect, `false` otherwise.
+	 */
+	isIntersecting( otherRange ) {
+		return this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start );
+	}
+
+	/**
+	 * Computes which part(s) of this {@link engine.model.Range range} is not a part of given {@link engine.model.Range range}.
+	 * Returned array contains zero, one or two {@link engine.model.Range ranges}.
 	 *
 	 * Examples:
 	 *
@@ -169,8 +184,8 @@ export default class Range {
 	}
 
 	/**
-	 * Returns an intersection of this {@link engine.model.Range range} and given {@link engine.model.Range range}. Intersection
-	 * is a common part of both of those ranges. If ranges has no common part, returns `null`.
+	 * Returns an intersection of this {@link engine.model.Range range} and given {@link engine.model.Range range}.
+	 * Intersection is a common part of both of those ranges. If ranges has no common part, returns `null`.
 	 *
 	 * Examples:
 	 *
@@ -182,7 +197,7 @@ export default class Range {
 	 *		transformed = range.getIntersection( otherRange ); // range from [ 3 ] to [ 4, 0, 1 ]
 	 *
 	 * @param {engine.model.Range} otherRange Range to check for intersection.
-	 * @returns {engine.model.Range|null} A common part of given ranges or null if ranges have no common part.
+	 * @returns {engine.model.Range|null} A common part of given ranges or `null` if ranges have no common part.
 	 */
 	getIntersection( otherRange ) {
 		if ( this.isIntersecting( otherRange ) ) {
@@ -211,8 +226,9 @@ export default class Range {
 	}
 
 	/**
-	 * Computes and returns the smallest set of {@link #isFlat flat} ranges, that covers this range in whole.
-	 * Assuming that tree model model structure is ("[" and "]" are range boundaries):
+	 * Computes and returns the smallest set of {@link engine.model.Range#isFlat flat} ranges, that covers this range in whole.
+	 *
+	 * See an example of model structure (`[` and `]` are range boundaries):
 	 *
 	 *		root                                                            root
 	 *		 |- element DIV                         DIV             P2              P3             DIV
@@ -230,8 +246,8 @@ export default class Range {
 	 *		 |   |- element P4
 	 *		 |   |   |- "ipsum"
 	 *
-	 * As it can be seen, letters contained in the range are stloremfoobarse, spread across different parents.
-	 * We are looking for minimal set of {@link #isFlat flat} ranges that contains the same nodes.
+	 * As it can be seen, letters contained in the range are: `stloremfoobarse`, spread across different parents.
+	 * We are looking for minimal set of flat ranges that contains the same nodes.
 	 *
 	 * Minimal flat ranges for above range `( [ 0, 0, 3 ], [ 3, 0, 2 ] )` will be:
 	 *
@@ -240,10 +256,13 @@ export default class Range {
 	 *		( [ 1 ], [ 3 ] ) = element P2, element P3 ("foobar")
 	 *		( [ 3, 0, 0 ], [ 3, 0, 2 ] ) = "se"
 	 *
-	 * **Note:** this method is not returning flat ranges that contain no nodes. It may also happen that not-collapsed
-	 * range will return an empty array of flat ranges.
+	 * **Note:** if an {@link engine.model.Element element} is not contained wholly in this range, it won't be returned
+	 * in any of returned flat ranges. See in an example, how `H` elements at the beginning and at the end of the range
+	 * were omitted. Only it's parts that were wholly in the range were returned.
 	 *
-	 * @returns {Array.<engine.model.Range>} Array of flat ranges.
+	 * **Note:** this method is not returning flat ranges that contain no nodes.
+	 *
+	 * @returns {Array.<engine.model.Range>} Array of flat ranges covering this range.
 	 */
 	getMinimalFlatRanges() {
 		let ranges = [];
@@ -258,7 +277,7 @@ export default class Range {
 
 		// Go up.
 		while ( pos.path.length > diffAt + 1 ) {
-			let howMany = posParent.getChildCount() - pos.offset;
+			let howMany = posParent.getMaxOffset() - pos.offset;
 
 			if ( howMany !== 0 ) {
 				ranges.push( new Range( pos, pos.getShiftedBy( howMany ) ) );
@@ -286,7 +305,7 @@ export default class Range {
 	}
 
 	/**
-	 * Creates a {@link engine.model.TreeWalker} instance with this range as a boundary.
+	 * Creates a {@link engine.model.TreeWalker TreeWalker} instance with this range as a boundary.
 	 *
 	 * @param {Object} options Object with configuration options. See {@link engine.model.TreeWalker}.
 	 * @param {engine.model.Position} [options.startPosition]
@@ -302,15 +321,15 @@ export default class Range {
 
 	/**
 	 * Returns an iterator that iterates over all {@link engine.model.Item items} that are in this range and returns
-	 * them. It iterates over all {@link engine.model.CharacterProxy characters} or
-	 * {@link engine.model.TextProxy texts} that are inside the range and all the {@link engine.model.Element}s
-	 * we enter into when iterating over this range. Note that it use {@link engine.model.TreeWalker} with the
-	 * {@link engine.model.TreeWalker#ignoreElementEnd ignoreElementEnd} option set to true.
+	 * them.
+	 *
+	 * This method uses {@link engine.model.TreeWalker} with `boundaries` set to this range and `ignoreElementEnd` option
+	 * set to `true`. However it returns only {@link engine.model.Item model items}, not {@link engine.model.TreeWalkerValue}.
+	 *
+	 * You may specify additional options for the tree walker. See {@link engine.model.TreeWalker} for
+	 * a full list of available options.
 	 *
 	 * @param {Object} options Object with configuration options. See {@link engine.model.TreeWalker}.
-	 * @param {engine.model.Position} [options.startPosition]
-	 * @param {Boolean} [options.singleCharacters=false]
-	 * @param {Boolean} [options.shallow=false]
 	 * @returns {Iterable.<engine.model.Item>}
 	 */
 	*getItems( options = {} ) {
@@ -328,9 +347,13 @@ export default class Range {
 	 * Returns an iterator that iterates over all {@link engine.model.Position positions} that are boundaries or
 	 * contained in this range.
 	 *
+	 * This method uses {@link engine.model.TreeWalker} with `boundaries` set to this range. However it returns only
+	 * {@link engine.model.Position positions}, not {@link engine.model.TreeWalkerValue}.
+	 *
+	 * You may specify additional options for the tree walker. See {@link engine.model.TreeWalker} for
+	 * a full list of available options.
+	 *
 	 * @param {Object} options Object with configuration options. See {@link engine.model.TreeWalker}.
-	 * @param {Boolean} [options.singleCharacters=false]
-	 * @param {Boolean} [options.shallow=false]
 	 * @returns {Iterable.<engine.model.Position>}
 	 */
 	*getPositions( options = {} ) {
@@ -346,29 +369,29 @@ export default class Range {
 	}
 
 	/**
-	 * Returns an array containing one or two {engine.model.Range ranges} that are a result of transforming this
+	 * Returns an array containing one or two {@link engine.model.Range ranges} that are a result of transforming this
 	 * {@link engine.model.Range range} by inserting `howMany` nodes at `insertPosition`. Two {@link engine.model.Range ranges} are
 	 * returned if the insertion was inside this {@link engine.model.Range range} and `spread` is set to `true`.
 	 *
 	 * Examples:
 	 *
 	 *		let range = new Range( new Position( root, [ 2, 7 ] ), new Position( root, [ 4, 0, 1 ] ) );
-	 *		let transformed = range.getTransformedByInsertion( new Position( root, [ 1 ] ), 2 );
+	 *		let transformed = range._getTransformedByInsertion( new Position( root, [ 1 ] ), 2 );
 	 *		// transformed array has one range from [ 4, 7 ] to [ 6, 0, 1 ]
 	 *
-	 *		transformed = range.getTransformedByInsertion( new Position( root, [ 4, 0, 0 ] ), 4 );
+	 *		transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 0 ] ), 4 );
 	 *		// transformed array has one range from [ 2, 7 ] to [ 4, 0, 5 ]
 	 *
-	 *		transformed = range.getTransformedByInsertion( new Position( root, [ 3, 2 ] ), 4 );
+	 *		transformed = range._getTransformedByInsertion( new Position( root, [ 3, 2 ] ), 4 );
 	 *		// transformed array has one range, which is equal to original range
 	 *
-	 *		transformed = range.getTransformedByInsertion( new Position( root, [ 3, 2 ] ), 4, true );
+	 *		transformed = range._getTransformedByInsertion( new Position( root, [ 3, 2 ] ), 4, true );
 	 *		// transformed array has two ranges: from [ 2, 7 ] to [ 3, 2 ] and from [ 3, 6 ] to [ 4, 0, 1 ]
 	 *
-	 *		transformed = range.getTransformedByInsertion( new Position( root, [ 4, 0, 1 ] ), 4, false, false );
+	 *		transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 1 ] ), 4, false, false );
 	 *		// transformed array has one range which is equal to original range because insertion is after the range boundary
 	 *
-	 *		transformed = range.getTransformedByInsertion( new Position( root, [ 4, 0, 1 ] ), 4, false, true );
+	 *		transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 1 ] ), 4, false, true );
 	 *		// transformed array has one range: from [ 2, 7 ] to [ 4, 0, 5 ] because range was expanded
 	 *
 	 * @protected
@@ -380,7 +403,7 @@ export default class Range {
 	 * range boundary. Defaults to `false`.
 	 * @returns {Array.<engine.model.Range>} Result of the transformation.
 	 */
-	getTransformedByInsertion( insertPosition, howMany, spread = false, isSticky = false ) {
+	_getTransformedByInsertion( insertPosition, howMany, spread = false, isSticky = false ) {
 		if ( spread && this.containsPosition( insertPosition ) ) {
 			// Range has to be spread. The first part is from original start to the spread point.
 			// The other part is from spread point to the original end, but transformed by
@@ -389,8 +412,8 @@ export default class Range {
 			return [
 				new Range( this.start, insertPosition ),
 				new Range(
-					insertPosition.getTransformedByInsertion( insertPosition, howMany, true ),
-					this.end.getTransformedByInsertion( insertPosition, howMany, this.isCollapsed )
+					insertPosition._getTransformedByInsertion( insertPosition, howMany, true ),
+					this.end._getTransformedByInsertion( insertPosition, howMany, this.isCollapsed )
 				)
 			];
 		} else {
@@ -399,17 +422,18 @@ export default class Range {
 			let insertBeforeStart = range.isCollapsed ? isSticky : !isSticky;
 			let insertBeforeEnd = isSticky;
 
-			range.start = range.start.getTransformedByInsertion( insertPosition, howMany, insertBeforeStart );
-			range.end = range.end.getTransformedByInsertion( insertPosition, howMany, insertBeforeEnd );
+			range.start = range.start._getTransformedByInsertion( insertPosition, howMany, insertBeforeStart );
+			range.end = range.end._getTransformedByInsertion( insertPosition, howMany, insertBeforeEnd );
 
 			return [ range ];
 		}
 	}
 
 	/**
-	 * Returns an array containing {engine.model.Range ranges} that are a result of transforming this
+	 * Returns an array containing {@link engine.model.Range ranges} that are a result of transforming this
 	 * {@link engine.model.Range range} by moving `howMany` nodes from `sourcePosition` to `targetPosition`.
 	 *
+	 * @protected
 	 * @param {engine.model.Position} sourcePosition Position from which nodes are moved.
 	 * @param {engine.model.Position} targetPosition Position to where nodes are moved.
 	 * @param {Number} howMany How many nodes are moved.
@@ -417,7 +441,7 @@ export default class Range {
 	 * was inside the range. Defaults to `false`.
 	 * @returns {Array.<engine.model.Range>} Result of the transformation.
 	 */
-	getTransformedByMove( sourcePosition, targetPosition, howMany, spread, isSticky = false ) {
+	_getTransformedByMove( sourcePosition, targetPosition, howMany, spread, isSticky = false ) {
 		let result;
 
 		const moveRange = new Range( sourcePosition, sourcePosition.getShiftedBy( howMany ) );
@@ -427,25 +451,25 @@ export default class Range {
 
 		if ( differenceSet.length == 1 ) {
 			difference = new Range(
-				differenceSet[ 0 ].start.getTransformedByDeletion( sourcePosition, howMany ),
-				differenceSet[ 0 ].end.getTransformedByDeletion( sourcePosition, howMany )
+				differenceSet[ 0 ].start._getTransformedByDeletion( sourcePosition, howMany ),
+				differenceSet[ 0 ].end._getTransformedByDeletion( sourcePosition, howMany )
 			);
 		} else if ( differenceSet.length == 2 ) {
 			// This means that ranges were moved from the inside of this range.
 			// So we can operate on this range positions and we don't have to transform starting position.
 			difference = new Range(
 				this.start,
-				this.end.getTransformedByDeletion( sourcePosition, howMany )
+				this.end._getTransformedByDeletion( sourcePosition, howMany )
 			);
 		} else {
 			// 0.
 			difference = null;
 		}
 
-		const insertPosition = targetPosition.getTransformedByDeletion( sourcePosition, howMany );
+		const insertPosition = targetPosition._getTransformedByDeletion( sourcePosition, howMany );
 
 		if ( difference ) {
-			result = difference.getTransformedByInsertion( insertPosition, howMany, spread, isSticky );
+			result = difference._getTransformedByInsertion( insertPosition, howMany, spread, isSticky );
 		} else {
 			result = [];
 		}
@@ -465,54 +489,40 @@ export default class Range {
 	}
 
 	/**
-	 * Two ranges equal if their start and end positions equal.
-	 *
-	 * @param {engine.model.Range} otherRange Range to compare with.
-	 * @returns {Boolean} True if ranges equal.
-	 */
-	isEqual( otherRange ) {
-		return this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end );
-	}
-
-	/**
-	 * Checks and returns whether this range intersects with given range.
-	 *
-	 * @param {engine.model.Range} otherRange Range to compare with.
-	 * @returns {Boolean} True if ranges intersect.
-	 */
-	isIntersecting( otherRange ) {
-		return this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start );
-	}
-
-	/**
-	 * Creates a range inside an element which starts before the first child and ends after the last child.
+	 * Creates a range inside an {@link engine.model.Element element} which starts before the first child of
+	 * that element and ends after the last child of that element.
 	 *
 	 * @param {engine.model.Element} element Element which is a parent for the range.
-	 * @returns {engine.model.Range} Created range.
+	 * @returns {engine.model.Range}
 	 */
 	static createFromElement( element ) {
-		return this.createFromParentsAndOffsets( element, 0, element, element.getChildCount() );
+		return this.createFromParentsAndOffsets( element, 0, element, element.getMaxOffset() );
 	}
 
 	/**
-	 * Creates a range on given element only. The range starts just before the element and ends before the first child of the element.
+	 * Creates a range on given {@link engine.model.Element element} only. The range starts directly before that element
+	 * and ends before the first child of that element.
 	 *
 	 * @param {engine.model.Element} element Element on which range should be created.
-	 * @returns {engine.model.Range} Created range.
+	 * @returns {engine.model.Range}
 	 */
 	static createOnElement( element ) {
-		return this.createFromParentsAndOffsets( element.parent, element.getIndex(), element, 0 );
+		return this.createFromParentsAndOffsets( element.parent, element.startOffset, element, 0 );
 	}
 
 	/**
-	 * Creates a new range spreading from specified position to the same position moved by given shift.
+	 * Creates a new range, spreading from specified {@link engine.model.Position position} to a position moved by
+	 * given `shift`. If `shift` is a negative value, shifted position is treated as the beginning of the range.
 	 *
 	 * @param {engine.model.Position} position Beginning of the range.
 	 * @param {Number} shift How long the range should be.
 	 * @returns {engine.model.Range}
 	 */
 	static createFromPositionAndShift( position, shift ) {
-		return new this( position, position.getShiftedBy( shift ) );
+		const start = position;
+		const end = position.getShiftedBy( shift );
+
+		return shift > 0 ? new this( start, end ) : new this( end, start );
 	}
 
 	/**
@@ -522,7 +532,7 @@ export default class Range {
 	 * @param {Number} startOffset Start position offset.
 	 * @param {engine.model.Element} endElement End position parent element.
 	 * @param {Number} endOffset End position offset.
-	 * @returns {engine.model.Range} Created range.
+	 * @returns {engine.model.Range}
 	 */
 	static createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
 		return new this(
@@ -532,7 +542,7 @@ export default class Range {
 	}
 
 	/**
-	 * Creates and returns a new instance of Range which is equal to passed range.
+	 * Creates a new instance of `Range` which is equal to passed range.
 	 *
 	 * @param {engine.model.Range} range Range to clone.
 	 * @returns {engine.model.Range}
@@ -542,11 +552,10 @@ export default class Range {
 	}
 
 	/**
-	 * Creates Range from deserilized object, ie. from parsed JSON string.
+	 * Creates a `Range` instance from given plain object (i.e. parsed JSON string).
 	 *
-	 * @param {Object} json Deserialized JSON object.
-	 * @param {engine.model.Document} doc Document on which this operation will be applied.
-	 * @returns {engine.model.Range}
+	 * @param {Object} json Plain object to be converted to `Range`.
+	 * @returns {engine.model.Element} `Range` instance created using given plain object.
 	 */
 	static fromJSON( json, doc ) {
 		return new this( Position.fromJSON( json.start, doc ), Position.fromJSON( json.end, doc ) );

+ 22 - 13
packages/ckeditor5-engine/src/model/rootelement.js

@@ -3,12 +3,10 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Element from './element.js';
 
 /**
- * Class for nodes that are roots of trees in data model.
+ * Type of {@link engine.model.Element} that is a root of a model tree.
  *
  * @memberOf engine.model
  * @extends engine.model.Element
@@ -17,23 +15,23 @@ export default class RootElement extends Element {
 	/**
 	 * Creates root element.
 	 *
-	 * @param {engine.model.Document} doc {@link engine.model.Document} that is an owner of the root.
+	 * @param {engine.model.Document} doc Document that is an owner of this root.
 	 * @param {String} name Node name.
-	 * @param {String} [rootName='main'] Root name inside parent {@link engine.model.Document}.
+	 * @param {String} [rootName='main'] Unique root name used to identify this root element by {@link engine.model.Document}.
 	 */
 	constructor( doc, name, rootName = 'main' ) {
 		super( name );
 
 		/**
-		 * {@link engine.model.Document} that is an owner of this root.
+		 * Document that is an owner of this root.
 		 *
-		 * @readonly
-		 * @member {engine.model.Document} engine.model.RootElement#document
+		 * @private
+		 * @member {engine.model.Document} engine.model.RootElement#_doc
 		 */
-		this.document = doc;
+		this._doc = doc;
 
 		/**
-		 * Name of this root inside {@link engine.model.Document} that is an owner of this root.
+		 * Unique root name used to identify this root element by {@link engine.model.Document}.
 		 *
 		 * @readonly
 		 * @member {String} engine.model.RootElement#rootName
@@ -42,10 +40,21 @@ export default class RootElement extends Element {
 	}
 
 	/**
-	 * Custom toJSON method to solve child-parent circular dependencies.
+	 * {@link engine.model.Document Document} that owns this root element.
+	 *
+	 * In contrary, to {@link engine.model.Node node}, root element always have a `document`.
+	 *
+	 * @readonly
+	 * @type {engine.model.Document|null}
+	 */
+	get document() {
+		return this._doc;
+	}
+
+	/**
+	 * Converts `RootElement` instance to `String` containing it's name.
 	 *
-	 * @method engine.model.RootElement#toJSON
-	 * @returns {String} Name of this root inside {@link engine.model.Document} that is an owner of this root.
+	 * @returns {String} `RootElement` instance converted to `String`.
 	 */
 	toJSON() {
 		return this.rootName;

+ 5 - 7
packages/ckeditor5-engine/src/model/schema.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Position from './position.js';
 import Element from './element.js';
 import clone from '../../utils/lib/lodash/clone.js';
@@ -115,12 +113,12 @@ export class SchemaItem {
 	 * Returns all paths of given type that were previously registered in the item.
 	 *
 	 * @private
-	 * @param {String} type Paths' type. Possible values are `ALLOW` or `DISALLOW`.
+	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
 	 * @param {String} [attribute] If set, only paths registered for given attribute will be returned.
 	 * @returns {Array} Paths registered in the item.
 	 */
 	_getPaths( type, attribute ) {
-		const source = type === 'ALLOW' ? this._allowed : this._disallowed;
+		const source = type === 'allow' ? this._allowed : this._disallowed;
 		const paths = [];
 
 		for ( let item of source ) {
@@ -165,7 +163,7 @@ export class SchemaItem {
 	 * Checks whether this item has any registered path of given type that matches provided path.
 	 *
 	 * @protected
-	 * @param {String} type Paths' type. Possible values are `ALLOW` or `DISALLOW`.
+	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
 	 * @param {Array.<String>} checkPath Path to check.
 	 * @param {String} [attribute] If set, only paths registered for given attribute will be checked.
 	 * @returns {Boolean} `true` if item has any registered matching path, `false` otherwise.
@@ -384,7 +382,7 @@ export default class Schema {
 		// If there is matching disallow path, this query is not valid with schema.
 		for ( let attribute of query.attributes ) {
 			for ( let schemaItem of schemaItems ) {
-				if ( schemaItem._hasMatchingPath( 'DISALLOW', path, attribute ) ) {
+				if ( schemaItem._hasMatchingPath( 'disallow', path, attribute ) ) {
 					return false;
 				}
 			}
@@ -400,7 +398,7 @@ export default class Schema {
 			let matched = false;
 
 			for ( let schemaItem of schemaItems ) {
-				if ( schemaItem._hasMatchingPath( 'ALLOW', path, attribute ) ) {
+				if ( schemaItem._hasMatchingPath( 'allow', path, attribute ) ) {
 					matched = true;
 					break;
 				}

+ 252 - 139
packages/ckeditor5-engine/src/model/selection.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Position from './position.js';
 import Range from './range.js';
 import EmitterMixin from '../../utils/emittermixin.js';
@@ -14,7 +12,8 @@ import toMap from '../../utils/tomap.js';
 
 /**
  * `Selection` is a group of {@link engine.model.Range ranges} which has a direction specified by
- * {@link engine.model.Selection#anchor anchor} and {@link engine.model.Selection#focus focus}.
+ * {@link engine.model.Selection#anchor anchor} and {@link engine.model.Selection#focus focus}. Additionally,
+ * `Selection` may have it's own attributes.
  *
  * @memberOf engine.model
  */
@@ -32,7 +31,7 @@ export default class Selection {
 		this._lastRangeBackward = false;
 
 		/**
-		 * Stores all ranges that are selected.
+		 * Stores selection ranges.
 		 *
 		 * @protected
 		 * @member {Array.<engine.model.Range>} engine.model.Selection#_ranges
@@ -49,90 +48,15 @@ export default class Selection {
 	}
 
 	/**
-	 * Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
-	 *
-	 * @param {String} key Key of attribute to look for.
-	 * @returns {*} Attribute value or `undefined`.
-	 */
-	getAttribute( key ) {
-		return this._attrs.get( key );
-	}
-
-	/**
-	 * Returns iterator that iterates over this selection attributes.
-	 *
-	 * @returns {Iterable.<*>}
-	 */
-	getAttributes() {
-		return this._attrs[ Symbol.iterator ]();
-	}
-
-	/**
-	 * Checks if the selection has an attribute for given key.
-	 *
-	 * @param {String} key Key of attribute to check.
-	 * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
-	 */
-	hasAttribute( key ) {
-		return this._attrs.has( key );
-	}
-
-	/**
-	 * Removes all attributes from the selection.
-	 *
-	 * @fires engine.model.Selection#change:attribute
-	 */
-	clearAttributes() {
-		this._attrs.clear();
-
-		this.fire( 'change:attribute' );
-	}
-
-	/**
-	 * Removes an attribute with given key from the selection.
-	 *
-	 * @fires engine.model.Selection#change:attribute
-	 * @param {String} key Key of attribute to remove.
-	 */
-	removeAttribute( key ) {
-		this._attrs.delete( key );
-
-		this.fire( 'change:attribute' );
-	}
-
-	/**
-	 * Sets attribute on the selection. If attribute with the same key already is set, it overwrites its values.
-	 *
-	 * @fires engine.model.Selection#change:attribute
-	 * @param {String} key Key of attribute to set.
-	 * @param {*} value Attribute value.
-	 */
-	setAttribute( key, value ) {
-		this._attrs.set( key, value );
-
-		this.fire( 'change:attribute' );
-	}
-
-	/**
-	 * Removes all attributes from the selection and sets given attributes.
-	 *
-	 * @fires engine.model.Selection#change:attribute
-	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
-	 */
-	setAttributesTo( attrs ) {
-		this._attrs = toMap( attrs );
-
-		this.fire( 'change:attribute' );
-	}
-
-	/**
-	 * Selection anchor. Anchor may be described as a position where the selection starts. Together with
-	 * {@link engine.model.Selection#focus} they define the direction of selection, which is important
-	 * when expanding/shrinking selection. Anchor is always the start or end of the most recent added range.
+	 * Selection anchor. Anchor may be described as a position where the most recent part of the selection starts.
+	 * Together with {@link engine.model.Selection#focus} they define the direction of selection, which is important
+	 * when expanding/shrinking selection. Anchor is always {@link engine.model.Range#start start} or
+	 * {@link engine.model.Range#end end} position of the most recently added range.
 	 *
 	 * Is set to `null` if there are no ranges in selection.
 	 *
 	 * @see engine.model.Selection#focus
+	 * @readonly
 	 * @type {engine.model.Position|null}
 	 */
 	get anchor() {
@@ -151,6 +75,7 @@ export default class Selection {
 	 * Is set to `null` if there are no ranges in selection.
 	 *
 	 * @see engine.model.Selection#anchor
+	 * @readonly
 	 * @type {engine.model.Position|null}
 	 */
 	get focus() {
@@ -167,6 +92,7 @@ export default class Selection {
 	 * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
 	 * collapsed.
 	 *
+	 * @readonly
 	 * @type {Boolean}
 	 */
 	get isCollapsed() {
@@ -183,7 +109,7 @@ export default class Selection {
 	 * Returns number of ranges in selection.
 	 *
 	 * @type {Number}
-     */
+	 */
 	get rangeCount() {
 		return this._ranges.length;
 	}
@@ -198,24 +124,25 @@ export default class Selection {
 	}
 
 	/**
-	 * Adds a range to the selection. Added range is copied. This means that passed range is not saved in `Selection`
-	 * instance and operating on it will not change `Selection` state.
-	 *
-	 * Accepts a flag describing in which way the selection is made - passed range might be selected from
-	 * {@link engine.model.Range#start start} to {@link engine.model.Range#end end} or from {@link engine.model.Range#end end}
-	 * to {@link engine.model.Range#start start}. The flag is used to set {@link engine.model.Selection#anchor} and
-	 * {@link engine.model.Selection#focus} properties.
+	 * Checks whether, this selection is equal to given selection. Selections equal if they have the same ranges and directions.
 	 *
-	 * @fires engine.model.Selection#change:range
-	 * @param {engine.model.Range} range Range to add.
-	 * @param {Boolean} [isBackward] Flag describing if added range was selected forward - from start to end (`false`)
-	 * or backward - from end to start (`true`). Defaults to `false`.
+	 * @param {engine.model.Selection} otherSelection Selection to compare with.
+	 * @returns {Boolean} `true` if selections are equal, `false` otherwise.
 	 */
-	addRange( range, isBackward ) {
-		this._pushRange( range );
-		this._lastRangeBackward = !!isBackward;
+	isEqual( otherSelection ) {
+		const rangeCount = this.rangeCount;
 
-		this.fire( 'change:range' );
+		if ( rangeCount != otherSelection.rangeCount ) {
+			return false;
+		}
+
+		for ( let i = 0; i < this.rangeCount; i++ ) {
+			if ( !this._ranges[ i ].isEqual( otherSelection._ranges[ i ] ) ) {
+				return false;
+			}
+		}
+
+		return this.isBackward === otherSelection.isBackward;
 	}
 
 	/**
@@ -241,9 +168,7 @@ export default class Selection {
 	getFirstRange() {
 		let first = null;
 
-		for ( let i = 0; i < this._ranges.length; i++ ) {
-			let range = this._ranges[ i ];
-
+		for ( let range of this._ranges ) {
 			if ( !first || range.start.isBefore( first.start ) ) {
 				first = range;
 			}
@@ -252,9 +177,30 @@ export default class Selection {
 		return first ? Range.createFromRange( first ) : null;
 	}
 
+	/**
+	 * Returns a copy of the last range in the selection. Last range is the one which {@link engine.model.Range#end end} position
+	 * {@link engine.model.Position#isAfter is after} end position of all other ranges (not to confuse with the range most
+	 * recently added to the selection).
+	 *
+	 * Returns `null` if there are no ranges in selection.
+	 *
+	 * @returns {engine.model.Range|null}
+	 */
+	getLastRange() {
+		let last = null;
+
+		for ( let range of this._ranges ) {
+			if ( !last || range.end.isAfter( last.end ) ) {
+				last = range;
+			}
+		}
+
+		return last ? Range.createFromRange( last ) : null;
+	}
+
 	/**
 	 * Returns the first position in the selection. First position is the position that {@link engine.model.Position#isBefore is before}
-	 * any other position in the selection ranges.
+	 * any other position in the selection.
 	 *
 	 * Returns `null` if there are no ranges in selection.
 	 *
@@ -267,7 +213,42 @@ export default class Selection {
 	}
 
 	/**
-	 * Removes all ranges that were added to the selection. Fires update event.
+	 * Returns the last position in the selection. Last position is the position that {@link engine.model.Position#isAfter is after}
+	 * any other position in the selection.
+	 *
+	 * Returns `null` if there are no ranges in selection.
+	 *
+	 * @returns {engine.model.Position|null}
+	 */
+	getLastPosition() {
+		const lastRange = this.getLastRange();
+
+		return lastRange ? Position.createFromPosition( lastRange.end ) : null;
+	}
+
+	/**
+	 * Adds a range to this selection. Added range is copied. This means that passed range is not saved in `Selection`
+	 * instance and operating on it will not change `Selection` state.
+	 *
+	 * Accepts a flag describing in which way the selection is made - passed range might be selected from
+	 * {@link engine.model.Range#start start} to {@link engine.model.Range#end end} or from {@link engine.model.Range#end end}
+	 * to {@link engine.model.Range#start start}. The flag is used to set {@link engine.model.Selection#anchor} and
+	 * {@link engine.model.Selection#focus} properties.
+	 *
+	 * @fires engine.model.Selection#change:range
+	 * @param {engine.model.Range} range Range to add.
+	 * @param {Boolean} [isBackward=false] Flag describing if added range was selected forward - from start to end (`false`)
+	 * or backward - from end to start (`true`).
+	 */
+	addRange( range, isBackward = false ) {
+		this._pushRange( range );
+		this._lastRangeBackward = !!isBackward;
+
+		this.fire( 'change:range' );
+	}
+
+	/**
+	 * Removes all ranges that were added to the selection.
 	 *
 	 * @fires engine.model.Selection#change:range
 	 */
@@ -279,13 +260,14 @@ export default class Selection {
 
 	/**
 	 * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
-	 * is treated like the last added range and is used to set {@link #anchor} and {@link #focus}. Accepts a flag
-	 * describing in which way the selection is made (see {@link #addRange}).
+	 * is treated like the last added range and is used to set {@link engine.model.Selection#anchor} and
+	 * {@link engine.model.Selection#focus}. Accepts a flag describing in which direction the selection is made
+	 * (see {@link engine.model.Selection#addRange}).
 	 *
 	 * @fires engine.model.Selection#change:range
-	 * @param {Iterable.<engine.model.Range>} newRanges Iterable set of ranges that should be set.
-	 * @param {Boolean} [isLastBackward] Flag describing if last added range was selected forward - from start to end (`false`)
-	 * or backward - from end to start (`true`). Defaults to `false`.
+	 * @param {Iterable.<engine.model.Range>} newRanges Ranges to set.
+	 * @param {Boolean} [isLastBackward=false] Flag describing if last added range was selected forward - from start to end (`false`)
+	 * or backward - from end to start (`true`).
 	 */
 	setRanges( newRanges, isLastBackward ) {
 		this._ranges = [];
@@ -299,34 +281,162 @@ export default class Selection {
 		this.fire( 'change:range' );
 	}
 
+	/**
+	 * Sets this selection's ranges and direction to the ranges and direction of the given selection.
+	 *
+	 * @param {engine.model.Selection} otherSelection
+	 */
+	setTo( otherSelection ) {
+		this.setRanges( otherSelection.getRanges(), otherSelection.isBackward );
+	}
+
 	/**
 	 * Sets collapsed selection in the specified location.
 	 *
 	 * The location can be specified in the same form as {@link engine.model.Position.createAt} parameters.
 	 *
 	 * @fires engine.model.Selection#change:range
-	 * @param {engine.model.Node|engine.model.Position} nodeOrPosition
-	 * @param {Number|'END'|'BEFORE'|'AFTER'} [offset=0] Offset or one of the flags. Used only when
-	 * first parameter is a node.
+	 * @param {engine.model.Item|engine.model.Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * first parameter is a {@link engine.model.Item model item}.
 	 */
-	collapse( nodeOrPosition, offset ) {
-		const pos = Position.createAt( nodeOrPosition, offset );
+	collapse( itemOrPosition, offset ) {
+		const pos = Position.createAt( itemOrPosition, offset );
 		const range = new Range( pos, pos );
 
 		this.setRanges( [ range ] );
 	}
 
 	/**
-	 * Sets {@link engine.model.Selection#focus} in the specified location.
+	 * Collapses selection to the selection's {@link engine.model.Selection#getFirstPosition first position}.
+	 * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
+	 * inside selection.
+	 *
+	 * @fires engine.view.Selection#change
+	 */
+	collapseToStart() {
+		const startPosition = this.getFirstPosition();
+
+		if ( startPosition !== null ) {
+			this.setRanges( [ new Range( startPosition, startPosition ) ] );
+		}
+	}
+
+	/**
+	 * Collapses selection to the selection's {@link engine.model.Selection#getLastPosition last position}.
+	 * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
+	 * inside selection.
+	 *
+	 * @fires engine.view.Selection#change
+	 */
+	collapseToEnd() {
+		const endPosition = this.getLastPosition();
+
+		if ( endPosition !== null ) {
+			this.setRanges( [ new Range( endPosition, endPosition ) ] );
+		}
+	}
+
+	/**
+	 * Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
+	 *
+	 * @param {String} key Key of attribute to look for.
+	 * @returns {*} Attribute value or `undefined`.
+	 */
+	getAttribute( key ) {
+		return this._attrs.get( key );
+	}
+
+	/**
+	 * Returns iterator that iterates over this selection's attributes.
+	 *
+	 * Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
+	 * This format is accepted by native `Map` object and also can be passed in `Node` constructor.
+	 *
+	 * @returns {Iterable.<*>}
+	 */
+	getAttributes() {
+		return this._attrs.entries();
+	}
+
+	/**
+	 * Returns iterator that iterates over this selection's attribute keys.
+	 *
+	 * @returns {Iterator.<String>}
+	 */
+	getAttributeKeys() {
+		return this._attrs.keys();
+	}
+
+	/**
+	 * Checks if the selection has an attribute for given key.
+	 *
+	 * @param {String} key Key of attribute to check.
+	 * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
+	 */
+	hasAttribute( key ) {
+		return this._attrs.has( key );
+	}
+
+	/**
+	 * Removes all attributes from the selection.
+	 *
+	 * @fires engine.model.Selection#change:attribute
+	 */
+	clearAttributes() {
+		this._attrs.clear();
+
+		this.fire( 'change:attribute' );
+	}
+
+	/**
+	 * Removes an attribute with given key from the selection.
+	 *
+	 * @fires engine.model.Selection#change:attribute
+	 * @param {String} key Key of attribute to remove.
+	 */
+	removeAttribute( key ) {
+		this._attrs.delete( key );
+
+		this.fire( 'change:attribute' );
+	}
+
+	/**
+	 * Sets attribute on the selection. If attribute with the same key already is set, it's value is overwritten.
+	 *
+	 * @fires engine.model.Selection#change:attribute
+	 * @param {String} key Key of attribute to set.
+	 * @param {*} value Attribute value.
+	 */
+	setAttribute( key, value ) {
+		this._attrs.set( key, value );
+
+		this.fire( 'change:attribute' );
+	}
+
+	/**
+	 * Removes all attributes from the selection and sets given attributes.
+	 *
+	 * @fires engine.model.Selection#change:attribute
+	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
+	 */
+	setAttributesTo( attrs ) {
+		this._attrs = toMap( attrs );
+
+		this.fire( 'change:attribute' );
+	}
+
+	/**
+	 * Sets {@link engine.model.Selection#focus} to the specified location.
 	 *
 	 * The location can be specified in the same form as {@link engine.model.Position.createAt} parameters.
 	 *
 	 * @fires engine.model.Selection#change:range
-	 * @param {engine.model.Node|engine.model.Position} nodeOrPosition
-	 * @param {Number|'END'|'BEFORE'|'AFTER'} [offset=0] Offset or one of the flags. Used only when
-	 * first parameter is a node.
+	 * @param {engine.model.Item|engine.model.Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * first parameter is a {@link engine.model.Item model item}.
 	 */
-	setFocus( nodeOrPosition, offset ) {
+	setFocus( itemOrPosition, offset ) {
 		if ( this.anchor === null ) {
 			/**
 			 * Cannot set selection focus if there are no ranges in selection.
@@ -336,9 +446,9 @@ export default class Selection {
 			throw new CKEditorError( 'selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.' );
 		}
 
-		const newFocus = Position.createAt( nodeOrPosition, offset );
+		const newFocus = Position.createAt( itemOrPosition, offset );
 
-		if ( newFocus.compareWith( this.focus ) == 'SAME' ) {
+		if ( newFocus.compareWith( this.focus ) == 'same' ) {
 			return;
 		}
 
@@ -348,7 +458,7 @@ export default class Selection {
 			this._popRange();
 		}
 
-		if ( newFocus.compareWith( anchor ) == 'BEFORE' ) {
+		if ( newFocus.compareWith( anchor ) == 'before' ) {
 			this.addRange( new Range( newFocus, anchor ), true );
 		} else {
 			this.addRange( new Range( anchor, newFocus ) );
@@ -356,22 +466,37 @@ export default class Selection {
 	}
 
 	/**
-	 * Creates and returns an instance of {@link engine.model.Selection} that is a clone of given selection,
-	 * meaning that it has same ranges and same direction as it.
+	 * Creates and returns an instance of `Selection` that is a clone of given selection, meaning that it has same
+	 * ranges and same direction as this selection.
 	 *
 	 * @params {engine.model.Selection} otherSelection Selection to be cloned.
 	 * @returns {engine.model.Selection} `Selection` instance that is a clone of given selection.
 	 */
 	static createFromSelection( otherSelection ) {
 		const selection = new this();
-		selection.setRanges( otherSelection.getRanges(), otherSelection.isBackward );
+		selection.setTo( otherSelection );
 
 		return selection;
 	}
 
+	/**
+	 * Adds given range to internal {@link engine.model.Selection#_ranges ranges array}. Throws an error
+	 * if given range is intersecting with any range that is already stored in this selection.
+	 *
+	 * @protected
+	 * @param {engine.model.Range} range Range to add.
+	 */
+	_pushRange( range ) {
+		if ( !( range instanceof Range ) ) {
+			throw new CKEditorError( 'selection-added-not-range: Trying to add an object that is not an instance of Range.' );
+		}
+
+		this._checkRange( range );
+		this._ranges.push( Range.createFromRange( range ) );
+	}
+
 	/**
 	 * Checks if given range intersects with ranges that are already in the selection. Throws an error if it does.
-	 * This method is extracted from {@link engine.model.Selection#_pushRange } so it is easier to override it.
 	 *
 	 * @param {engine.model.Range} range Range to check.
 	 * @protected
@@ -402,18 +527,6 @@ export default class Selection {
 	_popRange() {
 		this._ranges.pop();
 	}
-
-	/**
-	 * Adds given range to internal {@link engine.model.Selection#_ranges ranges array}. Throws an error
-	 * if given range is intersecting with any range that is already stored in this selection.
-	 *
-	 * @protected
-	 * @param {engine.model.Range} range Range to add.
-	 */
-	_pushRange( range ) {
-		this._checkRange( range );
-		this._ranges.push( Range.createFromRange( range ) );
-	}
 }
 
 mix( Selection, EmitterMixin );

+ 35 - 84
packages/ckeditor5-engine/src/model/text.js

@@ -3,122 +3,73 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
-import toMap from '../../utils/tomap.js';
+import Node from './node.js';
 
 /**
- * Data structure for text with attributes. Note that `Text` is not a {@link engine.model.Node}. This class is used
- * as an aggregator for multiple characters that have same attributes. Example usage:
+ * Model text node. Type of {@link engine.model.Node node} that contains {@link engine.model.Text#data text data}.
+ *
+ * **Important:** see {@link engine.model.Node} to read about restrictions using `Text` and `Node` API.
  *
- *        let myElem = new Element( 'li', [], new Text( 'text with attributes', { foo: true, bar: true } ) );
+ * **Note:** keep in mind that `Text` instances might indirectly got removed from model tree when model is changed.
+ * This happens when {@link engine.model.writer model writer} is used to change model and the text node is merged with
+ * another text node. Then, both text nodes are removed and a new text node is inserted into the model. Because of
+ * this behavior, keeping references to `Text` is not recommended. Instead, consider creating
+ * {@link engine.model.LivePosition live position} placed before the text node.
  *
  * @memberOf engine.model
  */
-export default class Text {
+export default class Text extends Node {
 	/**
-	 * Creates a text with attributes.
+	 * Creates a text node.
 	 *
-	 * @param {String} text Described text.
-	 * @param {Iterable|Object} [attrs] Iterable collection of attributes.
+	 * @param {String} data Node's text.
+	 * @param {Object} [attrs] Node's attributes. See {@link utils.toMap} for a list of accepted values.
 	 */
-	constructor( text, attrs ) {
-		/**
-		 * Text.
-		 *
-		 * @readonly
-		 * @member {String} engine.model.Text#text
-		 */
-		this.text = text || '';
+	constructor( data, attrs ) {
+		super( attrs );
 
 		/**
-		 * List of attributes bound with the text.
+		 * Text data contained in this text node.
 		 *
-		 * @protected
-		 * @member {Map} engine.model.Text#_attrs
+		 * @type {String}
 		 */
-		this._attrs = toMap( attrs );
-	}
-
-	/**
-	 * Checks if the text has an attribute for given key.
-	 *
-	 * @param {String} key Key of attribute to check.
-	 * @returns {Boolean} `true` if attribute with given key is set on text, `false` otherwise.
-	 */
-	hasAttribute( key ) {
-		return this._attrs.has( key );
-	}
-
-	/**
-	 * Gets an attribute value for given key or undefined if that attribute is not set on text.
-	 *
-	 * @param {String} key Key of attribute to look for.
-	 * @returns {*} Attribute value or null.
-	 */
-	getAttribute( key ) {
-		return this._attrs.get( key );
+		this.data = data || '';
 	}
 
 	/**
-	 * Returns iterator that iterates over this text attributes.
-	 *
-	 * @returns {Iterable.<*>}
+	 * @inheritDoc
 	 */
-	getAttributes() {
-		return this._attrs[ Symbol.iterator ]();
+	get offsetSize() {
+		return this.data.length;
 	}
 
 	/**
-	 * Sets attribute on text. If attribute with the same key already is set, it overwrites its value.
-	 *
-	 * @param {String} key Key of attribute to set.
-	 * @param {*} value Attribute value.
+	 * Creates a copy of this text node and returns it. Created text node has same text data and attributes as original text node.
 	 */
-	setAttribute( key, value ) {
-		this._attrs.set( key, value );
+	clone() {
+		return new Text( this.data, this.getAttributes() );
 	}
 
 	/**
-	 * Removes all attributes from text and sets given attributes.
+	 * Converts `Text` instance to plain object and returns it.
 	 *
-	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set. See {@link engine.model.Text#getAttributes}.
+	 * @returns {Object} `Text` instance converted to plain object.
 	 */
-	setAttributesTo( attrs ) {
-		this._attrs = toMap( attrs );
-	}
+	toJSON() {
+		let json = super.toJSON();
 
-	/**
-	 * Removes an attribute with given key from text.
-	 *
-	 * @param {String} key Key of attribute to remove.
-	 * @returns {Boolean} `true` if the attribute was set on text, `false` otherwise.
-	 */
-	removeAttribute( key ) {
-		return this._attrs.delete( key );
-	}
+		json.data = this.data;
 
-	/**
-	 * Removes all attributes from text.
-	 */
-	clearAttributes() {
-		this._attrs.clear();
+		return json;
 	}
 
 	/**
-	 * Custom toJSON method to solve child-parent circular dependencies.
+	 * Creates a `Text` instance from given plain object (i.e. parsed JSON string).
 	 *
-	 * @returns {Object} Clone of this object with the parent property replaced with its name.
+	 * @param {Object} json Plain object to be converted to `Text`.
+	 * @returns {engine.model.Text} `Text` instance created using given plain object.
 	 */
-	toJSON() {
-		let json = {
-			text: this.text
-		};
-
-		if ( this._attrs.size ) {
-			json.attributes = [ ...this._attrs ];
-		}
-
-		return json;
+	static fromJSON( json ) {
+		return new Text( json.data, json.attributes );
 	}
 }

+ 106 - 91
packages/ckeditor5-engine/src/model/textproxy.js

@@ -3,173 +3,188 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
-import CharacterProxy from './characterproxy.js';
-import toMap from '../../utils/tomap.js';
-
 /**
- * TextProxy is an aggregator for multiple CharacterProxy instances that are placed next to each other in
- * tree model, in the same parent, and all have same attributes set. Instances of this class are created and returned
- * in various algorithms that "merge characters" (see {@link engine.model.TreeWalker}, {@link engine.model.Range}).
+ * `TextProxy` represents a part of {@link engine.model.Text text node}.
+ *
+ * Since {@link engine.model.Position positions} can be placed between characters of a text node,
+ * {@link engine.model.Range ranges} may contain only parts of text nodes. When {@link engine.model.Range#getItems getting items}
+ * contained in such range, we need to represent a part of that text node, since returning the whole text node would be incorrect.
+ * `TextProxy` solves this issue.
+ *
+ * `TextProxy` has an API similar to {@link engine.model.Text Text} and allows to do most of the common tasks performed
+ * on model nodes.
+ *
+ * **Note:** Some `TextProxy` instances may represent whole text node, not just a part of it.
  *
- * **Note:** TextProxy instances are created on the fly basing on the current state of tree model and attributes
- * set on characters. Because of this it is highly unrecommended to store references to TextProxy instances
- * because they might get invalidated due to operations on Document. This is especially true when you change
- * attributes of TextProxy.
+ * **Note:** `TextProxy` is not an instance of {@link engine.model.Node node}. Keep this in mind when using it as a
+ * parameter of methods.
  *
- * Difference between {@link engine.model.TextProxy} and {@link engine.model.Text} is that the former is a set of
- * nodes taken from tree model, while {@link engine.model.Text} is simply a string with attributes set.
+ * **Note:** `TextProxy` is readonly interface. If you want to perform changes on model data represented by a `TextProxy`
+ * use {@link engine.model.writer model writer API}.
  *
- * You should never create an instance of this class by your own. Instead, use string literals or {@link engine.model.Text}.
+ * **Note:** `TextProxy` instances are created on the fly, basing on the current state of model. Because of this, it is
+ * highly unrecommended to store references to `TextProxy` instances. `TextProxy` instances are not refreshed when
+ * model changes, so they might get invalidated. Instead, consider creating {@link engine.model.LivePosition live position}.
+ *
+ * `TextProxy` instances are created by {@link engine.model.TreeWalker model tree walker}. You should not need to create
+ * an instance of this class by your own.
  *
  * @memberOf engine.model
  */
 export default class TextProxy {
 	/**
-	 * Creates a text fragment.
+	 * Creates a text proxy.
 	 *
 	 * @protected
-	 * @param {engine.model.CharacterProxy} firstCharacter First character node contained in {@link engine.model.TextProxy}.
-	 * @param {Number} length Whole text contained in {@link engine.model.TextProxy}.
+	 * @param {engine.model.Text} textNode Text node which part is represented by this text proxy.
+	 * @param {Number} offsetInText Offset in {@link engine.model.TextProxy#textNode text node} from which the text proxy starts.
+	 * @param {Number} length Text proxy length, that is how many text node's characters, starting from `offsetInText` it represents.
 	 * @constructor
 	 */
-	constructor( firstCharacter, length ) {
+	constructor( textNode, offsetInText, length ) {
 		/**
-		 * First character node contained in {@link engine.model.TextProxy}.
+		 * Text node which part is represented by this text proxy.
 		 *
 		 * @readonly
-		 * @member {engine.model.CharacterProxy} engine.model.TextProxy#first
+		 * @member {engine.model.Text} engine.model.TextProxy#textNode
 		 */
-		this.first = firstCharacter;
+		this.textNode = textNode;
 
 		/**
-		 * Characters contained in {@link engine.model.TextProxy}.
+		 * Text data represented by this text proxy.
 		 *
 		 * @readonly
-		 * @member {String} engine.model.TextProxy#text
+		 * @member {String} engine.model.TextProxy#data
 		 */
-		this.text = firstCharacter._nodeListText.text.substr( this.first._index, length );
+		this.data = textNode.data.substring( offsetInText, offsetInText + length );
 
 		/**
-		 * Last {@link engine.model.CharacterProxy character node} contained in {@link engine.model.TextProxy}.
+		 * Offset in {@link engine.model.TextProxy#textNode text node} from which the text proxy starts.
 		 *
 		 * @readonly
-		 * @member {engine.model.CharacterProxy} engine.model.TextProxy#last
+		 * @member {Number} engine.model.TextProxy#offsetInText
 		 */
-		this.last = this.getCharAt( this.text.length - 1 );
+		this.offsetInText = offsetInText;
 	}
 
 	/**
-	 * A common parent of all character nodes contained in {@link engine.model.TextProxy}.
+	 * Parent of this text proxy, which is same as parent of text node represented by this text proxy.
 	 *
-	 * @type {engine.model.Element}
+	 * @readonly
+	 * @type {engine.model.Element|engine.model.DocumentFragment|null}
 	 */
-	get commonParent() {
-		return this.first.parent;
+	get parent() {
+		return this.textNode.parent;
 	}
 
 	/**
-	 * Gets a character at given index and creates a {@link engine.model.CharacterProxy} out of it.
+	 * Root of this text proxy, which is same as root of text node represented by this text proxy.
 	 *
-	 * @param {Number} index Character index.
-	 * @returns {engine.model.CharacterProxy}
+	 * @readonly
+	 * @type {engine.model.Element|engine.model.DocumentFragment}
 	 */
-	getCharAt( index ) {
-		if ( index < 0 || index >= this.text.length ) {
-			return null;
-		}
-
-		return new CharacterProxy( this.first._nodeListText, this.first._index + index );
+	get root() {
+		return this.textNode.root;
 	}
 
 	/**
-	 * Checks if the text fragment has an attribute for given key.
+	 * {@link engine.model.Document Document} that owns text node represented by this text proxy or `null` if the text node
+	 * has no parent or is inside a {@link engine.model.DocumentFragment DocumentFragment}.
 	 *
-	 * @param {String} key Key of attribute to check.
-	 * @returns {Boolean} `true` if attribute with given key is set on text fragment, `false` otherwise.
+	 * @returns {engine.model.Document|null}
 	 */
-	hasAttribute( key ) {
-		return this.first.hasAttribute( key );
+	get document() {
+		return this.textNode.document;
 	}
 
 	/**
-	 * Gets an attribute value for given key or undefined it that attribute is not set on text fragment.
+	 * Offset at which this text proxy starts in it's parent.
 	 *
-	 * @param {String} key Key of attribute to look for.
-	 * @returns {*} Attribute value or null.
+	 * @see engine.model.Node#startOffset
+	 * @readonly
+	 * @type {Number}
 	 */
-	getAttribute( key ) {
-		return this.first.getAttribute( key );
+	get startOffset() {
+		return this.textNode.startOffset !== null ? this.textNode.startOffset + this.offsetInText : null;
 	}
 
 	/**
-	 * Returns iterator that iterates over this text fragment attributes.
+	 * Offset size of this text proxy. Equal to the number of characters represented by the text proxy.
 	 *
-	 * @returns {Iterable.<*>}
+	 * @see engine.model.Node#offsetSize
+	 * @readonly
+	 * @type {Number}
 	 */
-	getAttributes() {
-		return this.first.getAttributes();
+	get offsetSize() {
+		return this.data.length;
 	}
 
 	/**
-	 * Sets attribute on the text fragment. If attribute with the same key already is set, it overwrites its values.
+	 * Offset at which this text proxy ends in it's parent.
 	 *
-	 * **Note:** Changing attributes of text fragment affects document state. This TextProxy instance properties
-	 * will be refreshed, but other may get invalidated. It is highly unrecommended to store references to TextProxy instances.
+	 * @see engine.model.Node#endOffset
+	 * @readonly
+	 * @type {Number}
+	 */
+	get endOffset() {
+		return this.startOffset !== null ? this.startOffset + this.offsetSize : null;
+	}
+
+	/**
+	 * Gets path to this text proxy.
 	 *
-	 * @param {String} key Key of attribute to set.
-	 * @param {*} value Attribute value.
+	 * @see engine.model.Node#getPath
+	 * @readonly
+	 * @type {Array.<Number>}
 	 */
-	setAttribute( key, value ) {
-		let index = this.first.getIndex();
+	getPath() {
+		const path = this.textNode.getPath();
 
-		this.commonParent._children.setAttribute( this.first.getIndex(), this.text.length, key, value );
+		if ( path.length > 0 ) {
+			path[ path.length - 1 ] += this.offsetInText;
+		}
 
-		this.first = this.commonParent.getChild( index );
-		this.last = this.getCharAt( this.text.length - 1 );
+		return path;
 	}
 
 	/**
-	 * Removes all attributes from the text fragment and sets given attributes.
+	 * Checks if this text proxy has an attribute for given key.
 	 *
-	 * **Note:** Changing attributes of text fragment affects document state. This `TextProxy` instance properties
-	 * will be refreshed, but other may get invalidated. It is highly unrecommended to store references to TextProxy instances.
-	 *
-	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
-	 * See {@link engine.model.TextProxy#getAttributes}.
+	 * @param {String} key Key of attribute to check.
+	 * @returns {Boolean} `true` if attribute with given key is set on text proxy, `false` otherwise.
 	 */
-	setAttributesTo( attrs ) {
-		let attrsMap = toMap( attrs );
-
-		this.clearAttributes();
+	hasAttribute( key ) {
+		return this.textNode.hasAttribute( key );
+	}
 
-		for ( let attr of attrsMap ) {
-			this.setAttribute( attr[ 0 ], attr[ 1 ] );
-		}
+	/**
+	 * Gets an attribute value for given key or `undefined` if that attribute is not set on text proxy.
+	 *
+	 * @param {String} key Key of attribute to look for.
+	 * @returns {*} Attribute value or `undefined`.
+	 */
+	getAttribute( key ) {
+		return this.textNode.getAttribute( key );
 	}
 
 	/**
-	 * Removes an attribute with given key from the text fragment.
+	 * Returns iterator that iterates over this node's attributes. Attributes are returned as arrays containing two
+	 * items. First one is attribute key and second is attribute value.
 	 *
-	 * **Note:** Changing attributes of text fragment affects document state. This `TextProxy` instance properties
-	 * will be refreshed, but other may get invalidated. It is highly unrecommended to store references to TextProxy instances.
+	 * This format is accepted by native `Map` object and also can be passed in `Node` constructor.
 	 *
-	 * @param {String} key Key of attribute to remove.
+	 * @returns {Iterable.<*>}
 	 */
-	removeAttribute( key ) {
-		this.setAttribute( key, null );
+	getAttributes() {
+		return this.textNode.getAttributes();
 	}
 
 	/**
-	 * Removes all attributes from the text fragment.
+	 * Returns iterator that iterates over this node's attribute keys.
 	 *
-	 * **Note:** Changing attributes of text fragment affects document state. This `TextProxy` instance properties
-	 * will be refreshed, but other may get invalidated. It is highly unrecommended to store references to TextProxy instances.
+	 * @returns {Iterator.<String>}
 	 */
-	clearAttributes() {
-		for ( let attr of this.getAttributes() ) {
-			this.removeAttribute( attr[ 0 ] );
-		}
+	getAttributeKeys() {
+		return this.textNode.getAttributeKeys();
 	}
 }

+ 64 - 64
packages/ckeditor5-engine/src/model/treewalker.js

@@ -3,9 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
-import CharacterProxy from './characterproxy.js';
+import Text from './text.js';
 import TextProxy from './textproxy.js';
 import Element from './element.js';
 import Position from './position.js';
@@ -22,18 +20,18 @@ export default class TreeWalker {
 	 *
 	 * @constructor
 	 * @param {Object} [options={}] Object with configuration.
-	 * @param {'FORWARD'|'BACKWARD'} [options.direction='FORWARD'] Walking direction.
+	 * @param {'forward'|'backward'} [options.direction='forward'] Walking direction.
 	 * @param {engine.model.Range} [options.boundaries=null] Range to define boundaries of the iterator.
 	 * @param {engine.model.Position} [options.startPosition] Starting position.
 	 * @param {Boolean} [options.singleCharacters=false] Flag indicating whether all consecutive characters with the same attributes
 	 * should be returned one by one as multiple {@link engine.model.CharacterProxy} (`true`) objects or as one
 	 * {@link engine.model.TextProxy} (`false`).
 	 * @param {Boolean} [options.shallow=false] Flag indicating whether iterator should enter elements or not. If the
-	 * iterator is shallow child nodes of any iterated node will not be returned along with `ELEMENT_END` tag.
-	 * @param {Boolean} [options.ignoreElementEnd=false] Flag indicating whether iterator should ignore `ELEMENT_END`
+	 * iterator is shallow child nodes of any iterated node will not be returned along with `elementEnd` tag.
+	 * @param {Boolean} [options.ignoreElementEnd=false] Flag indicating whether iterator should ignore `elementEnd`
 	 * tags. If the option is true walker will not return a parent node of start position. If this option is `true`
 	 * each {@link engine.model.Element} will be returned once, while if the option is `false` they might be returned
-	 * twice: for `'ELEMENT_START'` and `'ELEMENT_END'`.
+	 * twice: for `'elementStart'` and `'elementEnd'`.
 	 */
 	constructor( options = {} ) {
 		if ( !options.boundaries && !options.startPosition ) {
@@ -45,27 +43,27 @@ export default class TreeWalker {
 			throw new CKEditorError( 'tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
 		}
 
-		const direction = options.direction || 'FORWARD';
+		const direction = options.direction || 'forward';
 
-		if ( direction != 'FORWARD' && direction != 'BACKWARD' ) {
+		if ( direction != 'forward' && direction != 'backward' ) {
 			throw new CKEditorError(
-				'tree-walker-unknown-direction: Only `BACKWARD` and `FORWARD` direction allowed.',
+				'tree-walker-unknown-direction: Only `backward` and `forward` direction allowed.',
 				{ direction }
 			);
 		}
 
 		/**
-		 * Walking direction. Defaults `FORWARD`.
+		 * Walking direction. Defaults `'forward'`.
 		 *
 		 * @readonly
-		 * @member {'BACKWARD'|'FORWARD'} engine.model.TreeWalker#direction
+		 * @member {'backward'|'forward'} engine.model.TreeWalker#direction
 		 */
 		this.direction = direction;
 
 		/**
 		 * Iterator boundaries.
 		 *
-		 * When the iterator is walking `FORWARD` on the end of boundary or is walking `BACKWARD`
+		 * When the iterator is walking `'forward'` on the end of boundary or is walking `'backward'`
 		 * on the start of boundary, then `{ done: true }` is returned.
 		 *
 		 * If boundaries are not defined they are set before first and after last child of the root node.
@@ -78,8 +76,8 @@ export default class TreeWalker {
 		/**
 		 * Iterator position. This is always static position, even if the initial position was a
 		 * {@link engine.model.LivePosition live position}. If start position is not defined then position depends
-		 * on {@link #direction}. If direction is `FORWARD` position starts form the beginning, when direction
-		 * is `BACKWARD` position starts from the end.
+		 * on {@link #direction}. If direction is `'forward'` position starts form the beginning, when direction
+		 * is `'backward'` position starts from the end.
 		 *
 		 * @readonly
 		 * @member {engine.model.Position} engine.model.TreeWalker#position
@@ -87,7 +85,7 @@ export default class TreeWalker {
 		if ( options.startPosition ) {
 			this.position = Position.createFromPosition( options.startPosition );
 		} else {
-			this.position = Position.createFromPosition( this.boundaries[ this.direction == 'BACKWARD' ? 'end' : 'start' ] );
+			this.position = Position.createFromPosition( this.boundaries[ this.direction == 'backward' ? 'end' : 'start' ] );
 		}
 
 		/**
@@ -101,7 +99,7 @@ export default class TreeWalker {
 
 		/**
 		 * Flag indicating whether iterator should enter elements or not. If the iterator is shallow child nodes of any
-		 * iterated node will not be returned along with `ELEMENT_END` tag.
+		 * iterated node will not be returned along with `elementEnd` tag.
 		 *
 		 * @readonly
 		 * @member {Boolean} engine.model.TreeWalker#shallow
@@ -109,10 +107,10 @@ export default class TreeWalker {
 		this.shallow = !!options.shallow;
 
 		/**
-		 * Flag indicating whether iterator should ignore `ELEMENT_END` tags. If the option is true walker will not
+		 * Flag indicating whether iterator should ignore `elementEnd` tags. If the option is true walker will not
 		 * return a parent node of the start position. If this option is `true` each {@link engine.model.Element} will
 		 * be returned once, while if the option is `false` they might be returned twice:
-		 * for `'ELEMENT_START'` and `'ELEMENT_END'`.
+		 * for `'elementStart'` and `'elementEnd'`.
 		 *
 		 * @readonly
 		 * @member {Boolean} engine.model.TreeWalker#ignoreElementEnd
@@ -158,7 +156,7 @@ export default class TreeWalker {
 	 * @returns {Object} Object implementing iterator interface, returning information about taken step.
 	 */
 	next() {
-		if ( this.direction == 'FORWARD' ) {
+		if ( this.direction == 'forward' ) {
 			return this._next();
 		} else {
 			return this._previous();
@@ -179,7 +177,7 @@ export default class TreeWalker {
 		const parent = this._visitedParent;
 
 		// We are at the end of the root.
-		if ( parent.parent === null && position.offset === parent.getChildCount() ) {
+		if ( parent.parent === null && position.offset === parent.getMaxOffset() ) {
 			return { done: true };
 		}
 
@@ -188,7 +186,7 @@ export default class TreeWalker {
 			return { done: true };
 		}
 
-		const node = parent.getChild( position.offset );
+		const node = position.textNode ? position.textNode : position.nodeAfter;
 
 		if ( node instanceof Element ) {
 			if ( !this.shallow ) {
@@ -201,29 +199,30 @@ export default class TreeWalker {
 
 			this.position = position;
 
-			return formatReturnValue( 'ELEMENT_START', node, previousPosition, position, 1 );
-		} else if ( node instanceof CharacterProxy ) {
-			if ( this.singleCharacters ) {
-				position.offset++;
-				this.position = position;
+			return formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
+		} else if ( node instanceof Text ) {
+			let charactersCount, offsetInTextNode;
 
-				return formatReturnValue( 'CHARACTER', node, previousPosition, position, 1 );
+			if ( this.singleCharacters ) {
+				charactersCount = 1;
 			} else {
-				let charactersCount = node._nodeListText.text.length - node._index;
-				let offset = position.offset + charactersCount;
+				let offset = node.endOffset;
 
 				if ( this._boundaryEndParent == parent && this.boundaries.end.offset < offset ) {
 					offset = this.boundaries.end.offset;
-					charactersCount = offset - position.offset;
 				}
 
-				let textProxy = new TextProxy( node, charactersCount );
+				charactersCount = offset - position.offset;
+			}
 
-				position.offset = offset;
-				this.position = position;
+			offsetInTextNode = position.offset - node.startOffset;
 
-				return formatReturnValue( 'TEXT', textProxy, previousPosition, position, charactersCount );
-			}
+			const item = new TextProxy( node, offsetInTextNode, charactersCount );
+
+			position.offset += charactersCount;
+			this.position = position;
+
+			return formatReturnValue( 'text', item, previousPosition, position, charactersCount );
 		} else {
 			// `node` is not set, we reached the end of current `parent`.
 			position.path.pop();
@@ -234,7 +233,7 @@ export default class TreeWalker {
 			if ( this.ignoreElementEnd ) {
 				return this._next();
 			} else {
-				return formatReturnValue( 'ELEMENT_END', parent, previousPosition, position );
+				return formatReturnValue( 'elementEnd', parent, previousPosition, position );
 			}
 		}
 	}
@@ -263,55 +262,56 @@ export default class TreeWalker {
 		}
 
 		// Get node just before current position
-		const node = parent.getChild( position.offset - 1 );
+		const node = position.textNode ? position.textNode : position.nodeBefore;
 
 		if ( node instanceof Element ) {
 			position.offset--;
 
 			if ( !this.shallow ) {
-				position.path.push( node.getChildCount() );
+				position.path.push( node.getMaxOffset() );
 				this.position = position;
 				this._visitedParent = node;
 
 				if ( this.ignoreElementEnd ) {
 					return this._previous();
 				} else {
-					return formatReturnValue( 'ELEMENT_END', node, previousPosition, position );
+					return formatReturnValue( 'elementEnd', node, previousPosition, position );
 				}
 			} else {
 				this.position = position;
 
-				return formatReturnValue( 'ELEMENT_START', node, previousPosition, position, 1 );
+				return formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
 			}
-		} else if ( node instanceof CharacterProxy ) {
-			if ( this.singleCharacters ) {
-				position.offset--;
-				this.position = position;
+		} else if ( node instanceof Text ) {
+			let charactersCount, offsetInTextNode;
 
-				return formatReturnValue( 'CHARACTER', node, previousPosition, position, 1 );
+			if ( this.singleCharacters ) {
+				charactersCount = 1;
 			} else {
-				let charactersCount = node._index + 1;
-				let offset = position.offset - charactersCount;
+				let offset = node.startOffset;
 
 				if ( this._boundaryStartParent == parent && this.boundaries.start.offset > offset ) {
 					offset = this.boundaries.start.offset;
-					charactersCount = position.offset - offset;
 				}
 
-				let textFragment = new TextProxy( parent.getChild( offset ), charactersCount );
+				charactersCount = position.offset - offset;
+			}
+
+			offsetInTextNode = position.offset - node.startOffset;
 
-				position.offset = offset;
-				this.position = position;
+			const item = new TextProxy( node, offsetInTextNode - charactersCount, charactersCount );
 
-				return formatReturnValue( 'TEXT', textFragment, previousPosition, position, charactersCount );
-			}
+			position.offset -= charactersCount;
+			this.position = position;
+
+			return formatReturnValue( 'text', item, previousPosition, position, charactersCount );
 		} else {
 			// `node` is not set, we reached the beginning of current `parent`.
 			position.path.pop();
 			this.position = position;
 			this._visitedParent = parent.parent;
 
-			return formatReturnValue( 'ELEMENT_START', parent, previousPosition, position, 1 );
+			return formatReturnValue( 'elementStart', parent, previousPosition, position, 1 );
 		}
 	}
 }
@@ -331,8 +331,8 @@ function formatReturnValue( type, item, previousPosition, nextPosition, length )
 
 /**
  * Type of the step made by {@link engine.model.TreeWalker}.
- * Possible values: `'ELEMENT_START'` if walker is at the beginning of a node, `'ELEMENT_END'` if walker is at the end of node,
- * `'CHARACTER'` if walker traversed over a character, or `'TEXT'` if walker traversed over multiple characters (available in
+ * Possible values: `'elementStart'` if walker is at the beginning of a node, `'elementEnd'` if walker is at the end of node,
+ * `'character'` if walker traversed over a character, or `'text'` if walker traversed over multiple characters (available in
  * character merging mode, see {@link engine.model.TreeWalker#constructor}).
  *
  * @typedef {String} engine.model.TreeWalkerValueType
@@ -345,24 +345,24 @@ function formatReturnValue( type, item, previousPosition, nextPosition, length )
  * @property {engine.model.TreeWalkerValueType} type
  * @property {engine.model.Item} item Item between old and new positions of {@link engine.model.TreeWalker}.
  * @property {engine.model.Position} previousPosition Previous position of the iterator.
- * * Forward iteration: For `'ELEMENT_END'` it is the last position inside the element. For all other types it is the
+ * * Forward iteration: For `'elementEnd'` it is the last position inside the element. For all other types it is the
  * position before the item. Note that it is more efficient to use this position then calculate the position before
  * the node using {@link engine.model.Position.createBefore}. It is also more efficient to get the
  * position after node by shifting `previousPosition` by `length`, using {@link engine.model.Position#getShiftedBy},
  * then calculate the position using {@link engine.model.Position.createAfter}.
- * * Backward iteration: For `'ELEMENT_START'` it is the first position inside the element. For all other types it is
+ * * Backward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is
  * the position after item.
  * @property {engine.model.Position} nextPosition Next position of the iterator.
- * * Forward iteration: For `'ELEMENT_START'` it is the first position inside the element. For all other types it is
+ * * Forward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is
  * the position after the item.
- * * Backward iteration: For `'ELEMENT_END'` it is last position inside element. For all other types it is the position
+ * * Backward iteration: For `'elementEnd'` it is last position inside element. For all other types it is the position
  * before the item.
- * @property {Number} [length] Length of the item. For `'ELEMENT_START'` and `'CHARACTER'` it is 1. For `'TEXT'` it is
- * the length of the text. For `'ELEMENT_END'` it is undefined.
+ * @property {Number} [length] Length of the item. For `'elementStart'` and `'character'` it is 1. For `'text'` it is
+ * the length of the text. For `'elementEnd'` it is undefined.
  */
 
 /**
  * Tree walking directions.
  *
- * @typedef {'FORWARD'|'BACKWARD'} engine.view.TreeWalkerDirection
+ * @typedef {'forward'|'backward'} engine.view.TreeWalkerDirection
  */

+ 316 - 0
packages/ckeditor5-engine/src/model/writer.js

@@ -0,0 +1,316 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Node from './node.js';
+import Text from './text.js';
+import TextProxy from './textproxy.js';
+import Range from './range.js';
+import DocumentFragment from './documentfragment.js';
+import NodeList from './nodelist.js';
+import CKEditorError from '../../utils/ckeditorerror.js';
+
+/**
+ * Contains functions used for composing model tree, grouped together under "model writer" name. Those functions
+ * are built on top of {@link engine.model.Node node}, and it's child classes', APIs.
+ *
+ * Model writer API has multiple advantages and it is highly recommended to use it when changing model tree and nodes:
+ * * model writer API {@link engine.model.writer.normalizeNodes normalizes inserted nodes}, which means that you can insert
+ * not only {@link engine.model.Node nodes}, but also `String`s, {@link engine.model.TextProxy text proxies} and
+ * {@link engine.model.DocumentFragment document fragments},
+ * * model writer API operates on {@link engine.model.Position positions}, which means that you have
+ * better control over manipulating model tree as positions operate on offsets rather than indexes,
+ * * model writer API automatically merges {@link engine.model.TextNode text nodes} with same attributes, which means
+ * lower memory usage and better efficiency.
+ *
+ * @namespace engine.model.writer
+ */
+
+export default {
+	insert,
+	remove,
+	move,
+	setAttribute,
+	removeAttribute,
+	normalizeNodes
+};
+
+/**
+ * Inserts given nodes at given position.
+ *
+ * @function engine.model.writer.insert
+ * @param {engine.model.Position} position Position at which nodes should be inserted.
+ * @param {engine.model.NodeSet} nodes Nodes to insert.
+ * @returns {engine.model.Range} Range spanning over inserted elements.
+ */
+export function insert( position, nodes ) {
+	nodes = normalizeNodes( nodes );
+
+	// We have to count offset before inserting nodes because they can get merged and we would get wrong offsets.
+	const offset = nodes.reduce( ( sum, node ) => sum + node.offsetSize, 0 );
+	const parent = position.parent;
+
+	// Insertion might be in a text node, we should split it if that's the case.
+	let index = _splitNodeAtPosition( position );
+
+	// Insert nodes at given index. After splitting we have a proper index and insertion is between nodes,
+	// using basic `Element` API.
+	parent.insertChildren( index, nodes );
+
+	// Merge text nodes, if possible. Merging is needed only at points where inserted nodes "touch" "old" nodes.
+	_mergeNodesAtIndex( parent, index + nodes.length );
+	_mergeNodesAtIndex( parent, index );
+
+	return new Range( position, position.getShiftedBy( offset ) );
+}
+
+/**
+ * Removed nodes in given range. Only {@link engine.model.Range#isFlat flat} ranges are accepted.
+ *
+ * @function engine.model.writer.remove
+ * @param {engine.model.Range} range Range containing nodes to remove.
+ * @returns {Array.<engine.model.Node>}
+ */
+export function remove( range ) {
+	if ( !range.isFlat ) {
+		/**
+		 * Trying to remove a range that starts and ends in different element.
+		 *
+		 * @error model-writer-remove-range-not-flat
+		 */
+		throw new CKEditorError( 'model-writer-remove-range-not-flat: ' +
+			'Trying to remove a range that starts and ends in different element.' );
+	}
+
+	const parent = range.start.parent;
+
+	// Range may be inside text nodes, we have to split them if that's the case.
+	const indexStart = _splitNodeAtPosition( range.start );
+	const indexEnd = _splitNodeAtPosition( range.end );
+
+	// Remove the text nodes using basic `Element` API.
+	const removed = parent.removeChildren( indexStart, indexEnd - indexStart );
+
+	// Merge text nodes, if possible. After some nodes were removed, node before and after removed range will be
+	// touching at the position equal to the removed range beginning. We check merging possibility there.
+	_mergeNodesAtIndex( parent, indexStart );
+
+	return removed;
+}
+
+/**
+ * Moves nodes in given range to given target position. Only {@link engine.model.Range#isFlat flat} ranges are accepted.
+ *
+ * @param {engine.model.Range} sourceRange Range containing nodes to move.
+ * @param {engine.model.Position} targetPosition Position to which nodes should be moved.
+ * @returns {engine.model.Range} Range containing moved nodes.
+ */
+export function move( sourceRange, targetPosition ) {
+	/* jshint validthis:true */
+	if ( !sourceRange.isFlat ) {
+		/**
+		 * Trying to move a range that starts and ends in different element.
+		 *
+		 * @error model-writer-move-range-not-flat
+		 */
+		throw new CKEditorError( 'model-writer-move-range-not-flat: ' +
+			'Trying to move a range that starts and ends in different element.' );
+	}
+
+	const nodes = this.remove( sourceRange );
+
+	// We have to fix `targetPosition` because model changed after nodes from `sourceRange` got removed and
+	// that change might have an impact on `targetPosition`.
+	targetPosition = targetPosition._getTransformedByDeletion( sourceRange.start, sourceRange.end.offset - sourceRange.start.offset );
+
+	return this.insert( targetPosition, nodes );
+}
+
+/**
+ * Sets given attribute on nodes in given range.
+ *
+ * @param {engine.model.Range} range Range containing nodes that should have the attribute set.
+ * @param {String} key Key of attribute to set.
+ * @param {*} value Attribute value.
+ */
+export function setAttribute( range, key, value ) {
+	// Range might start or end in text nodes, so we have to split them.
+	_splitNodeAtPosition( range.start );
+	const indexEnd = _splitNodeAtPosition( range.end );
+
+	// Iterate over all items in the range.
+	for ( let item of range.getItems() ) {
+		// Iterator will return `TextProxy` instances but we know that those text proxies will
+		// always represent full text nodes (this is guaranteed thanks to splitting we did before).
+		// So, we can operate on those text proxies' text nodes.
+		let node = item instanceof TextProxy ? item.textNode : item;
+
+		if ( value !== null ) {
+			node.setAttribute( key, value );
+		} else {
+			node.removeAttribute( key );
+		}
+
+		// After attributes changing it may happen that some text nodes can be merged. Try to merge with previous node.
+		_mergeNodesAtIndex( node.parent, node.getIndex() );
+	}
+
+	// Try to merge last changed node with it's previous sibling (not covered by the loop above).
+	_mergeNodesAtIndex( range.end.parent, indexEnd );
+}
+
+/**
+ * Removes given attribute from nodes in given range.
+ *
+ * @param {engine.model.Range} range Range containing nodes that should have the attribute removed.
+ * @param {String} key Key of attribute to remove.
+ */
+export function removeAttribute( range, key ) {
+	/* jshint validthis:true */
+	this.setAttribute( range, key, null );
+}
+
+/**
+ * Normalizes given object or an array of objects to an array of {@link engine.model.Node nodes}. See
+ * {@link engine.model.NodeSet NodeSet} for details on how normalization is performed.
+ *
+ * @param {engine.model.NodeSet} nodes Objects to normalize.
+ * @returns {Array.<engine.model.Node>} Normalized nodes.
+ */
+export function normalizeNodes( nodes ) {
+	const normalized = [];
+
+	if ( !( nodes instanceof Array ) ) {
+		nodes = [ nodes ];
+	}
+
+	// Convert instances of classes other than Node.
+	for ( let i = 0; i < nodes.length; i++ ) {
+		if ( typeof nodes[ i ] == 'string' ) {
+			normalized.push( new Text( nodes[ i ] ) );
+		} else if ( nodes[ i ] instanceof TextProxy ) {
+			normalized.push( new Text( nodes[ i ].data, nodes[ i ].getAttributes() ) );
+		} else if ( nodes[ i ] instanceof DocumentFragment || nodes[ i ] instanceof NodeList ) {
+			for ( let child of nodes[ i ] ) {
+				normalized.push( child );
+			}
+		} else if ( nodes[ i ] instanceof Node ) {
+			normalized.push( nodes[ i ] );
+		}
+		// Skip unrecognized type.
+	}
+
+	// Merge text nodes.
+	for ( let i = 1; i < normalized.length; i++ ) {
+		const node = normalized[ i ];
+		const prev = normalized[ i - 1 ];
+
+		if ( node instanceof Text && prev instanceof Text && _haveSameAttributes( node, prev ) ) {
+			// Doing this instead changing prev.data because .data is readonly.
+			normalized.splice( i - 1, 2, new Text( prev.data + node.data, prev.getAttributes() ) );
+			i--;
+		}
+	}
+
+	return normalized;
+}
+
+/**
+ * Checks if nodes before and after given index in given element are {@link engine.model.Text text nodes} and
+ * merges them into one node if they have same attributes.
+ *
+ * Merging is done by removing two text nodes and inserting a new text node containing data from both merged text nodes.
+ *
+ * @ignore
+ * @private
+ * @param {engine.model.Element} element Parent element of nodes to merge.
+ * @param {Number} index Index between nodes to merge.
+ */
+function _mergeNodesAtIndex( element, index ) {
+	const nodeBefore = element.getChild( index - 1 );
+	const nodeAfter = element.getChild( index );
+
+	// Check if both of those nodes are text objects with same attributes.
+	if ( nodeBefore instanceof Text && nodeAfter instanceof Text && _haveSameAttributes( nodeBefore, nodeAfter ) ) {
+		// Append text of text node after index to the before one.
+		const mergedNode = new Text( nodeBefore.data + nodeAfter.data, nodeBefore.getAttributes() );
+
+		// Remove separate text nodes.
+		element.removeChildren( index - 1, 2 );
+
+		// Insert merged text node.
+		element.insertChildren( index - 1, mergedNode );
+	}
+}
+
+/**
+ * Checks if given position is in a text node, and if so, splits the text node in two text nodes, each of them
+ * containing a part of original text node.
+ *
+ * @ignore
+ * @private
+ * @param {engine.model.Position} position Position at which node should be split.
+ * @returns {Number} Index, in position's parent element, between split nodes.
+ */
+function _splitNodeAtPosition( position ) {
+	const textNode = position.textNode;
+	const element = position.parent;
+
+	if ( textNode ) {
+		const offsetDiff = position.offset - textNode.startOffset;
+		const index = textNode.getIndex();
+
+		element.removeChildren( index, 1 );
+
+		const firstPart = new Text( textNode.data.substr( 0, offsetDiff ), textNode.getAttributes() );
+		const secondPart = new Text( textNode.data.substr( offsetDiff ), textNode.getAttributes() );
+
+		element.insertChildren( index, [ firstPart, secondPart ] );
+
+		return index + 1;
+	}
+
+	return element.offsetToIndex( position.offset );
+}
+
+/**
+ * Checks whether two given nodes have same attributes.
+ *
+ * @ignore
+ * @private
+ * @param {engine.model.Node} nodeA Node to check.
+ * @param {engine.model.Node} nodeB Node to check.
+ * @returns {Boolean} `true` if nodes have same attributes, `false` otherwise.
+ */
+function _haveSameAttributes( nodeA, nodeB ) {
+	const iteratorA = nodeA.getAttributes();
+	const iteratorB = nodeB.getAttributes();
+
+	for ( let attr of iteratorA ) {
+		if ( attr[ 1 ] !== nodeB.getAttribute( attr[ 0 ] ) ) {
+			return false;
+		}
+
+		iteratorB.next();
+	}
+
+	return iteratorB.next().done;
+}
+
+/**
+ * Value that can be normalized to an array of {@link engine.model.Node nodes}.
+ *
+ * Non-arrays are normalized as follows:
+ * * {@link engine.model.Node Node} is left as is,
+ * * {@link engine.model.TextProxy TextProxy} and `String` are normalized to {@link engine.model.Text Text},
+ * * {@link engine.model.NodeList NodeList} is normalized to an array containing all nodes that are in that node list,
+ * * {@link engine.model.DocumentFragment DocumentFragment} is normalized to an array containing all of it's children.
+ *
+ * Arrays are processed item by item like non-array values and flattened to one array. Normalization always results in
+ * a flat array of {@link engine.model.Node nodes}. Consecutive text nodes (or items normalized to text nodes) will be
+ * merged if they have same attributes.
+ *
+ * @typedef {engine.model.Node|engine.model.TextProxy|String|engine.model.NodeList|engine.model.DocumentFragment|Iterable}
+ * engine.model.NodeSet
+ */

+ 0 - 2
packages/ckeditor5-engine/src/view/attributeelement.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Element from './element.js';
 
 // Default attribute priority.

+ 0 - 2
packages/ckeditor5-engine/src/view/containerelement.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Element from './element.js';
 
 /**

+ 17 - 3
packages/ckeditor5-engine/src/view/document.js

@@ -3,15 +3,16 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Selection from './selection.js';
 import Renderer from './renderer.js';
 import DomConverter from './domconverter.js';
 import RootEditableElement from './rooteditableelement.js';
 import { injectQuirksHandling } from './filler.js';
 import log from '../../utils/log.js';
-
+import MutationObserver from './observer/mutationobserver.js';
+import SelectionObserver from './observer/selectionobserver.js';
+import FocusObserver from './observer/focusobserver.js';
+import KeyObserver from './observer/keyobserver.js';
 import mix from '../../utils/mix.js';
 import ObservableMixin from '../../utils/observablemixin.js';
 
@@ -24,6 +25,13 @@ import ObservableMixin from '../../utils/observablemixin.js';
  * If you want to only transform the tree of view elements to the DOM elements you can use the
  * {@link engine.view.DomConverter DomConverter}.
  *
+ * Note that the following observers are added by the class constructor and are always available:
+ *
+ * * {@link view.observer.MutationObserver},
+ * * {@link view.observer.SelectionObserver},
+ * * {@link view.observer.FocusObserver},
+ * * {@link view.observer.KeyObserver}.
+ *
  * @memberOf engine.view
  * @mixes utils.EmitterMixin
  */
@@ -94,6 +102,12 @@ export default class Document {
 		 */
 		this._observers = new Map();
 
+		// Add default observers.
+		this.addObserver( MutationObserver );
+		this.addObserver( SelectionObserver );
+		this.addObserver( FocusObserver );
+		this.addObserver( KeyObserver );
+
 		injectQuirksHandling( this );
 
 		// Listens `render` event on default priority.

+ 1 - 3
packages/ckeditor5-engine/src/view/documentfragment.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import mix from '../../utils/mix.js';
 import isIterable from '../../utils/isiterable.js';
 import EmitterMixin from '../../utils/emittermixin.js';
@@ -144,7 +142,7 @@ export default class DocumentFragment {
 	 * @fires engine.view.Node#change
 	 */
 	_fireChange( type, node ) {
-		this.fire( 'change', type, node );
+		this.fire( 'change:' + type, node );
 	}
 }
 

+ 0 - 2
packages/ckeditor5-engine/src/view/domconverter.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import ViewText from './text.js';
 import ViewElement from './element.js';
 import ViewPosition from './position.js';

+ 0 - 2
packages/ckeditor5-engine/src/view/editableelement.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import ContainerElement from './containerelement.js';
 
 import mix from '../../utils/mix.js';

+ 0 - 2
packages/ckeditor5-engine/src/view/element.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Node from './node.js';
 import objectToMap from '../../utils/objecttomap.js';
 import isIterable from '../../utils/isiterable.js';

+ 0 - 2
packages/ckeditor5-engine/src/view/filler.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import { keyCodes } from '../../utils/keyboard.js';
 
 /**

+ 0 - 2
packages/ckeditor5-engine/src/view/matcher.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 /**
  * View matcher class.
  * Instance of this class can be used to find {@link engine.view.Element elements} that match given pattern.

+ 0 - 2
packages/ckeditor5-engine/src/view/node.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import CKEditorError from '../../utils/ckeditorerror.js';
 import EmitterMixin from '../../utils/emittermixin.js';
 import mix from '../../utils/mix.js';

+ 0 - 2
packages/ckeditor5-engine/src/view/observer/domeventdata.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import extend from '../../../utils/lib/lodash/extend.js';
 
 /**

+ 0 - 2
packages/ckeditor5-engine/src/view/observer/domeventobserver.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Observer from './observer.js';
 import DomEventData from './domeventdata.js';
 

+ 3 - 5
packages/ckeditor5-engine/src/view/observer/focusobserver.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import DomEventObserver from './domeventobserver.js';
 
 /**
@@ -12,7 +10,7 @@ import DomEventObserver from './domeventobserver.js';
  * Focus observer handle also {@link engine.view.RootEditableElement#isFocused isFocused} property of the
  * {@link engine.view.RootEditableElement root elements}.
  *
- * Note that this observer is attached by the {@link engine.EditingController} and is available by default.
+ * Note that this observer is attached by the {@link engine.view.Document} and is available by default.
  *
  * @memberOf engine.view.observer
  * @extends engine.view.observer.DomEventObserver
@@ -46,7 +44,7 @@ export default class FocusObserver extends DomEventObserver {
  *
  * Introduced by {@link engine.view.observer.FocusObserver}.
  *
- * Note that because {@link engine.view.observer.FocusObserver} is attached by the {@link engine.EditingController}
+ * Note that because {@link engine.view.observer.FocusObserver} is attached by the {@link engine.view.Document}
  * this event is available by default.
  *
  * @see engine.view.observer.FocusObserver
@@ -59,7 +57,7 @@ export default class FocusObserver extends DomEventObserver {
  *
  * Introduced by {@link engine.view.observer.FocusObserver}.
  *
- * Note that because {@link engine.view.observer.FocusObserver} is attached by the {@link engine.EditingController}
+ * Note that because {@link engine.view.observer.FocusObserver} is attached by the {@link engine.view.Document}
  * this event is available by default.
  *
  * @see engine.view.observer.FocusObserver

+ 2 - 4
packages/ckeditor5-engine/src/view/observer/keyobserver.js

@@ -3,15 +3,13 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import DomEventObserver from './domeventobserver.js';
 import { getCode } from '../../../utils/keyboard.js';
 
 /**
  * {@link engine.view.Document#keydown Key down} event observer.
  *
- * Note that this observer is attached by the {@link engine.EditingController} and is available by default.
+ * Note that this observer is attached by the {@link engine.view.Document} and is available by default.
  *
  * @memberOf engine.view.observer
  * @extends engine.view.observer.DomEventObserver
@@ -43,7 +41,7 @@ export default class KeyObserver extends DomEventObserver {
  *
  * Introduced by {@link engine.view.observer.KeyObserver}.
  *
- * Note that because {@link engine.view.observer.KeyObserver} is attached by the {@link engine.EditingController}
+ * Note that because {@link engine.view.observer.KeyObserver} is attached by the {@link engine.view.Document}
  * this event is available by default.
  *
  * @see engine.view.observer.KeyObserver

+ 2 - 4
packages/ckeditor5-engine/src/view/observer/mutationobserver.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Observer from './observer.js';
 import { startsWithFiller, getDataWithoutFiller } from '../filler.js';
 
@@ -19,7 +17,7 @@ import { startsWithFiller, getDataWithoutFiller } from '../filler.js';
  * mutations on elements which do not have corresponding view elements. Also
  * {@link engine.view.Document.MutatatedText text mutation} is fired only if parent element do not change child list.
  *
- * Note that this observer is attached by the {@link engine.EditingController} and is available by default.
+ * Note that this observer is attached by the {@link engine.view.Document} and is available by default.
  *
  * @memberOf engine.view.observer
  * @extends engine.view.observer.Observer
@@ -203,7 +201,7 @@ export default class MutationObserver extends Observer {
  *
  * Introduced by {@link engine.view.observer.MutationObserver}.
  *
- * Note that because {@link engine.view.observer.MutationObserver} is attached by the {@link engine.EditingController}
+ * Note that because {@link engine.view.observer.MutationObserver} is attached by the {@link engine.view.Document}
  * this event is available by default.
  *
  * @see engine.view.observer.MutationObserver

+ 0 - 2
packages/ckeditor5-engine/src/view/observer/observer.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 /**
  * Abstract base observer class. Observers are classes which observe changes on DOM elements, do the preliminary
  * processing and fire events on the {@link engine.view.Document} objects. Observers can also add features to the view,

+ 2 - 4
packages/ckeditor5-engine/src/view/observer/selectionobserver.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Observer from './observer.js';
 import MutationObserver from './mutationobserver.js';
 
@@ -17,7 +15,7 @@ import log from '../../../utils/log.js';
  * {@link engine.view.Document#selectionChange} event only if selection change was the only change in the document
  * and DOM selection is different then the view selection.
  *
- * Note that this observer is attached by the {@link engine.EditingController} and is available by default.
+ * Note that this observer is attached by the {@link engine.view.Document} and is available by default.
  *
  * @see engine.view.MutationObserver
  * @memberOf engine.view.observer
@@ -194,7 +192,7 @@ export default class SelectionObserver extends Observer {
  *
  * Introduced by {@link engine.view.observer.SelectionObserver}.
  *
- * Note that because {@link engine.view.observer.SelectionObserver} is attached by the {@link engine.EditingController}
+ * Note that because {@link engine.view.observer.SelectionObserver} is attached by the {@link engine.view.Document}
  * this event is available by default.
  *
  * @see engine.view.observer.SelectionObserver

+ 12 - 14
packages/ckeditor5-engine/src/view/position.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Text from './text.js';
 import TextProxy from './textproxy.js';
 
@@ -106,7 +104,7 @@ export default class Position {
 	 * @returns {Boolean} Returns `true` if this position is before given position.
 	 */
 	isBefore( otherPosition ) {
-		return this.compareWith( otherPosition ) == 'BEFORE';
+		return this.compareWith( otherPosition ) == 'before';
 	}
 
 	/**
@@ -120,7 +118,7 @@ export default class Position {
 	 * @returns {Boolean} Returns `true` if this position is after given position.
 	 */
 	isAfter( otherPosition ) {
-		return this.compareWith( otherPosition ) == 'AFTER';
+		return this.compareWith( otherPosition ) == 'after';
 	}
 
 	/**
@@ -132,12 +130,12 @@ export default class Position {
 	 */
 	compareWith( otherPosition ) {
 		if ( this.isEqual( otherPosition ) ) {
-			return 'SAME';
+			return 'same';
 		}
 
 		// If positions have same parent.
 		if ( this.parent === otherPosition.parent ) {
-			return this.offset - otherPosition.offset < 0 ? 'BEFORE' : 'AFTER';
+			return this.offset - otherPosition.offset < 0 ? 'before' : 'after';
 		}
 
 		// Get path from root to position's parent element.
@@ -152,13 +150,13 @@ export default class Position {
 		switch ( result ) {
 			case 0:
 				// No common ancestors found.
-				return 'DIFFERENT';
+				return 'different';
 
-			case 'PREFIX':
+			case 'prefix':
 				commonAncestorIndex = path.length - 1;
 				break;
 
-			case 'EXTENSION':
+			case 'extension':
 				commonAncestorIndex = otherPath.length - 1;
 				break;
 
@@ -175,17 +173,17 @@ export default class Position {
 		if ( commonAncestor === this.parent ) {
 			const index = this.offset - nextAncestor2.getIndex();
 
-			return index <= 0 ? 'BEFORE' : 'AFTER';
+			return index <= 0 ? 'before' : 'after';
 		} else if ( commonAncestor === otherPosition.parent ) {
 			const index = nextAncestor1.getIndex() - otherPosition.offset;
 
-			return index < 0 ? 'BEFORE' : 'AFTER';
+			return index < 0 ? 'before' : 'after';
 		}
 
 		const index = nextAncestor1.getIndex() - nextAncestor2.getIndex();
 
 		// Compare indexes of next ancestors inside common one.
-		return index < 0 ? 'BEFORE' : 'AFTER';
+		return index < 0 ? 'before' : 'after';
 	}
 
 	/**
@@ -269,8 +267,8 @@ export default class Position {
 }
 
 /**
- * A flag indicating whether this position is `'BEFORE'` or `'AFTER'` or `'SAME'` as given position.
- * If positions are in different roots `'DIFFERENT'` flag is returned.
+ * A flag indicating whether this position is `'before'` or `'after'` or `'same'` as given position.
+ * If positions are in different roots `'different'` flag is returned.
  *
  * @typedef {String} engine.view.PositionRelation
  */

+ 0 - 2
packages/ckeditor5-engine/src/view/range.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Position from './position.js';
 
 /**

+ 4 - 6
packages/ckeditor5-engine/src/view/renderer.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import ViewText from './text.js';
 import ViewElement from './element.js';
 import ViewPosition from './position.js';
@@ -89,7 +87,7 @@ export default class Renderer {
 
 		/**
 		 * Position of the inline {@link engine.view.filler filler}.
-		 * It should always be put BEFORE the text which contains filler.
+		 * It should always be put before the text which contains filler.
 		 *
 		 * @private
 		 * @member {engine.view.Position} engine.view.Renderer#_inlineFillerPosition
@@ -383,12 +381,12 @@ export default class Renderer {
 		let i = 0;
 
 		for ( let action of actions ) {
-			if ( action === 'INSERT' ) {
+			if ( action === 'insert' ) {
 				insertAt( domElement, i, expectedDomChildren[ i ] );
 				i++;
-			} else if ( action === 'DELETE' ) {
+			} else if ( action === 'delete' ) {
 				remove( actualDomChildren[ i ] );
-			} else { // 'EQUAL'
+			} else { // 'equal'
 				i++;
 			}
 		}

+ 0 - 2
packages/ckeditor5-engine/src/view/rooteditableelement.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import EditableElement from './editableelement.js';
 
 /**

+ 12 - 6
packages/ckeditor5-engine/src/view/selection.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import CKEditorError from '../../utils/ckeditorerror.js';
 import Range from './range.js';
 import Position from './position.js';
@@ -122,13 +120,17 @@ export default class Selection {
 	 * The flag is used to set {@link engine.view.Selection#anchor anchor} and
 	 * {@link engine.view.Selection#focus focus} properties.
 	 *
-	 * Throws {@link utils.CKEditorError CKEditorError} `view-selection-range-intersects` if added range intersects
+	 * Throws {@link utils.CKEditorError CKEditorError} `selection-range-intersects` if added range intersects
 	 * with ranges already stored in Selection instance.
 	 *
 	 * @fires engine.view.Selection#change
 	 * @param {engine.view.Range} range
 	 */
 	addRange( range, isBackward ) {
+		if ( !( range instanceof Range ) ) {
+			throw new CKEditorError( 'selection-invalid-range: Invalid Range.' );
+		}
+
 		this._pushRange( range );
 		this._lastRangeBackward = !!isBackward;
 		this.fire( 'change' );
@@ -259,6 +261,10 @@ export default class Selection {
 		this._ranges = [];
 
 		for ( let range of newRanges ) {
+			if ( !( range instanceof Range ) ) {
+				throw new CKEditorError( 'selection-invalid-range: Invalid Range.' );
+			}
+
 			this._pushRange( range );
 		}
 
@@ -330,7 +336,7 @@ export default class Selection {
 	/**
 	 * Adds range to selection - creates copy of given range so it can be safely used and modified.
 	 *
-	 * Throws {@link utils.CKEditorError CKEditorError} `view-selection-range-intersects` if added range intersects
+	 * Throws {@link utils.CKEditorError CKEditorError} `selection-range-intersects` if added range intersects
 	 * with ranges already stored in selection instance.
 	 *
 	 * @private
@@ -342,12 +348,12 @@ export default class Selection {
 				/**
 				 * Trying to add a range that intersects with another range from selection.
 				 *
-				 * @error view-selection-range-intersects
+				 * @error selection-range-intersects
 				 * @param {engine.view.Range} addedRange Range that was added to the selection.
 				 * @param {engine.view.Range} intersectingRange Range from selection that intersects with `addedRange`.
 				 */
 				throw new CKEditorError(
-					'view-selection-range-intersects: Trying to add a range that intersects with another range from selection.',
+					'selection-range-intersects: Trying to add a range that intersects with another range from selection.',
 					{ addedRange: range, intersectingRange: storedRange }
 				);
 			}

+ 0 - 2
packages/ckeditor5-engine/src/view/text.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Node from './node.js';
 
 /**

+ 0 - 2
packages/ckeditor5-engine/src/view/textproxy.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 /**
  * TextProxy is a wrapper for substring of {@link engine.view.Text}. Instance of this class is created by
  * {@link engine.view.TreeWalker} when only a part of {@link engine.view.Text} needs to be returned.

+ 37 - 39
packages/ckeditor5-engine/src/view/treewalker.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Element from './element.js';
 import Text from './text.js';
 import TextProxy from './textproxy.js';
@@ -24,16 +22,16 @@ export default class TreeWalker {
 	 * @param {Object} options Object with configuration.
 	 * @param {engine.view.Range} [options.boundaries=null] Range to define boundaries of the iterator.
 	 * @param {engine.view.Position} [options.startPosition] Starting position.
-	 * @param {'FORWARD'|'BACKWARD'} [options.direction='FORWARD'] Walking direction.
+	 * @param {'forward'|'backward'} [options.direction='forward'] Walking direction.
 	 * @param {Boolean} [options.singleCharacters=false] Flag indicating whether all characters from
 	 * {@link engine.view.Text} should be returned as one {@link engine.view.Text} (`false`) ore one by one as
 	 * {@link engine.view.TextProxy} (`true`).
 	 * @param {Boolean} [options.shallow=false] Flag indicating whether iterator should enter elements or not. If the
-	 * iterator is shallow child nodes of any iterated node will not be returned along with `ELEMENT_END` tag.
-	 * @param {Boolean} [options.ignoreElementEnd=false] Flag indicating whether iterator should ignore `ELEMENT_END`
+	 * iterator is shallow child nodes of any iterated node will not be returned along with `elementEnd` tag.
+	 * @param {Boolean} [options.ignoreElementEnd=false] Flag indicating whether iterator should ignore `elementEnd`
 	 * tags. If the option is true walker will not return a parent node of start position. If this option is `true`
 	 * each {@link engine.view.Element} will be returned once, while if the option is `false` they might be returned
-	 * twice: for `'ELEMENT_START'` and `'ELEMENT_END'`.
+	 * twice: for `'elementStart'` and `'elementEnd'`.
 	 */
 	constructor( options = {} ) {
 		if ( !options.boundaries && !options.startPosition ) {
@@ -45,9 +43,9 @@ export default class TreeWalker {
 			throw new CKEditorError( 'tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
 		}
 
-		if ( options.direction && options.direction != 'FORWARD' && options.direction != 'BACKWARD' ) {
+		if ( options.direction && options.direction != 'forward' && options.direction != 'backward' ) {
 			throw new CKEditorError(
-				'tree-walker-unknown-direction: Only `BACKWARD` and `FORWARD` direction allowed.',
+				'tree-walker-unknown-direction: Only `backward` and `forward` direction allowed.',
 				{ direction: options.direction }
 			);
 		}
@@ -55,7 +53,7 @@ export default class TreeWalker {
 		/**
 		 * Iterator boundaries.
 		 *
-		 * When the iterator is walking `FORWARD` on the end of boundary or is walking `BACKWARD`
+		 * When the iterator is walking `'forward'` on the end of boundary or is walking `'backward'`
 		 * on the start of boundary, then `{ done: true }` is returned.
 		 *
 		 * If boundaries are not defined they are set before first and after last child of the root node.
@@ -67,7 +65,7 @@ export default class TreeWalker {
 
 		/**
 		 * Iterator position. If start position is not defined then position depends on {@link #direction}. If direction is
-		 * `FORWARD` position starts form the beginning, when direction is `BACKWARD` position starts from the end.
+		 * `'forward'` position starts form the beginning, when direction is `'backward'` position starts from the end.
 		 *
 		 * @readonly
 		 * @member {engine.view.Position} engine.view.TreeWalker#position
@@ -75,16 +73,16 @@ export default class TreeWalker {
 		if ( options.startPosition ) {
 			this.position = Position.createFromPosition( options.startPosition );
 		} else {
-			this.position = Position.createFromPosition( options.boundaries[ options.direction == 'BACKWARD' ? 'end' : 'start' ] );
+			this.position = Position.createFromPosition( options.boundaries[ options.direction == 'backward' ? 'end' : 'start' ] );
 		}
 
 		/**
-		 * Walking direction. Defaults `FORWARD`.
+		 * Walking direction. Defaults `'forward'`.
 		 *
 		 * @readonly
-		 * @member {'BACKWARD'|'FORWARD'} engine.view.TreeWalker#direction
+		 * @member {'backward'|'forward'} engine.view.TreeWalker#direction
 		 */
-		this.direction = options.direction || 'FORWARD';
+		this.direction = options.direction || 'forward';
 
 		/**
 		 * Flag indicating whether all characters from {@link engine.view.Text} should be returned as one
@@ -97,7 +95,7 @@ export default class TreeWalker {
 
 		/**
 		 * Flag indicating whether iterator should enter elements or not. If the iterator is shallow child nodes of any
-		 * iterated node will not be returned along with `ELEMENT_END` tag.
+		 * iterated node will not be returned along with `elementEnd` tag.
 		 *
 		 * @readonly
 		 * @member {Boolean} engine.view.TreeWalker#shallow
@@ -105,9 +103,9 @@ export default class TreeWalker {
 		this.shallow = !!options.shallow;
 
 		/**
-		 * Flag indicating whether iterator should ignore `ELEMENT_END` tags. If set to `true`, walker will not
+		 * Flag indicating whether iterator should ignore `elementEnd` tags. If set to `true`, walker will not
 		 * return a parent node of the start position. Each {@link engine.view.Element} will be returned once.
-		 * When set to `false` each element might be returned twice: for `'ELEMENT_START'` and `'ELEMENT_END'`.
+		 * When set to `false` each element might be returned twice: for `'elementStart'` and `'elementEnd'`.
 		 *
 		 * @readonly
 		 * @member {Boolean} engine.view.TreeWalker#ignoreElementEnd
@@ -145,7 +143,7 @@ export default class TreeWalker {
 	 * @returns {Object} Object implementing iterator interface, returning information about taken step.
 	 */
 	next() {
-		if ( this.direction == 'FORWARD' ) {
+		if ( this.direction == 'forward' ) {
 			return this._next();
 		} else {
 			return this._previous();
@@ -194,7 +192,7 @@ export default class TreeWalker {
 
 			this.position = position;
 
-			return this._formatReturnValue( 'ELEMENT_START', node, previousPosition, position, 1 );
+			return this._formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
 		} else if ( node instanceof Text ) {
 			if ( this.singleCharacters ) {
 				position = new Position( node, 0 );
@@ -217,7 +215,7 @@ export default class TreeWalker {
 
 				this.position = position;
 
-				return this._formatReturnValue( 'TEXT', item, previousPosition, position, charactersCount );
+				return this._formatReturnValue( 'text', item, previousPosition, position, charactersCount );
 			}
 		} else if ( typeof node == 'string' ) {
 			let textLength;
@@ -236,7 +234,7 @@ export default class TreeWalker {
 			position.offset += textLength;
 			this.position = position;
 
-			return this._formatReturnValue( 'TEXT', textProxy, previousPosition, position, textLength );
+			return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength );
 		} else {
 			// `node` is not set, we reached the end of current `parent`.
 			position = Position.createAfter( parent );
@@ -245,7 +243,7 @@ export default class TreeWalker {
 			if ( this.ignoreElementEnd ) {
 				return this._next();
 			} else {
-				return this._formatReturnValue( 'ELEMENT_END', parent, previousPosition, position );
+				return this._formatReturnValue( 'elementEnd', parent, previousPosition, position );
 			}
 		}
 	}
@@ -291,13 +289,13 @@ export default class TreeWalker {
 				if ( this.ignoreElementEnd ) {
 					return this._previous();
 				} else {
-					return this._formatReturnValue( 'ELEMENT_END', node, previousPosition, position );
+					return this._formatReturnValue( 'elementEnd', node, previousPosition, position );
 				}
 			} else {
 				position.offset--;
 				this.position = position;
 
-				return this._formatReturnValue( 'ELEMENT_START', node, previousPosition, position, 1 );
+				return this._formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
 			}
 		} else if ( node instanceof Text ) {
 			if ( this.singleCharacters ) {
@@ -323,7 +321,7 @@ export default class TreeWalker {
 
 				this.position = position;
 
-				return this._formatReturnValue( 'TEXT', item, previousPosition, position, charactersCount );
+				return this._formatReturnValue( 'text', item, previousPosition, position, charactersCount );
 			}
 		} else if ( typeof node == 'string' ) {
 			let textLength;
@@ -343,13 +341,13 @@ export default class TreeWalker {
 
 			this.position = position;
 
-			return this._formatReturnValue( 'TEXT', textProxy, previousPosition, position, textLength );
+			return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength );
 		} else {
 			// `node` is not set, we reached the beginning of current `parent`.
 			position = Position.createBefore( parent );
 			this.position = position;
 
-			return this._formatReturnValue( 'ELEMENT_START', parent, previousPosition, position, 1 );
+			return this._formatReturnValue( 'elementStart', parent, previousPosition, position, 1 );
 		}
 	}
 
@@ -372,7 +370,7 @@ export default class TreeWalker {
 		if ( item instanceof TextProxy ) {
 			// Position is at the end of Text.
 			if ( item.index + item.data.length == item.textNode.data.length ) {
-				if ( this.direction == 'FORWARD' ) {
+				if ( this.direction == 'forward' ) {
 					nextPosition = Position.createAfter( item.textNode );
 					// When we change nextPosition of returned value we need also update walker current position.
 					this.position = nextPosition;
@@ -383,7 +381,7 @@ export default class TreeWalker {
 
 			// Position is at the begining ot the text.
 			if ( item.index === 0 ) {
-				if ( this.direction == 'FORWARD' ) {
+				if ( this.direction == 'forward' ) {
 					previousPosition = Position.createBefore( item.textNode );
 				} else {
 					nextPosition = Position.createBefore( item.textNode );
@@ -408,9 +406,9 @@ export default class TreeWalker {
 
 /**
  * Type of the step made by {@link engine.view.TreeWalker}.
- * Possible values: `'ELEMENT_START'` if walker is at the beginning of a node, `'ELEMENT_END'` if walker is at the end
- * of node, or `'TEXT'` if walker traversed over single and multiple characters.
- * For {@link engine.view.Text} `ELEMENT_START` and `ELEMENT_END` is not returned.
+ * Possible values: `'elementStart'` if walker is at the beginning of a node, `'elementEnd'` if walker is at the end
+ * of node, or `'text'` if walker traversed over single and multiple characters.
+ * For {@link engine.view.Text} `elementStart` and `elementEnd` is not returned.
  *
  * @typedef {String} engine.view.TreeWalkerValueType
  */
@@ -422,26 +420,26 @@ export default class TreeWalker {
  * @property {engine.view.TreeWalkerValueType} type
  * @property {engine.view.Item} item Item between old and new positions of {@link engine.view.TreeWalker}.
  * @property {engine.view.Position} previousPosition Previous position of the iterator.
- * * Forward iteration: For `'ELEMENT_END'` it is the last position inside the element. For all other types it is the
+ * * Forward iteration: For `'elementEnd'` it is the last position inside the element. For all other types it is the
  * position before the item. Note that it is more efficient to use this position then calculate the position before
  * the node using {@link engine.view.Position.createBefore}.
- * * Backward iteration: For `'ELEMENT_START'` it is the first position inside the element. For all other types it is
+ * * Backward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is
  * the position after item.
  * * If the position is at the beginning or at the end of the {@link engine.view.Text} it is always moved from the
  * inside of the Text to its parent just before or just after Text.
  * @property {engine.view.Position} nextPosition Next position of the iterator.
- * * Forward iteration: For `'ELEMENT_START'` it is the first position inside the element. For all other types it is
+ * * Forward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is
  * the position after the item.
- * * Backward iteration: For `'ELEMENT_END'` it is last position inside element. For all other types it is the position
+ * * Backward iteration: For `'elementEnd'` it is last position inside element. For all other types it is the position
  * before the item.
  * * If the position is at the beginning or at the end of the {@link engine.view.Text} it is always moved from the
  * inside of the Text to its parent just before or just after Text.
- * @property {Number} [length] Length of the item. For `'ELEMENT_START'` it is 1. For `'TEXT'` it is
- * the length of the text. For `'ELEMENT_END'` it is undefined.
+ * @property {Number} [length] Length of the item. For `'elementStart'` it is 1. For `'text'` it is
+ * the length of the text. For `'elementEnd'` it is undefined.
  */
 
 /**
  * Tree walking directions.
  *
- * @typedef {'FORWARD'|'BACKWARD'} engine.view.TreeWalkerDirection
+ * @typedef {'forward'|'backward'} engine.view.TreeWalkerDirection
  */

+ 0 - 2
packages/ckeditor5-engine/src/view/writer.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Position from './position.js';
 import ContainerElement from './containerelement.js';
 import AttributeElement from './attributeelement.js';

+ 21 - 23
packages/ckeditor5-engine/tests/.jshintrc

@@ -1,25 +1,23 @@
 {
-	"browser": true,
-	"esnext": true,
-
-	"expr": true,
-	"immed": true,
-	"loopfunc": true,
-	"noarg": true,
-	"nonbsp": true,
-	"strict": "global",
-	"undef": true,
-	"unused": true,
-	"varstmt": true,
-
-	"globals": {
-		"after": false,
-		"afterEach": false,
-		"before": false,
-		"beforeEach": false,
-		"describe": false,
-		"expect": false,
-		"it": false,
-		"sinon": false
-	}
+  "browser": true,
+  "esnext": true,
+  "expr": true,
+  "immed": true,
+  "loopfunc": true,
+  "noarg": true,
+  "nonbsp": true,
+  "strict": "implied",
+  "undef": true,
+  "unused": true,
+  "varstmt": true,
+  "globals": {
+    "after": false,
+    "afterEach": false,
+    "before": false,
+    "beforeEach": false,
+    "describe": false,
+    "expect": false,
+    "it": false,
+    "sinon": false
+  }
 }

+ 30 - 18
packages/ckeditor5-engine/tests/_utils-tests/model.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import { stringify, parse, getData, setData } from '/tests/engine/_utils/model.js';
 import Document from '/ckeditor5/engine/model/document.js';
 import DocumentFragment from '/ckeditor5/engine/model/documentfragment.js';
@@ -31,7 +29,7 @@ describe( 'model test utils', () => {
 	describe( 'getData', () => {
 		it( 'should use stringify method', () => {
 			const stringifySpy = sandbox.spy( getData, '_stringify' );
-			root.appendChildren( new Element( 'b', null, [ 'btext' ] ) );
+			root.appendChildren( new Element( 'b', null, new Text( 'btext' ) ) );
 
 			expect( getData( document, { withoutSelection: true } ) ).to.equal( '<b>btext</b>' );
 			sinon.assert.calledOnce( stringifySpy );
@@ -40,7 +38,7 @@ describe( 'model test utils', () => {
 
 		it( 'should use stringify method with selection', () => {
 			const stringifySpy = sandbox.spy( getData, '_stringify' );
-			root.appendChildren( new Element( 'b', null, [ 'btext' ] ) );
+			root.appendChildren( new Element( 'b', null, new Text( 'btext' ) ) );
 			document.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
 
 			expect( getData( document ) ).to.equal( '<selection><b>btext</b></selection>' );
@@ -150,21 +148,29 @@ describe( 'model test utils', () => {
 		} );
 
 		it( 'should stringify element', () => {
-			const element = new Element( 'a', null, [ new Element( 'b', null, 'btext' ), 'atext' ] );
+			const element = new Element( 'a', null, [
+				new Element( 'b', null, new Text( 'btext' ) ),
+				new Text( 'atext' )
+			] );
+
 			expect( stringify( element ) ).to.equal( '<a><b>btext</b>atext</a>' );
 		} );
 
 		it( 'should stringify document fragment', () => {
-			const fragment = new DocumentFragment( [ new Element( 'b', null, 'btext' ), 'atext' ] );
+			const fragment = new DocumentFragment( [
+				new Element( 'b', null, new Text( 'btext' ) ),
+				new Text( 'atext' )
+			] );
+
 			expect( stringify( fragment ) ).to.equal( '<b>btext</b>atext' );
 		} );
 
 		it( 'writes elements and texts', () => {
 			root.appendChildren( [
-				new Element( 'a', null, 'atext' ),
+				new Element( 'a', null, new Text( 'atext' ) ),
 				new Element( 'b', null, [
 					new Element( 'c1' ),
-					'ctext',
+					new Text( 'ctext' ),
 					new Element( 'c2' )
 				] ),
 				new Element( 'd' )
@@ -192,7 +198,7 @@ describe( 'model test utils', () => {
 		it( 'writes text attributes', () => {
 			root.appendChildren( [
 				new Text( 'foo', { bold: true } ),
-				'bar',
+				new Text( 'bar' ),
 				new Text( 'bom', { bold: true, italic: true } ),
 				new Element( 'a', null, [
 					new Text( 'pom', { underline: true, bold: true } )
@@ -216,7 +222,7 @@ describe( 'model test utils', () => {
 
 				root.appendChildren( [
 					elA,
-					'foo',
+					new Text( 'foo' ),
 					new Text( 'bar', { bold: true } ),
 					elB
 				] );
@@ -252,7 +258,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'writes selection collapsed at the text left boundary', () => {
-				selection.collapse( elA, 'AFTER' );
+				selection.collapse( elA, 'after' );
 
 				expect( stringify( root, selection ) ).to.equal(
 					'<a></a><selection />foo<$text bold=true>bar</$text><b></b>'
@@ -260,7 +266,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'writes selection collapsed at the text right boundary', () => {
-				selection.collapse( elB, 'BEFORE' );
+				selection.collapse( elB, 'before' );
 
 				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>foo<$text bold=true>bar</$text><selection bold=true /><b></b>'
@@ -268,7 +274,7 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'writes selection collapsed at the end of the root', () => {
-				selection.collapse( root, 'END' );
+				selection.collapse( root, 'end' );
 
 				// Needed due to https://github.com/ckeditor/ckeditor5-engine/issues/320.
 				selection.clearAttributes();
@@ -417,9 +423,12 @@ describe( 'model test utils', () => {
 		test( 'sets text attributes', {
 			data: '<$text bold=true italic=true>foo</$text><$text bold=true>bar</$text>bom',
 			check( root ) {
-				expect( root.getChildCount() ).to.equal( 9 );
-				expect( root.getChild( 0 ) ).to.have.property( 'character', 'f' );
+				expect( root.getChildCount() ).to.equal( 3 );
+				expect( root.getMaxOffset() ).to.equal( 9 );
+				expect( root.getChild( 0 ) ).to.have.property( 'data', 'foo' );
 				expect( root.getChild( 0 ).getAttribute( 'italic' ) ).to.equal( true );
+				expect( root.getChild( 1 ) ).to.have.property( 'data', 'bar' );
+				expect( root.getChild( 1 ).getAttribute( 'bold' ) ).to.equal( true );
 			}
 		} );
 
@@ -495,8 +504,11 @@ describe( 'model test utils', () => {
 
 			test( 'sets collapsed selection within a text', {
 				data: 'foo<selection />bar',
-				check( root ) {
-					expect( root.getChildCount() ).to.equal( 6 );
+				check( text, selection ) {
+					expect( text.offsetSize ).to.equal( 6 );
+					expect( text.getPath() ).to.deep.equal( [ 0 ] );
+					expect( selection.getFirstRange().start.path ).to.deep.equal( [ 3 ] );
+					expect( selection.getFirstRange().end.path ).to.deep.equal( [ 3 ] );
 				}
 			} );
 
@@ -510,7 +522,7 @@ describe( 'model test utils', () => {
 			test( 'sets collapsed selection between text and text with attributes', {
 				data: 'foo<selection /><$text bold=true>bar</$text>',
 				check( root, selection ) {
-					expect( root.getChildCount() ).to.equal( 6 );
+					expect( root.getMaxOffset() ).to.equal( 6 );
 					expect( selection.getAttribute( 'bold' ) ).to.be.undefined;
 				}
 			} );

+ 0 - 2
packages/ckeditor5-engine/tests/_utils-tests/view.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 /* bender-tags: browser-only */
 
 import { parse, stringify, getData, setData }from '/tests/engine/_utils/view.js';

+ 13 - 14
packages/ckeditor5-engine/tests/_utils/model.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import TreeWalker from '/ckeditor5/engine/model/treewalker.js';
 import Range from '/ckeditor5/engine/model/range.js';
 import Position from '/ckeditor5/engine/model/position.js';
@@ -14,6 +12,7 @@ import Element from '/ckeditor5/engine/model/element.js';
 import DocumentFragment from '/ckeditor5/engine/model/documentfragment.js';
 import Selection from '/ckeditor5/engine/model/selection.js';
 import Document from '/ckeditor5/engine/model/document.js';
+import writer from '/ckeditor5/engine/model/writer.js';
 
 /**
  * Writes the contents of the {@link engine.model.Document Document} to an HTML-like string.
@@ -194,7 +193,7 @@ export function parse( data ) {
 
 	const handlers = {
 		text( token ) {
-			root.appendChildren( new Text( token.text, textAttributes ) );
+			writer.insert( Position.createFromParentAndOffset( root, root.getMaxOffset() ), new Text( token.data, textAttributes ) );
 		},
 
 		textStart( token ) {
@@ -212,7 +211,7 @@ export function parse( data ) {
 
 		openingTag( token ) {
 			let el = new Element( token.name, token.attributes );
-			root.appendChildren( el );
+			writer.insert( Position.createFromParentAndOffset( root, root.getMaxOffset() ), el );
 
 			root = el;
 
@@ -229,12 +228,12 @@ export function parse( data ) {
 
 		collapsedSelection( token ) {
 			withSelection = true;
-			selection.collapse( root, 'END' );
+			selection.collapse( root, 'end' );
 			selection.setAttributesTo( token.attributes );
 		},
 
 		selectionStart( token ) {
-			selectionStart = Position.createFromParentAndOffset( root, root.getChildCount() );
+			selectionStart = Position.createFromParentAndOffset( root, root.getMaxOffset() );
 			selectionAttributes = token.attributes;
 		},
 
@@ -244,7 +243,7 @@ export function parse( data ) {
 			}
 
 			withSelection = true;
-			selectionEnd = Position.createFromParentAndOffset( root, root.getChildCount() );
+			selectionEnd = Position.createFromParentAndOffset( root, root.getMaxOffset() );
 
 			selection.setRanges(
 				[ new Range( selectionStart, selectionEnd ) ],
@@ -290,7 +289,7 @@ function writeItem( walkerValue, selection, options ) {
 	const type = walkerValue.type;
 	const item = walkerValue.item;
 
-	if ( type == 'ELEMENT_START' ) {
+	if ( type == 'elementStart' ) {
 		let attrs = writeAttributes( item.getAttributes() );
 
 		if ( attrs ) {
@@ -300,7 +299,7 @@ function writeItem( walkerValue, selection, options ) {
 		return `<${ item.name }>`;
 	}
 
-	if ( type == 'ELEMENT_END' ) {
+	if ( type == 'elementEnd' ) {
 		return `</${ item.name }>`;
 	}
 
@@ -310,7 +309,7 @@ function writeItem( walkerValue, selection, options ) {
 function writeText( walkerValue, selection, options ) {
 	const item = walkerValue.item;
 	const attrs = writeAttributes( item.getAttributes() );
-	let text = Array.from( item.text );
+	let text = Array.from( item.data );
 
 	if ( options.selection ) {
 		const startIndex = walkerValue.previousPosition.offset + 1;
@@ -321,7 +320,7 @@ function writeText( walkerValue, selection, options ) {
 			// Add the selection marker without changing any indexes, so if second marker must be added
 			// in the same loop it does not blow up.
 			text[ index - startIndex ] +=
-				writeSelection( Position.createFromParentAndOffset( item.commonParent, index ), selection );
+				writeSelection( Position.createFromParentAndOffset( item.parent, index ), selection );
 
 			index++;
 		}
@@ -347,12 +346,12 @@ function writeSelection( currentPosition, selection ) {
 	const range = selection.getFirstRange();
 
 	// Handle end of the selection.
-	if ( !selection.isCollapsed && range.end.compareWith( currentPosition ) == 'SAME' ) {
+	if ( !selection.isCollapsed && range.end.compareWith( currentPosition ) == 'same' ) {
 		return '</selection>';
 	}
 
 	// Handle no match.
-	if ( range.start.compareWith( currentPosition ) != 'SAME' ) {
+	if ( range.start.compareWith( currentPosition ) != 'same' ) {
 		return '';
 	}
 
@@ -442,7 +441,7 @@ const handlers = {
 	text( match ) {
 		return {
 			type: 'text',
-			text: match[ 0 ]
+			data: match[ 0 ]
 		};
 	}
 };

+ 0 - 2
packages/ckeditor5-engine/tests/_utils/view.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-'use strict';
-
 import Document from '/ckeditor5/engine/view/document.js';
 import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
 import HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor.js';

+ 17 - 22
packages/ckeditor5-engine/tests/conversion/advanced-converters.js

@@ -5,8 +5,6 @@
 
 /* bender-tags: conversion */
 
-'use strict';
-
 import ModelDocument from '/ckeditor5/engine/model/document.js';
 import ModelDocumentFragment from '/ckeditor5/engine/model/documentfragment.js';
 import ModelElement from '/ckeditor5/engine/model/element.js';
@@ -15,6 +13,7 @@ import ModelTextProxy from '/ckeditor5/engine/model/textproxy.js';
 import ModelRange from '/ckeditor5/engine/model/range.js';
 import ModelPosition from '/ckeditor5/engine/model/position.js';
 import ModelWalker from '/ckeditor5/engine/model/treewalker.js';
+import modelWriter from '/ckeditor5/engine/model/writer.js';
 
 import ViewElement from '/ckeditor5/engine/view/element.js';
 import ViewContainerElement from '/ckeditor5/engine/view/containerelement.js';
@@ -111,7 +110,7 @@ function modelToString( item ) {
 	if ( item instanceof ModelTextProxy ) {
 		let attributes = modelAttributesToString( item );
 
-		result = attributes ? '<$text' + attributes + '>' + item.text + '</$text>' : item.text;
+		result = attributes ? '<$text' + attributes + '>' + item.data + '</$text>' : item.data;
 	} else {
 		let walker = new ModelWalker( { boundaries: ModelRange.createFromElement( item ), shallow: true } );
 
@@ -266,7 +265,9 @@ describe( 'image with caption converters', () => {
 	} );
 
 	it( 'should convert model images changes with caption to view', () => {
-		let modelElement = new ModelElement( 'image', { src: 'foo.jpg', title: 'foo' }, new ModelElement( 'caption', {}, 'foobar' ) );
+		let modelElement = new ModelElement( 'image', { src: 'foo.jpg', title: 'foo' }, [
+			new ModelElement( 'caption', {}, new ModelText( 'foobar' ) )
+		] );
 		modelRoot.appendChildren( modelElement );
 		modelDispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
 
@@ -493,17 +494,14 @@ describe( 'custom attribute handling for given element', () => {
 		expect( viewToString( viewRoot ) ).to.equal( '<div><a href="foo.html" title="Foo title">foo</a></div>' );
 
 		// Let's change link's attributes.
-		for ( let value of range ) {
-			value.item.setAttribute( 'linkHref', 'bar.html' );
-			value.item.setAttribute( 'linkTitle', 'Bar title' );
-		}
+		modelWriter.setAttribute( range, 'linkHref', 'bar.html' );
+		modelWriter.setAttribute( range, 'linkTitle', 'Bar title' );
 		modelDispatcher.convertAttribute( 'changeAttribute', range, 'linkHref', 'foo.html', 'bar.html' );
 		modelDispatcher.convertAttribute( 'changeAttribute', range, 'linkTitle', 'Foo title', 'Bar title' );
 
 		expect( viewToString( viewRoot ) ).to.equal( '<div><a href="bar.html" title="Bar title">foo</a></div>' );
 
-		// Let's remove a letter from the link.
-		const removed = modelRoot.removeChildren( 0, 1 );
+		const removed = modelWriter.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 1 ) );
 		modelDoc.graveyard.appendChildren( removed );
 		modelDispatcher.convertRemove(
 			ModelPosition.createFromParentAndOffset( modelRoot, 0 ),
@@ -515,17 +513,13 @@ describe( 'custom attribute handling for given element', () => {
 		range = ModelRange.createFromElement( modelRoot );
 
 		// Let's remove just one attribute.
-		for ( let value of range ) {
-			value.item.removeAttribute( 'linkTitle' );
-		}
+		modelWriter.removeAttribute( range, 'linkTitle' );
 		modelDispatcher.convertAttribute( 'removeAttribute', range, 'linkTitle', 'Bar title', null );
 
 		expect( viewToString( viewRoot ) ).to.equal( '<div><a href="bar.html">oo</a></div>' );
 
 		// Let's remove the other attribute.
-		for ( let value of range ) {
-			value.item.removeAttribute( 'linkHref' );
-		}
+		modelWriter.removeAttribute( range, 'linkHref' );
 		modelDispatcher.convertAttribute( 'removeAttribute', range, 'linkHref', 'bar.html', null );
 
 		expect( viewToString( viewRoot ) ).to.equal( '<div>oo</div>' );
@@ -537,13 +531,13 @@ describe( 'custom attribute handling for given element', () => {
 		let modelText = viewDispatcher.convert( viewElement )[ 0 ];
 
 		expect( modelText ).to.be.instanceof( ModelText );
-		expect( modelText.text ).to.equal( 'foo' );
+		expect( modelText.data ).to.equal( 'foo' );
 		expect( modelText.getAttribute( 'linkHref' ) ).to.equal( 'foo.html' );
 		expect( modelText.getAttribute( 'linkTitle' ) ).to.equal( 'Foo title' );
 	} );
 
 	it( 'should convert quote model element with linkHref and linkTitle attribute to view', () => {
-		let modelElement = new ModelElement( 'quote', { linkHref: 'foo.html', linkTitle: 'Foo source' }, 'foo' );
+		let modelElement = new ModelElement( 'quote', { linkHref: 'foo.html', linkTitle: 'Foo source' }, new ModelText( 'foo' ) );
 		modelRoot.appendChildren( modelElement );
 		modelDispatcher.convertInsert( ModelRange.createFromElement( modelRoot ) );
 
@@ -622,7 +616,8 @@ it( 'default table view to model converter', () => {
 
 			for ( let i = 1; i < children.length; i++ ) {
 				if ( children[ i ] instanceof ModelText && children[ i - 1 ] instanceof ModelText ) {
-					children[ i - 1 ].text += ' ';
+					children.splice( i, 0, new ModelText( ' ' ) );
+					i++;
 				}
 			}
 
@@ -718,12 +713,12 @@ describe( 'universal converter', () => {
 		let modelElement = new ModelElement( 'table', { cellpadding: 5, cellspacing: 5 }, [
 			new ModelElement( 'tr', null, [
 				new ModelElement( 'td', null, [
-					'foo ',
+					new ModelText( 'foo ' ),
 					new ModelText( 'abc', { bold: true } ),
-					' bar'
+					new ModelText( ' bar' )
 				] ),
 				new ModelElement( 'td', null, [
-					new ModelElement( 'foo', { foo: 'bar' }, 'bar' )
+					new ModelElement( 'foo', { foo: 'bar' }, new ModelText( 'bar' ) )
 				] )
 			] )
 		] );

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů