8
0
Просмотр исходного кода

Merge branch 'master' into t/ckeditor5-table/63

# Conflicts:
#	src/model/utils/selection-post-fixer.js
#	tests/model/utils/selection-post-fixer.js
Maciej Gołaszewski 7 лет назад
Родитель
Сommit
cfc627dc2b
100 измененных файлов с 3441 добавлено и 2715 удалено
  1. 3 0
      packages/ckeditor5-engine/.travis.yml
  2. 49 0
      packages/ckeditor5-engine/CHANGELOG.md
  3. 1 1
      packages/ckeditor5-engine/CONTRIBUTING.md
  4. 204 0
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/schema.md
  5. 21 21
      packages/ckeditor5-engine/package.json
  6. 5 5
      packages/ckeditor5-engine/src/controller/datacontroller.js
  7. 14 0
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  8. 13 10
      packages/ckeditor5-engine/src/conversion/downcast-converters.js
  9. 11 8
      packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
  10. 1 1
      packages/ckeditor5-engine/src/conversion/mapper.js
  11. 1 0
      packages/ckeditor5-engine/src/conversion/modelconsumable.js
  12. 5 6
      packages/ckeditor5-engine/src/conversion/upcast-converters.js
  13. 13 7
      packages/ckeditor5-engine/src/conversion/upcastdispatcher.js
  14. 4 17
      packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js
  15. 12 11
      packages/ckeditor5-engine/src/dev-utils/model.js
  16. 34 31
      packages/ckeditor5-engine/src/dev-utils/view.js
  17. 29 35
      packages/ckeditor5-engine/src/model/differ.js
  18. 4 5
      packages/ckeditor5-engine/src/model/document.js
  19. 1 1
      packages/ckeditor5-engine/src/model/documentfragment.js
  20. 6 4
      packages/ckeditor5-engine/src/model/documentselection.js
  21. 1 1
      packages/ckeditor5-engine/src/model/element.js
  22. 23 18
      packages/ckeditor5-engine/src/model/liveposition.js
  23. 33 23
      packages/ckeditor5-engine/src/model/liverange.js
  24. 5 7
      packages/ckeditor5-engine/src/model/markercollection.js
  25. 366 19
      packages/ckeditor5-engine/src/model/model.js
  26. 1 1
      packages/ckeditor5-engine/src/model/nodelist.js
  27. 1 1
      packages/ckeditor5-engine/src/model/operation/attributeoperation.js
  28. 2 3
      packages/ckeditor5-engine/src/model/operation/detachoperation.js
  29. 2 1
      packages/ckeditor5-engine/src/model/operation/insertoperation.js
  30. 2 2
      packages/ckeditor5-engine/src/model/operation/markeroperation.js
  31. 17 6
      packages/ckeditor5-engine/src/model/operation/mergeoperation.js
  32. 4 3
      packages/ckeditor5-engine/src/model/operation/moveoperation.js
  33. 0 4
      packages/ckeditor5-engine/src/model/operation/operationfactory.js
  34. 2 2
      packages/ckeditor5-engine/src/model/operation/renameoperation.js
  35. 57 40
      packages/ckeditor5-engine/src/model/operation/splitoperation.js
  36. 300 398
      packages/ckeditor5-engine/src/model/operation/transform.js
  37. 0 177
      packages/ckeditor5-engine/src/model/operation/unwrapoperation.js
  38. 0 227
      packages/ckeditor5-engine/src/model/operation/wrapoperation.js
  39. 77 147
      packages/ckeditor5-engine/src/model/position.js
  40. 79 118
      packages/ckeditor5-engine/src/model/range.js
  41. 29 163
      packages/ckeditor5-engine/src/model/schema.js
  42. 87 77
      packages/ckeditor5-engine/src/model/selection.js
  43. 6 9
      packages/ckeditor5-engine/src/model/treewalker.js
  44. 8 12
      packages/ckeditor5-engine/src/model/utils/deletecontent.js
  45. 8 11
      packages/ckeditor5-engine/src/model/utils/getselectedcontent.js
  46. 36 21
      packages/ckeditor5-engine/src/model/utils/insertcontent.js
  47. 2 2
      packages/ckeditor5-engine/src/model/utils/modifyselection.js
  48. 7 4
      packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js
  49. 149 71
      packages/ckeditor5-engine/src/model/writer.js
  50. 10 7
      packages/ckeditor5-engine/src/view/attributeelement.js
  51. 19 30
      packages/ckeditor5-engine/src/view/containerelement.js
  52. 7 3
      packages/ckeditor5-engine/src/view/documentfragment.js
  53. 18 23
      packages/ckeditor5-engine/src/view/documentselection.js
  54. 2 2
      packages/ckeditor5-engine/src/view/domconverter.js
  55. 218 51
      packages/ckeditor5-engine/src/view/downcastwriter.js
  56. 14 0
      packages/ckeditor5-engine/src/view/editableelement.js
  57. 19 23
      packages/ckeditor5-engine/src/view/element.js
  58. 4 1
      packages/ckeditor5-engine/src/view/emptyelement.js
  59. 10 7
      packages/ckeditor5-engine/src/view/node.js
  60. 50 25
      packages/ckeditor5-engine/src/view/position.js
  61. 65 65
      packages/ckeditor5-engine/src/view/range.js
  62. 45 17
      packages/ckeditor5-engine/src/view/renderer.js
  63. 48 46
      packages/ckeditor5-engine/src/view/selection.js
  64. 9 6
      packages/ckeditor5-engine/src/view/text.js
  65. 21 21
      packages/ckeditor5-engine/src/view/treewalker.js
  66. 17 3
      packages/ckeditor5-engine/src/view/uielement.js
  67. 205 6
      packages/ckeditor5-engine/src/view/upcastwriter.js
  68. 185 26
      packages/ckeditor5-engine/src/view/view.js
  69. 5 5
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  70. 151 22
      packages/ckeditor5-engine/tests/controller/editingcontroller.js
  71. 4 2
      packages/ckeditor5-engine/tests/conversion/conversion.js
  72. 40 30
      packages/ckeditor5-engine/tests/conversion/downcast-converters.js
  73. 25 28
      packages/ckeditor5-engine/tests/conversion/downcast-selection-converters.js
  74. 48 43
      packages/ckeditor5-engine/tests/conversion/downcastdispatcher.js
  75. 4 4
      packages/ckeditor5-engine/tests/conversion/mapper.js
  76. 4 4
      packages/ckeditor5-engine/tests/conversion/upcast-converters.js
  77. 7 7
      packages/ckeditor5-engine/tests/conversion/upcast-selection-converters.js
  78. 15 15
      packages/ckeditor5-engine/tests/conversion/upcastdispatcher.js
  79. 25 83
      packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js
  80. 10 8
      packages/ckeditor5-engine/tests/dev-utils/model.js
  81. 34 34
      packages/ckeditor5-engine/tests/dev-utils/view.js
  82. 2 5
      packages/ckeditor5-engine/tests/manual/highlight.js
  83. 1 1
      packages/ckeditor5-engine/tests/manual/markers.js
  84. 0 3
      packages/ckeditor5-engine/tests/manual/markers.md
  85. 1 1
      packages/ckeditor5-engine/tests/manual/selection.html
  86. 1 0
      packages/ckeditor5-engine/tests/manual/tickets/1439/1.html
  87. 33 0
      packages/ckeditor5-engine/tests/manual/tickets/1439/1.js
  88. 11 0
      packages/ckeditor5-engine/tests/manual/tickets/1439/1.md
  89. 2 0
      packages/ckeditor5-engine/tests/manual/tickets/462/1.md
  90. 2 2
      packages/ckeditor5-engine/tests/manual/tickets/475/1.js
  91. 1 1
      packages/ckeditor5-engine/tests/manual/tickets/ckeditor5-721/1.js
  92. 2 1
      packages/ckeditor5-engine/tests/model/_utils/utils.js
  93. 110 190
      packages/ckeditor5-engine/tests/model/differ.js
  94. 8 9
      packages/ckeditor5-engine/tests/model/document.js
  95. 20 20
      packages/ckeditor5-engine/tests/model/documentselection.js
  96. 25 28
      packages/ckeditor5-engine/tests/model/liveposition.js
  97. 42 50
      packages/ckeditor5-engine/tests/model/liverange.js
  98. 12 12
      packages/ckeditor5-engine/tests/model/markercollection.js
  99. 90 9
      packages/ckeditor5-engine/tests/model/model.js
  100. 5 5
      packages/ckeditor5-engine/tests/model/operation/detachoperation.js

+ 3 - 0
packages/ckeditor5-engine/.travis.yml

@@ -12,6 +12,9 @@ node_js:
 - '8'
 cache:
 - node_modules
+branches:
+   except:
+   - stable
 before_install:
 - export DISPLAY=:99.0
 - sh -e /etc/init.d/xvfb start

+ 49 - 0
packages/ckeditor5-engine/CHANGELOG.md

@@ -1,6 +1,55 @@
 Changelog
 =========
 
+## [11.0.0](https://github.com/ckeditor/ckeditor5-engine/compare/v10.2.0...v11.0.0) (2018-10-08)
+
+### Bug fixes
+
+* Range transformation by the split operation will expand it if `insertionPosition` is equal to the range end. Modified transformations to align with that change. Closes https://github.com/ckeditor/ckeditor5/issues/1278. ([e0e961f](https://github.com/ckeditor/ckeditor5-engine/commit/e0e961f))
+* `Schema#checkAttributeInSelection()` will include selection's attributes in the context of the check. Closes [#1546](https://github.com/ckeditor/ckeditor5-engine/issues/1546). ([8fa632c](https://github.com/ckeditor/ckeditor5-engine/commit/8fa632c))
+* `startsWithFiller` should correctly work with DOM `Text` nodes that are inside of an iframe. ([16b0280](https://github.com/ckeditor/ckeditor5-engine/commit/16b0280))
+
+  Huge thanks to [Dmitri Pisarev](https://github.com/dimaip) for this contribution!
+* Marked reused element attributes to be rendered if the element being replaced was also marked. Closes [#1560](https://github.com/ckeditor/ckeditor5-engine/issues/1560). Closes [#1561](https://github.com/ckeditor/ckeditor5-engine/issues/1561). ([6619a1f](https://github.com/ckeditor/ckeditor5-engine/commit/6619a1f))
+* Remove clone groups in `view.DowncastWriter` manually. Closes [#1571](https://github.com/ckeditor/ckeditor5-engine/issues/1571). ([420166a](https://github.com/ckeditor/ckeditor5-engine/commit/420166a))
+* Use numbers instead of booleans in `Array.sort()`. ([00fbf7f](https://github.com/ckeditor/ckeditor5-engine/commit/00fbf7f))
+
+### Other changes
+
+* Removed the concept of deltas. Added new operations (replacing removed deltas). Rewritten OT algorithms. Simple. 10k LOC added, 12.5k LOC removed. Closes [#1162](https://github.com/ckeditor/ckeditor5-engine/issues/1162). ([a5cf8b1](https://github.com/ckeditor/ckeditor5-engine/commit/a5cf8b1))
+* Added logging for new operations. Closes [#1491](https://github.com/ckeditor/ckeditor5-engine/issues/1491). ([5c0a34d](https://github.com/ckeditor/ckeditor5-engine/commit/5c0a34d))
+* Added new OT tests, reached 100% code coverage again, fixed multiple OT scenarios, removed unreachable code. Closes [#1474](https://github.com/ckeditor/ckeditor5-engine/issues/1474). ([6c2151a](https://github.com/ckeditor/ckeditor5-engine/commit/6c2151a))
+* Allowed using `Mapper` outside the conversion scope. Closes [#1415](https://github.com/ckeditor/ckeditor5-engine/issues/1415). ([6de6a00](https://github.com/ckeditor/ckeditor5-engine/commit/6de6a00))
+
+  Huge thanks to [Mate Bartus](https://github.com/CHItA) for this contribution!
+* Always update attributes of reused elements while rendering. Closes [#1560](https://github.com/ckeditor/ckeditor5-engine/issues/1560). ([9b95a8a](https://github.com/ckeditor/ckeditor5-engine/commit/9b95a8a))
+* Changed long name returned by `Operation.className` property to a short one. Closes [#1513](https://github.com/ckeditor/ckeditor5-engine/issues/1513). ([7765953](https://github.com/ckeditor/ckeditor5-engine/commit/7765953))
+* Made the view's `stringify()` dev util output the content of the `UIElement` (see [ckeditor/ckeditor5-media-embed#1](https://github.com/ckeditor/ckeditor5-media-embed/issues/1)). ([49cd795](https://github.com/ckeditor/ckeditor5-engine/commit/49cd795))
+* Made `toJSON()` methods serialize nested objects. Closes [#1477](https://github.com/ckeditor/ckeditor5-engine/issues/1477). ([27ab310](https://github.com/ckeditor/ckeditor5-engine/commit/27ab310))
+
+  Aligned `Schema#getValidRanges()` results to changes in `AttributeOperation`.
+
+  Unified `RemoveOperation` and `ReinsertOperation` to have just one `MoveOperation`.
+
+  Simplified `LiveRange#event:change` second parameter which is now an object containing `Position` not an `Operation`.
+* Prevent rendering when in the `model.change()` or `model.enqueueChange()` block. Closes [#1528](https://github.com/ckeditor/ckeditor5-engine/issues/1528). ([2ef33b1](https://github.com/ckeditor/ckeditor5-engine/commit/2ef33b1))
+* Renamed view `Writer` to `DowncastWriter`. Closes [#1515](https://github.com/ckeditor/ckeditor5-engine/issues/1515). ([5fd1ea5](https://github.com/ckeditor/ckeditor5-engine/commit/5fd1ea5))
+* Swapped parameters order in the `DowncastWriter#rename()` method. The `DowncastWriter#remove()` method now accepts range or item. Closes [#1521](https://github.com/ckeditor/ckeditor5-engine/issues/1521). ([d289b74](https://github.com/ckeditor/ckeditor5-engine/commit/d289b74))
+* The `model.insertContent()` accepts range and position. Closes [ckeditor/ckeditor5#1243](https://github.com/ckeditor/ckeditor5/issues/1243). ([bcdaaa9](https://github.com/ckeditor/ckeditor5-engine/commit/bcdaaa9))
+* View post-fixer should be called once while rendering model changes. Closes [#1564](https://github.com/ckeditor/ckeditor5-engine/issues/1564). ([2f5af98](https://github.com/ckeditor/ckeditor5-engine/commit/2f5af98))
+
+### BREAKING CHANGES
+
+* View post-fixers are now called only a single once when rendering model changes.
+* Swapped parameters order in the `DowncastWriter#rename()` method. See [#1521](https://github.com/ckeditor/ckeditor5-engine/issues/1521).
+* The `src/view/writer` module was renamed to `src//view/downcastwriter`.
+* `LiveRange#event:change` second parameter is now an object containing property `deletionPosition`. It can be `model.Position` instance, if the range was moved to the graveyard root. The position is equal to the position from which nodes were removed. Otherwise, it is set to `null`.
+* `Schema#getValidRanges()` will now return only flat ranges. If an attribute is allowed on some nodes and in those nodes children, multiple "nested" ranges will be returned.
+* `Schema#getValidRanges()` is now a generator.
+* The concept of deltas (sets of operations) was removed from the engine. They were replaced by opertations matching the types of removed deltas.
+* `model.Writer#setAttribute()` (and `AttributeOperation`) now applies attribute only to the top-level nodes in the `range` (instead of all the nodes in the range).
+
+
 ## [10.2.0](https://github.com/ckeditor/ckeditor5-engine/compare/v10.1.0...v10.2.0) (2018-07-18)
 
 ### Features

+ 1 - 1
packages/ckeditor5-engine/CONTRIBUTING.md

@@ -1,4 +1,4 @@
 Contributing
 ========================================
 
-Information about contributing can be found at the following page: <https://github.com/ckeditor/ckeditor5/blob/master/CONTRIBUTING.md>.
+See the [official contributors' guide to CKEditor 5](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/contributing.html) to learn more.

+ 204 - 0
packages/ckeditor5-engine/docs/framework/guides/deep-dive/schema.md

@@ -0,0 +1,204 @@
+---
+category: framework-deep-dive
+---
+
+# Schema
+
+This article assumes that you have already read the {@link framework/guides/architecture/editing-engine#schema "Schema"} section of the {@link framework/guides/architecture/editing-engine introduction to the editing engine architecture}.
+
+## Quick recap
+
+The editor's schema is available in the {@link module:engine/model/model~Model#schema `editor.model.schema`} property. It defines allowed model structures (how model elements can be nested) and allowed attributes (of both elements and text nodes). This information is later used by editing features and the editing engine to decide how to process the model, where to enable features, etc.
+
+Schema rules can be defined by using the {@link module:engine/model/schema~Schema#register `Schema#register()`} or {@link module:engine/model/schema~Schema#extend `Schema#extend()`} methods. The former can be used only once for a given item name which ensures that only a single editing feature can introduce this item. Similarly, `extend()` can only be used for defined items.
+
+Elements and attributes are checked by features separately by using the {@link module:engine/model/schema~Schema#checkChild `Schema#checkChild()`} and {@link module:engine/model/schema~Schema#checkAttribute `Schema#checkAttribute()`} methods.
+
+## Defining allowed structures
+
+When a feature introduces a model element, it should register it in the schema. Besides defining that such an element may exist in the model, the feature also needs to define where this element can be placed:
+
+```js
+schema.register( 'myElement', {
+	allowIn: '$root'
+} );
+```
+
+This lets the schema know that `<myElement>` can be a child of `<$root>`. The `$root` element is one of the generic nodes defined by the editing framework. By default, the editor names the main root element a `<$root>`, so the above definition allows `<myElement>` in the main editor element.
+
+In other words, this would be correct:
+
+```xml
+<$root>
+	<myElement></myElement>
+</$root>
+```
+
+While this would be incorrect:
+
+```js
+<$root>
+	<foo>
+		<myElement></myElement>
+	</foo>
+</$root>
+```
+
+## Generic items
+
+There are three basic generic items: `$root`, `$block` and `$text`. They are defined as follows:
+
+```js
+schema.register( '$root', {
+	isLimit: true
+} );
+schema.register( '$block', {
+	allowIn: '$root',
+	isBlock: true
+} );
+schema.register( '$text', {
+	allowIn: '$block'
+} );
+```
+
+These definitions can then be reused by features to create their own definitions in a more extensible way. For example, the {@link module:paragraph/paragraph~Paragraph} feature will define its item as:
+
+```js
+schema.register( 'paragraph', {
+	inheritAllFrom: '$block'
+} );
+```
+
+Which translates to:
+
+```js
+schema.register( 'paragraph', {
+	allowWhere: '$block',
+	allowContentOf: '$block',
+	allowAttributesOf: '$block',
+	inheritTypesFrom: '$block'
+} );
+```
+
+Which can be read as:
+
+* The `<paragraph>` element will be allowed in elements in which `<$block>` is allowed (e.g. in `<$root>`).
+* The `<paragraph>` element will allow all nodes that are allowed in `<$block>` (e.g. `$text`).
+* The `<paragraph>` element will allow all attributes allowed in `<$block>`.
+* The `<paragraph>` element will inherit all `is*` properties of `<$block>` (e.g. `isBlock`).
+
+Thanks to the fact that the `<paragraph>` definition is inherited from `<$block>` other features can use the `<$block>` type to indirectly extend the `<paragraph>` definition. For example, the {@link module:block-quote/blockquote~BlockQuote} feature does this:
+
+```js
+schema.register( 'blockQuote', {
+	allowWhere: '$block',
+	allowContentOf: '$root'
+} );
+```
+
+Thanks to that, despite the fact that block quote and paragraph features know nothing about themselves, paragraphs will be allowed in block quotes and block quotes will be allowed in all places where blocks are allowed. So if anyone registers a `<section>` element (with the `allowContentOf: '$root'` rule), that `<section>` elements will allow block quotes, too.
+
+The side effect of such a definition inheritance is that now `<blockQuote>` is allowed in `<blockQuote>` which needs to be resolved by a callback which will disallow this specific structure.
+
+<info-box>
+	You can read more about the format of the item definition in {@link module:engine/model/schema~SchemaItemDefinition}.
+</info-box>
+
+## Defining advanced rules in `checkChild()` callbacks
+
+The {@link module:engine/model/schema~Schema#checkChild `Schema#checkChild()`} method which is the base method used to check whether some element is allowed in a given structure is {@link module:utils/observablemixin~ObservableMixin#decorate a decorated method}. It means that you can add listeners to implement your specific rules which are not limited by the {@link module:engine/model/schema~SchemaItemDefinition declarative `SchemaItemDefinition` API}.
+
+These listeners can be added either by listening directly to the {@link module:engine/model/schema~Schema#event:checkChild} event or by using the handy {@link module:engine/model/schema~Schema#addChildCheck `Schema#addChildCheck()`} method.
+
+For instance, the block quote feature defines such a listener to disallow nested `<blockQuote>` structures:
+
+```js
+schema.addChildCheck( context, childDefinition ) => {
+	// Note that the context is automatically normalized to a SchemaContext instance and
+	// the child to its definition (SchemaCompiledItemDefinition).
+
+	// If checkChild() is called with a context that ends with blockQuote and blockQuote as a child
+	// to check, make the checkChild() method return false.
+	if ( context.endsWith( 'blockQuote' ) && childDefinition.name == 'blockQuote' ) {
+		return false;
+	}
+} );
+```
+<!--
+## Defining attributes
+
+TODO
+-->
+
+## Implementing additional constraints
+
+Schema's capabilities are limited to simple (and atomic) {@link module:engine/model/schema~Schema#checkChild `Schema#checkChild()`} and {@link module:engine/model/schema~Schema#checkAttribute `Schema#checkAttribute()`} checks on purpose. One may imagine that the schema should support defining more complex rules such as "element `<x>` must be always followed by `<y>`". While it is feasible to create an API that would enable feeding the schema with such definitions, it is unfortunately unrealistic to then expect that every editing feature will consider these rules when processing the model. It is also unrealistic to expect that it will be done automatically by the schema and the editing engine themselves.
+
+For instance, let's get back to the "element `<x>` must be always followed by `<y>`" rule and this initial content:
+
+```xml
+<$root>
+	<x>foo</x>
+	<y>bar[bom</y>
+	<z>bom]bar</z>
+</$root>
+```
+
+Now imagine that the user presses the "Block quote" button. Normally it would wrap the two selected blocks (`<y>` and `<z>`) with a `<blockQuote>` element:
+
+```xml
+<$root>
+	<x>foo</x>
+	<blockQuote>
+		<y>bar[bom</y>
+		<z>bom]bar</z>
+	</blockQuote>
+</$root>
+```
+
+But it turns out that this creates an incorrect structure &mdash; `<x>` is not followed by `<y>` anymore.
+
+What should happen instead? There are at least 4 possible solutions: the block quote feature should not be applicable in such a context, someone should create a new `<y>` right after `<x>`, `<x>` should be moved inside `<blockQuote>` together with `<y>` or vice versa.
+
+While this is a relatively simple scenario (unlike most real-time collaborative editing scenarios), it turns out that it is already hard to say what should happen and who should react to fix this content.
+
+Therefore, if your editor needs to implement such rules, you should do that through {@link module:engine/model/document~Document#registerPostFixer model's post-fixers} fixing incorrect content or actively prevent such situations (e.g. by disabling certain features). It means that these constraints will be defined specifically for your scenario by your code which makes their implementation much easier.
+
+To sum up, the answer to who and how should implement additional constraints is: Your features or your editor through the CKEditor 5 API.
+
+## Who checks the schema?
+
+The CKEditor 5 API exposes many ways to work on (change) the model. It can be done {@link framework/guides/architecture/editing-engine#changing-the-model through the writer}, via methods like {@link module:engine/model/model~Model#insertContent `Model#insertContent()`}, via commands and so on.
+
+### Low-level APIs
+
+The lowest-level API is the writer (to be precise, there are also raw operations below, but they are used for very special cases only). It allows applying atomic changes to the content like inserting, removing, moving or splitting nodes, setting and removing an attribute, etc. It is important to know that the **writer does not prevent from applying changes that violate rules defined in the schema**.
+
+The reason for this is that when you implement a command or any other feature you may need to perform multiple operations to do all the necessary changes. The state in the meantime (between these atomic operations) may be incorrect. The writer must allow that.
+
+For instance, you need to move `<foo>` from `<$root>` to `<bar>` and (at the same time) rename it to `<oof>`. But the schema defines that `<oof>` is not allowed in `<$root>` and `<foo>` is disallowed in `<bar>`. If the writer checked the schema, it would complain regardless of the order of `rename` and `move` operations.
+
+You can argue that the engine could handle this by checking the schema at the end of a {@link module:engine/model/model~Model#change `Model#change()` block} (it works like a transaction &mdash; the state needs to be correct at the end of it). In fact, we [plan to strip disallowed attributes](https://github.com/ckeditor/ckeditor5-engine/issues/1228) at the end of these blocks.
+
+There are problems, though:
+
+* How to fix the content after a transaction is committed? It is impossible to implement a reasonable heuristic that would not break the content from the user perspective.
+* The model can become invalid during real-time collaborative changes. Operational Transformation, while implemented by us in a very rich form (with 11 types of operations instead of the base 3), ensures conflict resolution and eventual consistency, but not the model's validity.
+
+Therefore, we chose to handle such situations on a case-by-case basis, using more expressive and flexible {@link module:engine/model/document~Document#registerPostFixer model's post-fixers}. Additionally, we moved the responsibility to check the schema to features. They can make a lot better decisions a priori, before doing changes. You can read more about this in the ["Implementing additional constraints"](#implementing-additional-constraints) section above.
+
+### High-level APIs
+
+What about other, higher-level methods? **We recommend that all APIs built on top of the writer should check the schema.**
+
+For instance, the {@link module:engine/model/model~Model#insertContent `Model#insertContent()`} method will make sure that inserted nodes are allowed in the place of their insertion. It may also attempt to split the insertion container (if allowed by the schema) if that will make the element to insert allowed, and so on.
+
+Similarly, commands &mdash; if implemented correctly &mdash; {@link module:core/command~Command#isEnabled get disabled} if they should not be executed in the current place.
+
+Finally, the schema plays a crucial role during the conversion from the view to the model (also called "upcasting"). During this process converters decide whether they can convert specific view elements or attributes to the given positions in the model. Thanks to that if you tried to load incorrect data to the editor or when you paste content copied from another website, the structure and attributes of the data get adjusted to the current schema rules.
+
+<info-box>
+	Some features may miss schema checks. If you happen to find such a scenario, do not hesitate to [report it to us](https://github.com/ckeditor/ckeditor5/issues).
+</info-box>
+
+

+ 21 - 21
packages/ckeditor5-engine/package.json

@@ -1,12 +1,8 @@
 {
   "name": "@ckeditor/ckeditor5-engine",
-  "version": "10.2.0",
+  "version": "11.0.0",
   "description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
   "keywords": [
-    "ckeditor5-lib",
-    "ckeditor",
-    "ckeditor5",
-    "ckeditor 5",
     "wysiwyg",
     "rich text",
     "editor",
@@ -18,27 +14,31 @@
     "collaboration",
     "collaborative",
     "real-time",
-    "framework"
+    "framework",
+    "ckeditor5-lib",
+    "ckeditor",
+    "ckeditor5",
+    "ckeditor 5"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-utils": "^10.2.0",
+    "@ckeditor/ckeditor5-utils": "^11.0.0",
     "lodash-es": "^4.17.10"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^10.0.2",
-    "@ckeditor/ckeditor5-block-quote": "^10.0.2",
-    "@ckeditor/ckeditor5-core": "^11.0.0",
-    "@ckeditor/ckeditor5-editor-classic": "^11.0.0",
-    "@ckeditor/ckeditor5-enter": "^10.1.1",
-    "@ckeditor/ckeditor5-essentials": "^10.1.1",
-    "@ckeditor/ckeditor5-heading": "^10.0.2",
-    "@ckeditor/ckeditor5-link": "^10.0.3",
-    "@ckeditor/ckeditor5-list": "^11.0.1",
-    "@ckeditor/ckeditor5-paragraph": "^10.0.2",
-    "@ckeditor/ckeditor5-typing": "^11.0.0",
-    "@ckeditor/ckeditor5-undo": "^10.0.2",
-    "@ckeditor/ckeditor5-widget": "^10.2.0",
-    "eslint": "^4.15.0",
+    "@ckeditor/ckeditor5-basic-styles": "^10.0.3",
+    "@ckeditor/ckeditor5-block-quote": "^10.1.0",
+    "@ckeditor/ckeditor5-core": "^11.0.1",
+    "@ckeditor/ckeditor5-editor-classic": "^11.0.1",
+    "@ckeditor/ckeditor5-enter": "^10.1.2",
+    "@ckeditor/ckeditor5-essentials": "^10.1.2",
+    "@ckeditor/ckeditor5-heading": "^10.1.0",
+    "@ckeditor/ckeditor5-link": "^10.0.4",
+    "@ckeditor/ckeditor5-list": "^11.0.2",
+    "@ckeditor/ckeditor5-paragraph": "^10.0.3",
+    "@ckeditor/ckeditor5-typing": "^11.0.1",
+    "@ckeditor/ckeditor5-undo": "^10.0.3",
+    "@ckeditor/ckeditor5-widget": "^10.3.0",
+    "eslint": "^5.5.0",
     "eslint-config-ckeditor5": "^1.0.7",
     "husky": "^0.14.3",
     "lint-staged": "^7.0.0"

+ 5 - 5
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -149,7 +149,7 @@ export default class DataController {
 		this.mapper.clearBindings();
 
 		// First, convert elements.
-		const modelRange = ModelRange.createIn( modelElementOrFragment );
+		const modelRange = ModelRange._createIn( modelElementOrFragment );
 
 		const viewDocumentFragment = new ViewDocumentFragment();
 
@@ -201,7 +201,7 @@ export default class DataController {
 		const modelRoot = this.model.document.getRoot( rootName );
 
 		this.model.enqueueChange( 'transparent', writer => {
-			writer.insert( this.parse( data, modelRoot ), modelRoot );
+			writer.insert( this.parse( data, modelRoot ), modelRoot, 0 );
 		} );
 
 		return Promise.resolve();
@@ -227,8 +227,8 @@ export default class DataController {
 			writer.setSelection( null );
 			writer.removeSelectionAttribute( this.model.document.selection.getAttributeKeys() );
 
-			writer.remove( ModelRange.createIn( modelRoot ) );
-			writer.insert( this.parse( data, modelRoot ), modelRoot );
+			writer.remove( writer.createRangeIn( modelRoot ) );
+			writer.insert( this.parse( data, modelRoot ), modelRoot, 0 );
 		} );
 	}
 
@@ -297,7 +297,7 @@ function _getMarkersRelativeToElement( element ) {
 		return [];
 	}
 
-	const elementRange = ModelRange.createIn( element );
+	const elementRange = ModelRange._createIn( element );
 
 	for ( const marker of doc.model.markers ) {
 		const intersection = elementRange.getIntersection( marker.getRange() );

+ 14 - 0
packages/ckeditor5-engine/src/controller/editingcontroller.js

@@ -70,6 +70,20 @@ export default class EditingController {
 		const selection = doc.selection;
 		const markers = this.model.markers;
 
+		// When plugins listen on model changes (on selection change, post fixers, etc) and change the view as a result of
+		// model's change, they might trigger view rendering before the conversion is completed (e.g. before the selection
+		// is converted). We disable rendering for the length of the outermost model change() block to prevent that.
+		//
+		// See  https://github.com/ckeditor/ckeditor5-engine/issues/1528
+		this.listenTo( this.model, '_beforeChanges', () => {
+			this.view._renderingDisabled = true;
+		}, { priority: 'highest' } );
+
+		this.listenTo( this.model, '_afterChanges', () => {
+			this.view._renderingDisabled = false;
+			this.view.render();
+		}, { priority: 'lowest' } );
+
 		// Whenever model document is changed, convert those changes to the view (using model.Document#differ).
 		// Do it on 'low' priority, so changes are converted after other listeners did their job.
 		// Also convert model selection.

+ 13 - 10
packages/ckeditor5-engine/src/conversion/downcast-converters.js

@@ -8,7 +8,6 @@ import ModelSelection from '../model/selection';
 import ModelElement from '../model/element';
 
 import ViewAttributeElement from '../view/attributeelement';
-import ViewRange from '../view/range';
 import DocumentSelection from '../model/documentselection';
 
 import { cloneDeep } from 'lodash-es';
@@ -47,7 +46,8 @@ import { cloneDeep } from 'lodash-es';
  * @param {Object} config Conversion configuration.
  * @param {String} config.model The name of the model element to convert.
  * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
- * that takes the model element and view writer as parameters and returns a view container element.
+ * that takes the model element and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}
+ * as parameters and returns a view container element.
  * @returns {Function} Conversion helper.
  */
 export function downcastElementToElement( config ) {
@@ -123,7 +123,8 @@ export function downcastElementToElement( config ) {
  * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
  * of `String`s with possible values if the model attribute is an enumerable.
  * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function
- * that takes the model attribute value and view writer as parameters and returns a view attribute element. If `config.model.values` is
+ * that takes the model attribute value and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}
+ * as parameters and returns a view attribute element. If `config.model.values` is
  * given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions.
  * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
  * @returns {Function} Conversion helper.
@@ -533,14 +534,14 @@ export function remove() {
 		const modelEnd = data.position.getShiftedBy( data.length );
 		const viewEnd = conversionApi.mapper.toViewPosition( modelEnd, { isPhantom: true } );
 
-		const viewRange = new ViewRange( viewStart, viewEnd );
+		const viewRange = conversionApi.writer.createRange( viewStart, viewEnd );
 
 		// Trim the range to remove in case some UI elements are on the view range boundaries.
 		const removed = conversionApi.writer.remove( viewRange.getTrimmed() );
 
 		// After the range is removed, unbind all view elements from the model.
 		// Range inside view document fragment is used to unbind deeply.
-		for ( const child of ViewRange.createIn( removed ).getItems() ) {
+		for ( const child of conversionApi.writer.createRangeIn( removed ).getItems() ) {
 			conversionApi.mapper.unbindViewElement( child );
 		}
 	};
@@ -625,12 +626,12 @@ export function removeUIElement() {
 
 		conversionApi.mapper.unbindElementsFromMarkerName( data.markerName );
 
-		const viewWriter = conversionApi.writer;
-
 		for ( const element of elements ) {
-			viewWriter.clear( ViewRange.createOn( element ), element );
+			conversionApi.writer.clear( conversionApi.writer.createRangeOn( element ), element );
 		}
 
+		conversionApi.writer.clearClonedElementsGroup( data.markerName );
+
 		evt.stop();
 	};
 }
@@ -901,7 +902,7 @@ export function highlightElement( highlightDescriptor ) {
 			conversionApi.consumable.consume( data.item, evt.name );
 
 			// Consume all children nodes.
-			for ( const value of ModelRange.createIn( data.item ) ) {
+			for ( const value of ModelRange._createIn( data.item ) ) {
 				conversionApi.consumable.consume( value.item, evt.name );
 			}
 
@@ -963,13 +964,15 @@ export function removeHighlight( highlightDescriptor ) {
 
 		for ( const element of elements ) {
 			if ( element.is( 'attributeElement' ) ) {
-				conversionApi.writer.unwrap( ViewRange.createOn( element ), viewHighlightElement );
+				conversionApi.writer.unwrap( conversionApi.writer.createRangeOn( element ), viewHighlightElement );
 			} else {
 				// if element.is( 'containerElement' ).
 				element.getCustomProperty( 'removeHighlight' )( element, descriptor.id, conversionApi.writer );
 			}
 		}
 
+		conversionApi.writer.clearClonedElementsGroup( data.markerName );
+
 		evt.stop();
 	};
 }

+ 11 - 8
packages/ckeditor5-engine/src/conversion/downcastdispatcher.js

@@ -131,7 +131,7 @@ export default class DowncastDispatcher {
 		// Convert changes that happened on model tree.
 		for ( const entry of differ.getChanges() ) {
 			if ( entry.type == 'insert' ) {
-				this.convertInsert( Range.createFromPositionAndShift( entry.position, entry.length ), writer );
+				this.convertInsert( Range._createFromPositionAndShift( entry.position, entry.length ), writer );
 			} else if ( entry.type == 'remove' ) {
 				this.convertRemove( entry.position, entry.length, entry.name, writer );
 			} else {
@@ -166,7 +166,7 @@ export default class DowncastDispatcher {
 		// Fire a separate insert event for each node and text fragment contained in the range.
 		for ( const value of range ) {
 			const item = value.item;
-			const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
+			const itemRange = Range._createFromPositionAndShift( value.previousPosition, value.length );
 			const data = {
 				item,
 				range: itemRange
@@ -226,7 +226,7 @@ export default class DowncastDispatcher {
 		// Create a separate attribute event for each node in the range.
 		for ( const value of range ) {
 			const item = value.item;
-			const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
+			const itemRange = Range._createFromPositionAndShift( value.previousPosition, value.length );
 			const data = {
 				item,
 				range: itemRange,
@@ -343,7 +343,7 @@ export default class DowncastDispatcher {
 				continue;
 			}
 
-			const data = { item, range: Range.createOn( item ), markerName, markerRange };
+			const data = { item, range: Range._createOn( item ), markerName, markerRange };
 
 			this.fire( eventName, data, this.conversionApi );
 		}
@@ -495,14 +495,17 @@ export default class DowncastDispatcher {
 	 *
 	 * @event remove
 	 * @param {Object} data Additional information about the change.
-	 * @param {module:engine/model/position~Position} data.sourcePosition Position from where the range has been removed.
-	 * @param {module:engine/model/range~Range} data.range Removed range (in {@link module:engine/model/document~Document#graveyard
-	 * graveyard root}).
+	 * @param {module:engine/model/position~Position} data.position Position from which the node has been removed.
+	 * @param {Number} data.length Offset size of the removed node.
 	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 
 	/**
-	 * Fired when attribute has been added/changed/removed from a node. Also fired when collapsed model selection attribute is converted.
+	 * Fired in the following cases:
+	 *
+	 * * when an attribute has been added, changed, or removed from a node,
+	 * * when a node with an attribute is inserted,
+	 * * when collapsed model selection attribute is converted.
 	 *
 	 * `attribute` is a namespace for a class of events. Names of actually called events follow this pattern:
 	 * `attribute:attributeKey:name`. `attributeKey` is the key of added/changed/removed attribute.

+ 1 - 1
packages/ckeditor5-engine/src/conversion/mapper.js

@@ -101,7 +101,7 @@ export default class Mapper {
 
 			const modelOffset = this._toModelOffset( data.viewPosition.parent, data.viewPosition.offset, viewBlock );
 
-			data.modelPosition = ModelPosition.createFromParentAndOffset( modelParent, modelOffset );
+			data.modelPosition = ModelPosition._createAt( modelParent, modelOffset );
 		}, { priority: 'low' } );
 	}
 

+ 1 - 0
packages/ckeditor5-engine/src/conversion/modelconsumable.js

@@ -61,6 +61,7 @@ import TextProxy from '../model/textproxy';
  *			// Maybe it will be "decorated" later.
  *			const viewImage = new ViewElement( 'img' );
  *			const insertPosition = conversionApi.mapper.toViewPosition( data.range.start );
+ *			const viewWriter = conversionApi.writer;
  *
  *			// Check if the `image` element has children.
  *			if ( data.item.childCount > 0 ) {

+ 5 - 6
packages/ckeditor5-engine/src/conversion/upcast-converters.js

@@ -6,7 +6,6 @@
 import Matcher from '../view/matcher';
 
 import ModelRange from '../model/range';
-import ModelPosition from '../model/position';
 
 import { cloneDeep } from 'lodash-es';
 
@@ -358,7 +357,7 @@ function _prepareToElementConverter( config ) {
 		conversionApi.writer.insert( modelElement, splitResult.position );
 
 		// Convert children and insert to element.
-		const childrenResult = conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( modelElement ) );
+		const childrenResult = conversionApi.convertChildren( data.viewItem, conversionApi.writer.createPositionAt( modelElement, 0 ) );
 
 		// Consume appropriate value from consumable values list.
 		conversionApi.consumable.consume( data.viewItem, match.match );
@@ -366,12 +365,12 @@ function _prepareToElementConverter( config ) {
 		// Set conversion result range.
 		data.modelRange = new ModelRange(
 			// Range should start before inserted element
-			ModelPosition.createBefore( modelElement ),
+			conversionApi.writer.createPositionBefore( modelElement ),
 			// Should end after but we need to take into consideration that children could split our
 			// element, so we need to move range after parent of the last converted child.
 			// before: <allowed>[]</allowed>
 			// after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
-			ModelPosition.createAfter( childrenResult.modelCursor.parent )
+			conversionApi.writer.createPositionAfter( childrenResult.modelCursor.parent )
 		);
 
 		// Now we need to check where the modelCursor should be.
@@ -380,7 +379,7 @@ function _prepareToElementConverter( config ) {
 		// before: <allowed><notAllowed>[]</notAllowed></allowed>
 		// after:  <allowed><notAllowed></notAllowed><converted></converted><notAllowed>[]</notAllowed></allowed>
 		if ( splitResult.cursorParent ) {
-			data.modelCursor = ModelPosition.createAt( splitResult.cursorParent );
+			data.modelCursor = conversionApi.writer.createPositionAt( splitResult.cursorParent, 0 );
 
 			// Otherwise just continue after inserted element.
 		} else {
@@ -602,7 +601,7 @@ export function convertText() {
 
 				conversionApi.writer.insert( text, data.modelCursor );
 
-				data.modelRange = ModelRange.createFromPositionAndShift( data.modelCursor, text.offsetSize );
+				data.modelRange = ModelRange._createFromPositionAndShift( data.modelCursor, text.offsetSize );
 				data.modelCursor = data.modelRange.end;
 			}
 		}

+ 13 - 7
packages/ckeditor5-engine/src/conversion/upcastdispatcher.js

@@ -71,10 +71,16 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
  *  	 				conversionApi.writer.insert( paragraph, splitResult.position );
  *
  *  	 				// Convert children to paragraph.
- *  	 				const { modelRange } = conversionApi.convertChildren( data.viewItem, Position.createAt( paragraph ) );
+ *  	 				const { modelRange } = conversionApi.convertChildren(
+ *  	 					data.viewItem,
+ *  	 					conversionApi.writer.createPositionAt( paragraph, 0 )
+ *  	 				);
  *
  * 						// Set as conversion result, attribute converters may use this property.
- *  	 				data.modelRange = new Range( Position.createBefore( paragraph ), modelRange.end );
+ *  	 				data.modelRange = conversionApi.writer.createRange(
+ *  	 					conversionApi.writer.createPositionBefore( paragraph ),
+ *  	 					modelRange.end
+ *  	 				);
  *
  *  	 				// Continue conversion inside paragraph.
  *  	 				data.modelCursor = data.modelRange.end;
@@ -372,7 +378,7 @@ function extractMarkersFromModelFragment( modelItem, writer ) {
 	const markers = new Map();
 
 	// Create ModelTreeWalker.
-	const range = ModelRange.createIn( modelItem ).getItems();
+	const range = ModelRange._createIn( modelItem ).getItems();
 
 	// Walk through DocumentFragment and collect marker elements.
 	for ( const item of range ) {
@@ -385,14 +391,14 @@ function extractMarkersFromModelFragment( modelItem, writer ) {
 	// Walk through collected marker elements store its path and remove its from the DocumentFragment.
 	for ( const markerElement of markerElements ) {
 		const markerName = markerElement.getAttribute( 'data-name' );
-		const currentPosition = ModelPosition.createBefore( markerElement );
+		const currentPosition = writer.createPositionBefore( markerElement );
 
 		// When marker of given name is not stored it means that we have found the beginning of the range.
 		if ( !markers.has( markerName ) ) {
-			markers.set( markerName, new ModelRange( ModelPosition.createFromPosition( currentPosition ) ) );
+			markers.set( markerName, new ModelRange( currentPosition.clone() ) );
 		// Otherwise is means that we have found end of the marker range.
 		} else {
-			markers.get( markerName ).end = ModelPosition.createFromPosition( currentPosition );
+			markers.get( markerName ).end = currentPosition.clone();
 		}
 
 		// Remove marker element from DocumentFragment.
@@ -419,7 +425,7 @@ function createContextTree( contextDefinition, writer ) {
 			writer.append( current, position );
 		}
 
-		position = ModelPosition.createAt( current );
+		position = ModelPosition._createAt( current, 0 );
 	}
 
 	return position;

+ 4 - 17
packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js

@@ -25,8 +25,6 @@ import MoveOperation from '../model/operation/moveoperation';
 import NoOperation from '../model/operation/nooperation';
 import RenameOperation from '../model/operation/renameoperation';
 import RootAttributeOperation from '../model/operation/rootattributeoperation';
-import WrapOperation from '../model/operation/wrapoperation';
-import UnwrapOperation from '../model/operation/unwrapoperation';
 import SplitOperation from '../model/operation/splitoperation';
 import MergeOperation from '../model/operation/mergeoperation';
 import Model from '../model/model';
@@ -313,7 +311,7 @@ function enableLoggingTools() {
 	} );
 
 	sandbox.mock( DetachOperation.prototype, 'toString', function() {
-		const range = ModelRange.createFromPositionAndShift( this.sourcePosition, this.howMany );
+		const range = ModelRange._createFromPositionAndShift( this.sourcePosition, this.howMany );
 		const nodes = Array.from( range.getItems() );
 		const nodeString = nodes.length > 1 ? `[ ${ nodes.length } ]` : nodes[ 0 ];
 
@@ -332,7 +330,7 @@ function enableLoggingTools() {
 	} );
 
 	sandbox.mock( MoveOperation.prototype, 'toString', function() {
-		const range = ModelRange.createFromPositionAndShift( this.sourcePosition, this.howMany );
+		const range = ModelRange._createFromPositionAndShift( this.sourcePosition, this.howMany );
 
 		return getClassName( this ) + `( ${ this.baseVersion } ): ${ range } -> ${ this.targetPosition }`;
 	} );
@@ -357,19 +355,8 @@ function enableLoggingTools() {
 	} );
 
 	sandbox.mock( SplitOperation.prototype, 'toString', function() {
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			`${ this.position } ( ${ this.howMany } )${ this.graveyardPosition ? ', ' + this.graveyardPosition : '' }`;
-	} );
-
-	sandbox.mock( WrapOperation.prototype, 'toString', function() {
-		const range = ModelRange.createFromPositionAndShift( this.position, this.howMany );
-
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			`${ range } with ${ this.element ? this.element : this.graveyardPosition }`;
-	} );
-
-	sandbox.mock( UnwrapOperation.prototype, 'toString', function() {
-		return getClassName( this ) + `( ${ this.baseVersion } ): ${ this.position } ( ${ this.howMany } ), ${ this.graveyardPosition }`;
+		return getClassName( this ) + `( ${ this.baseVersion } ): ${ this.splitPosition } ` +
+			`( ${ this.howMany } ) -> ${ this.insertionPosition }${ this.graveyardPosition ? ' with ' + this.graveyardPosition : '' }`;
 	} );
 
 	sandbox.mock( ViewText.prototype, 'toString', function() {

+ 12 - 11
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -38,7 +38,7 @@ import { isPlainObject } from 'lodash-es';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 
 /**
- * Writes the content of the {@link module:engine/model/document~Document document} to an HTML-like string.
+ * Writes the content of a model {@link module:engine/model/document~Document document} to an HTML-like string.
  *
  * **Note:** A {@link module:engine/model/text~Text text} node that contains attributes will be represented as:
  *
@@ -72,9 +72,10 @@ export function getData( model, options = {} ) {
 getData._stringify = stringify;
 
 /**
- * Sets the content of the {@link module:engine/model/document~Document document} provided as an HTML-like string.
+ * Sets the content of a model {@link module:engine/model/document~Document document} provided as an HTML-like string.
  *
- * **Note:** Remember to register elements in the {@link module:engine/model/model~Model#schema model's schema} before inserting them.
+ * **Note:** Remember to register elements in the {@link module:engine/model/model~Model#schema model's schema} before
+ * trying to use them.
  *
  * **Note:** To create a {@link module:engine/model/text~Text text} node that contains attributes use:
  *
@@ -115,7 +116,7 @@ export function setData( model, data, options = {} ) {
 
 	model.change( writer => {
 		// Replace existing model in document by new one.
-		writer.remove( ModelRange.createIn( modelRoot ) );
+		writer.remove( writer.createRangeIn( modelRoot ) );
 		writer.insert( modelDocumentFragment, modelRoot );
 
 		// Clean up previous document selection.
@@ -169,16 +170,16 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu
 
 	// Create a range witch wraps passed node.
 	if ( node instanceof RootElement || node instanceof ModelDocumentFragment ) {
-		range = ModelRange.createIn( node );
+		range = model.createRangeIn( node );
 	} else {
 		// Node is detached - create new document fragment.
 		if ( !node.parent ) {
 			const fragment = new ModelDocumentFragment( node );
-			range = ModelRange.createIn( fragment );
+			range = model.createRangeIn( fragment );
 		} else {
 			range = new ModelRange(
-				ModelPosition.createBefore( node ),
-				ModelPosition.createAfter( node )
+				model.createPositionBefore( node ),
+				model.createPositionAfter( node )
 			);
 		}
 	}
@@ -391,9 +392,9 @@ function convertToModelElement() {
 
 		conversionApi.mapper.bindElements( element, data.viewItem );
 
-		conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( element ) );
+		conversionApi.convertChildren( data.viewItem, ModelPosition._createAt( element, 0 ) );
 
-		data.modelRange = ModelRange.createOn( element );
+		data.modelRange = ModelRange._createOn( element );
 		data.modelCursor = data.modelRange.end;
 
 		evt.stop();
@@ -420,7 +421,7 @@ function convertToModelText( withAttributes = false ) {
 
 		conversionApi.writer.insert( node, data.modelCursor );
 
-		data.modelRange = ModelRange.createFromPositionAndShift( data.modelCursor, node.offsetSize );
+		data.modelRange = ModelRange._createFromPositionAndShift( data.modelCursor, node.offsetSize );
 		data.modelCursor = data.modelRange.end;
 
 		evt.stop();

+ 34 - 31
packages/ckeditor5-engine/src/dev-utils/view.js

@@ -80,7 +80,7 @@ export function getData( view, options = {} ) {
 getData._stringify = stringify;
 
 /**
- * Sets the content of the {@link module:engine/view/document~Document document} provided as an HTML-like string.
+ * Sets the content of a view {@link module:engine/view/document~Document document} provided as an HTML-like string.
  *
  * @param {module:engine/view/view~View} view
  * @param {String} data An HTML-like string to write into the document.
@@ -111,44 +111,47 @@ setData._parse = parse;
 
 /**
  * Converts view elements to HTML-like string representation.
+ *
  * A root element can be provided as {@link module:engine/view/text~Text text}:
  *
- *		const text = new Text( 'foobar' );
+ *		const text = downcastWriter.createText( 'foobar' );
  *		stringify( text ); // 'foobar'
  *
  * or as an {@link module:engine/view/element~Element element}:
  *
- *		const element = new Element( 'p', null, new Text( 'foobar' ) );
+ *		const element = downcastWriter.createElement( 'p', null, downcastWriter.createText( 'foobar' ) );
  *		stringify( element ); // '<p>foobar</p>'
  *
  * or as a {@link module:engine/view/documentfragment~DocumentFragment document fragment}:
  *
- *		const text = new Text( 'foobar' );
- *		const b = new Element( 'b', { name: 'test' }, text );
- *		const p = new Element( 'p', { style: 'color:red;' } );
- *		const fragment = new DocumentFragment( [ p, b ] );
+ *		const text = downcastWriter.createText( 'foobar' );
+ *		const b = downcastWriter.createElement( 'b', { name: 'test' }, text );
+ *		const p = downcastWriter.createElement( 'p', { style: 'color:red;' } );
+ *		const fragment = downcastWriter.createDocumentFragment( [ p, b ] );
  *
  *		stringify( fragment ); // '<p style="color:red;"></p><b name="test">foobar</b>'
  *
  * Additionally, a {@link module:engine/view/documentselection~DocumentSelection selection} instance can be provided.
- * Ranges from the selection will then be included in output data.
+ * Ranges from the selection will then be included in the output data.
  * If a range position is placed inside the element node, it will be represented with `[` and `]`:
  *
- *		const text = new Text( 'foobar' );
- *		const b = new Element( 'b', null, text );
- *		const p = new Element( 'p', null, b );
- *		const selection = new Selection(
- *			Range.createFromParentsAndOffsets( p, 0, p, 1 )
+ *		const text = downcastWriter.createText( 'foobar' );
+ *		const b = downcastWriter.createElement( 'b', null, text );
+ *		const p = downcastWriter.createElement( 'p', null, b );
+ *		const selection = downcastWriter.createSelection(
+ *			downcastWriter.createRangeIn( p )
  *		);
  *
  *		stringify( p, selection ); // '<p>[<b>foobar</b>]</p>'
  *
  * If a range is placed inside the text node, it will be represented with `{` and `}`:
  *
- *		const text = new Text( 'foobar' );
- *		const b = new Element( 'b', null, text );
- *		const p = new Element( 'p', null, b );
- *		const selection = new Selection( Range.createFromParentsAndOffsets( text, 1, text, 5 ) );
+ *		const text = downcastWriter.createText( 'foobar' );
+ *		const b = downcastWriter.createElement( 'b', null, text );
+ *		const p = downcastWriter.createElement( 'p', null, b );
+ *		const selection = downcastWriter.createSelection(
+ *			downcastWriter.createRange( downcastWriter.createPositionAt( text, 1 ), downcastWriter.createPositionAt( text, 5 ) )
+ *		);
  *
  *		stringify( p, selection ); // '<p><b>f{ooba}r</b></p>'
  *
@@ -159,10 +162,10 @@ setData._parse = parse;
  *
  * Multiple ranges are supported:
  *
- *		const text = new Text( 'foobar' );
- *		const selection = new Selection( [
- *			Range.createFromParentsAndOffsets( text, 0, text, 1 ) ),
- *			Range.createFromParentsAndOffsets( text, 3, text, 5 ) )
+ *		const text = downcastWriter.createText( 'foobar' );
+ *		const selection = downcastWriter.createSelection( [
+ *			downcastWriter.createRange( downcastWriter.createPositionAt( text, 0 ), downcastWriter.createPositionAt( text, 1 ) ),
+ *			downcastWriter.createRange( downcastWriter.createPositionAt( text, 3 ), downcastWriter.createPositionAt( text, 5 ) )
  *		] );
  *
  *		stringify( text, selection ); // '{f}oo{ba}r'
@@ -172,9 +175,9 @@ setData._parse = parse;
  * is provided, it will be converted to a selection containing this range. If a position instance is provided, it will
  * be converted to a selection containing one range collapsed at this position.
  *
- *		const text = new Text( 'foobar' );
- *		const range = Range.createFromParentsAndOffsets( text, 0, text, 1 );
- *		const position = new Position( text, 3 );
+ *		const text = downcastWriter.createText( 'foobar' );
+ *		const range = downcastWriter.createRange( downcastWriter.createPositionAt( text, 0 ), downcastWriter.createPositionAt( text, 1 ) );
+ *		const position = downcastWriter.createPositionAt( text, 3 );
  *
  *		stringify( text, range ); // '{f}oobar'
  *		stringify( text, position ); // 'foo{}bar'
@@ -186,10 +189,10 @@ setData._parse = parse;
  * {@link module:engine/view/emptyelement~EmptyElement empty elements}
  * and {@link module:engine/view/uielement~UIElement UI elements}:
  *
- *		const attribute = new AttributeElement( 'b' );
- *		const container = new ContainerElement( 'p' );
- *		const empty = new EmptyElement( 'img' );
- *		const ui = new UIElement( 'span' );
+ *		const attribute = downcastWriter.createAttributeElement( 'b' );
+ *		const container = downcastWriter.createContainerElement( 'p' );
+ *		const empty = downcastWriter.createEmptyElement( 'img' );
+ *		const ui = downcastWriter.createUIElement( 'span' );
  *		getData( attribute, null, { showType: true } ); // '<attribute:b></attribute:b>'
  *		getData( container, null, { showType: true } ); // '<container:p></container:p>'
  *		getData( empty, null, { showType: true } ); // '<empty:img></empty:img>'
@@ -198,14 +201,14 @@ setData._parse = parse;
  * If `options.showPriority` is set to `true`, a priority will be displayed for all
  * {@link module:engine/view/attributeelement~AttributeElement attribute elements}.
  *
- *		const attribute = new AttributeElement( 'b' );
+ *		const attribute = downcastWriter.createAttributeElement( 'b' );
  *		attribute._priority = 20;
  *		getData( attribute, null, { showPriority: true } ); // <b view-priority="20"></b>
  *
  * If `options.showAttributeElementId` is set to `true`, the attribute element's id will be displayed for all
  * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that have it set.
  *
- *		const attribute = new AttributeElement( 'span' );
+ *		const attribute = downcastWriter.createAttributeElement( 'span' );
  *		attribute._id = 'marker:foo';
  *		getData( attribute, null, { showAttributeElementId: true } ); // <span view-id="marker:foo"></span>
  *
@@ -249,7 +252,7 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
 }
 
 /**
- * Parses an HTML-like string and returns view tree nodes.
+ * Parses an HTML-like string and returns a view tree.
  * A simple string will be converted to a {@link module:engine/view/text~Text text} node:
  *
  *		parse( 'foobar' ); // Returns an instance of text.

+ 29 - 35
packages/ckeditor5-engine/src/model/differ.js

@@ -170,7 +170,7 @@ export default class Differ {
 				this._markRemove( operation.position.parent, operation.position.offset, 1 );
 				this._markInsert( operation.position.parent, operation.position.offset, 1 );
 
-				const range = Range.createFromPositionAndShift( operation.position, 1 );
+				const range = Range._createFromPositionAndShift( operation.position, 1 );
 
 				for ( const marker of this._markerCollection.getMarkersIntersectingRange( range ) ) {
 					const markerRange = marker.getRange();
@@ -181,49 +181,43 @@ export default class Differ {
 				break;
 			}
 			case 'split': {
-				const splitElement = operation.position.parent;
-				const howManyMoved = splitElement.maxOffset - operation.position.offset;
+				const splitElement = operation.splitPosition.parent;
 
+				// Mark that children of the split element were removed.
 				if ( !this._isInInsertedElement( splitElement ) ) {
-					this._markRemove( splitElement, operation.position.offset, howManyMoved );
+					this._markRemove( splitElement, operation.splitPosition.offset, operation.howMany );
 				}
 
-				if ( !this._isInInsertedElement( splitElement.parent ) ) {
-					this._markInsert( splitElement.parent, splitElement.startOffset + 1, 1 );
+				// Mark that the new element (split copy) was inserted.
+				if ( !this._isInInsertedElement( operation.insertionPosition.parent ) ) {
+					this._markInsert( operation.insertionPosition.parent, operation.insertionPosition.offset, 1 );
+				}
+
+				// If the split took the element from the graveyard, mark that the element from the graveyard was removed.
+				if ( operation.graveyardPosition ) {
+					this._markRemove( operation.graveyardPosition.parent, operation.graveyardPosition.offset, 1 );
 				}
 
 				break;
 			}
 			case 'merge': {
+				// Mark that the merged element was removed.
 				const mergedElement = operation.sourcePosition.parent;
-				const mergedIntoElement = operation.targetPosition.parent;
 
 				if ( !this._isInInsertedElement( mergedElement.parent ) ) {
 					this._markRemove( mergedElement.parent, mergedElement.startOffset, 1 );
 				}
 
-				if ( !this._isInInsertedElement( mergedIntoElement ) ) {
-					this._markInsert( mergedIntoElement, operation.targetPosition.offset, mergedElement.maxOffset );
-				}
+				// Mark that the merged element was inserted into graveyard.
+				const graveyardParent = operation.graveyardPosition.parent;
 
-				break;
-			}
-			case 'wrap': {
-				if ( !this._isInInsertedElement( operation.position.parent ) ) {
-					this._markRemove( operation.position.parent, operation.position.offset, operation.howMany );
-					this._markInsert( operation.position.parent, operation.position.offset, 1 );
-				}
+				this._markInsert( graveyardParent, operation.graveyardPosition.offset, 1 );
 
-				break;
-			}
-			case 'unwrap': {
-				const elementToUnwrap = operation.position.parent;
-				const offset = elementToUnwrap.startOffset;
-				const parent = elementToUnwrap.parent;
-
-				if ( !this._isInInsertedElement( parent ) ) {
-					this._markRemove( parent, offset, 1 );
-					this._markInsert( parent, offset, elementToUnwrap.maxOffset );
+				// Mark that children of merged element were inserted at new parent.
+				const mergedIntoElement = operation.targetPosition.parent;
+
+				if ( !this._isInInsertedElement( mergedIntoElement ) ) {
+					this._markInsert( mergedIntoElement, operation.targetPosition.offset, mergedElement.maxOffset );
 				}
 
 				break;
@@ -397,10 +391,10 @@ export default class Differ {
 					let range;
 
 					if ( elementChildren[ i ].name == '$text' ) {
-						range = Range.createFromParentsAndOffsets( element, i, element, i + 1 );
+						range = new Range( Position._createAt( element, i ), Position._createAt( element, i + 1 ) );
 					} else {
 						const index = element.offsetToIndex( i );
-						range = Range.createFromParentsAndOffsets( element, i, element.getChild( index ), 0 );
+						range = new Range( Position._createAt( element, i ), Position._createAt( element.getChild( index ), 0 ) );
 					}
 
 					// Generate diff items for this change (there might be multiple attributes changed and
@@ -429,7 +423,7 @@ export default class Differ {
 			// If change happens at the same position...
 			if ( a.position.isEqual( b.position ) ) {
 				// Keep chronological order of operations.
-				return a.changeCount < b.changeCount ? -1 : 1;
+				return a.changeCount - b.changeCount;
 			}
 
 			// If positions differ, position "on the left" should be earlier in the result.
@@ -832,7 +826,7 @@ export default class Differ {
 	_getInsertDiff( parent, offset, name ) {
 		return {
 			type: 'insert',
-			position: Position.createFromParentAndOffset( parent, offset ),
+			position: Position._createAt( parent, offset ),
 			name,
 			length: 1,
 			changeCount: this._changeCount++
@@ -851,7 +845,7 @@ export default class Differ {
 	_getRemoveDiff( parent, offset, name ) {
 		return {
 			type: 'remove',
-			position: Position.createFromParentAndOffset( parent, offset ),
+			position: Position._createAt( parent, offset ),
 			name,
 			length: 1,
 			changeCount: this._changeCount++
@@ -885,7 +879,7 @@ export default class Differ {
 				diffs.push( {
 					type: 'attribute',
 					position: range.start,
-					range: Range.createFromRange( range ),
+					range: range.clone(),
 					length: 1,
 					attributeKey: key,
 					attributeOldValue: oldValue,
@@ -904,7 +898,7 @@ export default class Differ {
 			diffs.push( {
 				type: 'attribute',
 				position: range.start,
-				range: Range.createFromRange( range ),
+				range: range.clone(),
 				length: 1,
 				attributeKey: key,
 				attributeOldValue: null,
@@ -954,7 +948,7 @@ export default class Differ {
 	 * @param {Number} howMany
 	 */
 	_removeAllNestedChanges( parent, offset, howMany ) {
-		const range = Range.createFromParentsAndOffsets( parent, offset, parent, offset + howMany );
+		const range = new Range( Position._createAt( parent, offset ), Position._createAt( parent, offset + howMany ) );
 
 		for ( const item of range.getItems( { shallow: true } ) ) {
 			if ( item.is( 'element' ) ) {

+ 4 - 5
packages/ckeditor5-engine/src/model/document.js

@@ -8,8 +8,6 @@
  */
 
 import Differ from './differ';
-import Range from './range';
-import Position from './position';
 import RootElement from './rootelement';
 import History from './history';
 import DocumentSelection from './documentselection';
@@ -334,14 +332,15 @@ export default class Document {
 	 */
 	_getDefaultRange() {
 		const defaultRoot = this._getDefaultRoot();
-		const schema = this.model.schema;
+		const model = this.model;
+		const schema = model.schema;
 
 		// Find the first position where the selection can be put.
-		const position = new Position( defaultRoot, [ 0 ] );
+		const position = model.createPositionFromPath( defaultRoot, [ 0 ] );
 		const nearestRange = schema.getNearestSelectionRange( position );
 
 		// If valid selection range is not found - return range collapsed at the beginning of the root.
-		return nearestRange || new Range( position );
+		return nearestRange || model.createRange( position );
 	}
 
 	/**

+ 1 - 1
packages/ckeditor5-engine/src/model/documentfragment.js

@@ -158,7 +158,7 @@ export default class DocumentFragment {
 
 	/**
 	 * Returns the starting offset of given child. Starting offset is equal to the sum of
-	 * {module:engine/model/node~Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if
+	 * {@link module:engine/model/node~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 {module:engine/model/node~Node} node Child node to look for.

+ 6 - 4
packages/ckeditor5-engine/src/model/documentselection.js

@@ -11,7 +11,6 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 
 import Selection from './selection';
-import Position from './position';
 import LiveRange from './liverange';
 import Text from './text';
 import TextProxy from './textproxy';
@@ -45,6 +44,8 @@ const storePrefix = 'selection:';
  * that are inside {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
  * If you need to represent a selection in document fragment,
  * use {@link module:engine/model/selection~Selection Selection class} instead.
+ *
+ * @mixes module:utils/emittermixin~EmitterMixin
  */
 export default class DocumentSelection {
 	/**
@@ -330,7 +331,8 @@ export default class DocumentSelection {
 	 * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
 	 * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionFocus} method.
 	 *
-	 * The location can be specified in the same form as {@link module:engine/model/position~Position.createAt} parameters.
+	 * The location can be specified in the same form as
+	 * {@link module:engine/model/writer~Writer#createPositionAt writer.createPositionAt()} parameters.
 	 *
 	 * @see module:engine/model/writer~Writer#setSelectionFocus
 	 * @protected
@@ -748,7 +750,7 @@ class LiveSelection extends Selection {
 			return;
 		}
 
-		const liveRange = LiveRange.createFromRange( range );
+		const liveRange = LiveRange.fromRange( range );
 
 		liveRange.on( 'change:range', ( evt, oldRange, data ) => {
 			this._hasChangedRange = true;
@@ -1012,7 +1014,7 @@ class LiveSelection extends Selection {
 	_fixGraveyardSelection( liveRange, removedRangeStart ) {
 		// The start of the removed range is the closest position to the `liveRange` - the original selection range.
 		// This is a good candidate for a fixed selection range.
-		const positionCandidate = Position.createFromPosition( removedRangeStart );
+		const positionCandidate = removedRangeStart.clone();
 
 		// Find a range that is a correct selection range and is closest to the start of removed range.
 		const selectionRange = this._model.schema.getNearestSelectionRange( positionCandidate );

+ 1 - 1
packages/ckeditor5-engine/src/model/element.js

@@ -146,7 +146,7 @@ export default class Element extends Node {
 
 	/**
 	 * Returns the starting offset of given child. Starting offset is equal to the sum of
-	 * {module:engine/model/node~Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if
+	 * {@link module:engine/model/node~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 {module:engine/model/node~Node} node Child node to look for.

+ 23 - 18
packages/ckeditor5-engine/src/model/liveposition.js

@@ -63,35 +63,40 @@ export default class LivePosition extends Position {
 	}
 
 	/**
-	 * @static
-	 * @method module:engine/model/liveposition~LivePosition.createAfter
-	 * @see module:engine/model/position~Position.createAfter
-	 * @param {module:engine/model/node~Node} node
-	 * @returns {module:engine/model/liveposition~LivePosition}
+	 * Creates a {@link module:engine/model/position~Position position instance}, which is equal to this live position.
+	 *
+	 * @returns {module:engine/model/position~Position}
 	 */
+	toPosition() {
+		return new Position( this.root, this.path.slice(), this.stickiness );
+	}
 
 	/**
-	 * @static
-	 * @method module:engine/model/liveposition~LivePosition.createBefore
-	 * @see module:engine/model/position~Position.createBefore
-	 * @param {module:engine/model/node~Node} node
-	 * @returns {module:engine/model/liveposition~LivePosition}
+	 * Creates a `LivePosition` instance that is equal to position.
+	 *
+	 * @param {module:engine/model/position~Position} position
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness]
+	 * @returns {module:engine/model/position~Position}
 	 */
+	static fromPosition( position, stickiness ) {
+		return new this( position.root, position.path.slice(), stickiness ? stickiness : position.stickiness );
+	}
 
 	/**
 	 * @static
-	 * @method module:engine/model/liveposition~LivePosition.createFromParentAndOffset
-	 * @see module:engine/model/position~Position.createFromParentAndOffset
-	 * @param {module:engine/model/element~Element} parent
-	 * @param {Number} offset
+	 * @protected
+	 * @method module:engine/model/liveposition~LivePosition._createAfter
+	 * @see module:engine/model/position~Position._createAfter
+	 * @param {module:engine/model/node~Node} node
 	 * @returns {module:engine/model/liveposition~LivePosition}
 	 */
 
 	/**
 	 * @static
-	 * @method module:engine/model/liveposition~LivePosition.createFromPosition
-	 * @see module:engine/model/position~Position.createFromPosition
-	 * @param {module:engine/model/position~Position} position
+	 * @protected
+	 * @method module:engine/model/liveposition~LivePosition._createBefore
+	 * @see module:engine/model/position~Position._createBefore
+	 * @param {module:engine/model/node~Node} node
 	 * @returns {module:engine/model/liveposition~LivePosition}
 	 */
 
@@ -132,7 +137,7 @@ function transform( operation ) {
 	const result = this.getTransformedByOperation( operation );
 
 	if ( !this.isEqual( result ) ) {
-		const oldPosition = Position.createFromPosition( this );
+		const oldPosition = this.toPosition();
 
 		this.path = result.path;
 		this.root = result.root;

+ 33 - 23
packages/ckeditor5-engine/src/model/liverange.js

@@ -41,38 +41,49 @@ export default class LiveRange extends Range {
 	}
 
 	/**
-	 * @see module:engine/model/range~Range.createIn
-	 * @static
-	 * @method module:engine/model/liverange~LiveRange.createIn
-	 * @param {module:engine/model/element~Element} element
+	 * Creates a {@link module:engine/model/range~Range range instance} that is equal to this live range.
+	 *
+	 * @returns {module:engine/model/range~Range}
+	 */
+	toRange() {
+		return new Range( this.start, this.end );
+	}
+
+	/**
+	 * Creates a `LiveRange` instance that is equal to the given range.
+	 *
+	 * @param {module:engine/model/range~Range} range
 	 * @returns {module:engine/model/liverange~LiveRange}
 	 */
+	static fromRange( range ) {
+		return new LiveRange( range.start, range.end );
+	}
 
 	/**
-	 * @see module:engine/model/range~Range.createFromPositionAndShift
+	 * @see module:engine/model/range~Range._createIn
 	 * @static
-	 * @method module:engine/model/liverange~LiveRange.createFromPositionAndShift
-	 * @param {module:engine/model/position~Position} position
-	 * @param {Number} shift
+	 * @protected
+	 * @method module:engine/model/liverange~LiveRange._createIn
+	 * @param {module:engine/model/element~Element} element
 	 * @returns {module:engine/model/liverange~LiveRange}
 	 */
 
 	/**
-	 * @see module:engine/model/range~Range.createFromParentsAndOffsets
+	 * @see module:engine/model/range~Range._createOn
 	 * @static
-	 * @method module:engine/model/liverange~LiveRange.createFromParentsAndOffsets
-	 * @param {module:engine/model/element~Element} startElement
-	 * @param {Number} startOffset
-	 * @param {module:engine/model/element~Element} endElement
-	 * @param {Number} endOffset
+	 * @protected
+	 * @method module:engine/model/liverange~LiveRange._createOn
+	 * @param {module:engine/model/element~Element} element
 	 * @returns {module:engine/model/liverange~LiveRange}
 	 */
 
 	/**
-	 * @see module:engine/model/range~Range.createFromRange
+	 * @see module:engine/model/range~Range._createFromPositionAndShift
 	 * @static
-	 * @method module:engine/model/liverange~LiveRange.createFromRange
-	 * @param {module:engine/model/range~Range} range
+	 * @protected
+	 * @method module:engine/model/liverange~LiveRange._createFromPositionAndShift
+	 * @param {module:engine/model/position~Position} position
+	 * @param {Number} shift
 	 * @returns {module:engine/model/liverange~LiveRange}
 	 */
 
@@ -129,7 +140,7 @@ function bindWithDocument() {
 function transform( operation ) {
 	// Transform the range by the operation. Join the result ranges if needed.
 	const ranges = this.getTransformedByOperation( operation );
-	const result = Range.createFromRanges( ranges );
+	const result = Range._createFromRanges( ranges );
 
 	const boundariesChanged = !result.isEqual( this );
 	const contentChanged = doesOperationChangeRangeContent( this, operation );
@@ -149,7 +160,7 @@ function transform( operation ) {
 			}
 		}
 
-		const oldRange = Range.createFromRange( this );
+		const oldRange = this.toRange();
 
 		this.start = result.start;
 		this.end = result.end;
@@ -157,7 +168,7 @@ function transform( operation ) {
 		this.fire( 'change:range', oldRange, { deletionPosition } );
 	} else if ( contentChanged ) {
 		// If range boundaries have not changed, but there was change inside the range, fire `change:content` event.
-		this.fire( 'change:content', Range.createFromRange( this ), { deletionPosition } );
+		this.fire( 'change:content', this.toRange(), { deletionPosition } );
 	}
 }
 
@@ -170,9 +181,6 @@ function transform( operation ) {
 function doesOperationChangeRangeContent( range, operation ) {
 	switch ( operation.type ) {
 		case 'insert':
-		case 'split':
-		case 'wrap':
-		case 'unwrap':
 			return range.containsPosition( operation.position );
 		case 'move':
 		case 'remove':
@@ -181,6 +189,8 @@ function doesOperationChangeRangeContent( range, operation ) {
 			return range.containsPosition( operation.sourcePosition ) ||
 				range.start.isEqual( operation.sourcePosition ) ||
 				range.containsPosition( operation.targetPosition );
+		case 'split':
+			return range.containsPosition( operation.splitPosition ) || range.containsPosition( operation.insertionPosition );
 	}
 
 	return false;

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

@@ -8,8 +8,6 @@
  */
 
 import LiveRange from './liverange';
-import Position from './position';
-import Range from './range';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
@@ -101,7 +99,7 @@ export default class MarkerCollection {
 			let hasChanged = false;
 
 			if ( !oldRange.isEqual( range ) ) {
-				oldMarker._attachLiveRange( LiveRange.createFromRange( range ) );
+				oldMarker._attachLiveRange( LiveRange.fromRange( range ) );
 				hasChanged = true;
 			}
 
@@ -122,7 +120,7 @@ export default class MarkerCollection {
 			return oldMarker;
 		}
 
-		const liveRange = LiveRange.createFromRange( range );
+		const liveRange = LiveRange.fromRange( range );
 		const marker = new Marker( markerName, liveRange, managedUsingOperations, affectsData );
 
 		this._markers.set( markerName, marker );
@@ -396,7 +394,7 @@ class Marker {
 			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
 		}
 
-		return Position.createFromPosition( this._liveRange.start );
+		return this._liveRange.start.clone();
 	}
 
 	/**
@@ -409,7 +407,7 @@ class Marker {
 			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
 		}
 
-		return Position.createFromPosition( this._liveRange.end );
+		return this._liveRange.end.clone();
 	}
 
 	/**
@@ -429,7 +427,7 @@ class Marker {
 			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
 		}
 
-		return Range.createFromRange( this._liveRange );
+		return this._liveRange.toRange();
 	}
 
 	/**

+ 366 - 19
packages/ckeditor5-engine/src/model/model.js

@@ -16,6 +16,8 @@ import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import ModelElement from './element';
 import ModelRange from './range';
+import ModelPosition from './position';
+import ModelSelection from './selection';
 
 import insertContent from './utils/insertcontent';
 import deleteContent from './utils/deletecontent';
@@ -207,26 +209,137 @@ export default class Model {
 	}
 
 	/**
-	 * {@link module:utils/observablemixin~ObservableMixin#decorate Decorated} function to apply
-	 * {@link module:engine/model/operation/operation~Operation operations} on the model.
+	 * {@link module:utils/observablemixin~ObservableMixin#decorate Decorated} function for applying
+	 * {@link module:engine/model/operation/operation~Operation operations} to the model.
 	 *
-	 * @param {module:engine/model/operation/operation~Operation} operation Operation to apply
+	 * This is a low-level way of changing the model. It is exposed for very specific use cases (like the undo feature).
+	 * Normally, to modify the model, you will want to use {@link module:engine/model/writer~Writer `Writer`}.
+	 * See also {@glink framework/guides/architecture/editing-engine#changing-the-model Changing the model} section
+	 * of the {@glink framework/guides/architecture/editing-engine Editing architecture} guide.
+	 *
+	 * @param {module:engine/model/operation/operation~Operation} operation The operation to apply.
 	 */
 	applyOperation( operation ) {
 		operation._execute();
 	}
 
 	/**
-	 * Inserts content into the editor (specified selection) as one would expect the paste
+	 * Inserts content at the position in the editor specified by the selection, as one would expect the paste
 	 * functionality to work.
 	 *
+	 * This is a high-level method. It takes the {@link #schema schema} into consideration when inserting
+	 * the content, clears the given selection's content before inserting nodes and moves the selection
+	 * to its target position at the end of the process.
+	 * It can split elements, merge them, wrap bare text nodes with paragraphs, etc. &mdash; just like the
+	 * pasting feature should do.
+	 *
+	 * For lower-level methods see {@link module:engine/model/writer~Writer `Writer`}.
+	 *
+	 * This method, unlike {@link module:engine/model/writer~Writer `Writer`}'s methods, does not have to be used
+	 * inside a {@link #change `change()` block}.
+	 *
+	 * # Conversion and schema
+	 *
+	 * Inserting elements and text nodes into the model is not enough to make CKEditor 5 render that content
+	 * to the user. CKEditor 5 implements a model-view-controller architecture and what `model.insertContent()` does
+	 * is only adding nodes to the model. Additionally, you need to define
+	 * {@glink framework/guides/architecture/editing-engine#conversion converters} between the model and view
+	 * and define those nodes in the {@glink framework/guides/architecture/editing-engine#schema schema}.
+	 *
+	 * So, while this method may seem similar to CKEditor 4 `editor.insertHtml()` (in fact, both methods
+	 * are used for paste-like content insertion), the CKEditor 5 method cannot be use to insert arbitrary HTML
+	 * unless converters are defined for all elements and attributes in that HTML.
+	 *
+	 * # Examples
+	 *
+	 * Using `insertContent()` with a manually created model structure:
+	 *
+	 *		// Let's create a document fragment containing such content as:
+	 *		//
+	 *		// <paragrap>foo</paragraph>
+	 *		// <blockQuote>
+	 *		//    <paragraph>bar</paragraph>
+	 *		// </blockQuote>
+	 *		const docFrag = editor.model.change( writer => {
+	 *			const p1 = writer.createElement( 'paragraph' );
+	 *			const p2 = writer.createElement( 'paragraph' );
+	 *			const blockQuote = writer.createElement( 'blockQuote' );
+	 *			const docFrag = writer.createDocumentFragment();
+	 *
+	 *			writer.append( p1, docFrag );
+	 *			writer.append( blockQuote, docFrag );
+	 *			writer.append( p2, blockQuote );
+	 *			writer.insertText( 'foo', p1 );
+	 *			writer.insertText( 'bar', p2 );
+	 *
+	 *			return docFrag;
+	 *		} );
+	 *
+	 *		// insertContent() does not have to be used in a change() block. It can, though,
+	 *		// so this code could be moved to the callback defined above.
+	 *		editor.model.insertContent( docFrag );
+	 *
+	 * Using `insertContent()` with an HTML string converted to a model document fragment (similar to the pasting mechanism):
+	 *
+	 *		// You can create your own HtmlDataProcessor instance or use editor.data.processor
+	 *		// if you have not overridden the default one (which is the HtmlDataProcessor instance).
+	 *		const htmlDP = new HtmlDataProcessor();
+	 *
+	 *		// Convert an HTML string to a view document fragment:
+	 *		const viewFragment = htmlDP.toView( htmlString );
+	 *
+	 *		// Convert the view document fragment to a model document fragment
+	 *		// in the context of $root. This conversion takes the schema into
+	 *		// account so if, for example, the view document fragment contained a bare text node,
+	 *		// this text node cannot be a child of $root, so it will be automatically
+	 *		// wrapped with a <paragraph>. You can define the context yourself (in the second parameter),
+	 *		// and e.g. convert the content like it would happen in a <paragraph>.
+	 *		// Note: The clipboard feature uses a custom context called $clipboardHolder
+	 *		// which has a loosened schema.
+	 *		const modelFragment = editor.data.toModel( viewFragment );
+	 *
+	 *		editor.model.insertContent( modelFragment );
+	 *
+	 * By default this method will use the document selection but it can also be used with a position, range or selection instance.
+	 *
+	 *		// Insert text at the current document selection position.
+	 *		editor.model.change( writer => {
+	 *			editor.model.insertContent( writer.createText( 'x' ) );
+	 *		} );
+	 *
+	 *		// Insert text at a given position - the document selection will not be modified.
+	 *		editor.model.change( writer => {
+	 *			editor.model.insertContent( writer.createText( 'x' ), doc.getRoot(), 2 );
+	 *
+	 *			// Which is a shorthand for:
+	 *			editor.model.insertContent( writer.createText( 'x' ), writer.createPositionAt( doc.getRoot(), 2 ) );
+	 *		} );
+	 *
+	 * If an instance of {@link module:engine/model/selection~Selection} is passed as `selectable`
+	 * it will be moved to the target position (where the document selection should be moved after the insertion).
+	 *
+	 *		editor.model.change( writer => {
+	 *			// Insert text replacing the given selection instance.
+	 *			const selection = writer.createSelection( paragraph, 'in' );
+	 *
+	 *			editor.model.insertContent( writer.createText( 'x' ), selection );
+	 *
+	 *			// insertContent() modifies the passed selection instance so it can be used to set the document selection.
+	 *			// Note: This is not necessary when you passed the document selection to insertContent().
+	 *			writer.setSelection( selection );
+	 *		} );
+	 *
 	 * @fires insertContent
 	 * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
-	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
-	 * Selection into which the content should be inserted.
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+	 * module:engine/model/position~Position|module:engine/model/item~Item|
+	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} [selectable=model.document.selection]
+	 * The selection into which the content should be inserted. If not provided the current model document selection will be used.
+	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] To be used when a model item was passed as `selectable`.
+	 * This param defines a position in relation to that item.
 	 */
-	insertContent( content, selection ) {
-		insertContent( this, content, selection );
+	insertContent( content, selectable, placeOrOffset ) {
+		insertContent( this, content, selectable, placeOrOffset );
 	}
 
 	/**
@@ -243,14 +356,13 @@ export default class Model {
 	 * @fires deleteContent
 	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
 	 * Selection of which the content should be deleted.
-	 * @param {module:engine/model/batch~Batch} batch Batch to which the operations will be added.
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.leaveUnmerged=false] Whether to merge elements after removing the content of the selection.
 	 *
-	 * For example `<heading>x[x</heading><paragraph>y]y</paragraph>` will become:
+	 * For example `<heading1>x[x</heading1><paragraph>y]y</paragraph>` will become:
 	 *
-	 * * `<heading>x^y</heading>` with the option disabled (`leaveUnmerged == false`)
-	 * * `<heading>x^</heading><paragraph>y</paragraph>` with enabled (`leaveUnmerged == true`).
+	 * * `<heading1>x^y</heading1>` with the option disabled (`leaveUnmerged == false`)
+	 * * `<heading1>x^</heading1><paragraph>y</paragraph>` with enabled (`leaveUnmerged == true`).
 	 *
 	 * Note: {@link module:engine/model/schema~Schema#isObject object} and {@link module:engine/model/schema~Schema#isLimit limit}
 	 * elements will not be merged.
@@ -258,10 +370,10 @@ export default class Model {
 	 * @param {Boolean} [options.doNotResetEntireContent=false] Whether to skip replacing the entire content with a
 	 * paragraph when the entire content was selected.
 	 *
-	 * For example `<heading>[x</heading><paragraph>y]</paragraph>` will become:
+	 * For example `<heading1>[x</heading1><paragraph>y]</paragraph>` will become:
 	 *
 	 * * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
-	 * * `<heading>^</heading>` with enabled (`doNotResetEntireContent == true`)
+	 * * `<heading1>^</heading1>` with enabled (`doNotResetEntireContent == true`)
 	 */
 	deleteContent( selection, options ) {
 		deleteContent( this, selection, options );
@@ -306,13 +418,22 @@ export default class Model {
 	 * For example, for the following selection:
 	 *
 	 * ```html
-	 * <p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p>
+	 * <paragraph>x</paragraph>
+	 * <blockQuote>
+	 *	<paragraph>y</paragraph>
+	 *	<heading1>fir[st</heading1>
+	 * </blockQuote>
+	 * <paragraph>se]cond</paragraph>
+	 * <paragraph>z</paragraph>
 	 * ```
 	 *
 	 * It will return a document fragment with such a content:
 	 *
 	 * ```html
-	 * <quote><h>st</h></quote><p>se</p>
+	 * <blockQuote>
+	 *	<heading1>st</heading1>
+	 * </blockQuote>
+	 * <paragraph>se</paragraph>
 	 * ```
 	 *
 	 * @fires getSelectedContent
@@ -325,17 +446,18 @@ export default class Model {
 	}
 
 	/**
-	 * Checks whether given {@link module:engine/model/range~Range range} or {@link module:engine/model/element~Element element}
+	 * Checks whether the given {@link module:engine/model/range~Range range} or
+	 * {@link module:engine/model/element~Element element}
 	 * has any content.
 	 *
-	 * Content is any text node or element which is registered in {@link module:engine/model/schema~Schema schema}.
+	 * Content is any text node or element which is registered in the {@link module:engine/model/schema~Schema schema}.
 	 *
 	 * @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
 	 * @returns {Boolean}
 	 */
 	hasContent( rangeOrElement ) {
 		if ( rangeOrElement instanceof ModelElement ) {
-			rangeOrElement = ModelRange.createIn( rangeOrElement );
+			rangeOrElement = ModelRange._createIn( rangeOrElement );
 		}
 
 		if ( rangeOrElement.isCollapsed ) {
@@ -352,6 +474,208 @@ export default class Model {
 		return false;
 	}
 
+	/**
+	 * Creates a position from the given root and path in that root.
+	 *
+	 * Note: This method is also available as
+	 * {@link module:engine/model/writer~Writer#createPositionFromPath `Writer#createPositionFromPath()`}.
+	 *
+	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} root Root of the position.
+	 * @param {Array.<Number>} path Position path. See {@link module:engine/model/position~Position#path}.
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
+	 * See {@link module:engine/model/position~PositionStickiness}.
+	 * @returns {module:engine/model/position~Position}
+	 */
+	createPositionFromPath( root, path, stickiness ) {
+		return new ModelPosition( root, path, stickiness );
+	}
+
+	/**
+	 * Creates position at the given location. The location can be specified as:
+	 *
+	 * * a {@link module:engine/model/position~Position position},
+	 * * a parent element and offset in that element,
+	 * * a parent element and `'end'` (the position will be set at the end of that element),
+	 * * a {@link module:engine/model/item~Item model item} and `'before'` or `'after'`
+	 * (the position will be set before or after the given model item).
+	 *
+	 * This method is a shortcut to other factory methods such as:
+	 *
+	 * * {@link module:engine/model/model~Model#createPositionBefore `createPositionBefore()`},
+	 * * {@link module:engine/model/model~Model#createPositionAfter `createPositionAfter()`}.
+	 *
+	 * Note: This method is also available as
+	 * {@link module:engine/model/writer~Writer#createPositionAt `Writer#createPositionAt()`},
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
+	 * first parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	createPositionAt( itemOrPosition, offset ) {
+		return ModelPosition._createAt( itemOrPosition, offset );
+	}
+
+	/**
+	 * Creates a new position after the given {@link module:engine/model/item~Item model item}.
+	 *
+	 * Note: This method is also available as
+	 * {@link module:engine/model/writer~Writer#createPositionAfter `Writer#createPositionAfter()`}.
+	 *
+	 * @param {module:engine/model/item~Item} item Item after which the position should be placed.
+	 * @returns {module:engine/model/position~Position}
+	 */
+	createPositionAfter( item ) {
+		return ModelPosition._createAfter( item );
+	}
+
+	/**
+	 * Creates a new position before the given {@link module:engine/model/item~Item model item}.
+	 *
+	 * Note: This method is also available as
+	 * {@link module:engine/model/writer~Writer#createPositionBefore `Writer#createPositionBefore()`}.
+	 *
+	 * @param {module:engine/model/item~Item} item Item before which the position should be placed.
+	 * @returns {module:engine/model/position~Position}
+	 */
+	createPositionBefore( item ) {
+		return ModelPosition._createBefore( item );
+	}
+
+	/**
+	 * Creates a range spanning from the `start` position to the `end` position.
+	 *
+	 * Note: This method is also available as
+	 * {@link module:engine/model/writer~Writer#createRange `Writer#createRange()`}:
+	 *
+	 *		model.change( writer => {
+	 *			const range = writer.createRange( start, end );
+	 *		} );
+	 *
+	 * @param {module:engine/model/position~Position} start Start position.
+	 * @param {module:engine/model/position~Position} [end] End position. If not set, the range will be collapsed
+	 * to the `start` position.
+	 * @returns {module:engine/model/range~Range}
+	 */
+	createRange( start, end ) {
+		return new ModelRange( start, end );
+	}
+
+	/**
+	 * Creates a range inside the given element which starts before the first child of
+	 * that element and ends after the last child of that element.
+	 *
+	 * Note: This method is also available as
+	 * {@link module:engine/model/writer~Writer#createRangeIn `Writer#createRangeIn()`}:
+	 *
+	 *		model.change( writer => {
+	 *			const range = writer.createRangeIn( paragraph );
+	 *		} );
+	 *
+	 * @param {module:engine/model/element~Element} element Element which is a parent for the range.
+	 * @returns {module:engine/model/range~Range}
+	 */
+	createRangeIn( element ) {
+		return ModelRange._createIn( element );
+	}
+
+	/**
+	 * Creates a range that starts before the given {@link module:engine/model/item~Item model item} and ends after it.
+	 *
+	 * Note: This method is also available on `writer` instance as
+	 * {@link module:engine/model/writer~Writer#createRangeOn `Writer.createRangeOn()`}:
+	 *
+	 *		model.change( writer => {
+	 *			const range = writer.createRangeOn( paragraph );
+	 *		} );
+	 *
+	 * @param {module:engine/model/item~Item} item
+	 * @returns {module:engine/model/range~Range}
+	 */
+	createRangeOn( item ) {
+		return ModelRange._createOn( item );
+	}
+
+	/**
+	 * Creates a new selection instance based on:
+	 *
+	 * * the given {@link module:engine/model/selection~Selection selection},
+	 * * or based on the given {@link module:engine/model/range~Range range},
+	 * * or based on the given iterable collection of {@link module:engine/model/range~Range ranges}
+	 * * or at the given {@link module:engine/model/position~Position position},
+	 * * or on the given {@link module:engine/model/element~Element element},
+	 * * or creates an empty selection if no arguments were passed.
+	 *
+	 * Note: This method is also available as
+	 * {@link module:engine/model/writer~Writer#createSelection `Writer#createSelection()`}.
+	 *
+	 *		// Creates empty selection without ranges.
+	 *		const selection = writer.createSelection();
+	 *
+	 *		// Creates selection at the given range.
+	 *		const range = writer.createRange( start, end );
+	 *		const selection = writer.createSelection( range );
+	 *
+	 *		// Creates selection at the given ranges
+	 *		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
+	 *		const selection = writer.createSelection( ranges );
+	 *
+	 *		// Creates selection from the other selection.
+	 *		// Note: It doesn't copies selection attributes.
+	 *		const otherSelection = writer.createSelection();
+	 *		const selection = writer.createSelection( otherSelection );
+	 *
+	 *		// Creates selection from the given document selection.
+	 *		// Note: It doesn't copies selection attributes.
+	 *		const documentSelection = model.document.selection;
+	 *		const selection = writer.createSelection( documentSelection );
+	 *
+	 *		// Creates selection at the given position.
+	 *		const position = writer.createPositionFromPath( root, path );
+	 *		const selection = writer.createSelection( position );
+	 *
+	 *		// Creates selection at the given offset in the given element.
+	 *		const paragraph = writer.createElement( 'paragraph' );
+	 *		const selection = writer.createSelection( paragraph, offset );
+	 *
+	 *		// Creates a range inside an {@link module:engine/model/element~Element element} which starts before the
+	 *		// first child of that element and ends after the last child of that element.
+	 *		const selection = writer.createSelection( paragraph, 'in' );
+	 *
+	 *		// Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends
+	 *		// just after the item.
+	 *		const selection = writer.createSelection( paragraph, 'on' );
+	 *
+	 *		// Additional options (`'backward'`) can be specified as the last argument.
+	 *
+	 *		// Creates backward selection.
+	 *		const selection = writer.createSelection( range, { backward: true } );
+	 *
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+	 * module:engine/model/position~Position|module:engine/model/element~Element|
+	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
+	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
+	 * @param {Object} [options]
+	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
+	 * @returns {module:engine/model/selection~Selection}
+	 */
+	createSelection( selectable, placeOrOffset, options ) {
+		return new ModelSelection( selectable, placeOrOffset, options );
+	}
+
+	/**
+	 * Creates a {@link module:engine/model/batch~Batch} instance.
+	 *
+	 * **Note:** In most cases creating a batch instance is not necessary as they are created when using:
+	 *
+	 * * {@link #change `change()`},
+	 * * {@link #enqueueChange `enqueueChange()`}.
+	 *
+	 * @returns {module:engine/model/batch~Batch}
+	 */
+	createBatch() {
+		return new Batch();
+	}
+
 	/**
 	 * Removes all events listeners set by model instance and destroys {@link module:engine/model/document~Document}.
 	 */
@@ -370,6 +694,8 @@ export default class Model {
 	_runPendingChanges() {
 		const ret = [];
 
+		this.fire( '_beforeChanges' );
+
 		while ( this._pendingChanges.length ) {
 			// Create a new writer using batch instance created for this chain of changes.
 			const currentBatch = this._pendingChanges[ 0 ].batch;
@@ -386,6 +712,8 @@ export default class Model {
 			this._currentWriter = null;
 		}
 
+		this.fire( '_afterChanges' );
+
 		return ret;
 	}
 
@@ -400,6 +728,22 @@ export default class Model {
 	 * @param {module:engine/model/writer~Writer} writer `Writer` instance that has been used in the change block.
 	 */
 
+	/**
+	 * Fired when entering the outermost {@link module:engine/model/model~Model#enqueueChange} or
+	 * {@link module:engine/model/model~Model#change} block.
+	 *
+	 * @protected
+	 * @event _beforeChanges
+	 */
+
+	/**
+	 * Fired when leaving the outermost {@link module:engine/model/model~Model#enqueueChange} or
+	 * {@link module:engine/model/model~Model#change} block.
+	 *
+	 * @protected
+	 * @event _afterChanges
+	 */
+
 	/**
 	 * Fired every time any {@link module:engine/model/operation/operation~Operation operation} is applied on the model
 	 * using {@link #applyOperation}.
@@ -427,6 +771,9 @@ export default class Model {
 	 * The {@link #insertContent default action of that method} is implemented as a
 	 * listener to this event so it can be fully customized by the features.
 	 *
+	 * **Note** The `selectable` parameter for the {@link #insertContent} is optional. When `undefined` value is passed the method uses
+	 * `model.document.selection`.
+	 *
 	 * @event insertContent
 	 * @param {Array} args The arguments passed to the original method.
 	 */

+ 1 - 1
packages/ckeditor5-engine/src/model/nodelist.js

@@ -91,7 +91,7 @@ export default class NodeList {
 
 	/**
 	 * Returns the starting offset of given node. Starting offset is equal to the sum of
-	 * {module:engine/model/node~Node#offsetSize offset sizes} of all nodes that are before this node in this node list.
+	 * {@link module:engine/model/node~Node#offsetSize offset sizes} of all nodes that are before this node in this node list.
 	 *
 	 * @param {module:engine/model/node~Node} node Node to look for.
 	 * @returns {Number|null} Node's starting offset.

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

@@ -49,7 +49,7 @@ export default class AttributeOperation extends Operation {
 		 * @readonly
 		 * @member {module:engine/model/range~Range}
 		 */
-		this.range = Range.createFromRange( range );
+		this.range = range.clone();
 
 		/**
 		 * Key of an attribute to change or remove.

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

@@ -8,7 +8,6 @@
  */
 
 import Operation from './operation';
-import Position from '../position';
 import Range from '../range';
 import { _remove } from './utils';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
@@ -36,7 +35,7 @@ export default class DetachOperation extends Operation {
 		 *
 		 * @member {module:engine/model/position~Position} #sourcePosition
 		 */
-		this.sourcePosition = Position.createFromPosition( sourcePosition );
+		this.sourcePosition = sourcePosition.clone();
 
 		/**
 		 * Offset size of moved range.
@@ -82,7 +81,7 @@ export default class DetachOperation extends Operation {
 	 * @inheritDoc
 	 */
 	_execute() {
-		_remove( Range.createFromPositionAndShift( this.sourcePosition, this.howMany ) );
+		_remove( Range._createFromPositionAndShift( this.sourcePosition, this.howMany ) );
 	}
 
 	/**

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

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

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

@@ -41,7 +41,7 @@ export default class MarkerOperation extends Operation {
 		 * @readonly
 		 * @member {module:engine/model/range~Range}
 		 */
-		this.oldRange = oldRange ? Range.createFromRange( oldRange ) : null;
+		this.oldRange = oldRange ? oldRange.clone() : null;
 
 		/**
 		 * Marker range after the change.
@@ -49,7 +49,7 @@ export default class MarkerOperation extends Operation {
 		 * @readonly
 		 * @member {module:engine/model/range~Range}
 		 */
-		this.newRange = newRange ? Range.createFromRange( newRange ) : null;
+		this.newRange = newRange ? newRange.clone() : null;
 
 		/**
 		 * Specifies whether the marker operation affects the data produced by the data pipeline

+ 17 - 6
packages/ckeditor5-engine/src/model/operation/mergeoperation.js

@@ -45,7 +45,7 @@ export default class MergeOperation extends Operation {
 		 *
 		 * @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#sourcePosition
 		 */
-		this.sourcePosition = Position.createFromPosition( sourcePosition );
+		this.sourcePosition = sourcePosition.clone();
 		// This is, and should always remain, the first position in its parent.
 		this.sourcePosition.stickiness = 'toPrevious';
 
@@ -61,7 +61,7 @@ export default class MergeOperation extends Operation {
 		 *
 		 * @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#targetPosition
 		 */
-		this.targetPosition = Position.createFromPosition( targetPosition );
+		this.targetPosition = targetPosition.clone();
 		// Except of a rare scenario in `MergeOperation` x `MergeOperation` transformation,
 		// this is, and should always remain, the last position in its parent.
 		this.targetPosition.stickiness = 'toNext';
@@ -71,7 +71,7 @@ export default class MergeOperation extends Operation {
 		 *
 		 * @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#graveyardPosition
 		 */
-		this.graveyardPosition = Position.createFromPosition( graveyardPosition );
+		this.graveyardPosition = graveyardPosition.clone();
 	}
 
 	/**
@@ -119,7 +119,18 @@ export default class MergeOperation extends Operation {
 	 * @returns {module:engine/model/operation/splitoperation~SplitOperation}
 	 */
 	getReversed() {
-		return new SplitOperation( this.targetPosition, this.howMany, this.graveyardPosition, this.baseVersion + 1 );
+		// Positions in this method are transformed by this merge operation because the split operation bases on
+		// the context after this merge operation happened (because split operation reverses it).
+		// So we need to acknowledge that the merge operation happened and those positions changed a little.
+		const targetPosition = this.targetPosition._getTransformedByMergeOperation( this );
+
+		const path = this.sourcePosition.path.slice( 0, -1 );
+		const insertionPosition = new Position( this.sourcePosition.root, path )._getTransformedByMergeOperation( this );
+
+		const split = new SplitOperation( targetPosition, this.howMany, this.graveyardPosition, this.baseVersion + 1 );
+		split.insertionPosition = insertionPosition;
+
+		return split;
 	}
 
 	/**
@@ -159,10 +170,10 @@ export default class MergeOperation extends Operation {
 	 */
 	_execute() {
 		const mergedElement = this.sourcePosition.parent;
-		const sourceRange = Range.createIn( mergedElement );
+		const sourceRange = Range._createIn( mergedElement );
 
 		_move( sourceRange, this.targetPosition );
-		_move( Range.createOn( mergedElement ), this.graveyardPosition );
+		_move( Range._createOn( mergedElement ), this.graveyardPosition );
 	}
 
 	/**

+ 4 - 3
packages/ckeditor5-engine/src/model/operation/moveoperation.js

@@ -40,7 +40,8 @@ export default class MoveOperation extends Operation {
 		 *
 		 * @member {module:engine/model/position~Position} module:engine/model/operation/moveoperation~MoveOperation#sourcePosition
 		 */
-		this.sourcePosition = Position.createFromPosition( sourcePosition );
+		this.sourcePosition = sourcePosition.clone();
+		// `'toNext'` because `sourcePosition` is a bit like a start of the moved range.
 		this.sourcePosition.stickiness = 'toNext';
 
 		/**
@@ -55,7 +56,7 @@ export default class MoveOperation extends Operation {
 		 *
 		 * @member {module:engine/model/position~Position} module:engine/model/operation/moveoperation~MoveOperation#targetPosition
 		 */
-		this.targetPosition = Position.createFromPosition( targetPosition );
+		this.targetPosition = targetPosition.clone();
 		this.targetPosition.stickiness = 'toNone';
 	}
 
@@ -171,7 +172,7 @@ export default class MoveOperation extends Operation {
 	 * @inheritDoc
 	 */
 	_execute() {
-		_move( Range.createFromPositionAndShift( this.sourcePosition, this.howMany ), this.targetPosition );
+		_move( Range._createFromPositionAndShift( this.sourcePosition, this.howMany ), this.targetPosition );
 	}
 
 	/**

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

@@ -17,8 +17,6 @@ import RenameOperation from '../operation/renameoperation';
 import RootAttributeOperation from '../operation/rootattributeoperation';
 import SplitOperation from '../operation/splitoperation';
 import MergeOperation from '../operation/mergeoperation';
-import WrapOperation from '../operation/wrapoperation';
-import UnwrapOperation from '../operation/unwrapoperation';
 
 const operations = {};
 operations[ AttributeOperation.className ] = AttributeOperation;
@@ -31,8 +29,6 @@ operations[ RenameOperation.className ] = RenameOperation;
 operations[ RootAttributeOperation.className ] = RootAttributeOperation;
 operations[ SplitOperation.className ] = SplitOperation;
 operations[ MergeOperation.className ] = MergeOperation;
-operations[ WrapOperation.className ] = WrapOperation;
-operations[ UnwrapOperation.className ] = UnwrapOperation;
 
 /**
  * A factory class for creating operations.

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

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

+ 57 - 40
packages/ckeditor5-engine/src/model/operation/splitoperation.js

@@ -17,7 +17,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * Operation to split {@link module:engine/model/element~Element an element} at given
- * {@link module:engine/model/operation/splitoperation~SplitOperation#position position} into two elements,
+ * {@link module:engine/model/operation/splitoperation~SplitOperation#splitPosition split position} into two elements,
  * both containing a part of the element's original content.
  *
  * @extends module:engine/model/operation/operation~Operation
@@ -26,25 +26,25 @@ export default class SplitOperation extends Operation {
 	/**
 	 * Creates a split operation.
 	 *
-	 * @param {module:engine/model/position~Position} position Position at which an element should be split.
+	 * @param {module:engine/model/position~Position} splitPosition Position at which an element should be split.
 	 * @param {Number} howMany Total offset size of elements that are in the split element after `position`.
 	 * @param {module:engine/model/position~Position|null} graveyardPosition Position in the graveyard root before the element which
 	 * should be used as a parent of the nodes after `position`. If it is not set, a copy of the the `position` parent will be used.
 	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
 	 * can be applied or `null` if the operation operates on detached (non-document) tree.
 	 */
-	constructor( position, howMany, graveyardPosition, baseVersion ) {
+	constructor( splitPosition, howMany, graveyardPosition, baseVersion ) {
 		super( baseVersion );
 
 		/**
 		 * Position at which an element should be split.
 		 *
-		 * @member {module:engine/model/position~Position} module:engine/model/operation/splitoperation~SplitOperation#position
+		 * @member {module:engine/model/position~Position} module:engine/model/operation/splitoperation~SplitOperation#splitPosition
 		 */
-		this.position = Position.createFromPosition( position );
+		this.splitPosition = splitPosition.clone();
 		// Keep position sticking to the next node. This way any new content added at the place where the element is split
 		// will be left in the original element.
-		this.position.stickiness = 'toNext';
+		this.splitPosition.stickiness = 'toNext';
 
 		/**
 		 * Total offset size of elements that are in the split element after `position`.
@@ -53,6 +53,14 @@ export default class SplitOperation extends Operation {
 		 */
 		this.howMany = howMany;
 
+		/**
+		 * Position at which the clone of split element (or element from graveyard) will be inserted.
+		 *
+		 * @member {module:engine/model/position~Position} module:engine/model/operation/splitoperation~SplitOperation#insertionPosition
+		 */
+		this.insertionPosition = SplitOperation.getInsertionPosition( splitPosition );
+		this.insertionPosition.stickiness = 'toNone';
+
 		/**
 		 * Position in the graveyard root before the element which should be used as a parent of the nodes after `position`.
 		 * If it is not set, a copy of the the `position` parent will be used.
@@ -61,7 +69,7 @@ export default class SplitOperation extends Operation {
 		 *
 		 * @member {module:engine/model/position~Position|null} #graveyardPosition
 		 */
-		this.graveyardPosition = graveyardPosition ? Position.createFromPosition( graveyardPosition ) : null;
+		this.graveyardPosition = graveyardPosition ? graveyardPosition.clone() : null;
 
 		if ( this.graveyardPosition ) {
 			this.graveyardPosition.stickiness = 'toNext';
@@ -75,21 +83,6 @@ export default class SplitOperation extends Operation {
 		return 'split';
 	}
 
-	/**
-	 * Position after the split element.
-	 *
-	 * This is a position at which the clone of split element (or element from graveyard) will be inserted.
-	 *
-	 * @readonly
-	 * @type {module:engine/model/position~Position}
-	 */
-	get insertionPosition() {
-		const path = this.position.path.slice( 0, -1 );
-		path[ path.length - 1 ]++;
-
-		return new Position( this.position.root, path );
-	}
-
 	/**
 	 * Position inside the new clone of a split element.
 	 *
@@ -99,24 +92,23 @@ export default class SplitOperation extends Operation {
 	 * @type {module:engine/model/position~Position}
 	 */
 	get moveTargetPosition() {
-		const path = this.position.path.slice( 0, -1 );
-		path[ path.length - 1 ]++;
+		const path = this.insertionPosition.path.slice();
 		path.push( 0 );
 
-		return new Position( this.position.root, path );
+		return new Position( this.insertionPosition.root, path );
 	}
 
 	/**
 	 * Artificial range that contains all the nodes from the split element that will be moved to the new element.
-	 * The range starts at {@link ~#position} and ends in the same parent, at `POSITIVE_INFINITY` offset.
+	 * The range starts at {@link ~#splitPosition} and ends in the same parent, at `POSITIVE_INFINITY` offset.
 	 *
 	 * @readonly
 	 * @type {module:engine/model/range~Range}
 	 */
 	get movedRange() {
-		const end = this.position.getShiftedBy( Number.POSITIVE_INFINITY );
+		const end = this.splitPosition.getShiftedBy( Number.POSITIVE_INFINITY );
 
-		return new Range( this.position, end );
+		return new Range( this.splitPosition, end );
 	}
 
 	/**
@@ -125,7 +117,10 @@ export default class SplitOperation extends Operation {
 	 * @returns {module:engine/model/operation/splitoperation~SplitOperation} Clone of this operation.
 	 */
 	clone() {
-		return new this.constructor( this.position, this.howMany, this.graveyardPosition, this.baseVersion );
+		const split = new this.constructor( this.splitPosition, this.howMany, this.graveyardPosition, this.baseVersion );
+		split.insertionPosition = this.insertionPosition;
+
+		return split;
 	}
 
 	/**
@@ -134,18 +129,18 @@ export default class SplitOperation extends Operation {
 	 * @returns {module:engine/model/operation/mergeoperation~MergeOperation}
 	 */
 	getReversed() {
-		const graveyard = this.position.root.document.graveyard;
+		const graveyard = this.splitPosition.root.document.graveyard;
 		const graveyardPosition = new Position( graveyard, [ 0 ] );
 
-		return new MergeOperation( this.moveTargetPosition, this.howMany, this.position, graveyardPosition, this.baseVersion + 1 );
+		return new MergeOperation( this.moveTargetPosition, this.howMany, this.splitPosition, graveyardPosition, this.baseVersion + 1 );
 	}
 
 	/**
 	 * @inheritDoc
 	 */
 	_validate() {
-		const element = this.position.parent;
-		const offset = this.position.offset;
+		const element = this.splitPosition.parent;
+		const offset = this.splitPosition.offset;
 
 		// Validate whether split operation has correct parameters.
 		if ( !element || element.maxOffset < offset ) {
@@ -162,7 +157,7 @@ export default class SplitOperation extends Operation {
 			 * @error split-operation-split-in-root
 			 */
 			throw new CKEditorError( 'split-operation-split-in-root: Cannot split root element.' );
-		} else if ( this.howMany != element.maxOffset - this.position.offset ) {
+		} else if ( this.howMany != element.maxOffset - this.splitPosition.offset ) {
 			/**
 			 * Split operation specifies wrong number of nodes to move.
 			 *
@@ -183,17 +178,20 @@ export default class SplitOperation extends Operation {
 	 * @inheritDoc
 	 */
 	_execute() {
-		const splitElement = this.position.parent;
+		const splitElement = this.splitPosition.parent;
 
 		if ( this.graveyardPosition ) {
-			_move( Range.createFromPositionAndShift( this.graveyardPosition, 1 ), this.insertionPosition );
+			_move( Range._createFromPositionAndShift( this.graveyardPosition, 1 ), this.insertionPosition );
 		} else {
 			const newElement = splitElement._clone();
 
 			_insert( this.insertionPosition, newElement );
 		}
 
-		const sourceRange = Range.createFromParentsAndOffsets( splitElement, this.position.offset, splitElement, splitElement.maxOffset );
+		const sourceRange = new Range(
+			Position._createAt( splitElement, this.splitPosition.offset ),
+			Position._createAt( splitElement, splitElement.maxOffset )
+		);
 
 		_move( sourceRange, this.moveTargetPosition );
 	}
@@ -204,7 +202,8 @@ export default class SplitOperation extends Operation {
 	toJSON() {
 		const json = super.toJSON();
 
-		json.position = this.position.toJSON();
+		json.splitPosition = this.splitPosition.toJSON();
+		json.insertionPosition = this.insertionPosition.toJSON();
 
 		if ( this.graveyardPosition ) {
 			json.graveyardPosition = this.graveyardPosition.toJSON();
@@ -220,6 +219,20 @@ export default class SplitOperation extends Operation {
 		return 'SplitOperation';
 	}
 
+	/**
+	 * Helper function that returns a default insertion position basing on given `splitPosition`. The default insertion
+	 * position is after the split element.
+	 *
+	 * @param {module:engine/model/position~Position} splitPosition
+	 * @returns {module:engine/model/position~Position}
+	 */
+	static getInsertionPosition( splitPosition ) {
+		const path = splitPosition.path.slice( 0, -1 );
+		path[ path.length - 1 ]++;
+
+		return new Position( splitPosition.root, path );
+	}
+
 	/**
 	 * Creates `SplitOperation` object from deserilized object, i.e. from parsed JSON string.
 	 *
@@ -228,9 +241,13 @@ export default class SplitOperation extends Operation {
 	 * @returns {module:engine/model/operation/splitoperation~SplitOperation}
 	 */
 	static fromJSON( json, document ) {
-		const position = Position.fromJSON( json.position, document );
+		const splitPosition = Position.fromJSON( json.splitPosition, document );
+		const insertionPosition = Position.fromJSON( json.insertionPosition, document );
 		const graveyardPosition = json.graveyardPosition ? Position.fromJSON( json.graveyardPosition, document ) : null;
 
-		return new this( position, json.howMany, graveyardPosition, json.baseVersion );
+		const split = new this( splitPosition, json.howMany, graveyardPosition, json.baseVersion );
+		split.insertionPosition = insertionPosition;
+
+		return split;
 	}
 }

Разница между файлами не показана из-за своего большого размера
+ 300 - 398
packages/ckeditor5-engine/src/model/operation/transform.js


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

@@ -1,177 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/operation/unwrapoperation
- */
-
-import Operation from './operation';
-import WrapOperation from './wrapoperation';
-import Position from '../position';
-import Range from '../range';
-import { _move } from './utils';
-
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
-/**
- * Operation to unwrap a {@link module:engine/model/element~Element model element}. In the result, the unwrapped element
- * is removed and its children are moved in its place.
- *
- * @extends module:engine/model/operation/operation~Operation
- */
-export default class UnwrapOperation extends Operation {
-	/**
-	 * Creates an unwrap operation.
-	 *
-	 * @param {module:engine/model/position~Position} position Position inside the element to unwrap.
-	 * @param {Number} howMany Total offset size of nodes that are inside unwrapped element.
-	 * @param {module:engine/model/position~Position} graveyardPosition Position in the graveyard root to which
-	 * the unwrapped element will be moved.
-	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
-	 * can be applied or `null` if the operation operates on detached (non-document) tree.
-	 */
-	constructor( position, howMany, graveyardPosition, baseVersion ) {
-		super( baseVersion );
-
-		/**
-		 * Position inside the element to unwrap.
-		 *
-		 * It should be a position at the beginning of that element.
-		 *
-		 * @member {module:engine/model/position~Position} module:engine/model/operation/unwrapoperation~UnwrapOperation#position
-		 */
-		this.position = Position.createFromPosition( position );
-		this.position.stickiness = 'toPrevious'; // Keep the position always at the beginning of the element.
-
-		/**
-		 * Position in the graveyard root to which the unwrapped element will be moved.
-		 *
-		 * @member {module:engine/model/position~Position} module:engine/model/operation/unwrapoperation~UnwrapOperation#graveyardPosition
-		 */
-		this.graveyardPosition = Position.createFromPosition( graveyardPosition );
-
-		/**
-		 * Total offset size of nodes that are inside unwrapped element.
-		 *
-		 * @member {Number} module:engine/model/operation/unwrapoperation~UnwrapOperation#howMany
-		 */
-		this.howMany = howMany;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	get type() {
-		return 'unwrap';
-	}
-
-	/**
-	 * A range containing all nodes that will be unwrapped.
-	 *
-	 * @readonly
-	 * @type {module:engine/model/range~Range}
-	 */
-	get unwrappedRange() {
-		return Range.createFromPositionAndShift( this.position, this.howMany );
-	}
-
-	/**
-	 * A position where the unwrapped nodes will be moved. At the same time, it is the position before the unwrapped element.
-	 *
-	 * @readonly
-	 * @type {module:engine/model/position~Position}
-	 */
-	get targetPosition() {
-		const path = this.position.path.slice( 0, -1 );
-
-		return new Position( this.position.root, path );
-	}
-
-	/**
-	 * Creates and returns an operation that has the same parameters as this operation.
-	 *
-	 * @returns {module:engine/model/operation/unwrapoperation~UnwrapOperation} Clone of this operation.
-	 */
-	clone() {
-		return new this.constructor( this.position, this.howMany, this.graveyardPosition, this.baseVersion );
-	}
-
-	/**
-	 * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
-	 *
-	 * @returns {module:engine/model/operation/wrapoperation~WrapOperation}
-	 */
-	getReversed() {
-		return new WrapOperation( this.targetPosition, this.howMany, this.graveyardPosition, this.baseVersion + 1 );
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	_validate() {
-		const element = this.position.parent;
-
-		// Validate whether unwrap operation has correct parameters.
-		if ( !element || !element.is( 'element' ) ) {
-			/**
-			 * Unwrap position is invalid.
-			 *
-			 * @error unwrap-operation-position-invalid
-			 */
-			throw new CKEditorError( 'unwrap-operation-position-invalid: Unwrap position is invalid.' );
-		} else if ( element.maxOffset !== this.howMany ) {
-			/**
-			 * Operation specifies incorrect number of nodes to unwrap.
-			 *
-			 * @error unwrap-operation-how-many-invalid
-			 */
-			throw new CKEditorError( 'unwrap-operation-how-many-invalid: Operation specifies incorrect number of nodes to unwrap.' );
-		}
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	_execute() {
-		const elementToUnwrap = this.position.parent;
-		const targetPosition = Position.createAfter( elementToUnwrap );
-
-		_move( this.unwrappedRange, targetPosition );
-		_move( Range.createOn( elementToUnwrap ), this.graveyardPosition );
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	toJSON() {
-		const json = super.toJSON();
-
-		json.position = this.position.toJSON();
-		json.graveyardPosition = this.graveyardPosition.toJSON();
-
-		return json;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'UnwrapOperation';
-	}
-
-	/**
-	 * Creates `UnwrapOperation` object from deserilized object, i.e. from parsed JSON string.
-	 *
-	 * @param {Object} json Deserialized JSON object.
-	 * @param {module:engine/model/document~Document} document Document on which this operation will be applied.
-	 * @returns {module:engine/model/operation/unwrapoperation~UnwrapOperation}
-	 */
-	static fromJSON( json, document ) {
-		const position = Position.fromJSON( json.position, document );
-		const graveyardPosition = Position.fromJSON( json.graveyardPosition, document );
-
-		return new this( position, json.howMany, graveyardPosition, json.baseVersion );
-	}
-}

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

@@ -1,227 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/operation/wrapoperation
- */
-
-import Operation from './operation';
-import UnwrapOperation from './unwrapoperation';
-import Position from '../position';
-import Range from '../range';
-import Element from '../element';
-import { _insert, _move } from './utils';
-
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
-/**
- * Operation to wrap a range of {@link module:engine/model/node~Node nodes} with an {@link module:engine/model/element~Element element}.
- *
- * @extends module:engine/model/operation/operation~Operation
- */
-export default class WrapOperation extends Operation {
-	/**
-	 * Creates a wrap operation.
-	 *
-	 * @param {module:engine/model/position~Position} position Position before
-	 * the first {@link module:engine/model/item~Item model item} to wrap.
-	 * @param {Number} howMany Offset size of wrapped range. Wrapped range will start at `position.offset` and end at
-	 * `position.offset + howMany`.
-	 * @param {module:engine/model/element~Element|module:engine/model/position~Position} elementOrPosition Wrapper
-	 * element or position in graveyard before the element which should be used as a wrapper.
-	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
-	 * can be applied or `null` if the operation operates on detached (non-document) tree.
-	 */
-	constructor( position, howMany, elementOrPosition, baseVersion ) {
-		super( baseVersion );
-
-		/**
-		 * Position before the first {@link module:engine/model/node~Node node} to wrap.
-		 *
-		 * @member {module:engine/model/position~Position} module:engine/model/operation/wrapoperation~WrapOperation#position
-		 */
-		this.position = Position.createFromPosition( position );
-		this.position.stickiness = 'toNext';
-
-		/**
-		 * Total offset size of the wrapped range.
-		 *
-		 * Wrapped range will start at `position.offset` and end at `position.offset + howMany`.
-		 *
-		 * @member {Number} module:engine/model/operation/wrapoperation~WrapOperation#howMany
-		 */
-		this.howMany = howMany;
-
-		/**
-		 * Wrapper element that will be used to wrap nodes.
-		 *
-		 * Is `null` if `elementOrPosition` was a {@link module:engine/model/position~Position}.
-		 *
-		 * @member {module:engine/model/element~Element} module:engine/model/operation/wrapoperation~WrapOperation#element
-		 */
-		this.element = elementOrPosition instanceof Element ? elementOrPosition : null;
-
-		/**
-		 * Position in the graveyard root before the element that will be used as a wrapper element.
-		 *
-		 * Is `null` if `elementOrPosition` was a {@link module:engine/model/element~Element}.
-		 *
-		 * @member {module:engine/model/element~Element} module:engine/model/operation/wrapoperation~WrapOperation#graveyardPosition
-		 */
-		this.graveyardPosition = elementOrPosition instanceof Element ? null : Position.createFromPosition( elementOrPosition );
-
-		if ( this.graveyardPosition ) {
-			this.graveyardPosition.stickiness = 'toNext';
-		}
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	get type() {
-		return 'wrap';
-	}
-
-	/**
-	 * Position to which the wrapped elements will be moved. This is a position at the beginning of the wrapping element.
-	 *
-	 * @readonly
-	 * @type {module:engine/model/position~Position}
-	 */
-	get targetPosition() {
-		const path = this.position.path.slice();
-		path.push( 0 );
-
-		return new Position( this.position.root, path );
-	}
-
-	/**
-	 * A range containing all nodes that will be wrapped.
-	 *
-	 * @readonly
-	 * @type {module:engine/model/range~Range}
-	 */
-	get wrappedRange() {
-		return Range.createFromPositionAndShift( this.position, this.howMany );
-	}
-
-	/**
-	 * Creates and returns an operation that has the same parameters as this operation.
-	 *
-	 * @returns {module:engine/model/operation/wrapoperation~WrapOperation} Clone of this operation.
-	 */
-	clone() {
-		const elementOrPosition = this.element ? this.element._clone() : this.graveyardPosition;
-
-		return new this.constructor( this.position, this.howMany, elementOrPosition, this.baseVersion );
-	}
-
-	/**
-	 * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
-	 *
-	 * @returns {module:engine/model/operation/unwrapoperation~UnwrapOperation}
-	 */
-	getReversed() {
-		const graveyard = this.position.root.document.graveyard;
-		const graveyardPosition = new Position( graveyard, [ 0 ] );
-
-		return new UnwrapOperation( this.targetPosition, this.howMany, graveyardPosition, this.baseVersion + 1 );
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	_validate() {
-		const element = this.position.parent;
-
-		// Validate whether wrap operation has correct parameters.
-		if ( !element || this.position.offset > element.maxOffset ) {
-			/**
-			 * Wrap position is invalid.
-			 *
-			 * @error wrap-operation-position-invalid
-			 */
-			throw new CKEditorError( 'wrap-operation-position-invalid: Wrap position is invalid.' );
-		} else if ( this.position.offset + this.howMany > element.maxOffset ) {
-			/**
-			 * Invalid number of nodes to wrap.
-			 *
-			 * @error wrap-operation-how-many-invalid
-			 */
-			throw new CKEditorError( 'wrap-operation-how-many-invalid: Invalid number of nodes to wrap.' );
-		} else if ( this.graveyardPosition && !this.graveyardPosition.nodeAfter ) {
-			/**
-			 * Graveyard position invalid.
-			 *
-			 * @error wrap-operation-graveyard-position-invalid
-			 */
-			throw new CKEditorError( 'wrap-operation-graveyard-position-invalid: Graveyard position invalid.' );
-		}
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	_execute() {
-		const wrappedRange = this.wrappedRange;
-
-		const insertPosition = Position.createFromPosition( wrappedRange.end );
-
-		const targetPath = insertPosition.path.slice();
-		targetPath.push( 0 );
-		const targetPosition = new Position( this.position.root, targetPath );
-
-		if ( this.element ) {
-			const originalElement = this.element;
-			this.element = this.element._clone();
-
-			_insert( insertPosition, originalElement );
-		} else {
-			_move( Range.createFromPositionAndShift( this.graveyardPosition, 1 ), insertPosition );
-		}
-
-		_move( wrappedRange, targetPosition );
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	toJSON() {
-		const json = super.toJSON();
-
-		json.position = this.position.toJSON();
-
-		if ( this.element ) {
-			json.element = this.element.toJSON();
-			delete json.graveyardPosition;
-		} else {
-			json.graveyardPosition = this.graveyardPosition.toJSON();
-			delete json.element;
-		}
-
-		return json;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'WrapOperation';
-	}
-
-	/**
-	 * Creates `WrapOperation` object from deserilized object, i.e. from parsed JSON string.
-	 *
-	 * @param {Object} json Deserialized JSON object.
-	 * @param {module:engine/model/document~Document} document Document on which this operation will be applied.
-	 * @returns {module:engine/model/operation/wrapoperation~WrapOperation}
-	 */
-	static fromJSON( json, document ) {
-		const position = Position.fromJSON( json.position, document );
-		const elementOrPosition = json.element ? Element.fromJSON( json.element ) : Position.fromJSON( json.graveyardPosition, document );
-
-		return new this( position, json.howMany, elementOrPosition, json.baseVersion );
-	}
-}

+ 77 - 147
packages/ckeditor5-engine/src/model/position.js

@@ -16,20 +16,25 @@ import { last } from 'lodash-es';
 /**
  * Represents a position in the model tree.
  *
- * **Note:** Position is based on offsets, not indexes. This means that position in element containing two text nodes
- * with data `foo` and `bar`, position between them has offset `3`, not `1`.
- * See {@link module:engine/model/position~Position#path} for more.
+ * A position is represented by its {@link module:engine/model/position~Position#root} and
+ * a {@link module:engine/model/position~Position#path} in that root.
  *
- * Since position in a model is represented by a {@link module:engine/model/position~Position#root position root} and
- * {@link module:engine/model/position~Position#path position path} it is possible to create positions placed in non-existing elements.
- * This requirement is important for operational transformation.
+ * You can create position instances via its constructor or the `createPosition*()` factory methods of
+ * {@link module:engine/model/model~Model} and {@link module:engine/model/writer~Writer}.
+ *
+ * **Note:** Position is based on offsets, not indexes. This means that a position between two text nodes
+ * `foo` and `bar` has offset `3`, not `1`. See {@link module:engine/model/position~Position#path} for more information.
+ *
+ * Since a position in the model is represented by a {@link module:engine/model/position~Position#root position root} and
+ * {@link module:engine/model/position~Position#path position path} it is possible to create positions placed in non-existing places.
+ * This requirement is important for operational transformation algorithms.
  *
  * Also, {@link module:engine/model/operation/operation~Operation operations}
- * kept in {@link module:engine/model/document~Document#history document history}
+ * kept in the {@link module:engine/model/document~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.
+ * after the document has changed.
  *
- * When changes are applied to model, it may also happen that {@link module:engine/model/position~Position#parent position parent}
+ * When changes are applied to the model, it may also happen that {@link module:engine/model/position~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 module:engine/model/position~Position#parent} and some other properties and methods will throw errors.
  *
@@ -360,7 +365,7 @@ export default class Position {
 	 * @returns {module:engine/model/position~Position} Shifted position.
 	 */
 	getShiftedBy( shift ) {
-		const shifted = Position.createFromPosition( this );
+		const shifted = this.clone();
 
 		const offset = shifted.offset + shift;
 		shifted.offset = offset < 0 ? 0 : offset;
@@ -448,13 +453,13 @@ export default class Position {
 				return true;
 
 			case 'before':
-				left = Position.createFromPosition( this );
-				right = Position.createFromPosition( otherPosition );
+				left = Position._createAt( this );
+				right = Position._createAt( otherPosition );
 				break;
 
 			case 'after':
-				left = Position.createFromPosition( otherPosition );
-				right = Position.createFromPosition( this );
+				left = Position._createAt( otherPosition );
+				right = Position._createAt( this );
 				break;
 
 			default:
@@ -537,14 +542,8 @@ export default class Position {
 			case 'merge':
 				result = this._getTransformedByMergeOperation( operation );
 				break;
-			case 'wrap':
-				result = this._getTransformedByWrapOperation( operation );
-				break;
-			case 'unwrap':
-				result = this._getTransformedByUnwrapOperation( operation );
-				break;
 			default:
-				result = Position.createFromPosition( this );
+				result = Position._createAt( this );
 				break;
 		}
 
@@ -587,7 +586,7 @@ export default class Position {
 			( movedRange.start.isEqual( this ) && this.stickiness == 'toNext' );
 
 		if ( isContained ) {
-			return this._getCombined( operation.position, operation.moveTargetPosition );
+			return this._getCombined( operation.splitPosition, operation.moveTargetPosition );
 		} else {
 			if ( operation.graveyardPosition ) {
 				return this._getTransformedByMove( operation.graveyardPosition, operation.insertionPosition, 1 );
@@ -618,7 +617,7 @@ export default class Position {
 				pos = pos._getTransformedByDeletion( operation.deletionPosition, 1 );
 			}
 		} else if ( this.isEqual( operation.deletionPosition ) ) {
-			pos = Position.createFromPosition( operation.deletionPosition );
+			pos = Position._createAt( operation.deletionPosition );
 		} else {
 			pos = this._getTransformedByMove( operation.deletionPosition, operation.graveyardPosition, 1 );
 		}
@@ -626,69 +625,6 @@ export default class Position {
 		return pos;
 	}
 
-	/**
-	 * Returns a copy of this position transformed by wrap operation.
-	 *
-	 * @protected
-	 * @param {module:engine/model/operation/wrapoperation~WrapOperation} operation
-	 * @returns {module:engine/model/position~Position}
-	 */
-	_getTransformedByWrapOperation( operation ) {
-		const wrappedRange = operation.wrappedRange;
-
-		const isContained = wrappedRange.containsPosition( this ) ||
-			( wrappedRange.start.isEqual( this ) && this.stickiness == 'toNext' ) ||
-			( wrappedRange.end.isEqual( this ) && this.stickiness == 'toPrevious' );
-
-		if ( isContained ) {
-			return this._getCombined( wrappedRange.start, operation.targetPosition );
-		} else if ( this.isEqual( operation.position ) ) {
-			return Position.createFromPosition( this );
-		} else {
-			if ( operation.graveyardPosition ) {
-				const pos = this._getTransformedByMove( operation.graveyardPosition, operation.position, 1 );
-
-				return pos._getTransformedByMove( operation.position.getShiftedBy( 1 ), operation.targetPosition, operation.howMany );
-			} else {
-				return this._getTransformedByDeletion( operation.position, operation.howMany - 1 );
-			}
-		}
-	}
-
-	/**
-	 * Returns a copy of this position transformed by unwrap operation.
-	 *
-	 * @protected
-	 * @param {module:engine/model/operation/unwrapoperation~UnwrapOperation} operation
-	 * @returns {module:engine/model/position~Position}
-	 */
-	_getTransformedByUnwrapOperation( operation ) {
-		const unwrappedRange = operation.unwrappedRange;
-
-		const isContained = unwrappedRange.containsPosition( this ) ||
-			unwrappedRange.start.isEqual( this ) ||
-			unwrappedRange.end.isEqual( this );
-
-		let pos;
-
-		if ( isContained ) {
-			pos = this._getCombined( operation.position, operation.targetPosition );
-		} else if ( this.isEqual( operation.targetPosition ) ) {
-			pos = Position.createFromPosition( this );
-		} else {
-			pos = this._getTransformedByInsertion( operation.targetPosition, operation.howMany );
-		}
-
-		const targetPosition = operation.targetPosition.getShiftedBy( operation.howMany );
-
-		if ( !targetPosition.isEqual( operation.graveyardPosition ) ) {
-			pos = pos._getTransformedByDeletion( targetPosition, 1 );
-			pos = pos._getTransformedByInsertion( operation.graveyardPosition, 1 );
-		}
-
-		return pos;
-	}
-
 	/**
 	 * 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.
@@ -699,7 +635,7 @@ export default class Position {
 	 * @returns {module:engine/model/position~Position|null} Transformed position or `null`.
 	 */
 	_getTransformedByDeletion( deletePosition, howMany ) {
-		const transformed = Position.createFromPosition( this );
+		const transformed = Position._createAt( this );
 
 		// This position can't be affected if deletion was in a different root.
 		if ( this.root != deletePosition.root ) {
@@ -747,7 +683,7 @@ export default class Position {
 	 * @returns {module:engine/model/position~Position} Transformed position.
 	 */
 	_getTransformedByInsertion( insertPosition, howMany ) {
-		const transformed = Position.createFromPosition( this );
+		const transformed = Position._createAt( this );
 
 		// This position can't be affected if insertion was in a different root.
 		if ( this.root != insertPosition.root ) {
@@ -790,7 +726,7 @@ export default class Position {
 
 		if ( sourcePosition.isEqual( targetPosition ) ) {
 			// If `targetPosition` is equal to `sourcePosition` this isn't really any move. Just return position as it is.
-			return Position.createFromPosition( this );
+			return Position._createAt( this );
 		}
 
 		// Moving a range removes nodes from their original position. We acknowledge this by proper transformation.
@@ -820,9 +756,9 @@ export default class Position {
 	 *
 	 * 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 original = model.createPositionFromPath( root, [ 2, 3, 1 ] );
+	 *		let source = model.createPositionFromPath( root, [ 2, 2 ] );
+	 *		let target = model.createPositionFromPath( otherRoot, [ 1, 1, 3 ] );
 	 *		original._getCombined( source, target ); // path is [ 1, 1, 4, 1 ], root is `otherRoot`
 	 *
 	 * Explanation:
@@ -843,7 +779,7 @@ export default class Position {
 		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.
-		const combined = Position.createFromPosition( target );
+		const combined = Position._createAt( target );
 		combined.stickiness = this.stickiness;
 
 		// Then we have to update the rest of the path.
@@ -869,6 +805,15 @@ export default class Position {
 		};
 	}
 
+	/**
+	 * Returns a new position that is equal to current position.
+	 *
+	 * @returns {module:engine/model/position~Position}
+	 */
+	clone() {
+		return new this.constructor( this.root, this.path, this.stickiness );
+	}
+
 	/**
 	 * Creates position at the given location. The location can be specified as:
 	 *
@@ -877,34 +822,54 @@ export default class Position {
 	 * * parent element and `'end'` (sets position at the end of that element),
 	 * * {@link module:engine/model/item~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:
+	 * This method is a shortcut to other factory methods such as:
 	 *
-	 * * {@link module:engine/model/position~Position.createBefore},
-	 * * {@link module:engine/model/position~Position.createAfter},
-	 * * {@link module:engine/model/position~Position.createFromParentAndOffset},
-	 * * {@link module:engine/model/position~Position.createFromPosition}.
+	 * * {@link module:engine/model/position~Position._createBefore},
+	 * * {@link module:engine/model/position~Position._createAfter}.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/model/item~Item model item}.
+	 * @protected
 	 */
-	static createAt( itemOrPosition, offset ) {
+	static _createAt( itemOrPosition, offset ) {
 		if ( itemOrPosition instanceof Position ) {
-			return this.createFromPosition( itemOrPosition );
+			return new Position( itemOrPosition.root, itemOrPosition.path, itemOrPosition.stickiness );
 		} else {
 			const node = itemOrPosition;
 
 			if ( offset == 'end' ) {
 				offset = node.maxOffset;
 			} else if ( offset == 'before' ) {
-				return this.createBefore( node );
+				return this._createBefore( node );
 			} else if ( offset == 'after' ) {
-				return this.createAfter( node );
-			} else if ( !offset ) {
-				offset = 0;
+				return this._createAfter( node );
+			} else if ( offset !== 0 && !offset ) {
+				/**
+				 * {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}
+				 * requires the offset to be specified when the first parameter is a model item.
+				 *
+				 * @error model-createPositionAt-offset-required
+				 */
+				throw new CKEditorError(
+					'model-createPositionAt-offset-required: ' +
+					'Model#createPositionAt() requires the offset when the first parameter is a model item.' );
+			}
+
+			if ( !node.is( 'element' ) && !node.is( 'documentFragment' ) ) {
+				/**
+				 * Position parent have to be a model element or model document fragment.
+				 *
+				 * @error model-position-parent-incorrect
+				 */
+				throw new CKEditorError( 'model-position-parent-incorrect: Position parent have to be a element or document fragment.' );
 			}
 
-			return this.createFromParentAndOffset( node, offset );
+			const path = node.getPath();
+
+			path.push( offset );
+
+			return new this( node.root, path );
 		}
 	}
 
@@ -913,8 +878,9 @@ export default class Position {
 	 *
 	 * @param {module:engine/model/item~Item} item Item after which the position should be placed.
 	 * @returns {module:engine/model/position~Position}
+	 * @protected
 	 */
-	static createAfter( item ) {
+	static _createAfter( item ) {
 		if ( !item.parent ) {
 			/**
 			 * You can not make a position after a root element.
@@ -925,7 +891,7 @@ export default class Position {
 			throw new CKEditorError( 'model-position-after-root: You cannot make a position after root.', { root: item } );
 		}
 
-		return this.createFromParentAndOffset( item.parent, item.endOffset );
+		return this._createAt( item.parent, item.endOffset );
 	}
 
 	/**
@@ -933,8 +899,9 @@ export default class Position {
 	 *
 	 * @param {module:engine/model/item~Item} item Item before which the position should be placed.
 	 * @returns {module:engine/model/position~Position}
+	 * @protected
 	 */
-	static createBefore( item ) {
+	static _createBefore( item ) {
 		if ( !item.parent ) {
 			/**
 			 * You can not make a position before a root element.
@@ -945,44 +912,7 @@ export default class Position {
 			throw new CKEditorError( 'model-position-before-root: You cannot make a position before root.', { root: item } );
 		}
 
-		return this.createFromParentAndOffset( item.parent, item.startOffset );
-	}
-
-	/**
-	 * Creates a new position from the parent element and an offset in that element.
-	 *
-	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent Position's parent.
-	 * @param {Number} offset Position's offset.
-	 * @returns {module:engine/model/position~Position}
-	 */
-	static createFromParentAndOffset( parent, offset ) {
-		if ( !parent.is( 'element' ) && !parent.is( 'documentFragment' ) ) {
-			/**
-			 * Position parent have to be a model element or model document fragment.
-			 *
-			 * @error model-position-parent-incorrect
-			 */
-			throw new CKEditorError( 'model-position-parent-incorrect: Position parent have to be a element or document fragment.' );
-		}
-
-		const path = parent.getPath();
-
-		path.push( offset );
-
-		return new this( parent.root, path );
-	}
-
-	/**
-	 * Creates a new position, which is equal to passed position.
-	 *
-	 * @param {module:engine/model/position~Position} position Position to be cloned.
-	 * @returns {module:engine/model/position~Position}
-	 */
-	static createFromPosition( position ) {
-		const newPos = new this( position.root, position.path.slice() );
-		newPos.stickiness = position.stickiness;
-
-		return newPos;
+		return this._createAt( item.parent, item.startOffset );
 	}
 
 	/**

+ 79 - 118
packages/ckeditor5-engine/src/model/range.js

@@ -13,14 +13,18 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
 
 /**
- * Range class. Range is iterable.
+ * Represents a range in the model tree.
+ *
+ * A range is defined by its {@link module:engine/model/range~Range#start} and {@link module:engine/model/range~Range#end}
+ * positions.
+ *
+ * You can create range instances via its constructor or the `createRange*()` factory methods of
+ * {@link module:engine/model/model~Model} and {@link module:engine/model/writer~Writer}.
  */
 export default class Range {
 	/**
 	 * Creates a range spanning from `start` position to `end` position.
 	 *
-	 * **Note:** Constructor creates it's own {@link module:engine/model/position~Position Position} instances basing on passed values.
-	 *
 	 * @param {module:engine/model/position~Position} start Start position.
 	 * @param {module:engine/model/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
 	 */
@@ -31,7 +35,7 @@ export default class Range {
 		 * @readonly
 		 * @member {module:engine/model/position~Position}
 		 */
-		this.start = Position.createFromPosition( start );
+		this.start = Position._createAt( start );
 
 		/**
 		 * End position.
@@ -39,7 +43,7 @@ export default class Range {
 		 * @readonly
 		 * @member {module:engine/model/position~Position}
 		 */
-		this.end = end ? Position.createFromPosition( end ) : Position.createFromPosition( start );
+		this.end = end ? Position._createAt( end ) : Position._createAt( start );
 
 		// If the range is collapsed, treat in a similar way as a position and set its boundaries stickiness to 'toNone'.
 		// In other case, make the boundaries stick to the "inside" of the range.
@@ -134,7 +138,7 @@ export default class Range {
 	 * @param {module:engine/model/item~Item} item Model item to check.
 	 */
 	containsItem( item ) {
-		const pos = Position.createBefore( item );
+		const pos = Position._createBefore( item );
 
 		return this.containsPosition( pos ) || this.start.isEqual( pos );
 	}
@@ -165,16 +169,19 @@ export default class Range {
 	 *
 	 * Examples:
 	 *
-	 *		let range = new Range( new Position( root, [ 2, 7 ] ), new Position( root, [ 4, 0, 1 ] ) );
-	 *		let otherRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 5 ] ) );
+	 *		let range = model.createRange(
+	 *			model.createPositionFromPath( root, [ 2, 7 ] ),
+	 *			model.createPositionFromPath( root, [ 4, 0, 1 ] )
+	 *		);
+	 *		let otherRange = model.createRange( model.createPositionFromPath( root, [ 1 ] ), model.createPositionFromPath( root, [ 5 ] ) );
 	 *		let transformed = range.getDifference( otherRange );
 	 *		// transformed array has no ranges because `otherRange` contains `range`
 	 *
-	 *		otherRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 3 ] ) );
+	 *		otherRange = model.createRange( model.createPositionFromPath( root, [ 1 ] ), model.createPositionFromPath( root, [ 3 ] ) );
 	 *		transformed = range.getDifference( otherRange );
 	 *		// transformed array has one range: from [ 3 ] to [ 4, 0, 1 ]
 	 *
-	 *		otherRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 4 ] ) );
+	 *		otherRange = model.createRange( model.createPositionFromPath( root, [ 3 ] ), model.createPositionFromPath( root, [ 4 ] ) );
 	 *		transformed = range.getDifference( otherRange );
 	 *		// transformed array has two ranges: from [ 2, 7 ] to [ 3 ] and from [ 4 ] to [ 4, 0, 1 ]
 	 *
@@ -200,7 +207,7 @@ export default class Range {
 			}
 		} else {
 			// Ranges do not intersect, return the original range.
-			ranges.push( Range.createFromRange( this ) );
+			ranges.push( new Range( this.start, this.end ) );
 		}
 
 		return ranges;
@@ -212,11 +219,14 @@ export default class Range {
 	 *
 	 * Examples:
 	 *
-	 *		let range = new Range( new Position( root, [ 2, 7 ] ), new Position( root, [ 4, 0, 1 ] ) );
-	 *		let otherRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
+	 *		let range = model.createRange(
+	 *			model.createPositionFromPath( root, [ 2, 7 ] ),
+	 *			model.createPositionFromPath( root, [ 4, 0, 1 ] )
+	 *		);
+	 *		let otherRange = model.createRange( model.createPositionFromPath( root, [ 1 ] ), model.createPositionFromPath( root, [ 2 ] ) );
 	 *		let transformed = range.getIntersection( otherRange ); // null - ranges have no common part
 	 *
-	 *		otherRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 5 ] ) );
+	 *		otherRange = model.createRange( model.createPositionFromPath( root, [ 3 ] ), model.createPositionFromPath( root, [ 5 ] ) );
 	 *		transformed = range.getIntersection( otherRange ); // range from [ 3 ] to [ 4, 0, 1 ]
 	 *
 	 * @param {module:engine/model/range~Range} otherRange Range to check for intersection.
@@ -291,7 +301,7 @@ export default class Range {
 		const ranges = [];
 		const diffAt = this.start.getCommonPath( this.end ).length;
 
-		const pos = Position.createFromPosition( this.start );
+		const pos = Position._createAt( this.start );
 		let posParent = pos.parent;
 
 		// Go up.
@@ -411,13 +421,9 @@ export default class Range {
 				return [ this._getTransformedBySplitOperation( operation ) ];
 			case 'merge':
 				return [ this._getTransformedByMergeOperation( operation ) ];
-			case 'wrap':
-				return [ this._getTransformedByWrapOperation( operation ) ];
-			case 'unwrap':
-				return [ this._getTransformedByUnwrapOperation( operation ) ];
 		}
 
-		return [ Range.createFromRange( this ) ];
+		return [ new Range( this.start, this.end ) ];
 	}
 
 	/**
@@ -428,7 +434,7 @@ export default class Range {
 	 * @returns {Array.<module:engine/model/range~Range>} Range which is the result of transformation.
 	 */
 	getTransformedByOperations( operations ) {
-		const ranges = [ Range.createFromRange( this ) ];
+		const ranges = [ new Range( this.start, this.end ) ];
 
 		for ( const operation of operations ) {
 			for ( let i = 0; i < ranges.length; i++ ) {
@@ -480,6 +486,15 @@ export default class Range {
 		};
 	}
 
+	/**
+	 * Returns a new range that is equal to current range.
+	 *
+	 * @returns {module:engine/model/range~Range}
+	 */
+	clone() {
+		return new this.constructor( this.start, this.end );
+	}
+
 	/**
 	 * Returns a result of transforming a copy of this range by insert operation.
 	 *
@@ -521,7 +536,18 @@ export default class Range {
 	 */
 	_getTransformedBySplitOperation( operation ) {
 		const start = this.start._getTransformedBySplitOperation( operation );
-		const end = this.end._getTransformedBySplitOperation( operation );
+		let end = this.end._getTransformedBySplitOperation( operation );
+
+		if ( this.end.isEqual( operation.insertionPosition ) ) {
+			end = this.end.getShiftedBy( 1 );
+		}
+
+		// Below may happen when range contains graveyard element used by split operation.
+		if ( start.root != end.root ) {
+			// End position was next to the moved graveyard element and was moved with it.
+			// Fix it by using old `end` which has proper `root`.
+			end = this.end.getShiftedBy( -1 );
+		}
 
 		return new Range( start, end );
 	}
@@ -575,7 +601,7 @@ export default class Range {
 
 			if ( operation.sourcePosition.isBefore( operation.targetPosition ) ) {
 				// Case 1.
-				start = Position.createFromPosition( end );
+				start = Position._createAt( end );
 				start.offset = 0;
 			} else {
 				if ( !operation.deletionPosition.isEqual( start ) ) {
@@ -593,38 +619,6 @@ export default class Range {
 		return new Range( start, end );
 	}
 
-	/**
-	 * Returns a result of transforming a copy of this range by wrap operation.
-	 *
-	 * Always one range is returned. The transformation is done in a way to not break the range.
-	 *
-	 * @protected
-	 * @param {module:engine/model/operation/wrapoperation~WrapOperation} operation
-	 * @returns {module:engine/model/range~Range}
-	 */
-	_getTransformedByWrapOperation( operation ) {
-		const start = this.start._getTransformedByWrapOperation( operation );
-		const end = this.end._getTransformedByWrapOperation( operation );
-
-		return new Range( start, end );
-	}
-
-	/**
-	 * Returns a result of transforming a copy of this range by unwrap operation.
-	 *
-	 * Always one range is returned. The transformation is done in a way to not break the range.
-	 *
-	 * @protected
-	 * @param {module:engine/model/operation/unwrapoperation~UnwrapOperation} operation
-	 * @returns {module:engine/model/range~Range}
-	 */
-	_getTransformedByUnwrapOperation( operation ) {
-		const start = this.start._getTransformedByUnwrapOperation( operation );
-		const end = this.end._getTransformedByUnwrapOperation( operation );
-
-		return new Range( start, end );
-	}
-
 	/**
 	 * Returns an array containing one or two {@link ~Range ranges} that are a result of transforming this
 	 * {@link ~Range range} by inserting `howMany` nodes at `insertPosition`. Two {@link ~Range ranges} are
@@ -632,17 +626,20 @@ export default class Range {
 	 *
 	 * 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 range = model.createRange(
+	 *			model.createPositionFromPath( root, [ 2, 7 ] ),
+	 *			model.createPositionFromPath( root, [ 4, 0, 1 ] )
+	 *		);
+	 *		let transformed = range._getTransformedByInsertion( model.createPositionFromPath( 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( model.createPositionFromPath( 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( model.createPositionFromPath( 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( model.createPositionFromPath( root, [ 3, 2 ] ), 4, true );
 	 *		// transformed array has two ranges: from [ 2, 7 ] to [ 3, 2 ] and from [ 3, 6 ] to [ 4, 0, 1 ]
 	 *
 	 * @protected
@@ -666,7 +663,7 @@ export default class Range {
 				)
 			];
 		} else {
-			const range = Range.createFromRange( this );
+			const range = new Range( this.start, this.end );
 
 			range.start = range.start._getTransformedByInsertion( insertPosition, howMany );
 			range.end = range.end._getTransformedByInsertion( insertPosition, howMany );
@@ -706,7 +703,7 @@ export default class Range {
 		// <div><p>ab</p><p>c[d</p></div><p>e]f</p> --> <div><p>ab</p>{</div>}<p>c[d</p><p>e]f</p>
 		//
 		// This special case is applied only if the range is to be kept together (not spread).
-		const moveRange = Range.createFromPositionAndShift( sourcePosition, howMany );
+		const moveRange = Range._createFromPositionAndShift( sourcePosition, howMany );
 		const insertPosition = targetPosition._getTransformedByDeletion( sourcePosition, howMany );
 
 		if ( this.containsPosition( targetPosition ) && !spread ) {
@@ -797,77 +794,39 @@ export default class Range {
 	 * Creates a new range, spreading from specified {@link module:engine/model/position~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.
 	 *
+	 * @protected
 	 * @param {module:engine/model/position~Position} position Beginning of the range.
 	 * @param {Number} shift How long the range should be.
 	 * @returns {module:engine/model/range~Range}
 	 */
-	static createFromPositionAndShift( position, shift ) {
+	static _createFromPositionAndShift( position, shift ) {
 		const start = position;
 		const end = position.getShiftedBy( shift );
 
 		return shift > 0 ? new this( start, end ) : new this( end, start );
 	}
 
-	/**
-	 * Creates a range from given parents and offsets.
-	 *
-	 * @param {module:engine/model/element~Element} startElement Start position parent element.
-	 * @param {Number} startOffset Start position offset.
-	 * @param {module:engine/model/element~Element} endElement End position parent element.
-	 * @param {Number} endOffset End position offset.
-	 * @returns {module:engine/model/range~Range}
-	 */
-	static createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
-		return new this(
-			Position.createFromParentAndOffset( startElement, startOffset ),
-			Position.createFromParentAndOffset( endElement, endOffset )
-		);
-	}
-
-	/**
-	 * Creates a new instance of `Range` which is equal to passed range.
-	 *
-	 * @param {module:engine/model/range~Range} range Range to clone.
-	 * @returns {module:engine/model/range~Range}
-	 */
-	static createFromRange( range ) {
-		return new this( range.start, range.end );
-	}
-
 	/**
 	 * Creates a range inside an {@link module:engine/model/element~Element element} which starts before the first child of
 	 * that element and ends after the last child of that element.
 	 *
+	 * @protected
 	 * @param {module:engine/model/element~Element} element Element which is a parent for the range.
 	 * @returns {module:engine/model/range~Range}
 	 */
-	static createIn( element ) {
-		return this.createFromParentsAndOffsets( element, 0, element, element.maxOffset );
+	static _createIn( element ) {
+		return new this( Position._createAt( element, 0 ), Position._createAt( element, element.maxOffset ) );
 	}
 
 	/**
 	 * Creates a range that starts before given {@link module:engine/model/item~Item model item} and ends after it.
 	 *
+	 * @protected
 	 * @param {module:engine/model/item~Item} item
 	 * @returns {module:engine/model/range~Range}
 	 */
-	static createOn( item ) {
-		return this.createFromPositionAndShift( Position.createBefore( item ), item.offsetSize );
-	}
-
-	/**
-	 * Creates a collapsed range at given {@link module:engine/model/position~Position position}
-	 * or on the given {@link module:engine/model/item~Item item}.
-	 *
-	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
-	 * first parameter is a {@link module:engine/model/item~Item model item}.
-	 */
-	static createCollapsedAt( itemOrPosition, offset ) {
-		const start = Position.createAt( itemOrPosition, offset );
-		const end = Position.createFromPosition( start );
-
-		return new Range( start, end );
+	static _createOn( item ) {
+		return this._createFromPositionAndShift( Position._createBefore( item ), item.offsetSize );
 	}
 
 	/**
@@ -885,17 +844,17 @@ export default class Range {
 	 * @param {Array.<module:engine/model/range~Range>} ranges Ranges to combine.
 	 * @returns {module:engine/model/range~Range} Combined range.
 	 */
-	static createFromRanges( ranges ) {
+	static _createFromRanges( ranges ) {
 		if ( ranges.length === 0 ) {
 			/**
 			 * At least one range has to be passed to
-			 * {@link module:engine/model/range~Range.createFromRanges `Range.createFromRanges()`}.
+			 * {@link module:engine/model/range~Range._createFromRanges `Range._createFromRanges()`}.
 			 *
 			 * @error range-create-from-ranges-empty-array
 			 */
 			throw new CKEditorError( 'range-create-from-ranges-empty-array: At least one range has to be passed.' );
 		} else if ( ranges.length == 1 ) {
-			return this.createFromRange( ranges[ 0 ] );
+			return ranges[ 0 ].clone();
 		}
 
 		// 1. Set the first range in `ranges` array as a reference range.
@@ -918,12 +877,14 @@ export default class Range {
 
 		// 5. Ranges should be checked and glued starting from the range that is closest to the reference range.
 		// Since ranges are sorted, start with the range with index that is closest to reference range index.
-		for ( let i = refIndex - 1; i >= 0; i++ ) {
-			if ( ranges[ i ].end.isEqual( result.start ) ) {
-				result.start = Position.createFromPosition( ranges[ i ].start );
-			} else {
-				// If ranges are not starting/ending at the same position there is no point in looking further.
-				break;
+		if ( refIndex > 0 ) {
+			for ( let i = refIndex - 1; true; i++ ) {
+				if ( ranges[ i ].end.isEqual( result.start ) ) {
+					result.start = Position._createAt( ranges[ i ].start );
+				} else {
+					// If ranges are not starting/ending at the same position there is no point in looking further.
+					break;
+				}
 			}
 		}
 
@@ -931,7 +892,7 @@ export default class Range {
 		// Since ranges are sorted, start with the range with index that is closest to reference range index.
 		for ( let i = refIndex + 1; i < ranges.length; i++ ) {
 			if ( ranges[ i ].start.isEqual( result.end ) ) {
-				result.end = Position.createFromPosition( ranges[ i ].end );
+				result.end = Position._createAt( ranges[ i ].end );
 			} else {
 				// If ranges are not starting/ending at the same position there is no point in looking further.
 				break;

+ 29 - 163
packages/ckeditor5-engine/src/model/schema.js

@@ -14,6 +14,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import Range from './range';
 import Position from './position';
 import Element from './element';
+import Text from './text';
 import TreeWalker from './treewalker';
 
 /**
@@ -23,163 +24,11 @@ import TreeWalker from './treewalker';
  *
  * The instance of schema is available in {@link module:engine/model/model~Model#schema `editor.model.schema`}.
  *
- * # Schema definitions
+ * Read more about the schema in:
  *
- * Schema defines allowed model structures and allowed attributes separately. They are also checked separately
- * by using the {@link ~Schema#checkChild} and {@link ~Schema#checkAttribute} methods.
- *
- * ## Defining allowed structures
- *
- * When a feature introduces a model element it should register it in the schema. Besides
- * defining that such an element may exist in the model, the feature also needs to define where
- * this element may be placed:
- *
- *		schema.register( 'myElement', {
- *			allowIn: '$root'
- *		} );
- *
- * This lets the schema know that `<myElement>` may be a child of the `<$root>` element. `$root` is one of generic
- * nodes defined by the editing framework. By default, the editor names the main root element a `<$root>`,
- * so the above definition allows `<myElement>` in the main editor element.
- *
- * In other words, this would be correct:
- *
- *		<$root><myElement></myElement></$root>
- *
- * While this would not be correct:
- *
- *		<$root><foo><myElement></myElement></foo></$root>
- *
- * ## Generic items
- *
- * There are three basic generic items: `$root`, `$block` and `$text`.
- * They are defined as follows:
- *
- *		this.schema.register( '$root', {
- *			isLimit: true
- *		} );
- *		this.schema.register( '$block', {
- *			allowIn: '$root',
- *			isBlock: true
- *		} );
- *		this.schema.register( '$text', {
- *			allowIn: '$block'
- *		} );
- *
- * These definitions can then be reused by features to create their own definitions in a more extensible way.
- * For example, the {@link module:paragraph/paragraph~Paragraph} feature will define its item as:
- *
- *		schema.register( 'paragraph', {
- *			inheritAllFrom: '$block'
- *		} );
- *
- * Which translates to:
- *
- *		schema.register( 'paragraph', {
- *			allowWhere: '$block',
- *			allowContentOf: '$block',
- *			allowAttributesOf: '$block',
- *			inheritTypesFrom: '$block'
- *		} );
- *
- * Which can be read as:
- *
- * * The `<paragraph>` element will be allowed in elements in which `<$block>` is allowed (e.g. in `<$root>`).
- * * The `<paragraph>` element will allow all nodes which are allowed in `<$block>` (e.g. `$text`).
- * * The `<paragraph>` element will allow all attributes allowed on `<$block>`.
- * * The `<paragraph>` element will inherit all `is*` properties of `<$block>` (e.g. `isBlock`).
- *
- * Thanks to the fact that `<paragraph>`'s definition is inherited from `<$block>` other features can use the `<$block>`
- * type to indirectly extend `<paragraph>`'s definition. For example, the {@link module:block-quote/blockquote~BlockQuote}
- * feature does this:
- *
- *		schema.register( 'blockQuote', {
- *			allowWhere: '$block',
- *			allowContentOf: '$root'
- *		} );
- *
- * Thanks to that, despite the fact that block quote and paragraph features know nothing about themselves, paragraphs
- * will be allowed in block quotes and block quotes will be allowed in all places where blocks are. So if anyone will
- * register a `<section>` element (with `allowContentOf: '$root'` rule), that `<section>` elements will allow
- * block quotes too.
- *
- * The side effect of such a definition inheritance is that now `<blockQuote>` is allowed in `<blockQuote>` which needs to be
- * resolved by a callback which will disallow this specific structure.
- *
- * You can read more about the format of an item definition in {@link module:engine/model/schema~SchemaItemDefinition}.
- *
- * ## Defining advanced rules in `checkChild()`'s callbacks
- *
- * The {@link ~Schema#checkChild} method which is the base method used to check whether some element is allowed in a given structure
- * is {@link module:utils/observablemixin~ObservableMixin#decorate a decorated method}.
- * It means that you can add listeners to implement your specific rules which are not limited by the declarative
- * {@link module:engine/model/schema~SchemaItemDefinition API}.
- *
- * Those listeners can be added either by listening directly to the {@link ~Schema#event:checkChild} event or
- * by using the handy {@link ~Schema#addChildCheck} method.
- *
- * For instance, the block quote feature defines such a listener to disallow nested `<blockQuote>` structures:
- *
- *		schema.addChildCheck( context, childDefinition ) => {
- *			// Note that context is automatically normalized to SchemaContext instance and
- *			// child to its definition (SchemaCompiledItemDefinition).
- *
- *			// If checkChild() is called with a context that ends with blockQuote and blockQuote as a child
- *			// to check, make the checkChild() method return false.
- *			if ( context.endsWith( 'blockQuote' ) && childDefinition.name == 'blockQuote' ) {
- *				return false;
- *			}
- *		} );
- *
- * ## Defining attributes
- *
- * TODO
- *
- * ## Implementing additional constraints
- *
- * Schema's capabilities were limited to simple (and atomic) {@link ~Schema#checkChild} and
- * {@link ~Schema#checkAttribute} checks on purpose.
- * One may imagine that schema should support defining more complex rules such as
- * "element `<x>` must be always followed by `<y>`".
- * While it is feasible to create an API which would enable feeding the schema with such definitions,
- * it is unfortunately unrealistic to then expect that every editing feature will consider those rules when processing the model.
- * It is also unrealistic to expect that it will be done automatically by the schema and the editing engine themselves.
- *
- * For instance, let's get back to the "element `<x>` must be always followed by `<y>`" rule and this initial content:
- *
- *		<$root>
- *			<x>foo</x>
- *			<y>bar[bom</y>
- *			<z>bom]bar</z>
- *		</$root>
- *
- * Now, imagine that the user presses the "block quote" button. Usually it would wrap the two selected blocks
- * (`<y>` and `<z>`) with a `<blockQuote>` element:
- *
- *		<$root>
- *			<x>foo</x>
- *			<blockQuote>
- *				<y>bar[bom</y>
- *				<z>bom]bar</z>
- *			</blockQuote>
- *		</$root>
- *
- * But it turns out that this creates an incorrect structure – `<x>` is not followed by `<y>` anymore.
- *
- * What should happen instead? There are at least 4 possible solutions: the block quote feature should not be
- * applicable in such a context, someone should create a new `<y>` right after `<x>`, `<x>` should be moved
- * inside `<blockQuote>` together with `<y>` or vice versa.
- *
- * While this is a relatively simple scenario (unlike most real-time collaboration scenarios),
- * it turns out that it's already hard to say what should happen and who should react to fix this content.
- *
- * Therefore, if your editor needs to implement such rules, you should do that through model's post-fixers
- * fixing incorrect content or actively prevent such situations (e.g. by disabling certain features).
- * It means that those constraints will be defined specifically for your scenario by your code which
- * makes their implementation much easier.
- *
- * So the answer for who and how should implement additional constraints is your features or your editor
- * through CKEditor 5's rich and open API.
+ * * {@glink framework/guides/architecture/editing-engine#schema "Schema"} section of the
+ * {@glink framework/guides/architecture/editing-engine Introduction to the "Editing engine architecture"}.
+ * * {@glink framework/guides/deep-dive/schema "Schema" deep dive} guide.
  *
  * @mixes module:utils/observablemixin~ObservableMixin
  */
@@ -640,8 +489,14 @@ export default class Schema {
 	 */
 	checkAttributeInSelection( selection, attribute ) {
 		if ( selection.isCollapsed ) {
+			const firstPosition = selection.getFirstPosition();
+			const context = [
+				...firstPosition.getAncestors(),
+				new Text( '', selection.getAttributes() )
+			];
+
 			// Check whether schema allows for a text with the attribute in the selection.
-			return this.checkAttribute( [ ...selection.getFirstPosition().getAncestors(), '$text' ], attribute );
+			return this.checkAttribute( context, attribute );
 		} else {
 			const ranges = selection.getRanges();
 
@@ -692,7 +547,7 @@ export default class Schema {
 
 		for ( const item of range.getItems( { shallow: true } ) ) {
 			if ( item.is( 'element' ) ) {
-				yield* this._getValidRangesForRange( Range.createIn( item ), attribute );
+				yield* this._getValidRangesForRange( Range._createIn( item ), attribute );
 			}
 
 			if ( !this.checkAttribute( item, attribute ) ) {
@@ -700,10 +555,10 @@ export default class Schema {
 					yield new Range( start, end );
 				}
 
-				start = Position.createAfter( item );
+				start = Position._createAfter( item );
 			}
 
-			end = Position.createAfter( item );
+			end = Position._createAfter( item );
 		}
 
 		if ( !start.isEqual( end ) ) {
@@ -752,7 +607,7 @@ export default class Schema {
 			const value = data.value;
 
 			if ( value.type == type && this.isObject( value.item ) ) {
-				return Range.createOn( value.item );
+				return Range._createOn( value.item );
 			}
 
 			if ( this.checkChild( value.nextPosition, '$text' ) ) {
@@ -812,6 +667,16 @@ export default class Schema {
 		}
 	}
 
+	/**
+	 * Creates an instance of the schema context.
+	 *
+	 * @param {module:engine/model/schema~SchemaContextDefinition} context
+	 * @returns {module:engine/model/schema~SchemaContext}
+	 */
+	createContext( context ) {
+		return new SchemaContext( context );
+	}
+
 	/**
 	 * @private
 	 */
@@ -1229,7 +1094,8 @@ export class SchemaContext {
 	 * 		// A string (element name).
 	 * 		const newContext = context.push( 'barElement' ); // [ '$root', 'barElement' ]
 	 *
-	 * **Note** {module:engine/model/node~Node} that is already in the model tree will be added as the only item (without ancestors).
+	 * **Note** {@link module:engine/model/node~Node} that is already in the model tree will be added as the only item
+	 * (without ancestors).
 	 *
 	 * @param {String|module:engine/model/node~Node|Array<String|module:engine/model/node~Node>} item An item that will be added
 	 * to the current context.
@@ -1306,7 +1172,7 @@ export class SchemaContext {
  *		schema.checkChild( contextDefinition, childToCheck );
  *
  *		// Also check in [ rootElement, blockQuoteElement, paragraphElement ].
- *		schema.checkChild( Position.createAt( paragraphElement ), 'foo' );
+ *		schema.checkChild( model.createPositionAt( paragraphElement, 0 ), 'foo' );
  *
  *		// Check in [ rootElement, paragraphElement ].
  *		schema.checkChild( [ rootElement, paragraphElement ], 'foo' );

+ 87 - 77
packages/ckeditor5-engine/src/model/selection.js

@@ -17,61 +17,65 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 
 /**
- * `Selection` is a group of {@link module:engine/model/range~Range ranges} which has a direction specified by
- * {@link module:engine/model/selection~Selection#anchor anchor} and {@link module:engine/model/selection~Selection#focus focus}.
- * Additionally, `Selection` may have it's own attributes.
+ * Selection is a set of {@link module:engine/model/range~Range ranges}. It has a direction specified by its
+ * {@link module:engine/model/selection~Selection#anchor anchor} and {@link module:engine/model/selection~Selection#focus focus}
+ * (it can be {@link module:engine/model/selection~Selection#isBackward forward or backward}).
+ * Additionally, selection may have its own attributes (think – whether text typed in in this selection
+ * should have those attributes – e.g. whether you type a bolded text).
  *
- * @mixes {module:utils/emittermixin~EmitterMixin}
+ * @mixes module:utils/emittermixin~EmitterMixin
  */
 export default class Selection {
 	/**
-	 * Creates new selection instance on the given
-	 * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
-	 * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
-	 * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges}
-	 * or creates an empty selection if no arguments passed.
+	 * Creates a new selection instance
+	 * based on the given {@link module:engine/model/selection~Selection selection},
+	 * or based on the given {@link module:engine/model/range~Range range},
+	 * or based on an iterable collection of {@link module:engine/model/range~Range ranges}
+	 * or at the given {@link module:engine/model/position~Position position},
+	 * or on the given {@link module:engine/model/element~Element element},
+	 * or creates an empty selection if no arguments were passed.
 	 *
-	 * 		// Creates empty selection without ranges.
-	 *		const selection = new Selection();
+	 *		// Creates empty selection without ranges.
+	 *		const selection = writer.createSelection();
 	 *
 	 *		// Creates selection at the given range.
-	 *		const range = new Range( start, end );
-	 *		const selection = new Selection( range );
+	 *		const range = writer.createRange( start, end );
+	 *		const selection = writer.createSelection( range );
 	 *
 	 *		// Creates selection at the given ranges
-	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
-	 *		const selection = new Selection( ranges );
+	 *		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
+	 *		const selection = writer.createSelection( ranges );
 	 *
 	 *		// Creates selection from the other selection.
 	 *		// Note: It doesn't copies selection attributes.
-	 *		const otherSelection = new Selection();
-	 *		const selection = new Selection( otherSelection );
+	 *		const otherSelection = writer.createSelection();
+	 *		const selection = writer.createSelection( otherSelection );
 	 *
-	 * 		// Creates selection from the given document selection.
+	 *		// Creates selection from the given document selection.
 	 *		// Note: It doesn't copies selection attributes.
-	 *		const documentSelection = new DocumentSelection( doc );
-	 *		const selection = new Selection( documentSelection );
+	 *		const documentSelection = model.document.selection;
+	 *		const selection = writer.createSelection( documentSelection );
 	 *
-	 * 		// Creates selection at the given position.
-	 *		const position = new Position( root, path );
-	 *		const selection = new Selection( position );
+	 *		// Creates selection at the given position.
+	 *		const position = writer.createPositionFromPath( root, path );
+	 *		const selection = writer.createSelection( position );
 	 *
-	 * 		// Creates selection at the start position of the given element.
+	 *		// Creates selection at the given offset in the given element.
 	 *		const paragraph = writer.createElement( 'paragraph' );
-	 *		const selection = new Selection( paragraph, offset );
+	 *		const selection = writer.createSelection( paragraph, offset );
 	 *
-	 * 		// Creates a range inside an {@link module:engine/model/element~Element element} which starts before the
-	 * 		// first child of that element and ends after the last child of that element.
-	 *		const selection = new Selection( paragraph, 'in' );
+	 *		// Creates a range inside an {@link module:engine/model/element~Element element} which starts before the
+	 *		// first child of that element and ends after the last child of that element.
+	 *		const selection = writer.createSelection( paragraph, 'in' );
 	 *
-	 * 		// Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends
-	 * 		// just after the item.
-	 *		const selection = new Selection( paragraph, 'on' );
+	 *		// Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends
+	 *		// just after the item.
+	 *		const selection = writer.createSelection( paragraph, 'on' );
 	 *
-	 * `Selection`'s constructor allow passing additional options (`backward`) as the last argument.
+	 * Selection's constructor allow passing additional options (`'backward'`) as the last argument.
 	 *
-	 * 		// Creates backward selection.
-	 *		const selection = new Selection( range, { backward: true } );
+	 *		// Creates backward selection.
+	 *		const selection = writer.createSelection( range, { backward: true } );
 	 *
 	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
 	 * module:engine/model/position~Position|module:engine/model/element~Element|
@@ -111,12 +115,17 @@ export default class Selection {
 	}
 
 	/**
-	 * Selection anchor. Anchor may be described as a position where the most recent part of the selection starts.
-	 * Together with {@link #focus} they define the direction of selection, which is important
-	 * when expanding/shrinking selection. Anchor is always {@link module:engine/model/range~Range#start start} or
-	 * {@link module:engine/model/range~Range#end end} position of the most recently added range.
+	 * Selection anchor. Anchor is the position from which the selection was started. If a user is making a selection
+	 * by dragging the mouse, the anchor is where the user pressed the mouse button (the beggining of the selection).
+	 *
+	 * Anchor and {@link #focus} define the direction of the selection, which is important
+	 * when expanding/shrinking selection. The focus moves, while the anchor should remain in the same place.
+	 *
+	 * Anchor is always set to the {@link module:engine/model/range~Range#start start} or
+	 * {@link module:engine/model/range~Range#end end} position of the last of selection's ranges. Whether it is
+	 * the `start` or `end` depends on the specified `options.backward`. See the {@link #setTo `setTo()`} method.
 	 *
-	 * Is set to `null` if there are no ranges in selection.
+	 * May be set to `null` if there are no ranges in the selection.
 	 *
 	 * @see #focus
 	 * @readonly
@@ -133,9 +142,10 @@ export default class Selection {
 	}
 
 	/**
-	 * Selection focus. Focus is a position where the selection ends.
+	 * Selection focus. Focus is the position where the selection ends. If a user is making a selection
+	 * by dragging the mouse, the focus is where the mouse cursor is.
 	 *
-	 * Is set to `null` if there are no ranges in selection.
+	 * May be set to `null` if there are no ranges in the selection.
 	 *
 	 * @see #anchor
 	 * @readonly
@@ -152,8 +162,8 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
-	 * collapsed.
+	 * Whether the selection is collapsed. Selection is collapsed when there is exactly one range in it
+	 * and it is collapsed.
 	 *
 	 * @readonly
 	 * @type {Boolean}
@@ -169,7 +179,7 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns number of ranges in selection.
+	 * Returns the number of ranges in the selection.
 	 *
 	 * @readonly
 	 * @type {Number}
@@ -179,8 +189,7 @@ export default class Selection {
 	}
 
 	/**
-	 * Specifies whether the {@link #focus}
-	 * precedes {@link #anchor}.
+	 * Specifies whether the selection's {@link #focus} precedes the selection's {@link #anchor}.
 	 *
 	 * @readonly
 	 * @type {Boolean}
@@ -190,8 +199,8 @@ export default class Selection {
 	}
 
 	/**
-	 * Checks whether this selection is equal to given selection. Selections are equal if they have same directions,
-	 * same number of ranges and all ranges from one selection equal to a range from other selection.
+	 * Checks whether this selection is equal to the given selection. Selections are equal if they have the same directions,
+	 * the same number of ranges and all ranges from one selection equal to ranges from the another selection.
 	 *
 	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} otherSelection
 	 * Selection to compare with.
@@ -227,13 +236,13 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns an iterable that iterates over copies of selection ranges.
+	 * Returns an iterable object that iterates over copies of selection ranges.
 	 *
 	 * @returns {Iterable.<module:engine/model/range~Range>}
 	 */
 	* getRanges() {
 		for ( const range of this._ranges ) {
-			yield Range.createFromRange( range );
+			yield new Range( range.start, range.end );
 		}
 	}
 
@@ -256,7 +265,7 @@ export default class Selection {
 			}
 		}
 
-		return first ? Range.createFromRange( first ) : null;
+		return first ? new Range( first.start, first.end ) : null;
 	}
 
 	/**
@@ -278,7 +287,7 @@ export default class Selection {
 			}
 		}
 
-		return last ? Range.createFromRange( last ) : null;
+		return last ? new Range( last.start, last.end ) : null;
 	}
 
 	/**
@@ -293,7 +302,7 @@ export default class Selection {
 	getFirstPosition() {
 		const first = this.getFirstRange();
 
-		return first ? Position.createFromPosition( first.start ) : null;
+		return first ? first.start.clone() : null;
 	}
 
 	/**
@@ -308,7 +317,7 @@ export default class Selection {
 	getLastPosition() {
 		const lastRange = this.getLastRange();
 
-		return lastRange ? Position.createFromPosition( lastRange.end ) : null;
+		return lastRange ? lastRange.end.clone() : null;
 	}
 
 	/**
@@ -317,32 +326,32 @@ export default class Selection {
 	 * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
 	 * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
 	 *
-	 * 		// Removes all selection's ranges.
+	 *		// Removes all selection's ranges.
 	 *		selection.setTo( null );
 	 *
 	 *		// Sets selection to the given range.
-	 *		const range = new Range( start, end );
+	 *		const range = writer.createRange( start, end );
 	 *		selection.setTo( range );
 	 *
 	 *		// Sets selection to given ranges.
-	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
 	 *		selection.setTo( ranges );
 	 *
 	 *		// Sets selection to other selection.
 	 *		// Note: It doesn't copies selection attributes.
-	 *		const otherSelection = new Selection();
+	 *		const otherSelection = writer.createSelection();
 	 *		selection.setTo( otherSelection );
 	 *
-	 * 		// Sets selection to the given document selection.
+	 *		// Sets selection to the given document selection.
 	 *		// Note: It doesn't copies selection attributes.
 	 *		const documentSelection = new DocumentSelection( doc );
 	 *		selection.setTo( documentSelection );
 	 *
-	 * 		// Sets collapsed selection at the given position.
-	 *		const position = new Position( root, path );
+	 *		// Sets collapsed selection at the given position.
+	 *		const position = writer.createPositionFromPath( root, path );
 	 *		selection.setTo( position );
 	 *
-	 * 		// Sets collapsed selection at the position of the given node and an offset.
+	 *		// Sets collapsed selection at the position of the given node and an offset.
 	 *		selection.setTo( paragraph, offset );
 	 *
 	 * Creates a range inside an {@link module:engine/model/element~Element element} which starts before the first child of
@@ -356,8 +365,8 @@ export default class Selection {
 	 *
 	 * `Selection#setTo()`' method allow passing additional options (`backward`) as the last argument.
 	 *
-	 * 		// Sets backward selection.
-	 *		const selection = new Selection( range, { backward: true } );
+	 *		// Sets backward selection.
+	 *		const selection = writer.createSelection( range, { backward: true } );
 	 *
 	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
 	 * module:engine/model/position~Position|module:engine/model/node~Node|
@@ -384,11 +393,11 @@ export default class Selection {
 			let range;
 
 			if ( placeOrOffset == 'in' ) {
-				range = Range.createIn( selectable );
+				range = Range._createIn( selectable );
 			} else if ( placeOrOffset == 'on' ) {
-				range = Range.createOn( selectable );
+				range = Range._createOn( selectable );
 			} else if ( placeOrOffset !== undefined ) {
-				range = Range.createCollapsedAt( selectable, placeOrOffset );
+				range = new Range( Position._createAt( selectable, placeOrOffset ) );
 			} else {
 				/**
 				 * selection.setTo requires the second parameter when the first parameter is a node.
@@ -472,11 +481,12 @@ export default class Selection {
 	/**
 	 * Moves {@link module:engine/model/selection~Selection#focus} to the specified location.
 	 *
-	 * The location can be specified in the same form as {@link module:engine/model/position~Position.createAt} parameters.
+	 * The location can be specified in the same form as
+	 * {@link module:engine/model/writer~Writer#createPositionAt writer.createPositionAt()} parameters.
 	 *
 	 * @fires change:range
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/model/item~Item model item}.
 	 */
 	setFocus( itemOrPosition, offset ) {
@@ -491,7 +501,7 @@ export default class Selection {
 			);
 		}
 
-		const newFocus = Position.createAt( itemOrPosition, offset );
+		const newFocus = Position._createAt( itemOrPosition, offset );
 
 		if ( newFocus.compareWith( this.focus ) == 'same' ) {
 			return;
@@ -656,7 +666,7 @@ export default class Selection {
 			const endBlock = getParentBlock( range.end, visited );
 
 			// #984. Don't return the end block if the range ends right at its beginning.
-			if ( endBlock && !range.end.isTouching( Position.createAt( endBlock ) ) ) {
+			if ( endBlock && !range.end.isTouching( Position._createAt( endBlock, 0 ) ) ) {
 				yield endBlock;
 			}
 		}
@@ -674,8 +684,8 @@ export default class Selection {
 	 * @returns {Boolean}
 	 */
 	containsEntireContent( element = this.anchor.root ) {
-		const limitStartPosition = Position.createAt( element );
-		const limitEndPosition = Position.createAt( element, 'end' );
+		const limitStartPosition = Position._createAt( element, 0 );
+		const limitEndPosition = Position._createAt( element, 'end' );
 
 		return limitStartPosition.isTouching( this.getFirstPosition() ) &&
 			limitEndPosition.isTouching( this.getLastPosition() );
@@ -690,7 +700,7 @@ export default class Selection {
 	 */
 	_pushRange( range ) {
 		this._checkRange( range );
-		this._ranges.push( Range.createFromRange( range ) );
+		this._ranges.push( new Range( range.start, range.end ) );
 	}
 
 	/**
@@ -703,14 +713,14 @@ export default class Selection {
 		for ( let i = 0; i < this._ranges.length; i++ ) {
 			if ( range.isIntersecting( this._ranges[ i ] ) ) {
 				/**
-				 * Trying to add a range that intersects with another range from selection.
+				 * Trying to add a range that intersects with another range in the selection.
 				 *
 				 * @error model-selection-range-intersects
 				 * @param {module:engine/model/range~Range} addedRange Range that was added to the selection.
-				 * @param {module:engine/model/range~Range} intersectingRange Range from selection that intersects with `addedRange`.
+				 * @param {module:engine/model/range~Range} intersectingRange Range in the selection that intersects with `addedRange`.
 				 */
 				throw new CKEditorError(
-					'model-selection-range-intersects: Trying to add a range that intersects with another range from selection.',
+					'model-selection-range-intersects: Trying to add a range that intersects with another range in the selection.',
 					{ addedRange: range, intersectingRange: this._ranges[ i ] }
 				);
 			}

+ 6 - 9
packages/ckeditor5-engine/src/model/treewalker.js

@@ -85,9 +85,9 @@ export default class TreeWalker {
 		 * @member {module:engine/model/position~Position} module:engine/model/treewalker~TreeWalker#position
 		 */
 		if ( options.startPosition ) {
-			this.position = Position.createFromPosition( options.startPosition );
+			this.position = options.startPosition.clone();
 		} else {
-			this.position = Position.createFromPosition( this.boundaries[ this.direction == 'backward' ? 'end' : 'start' ] );
+			this.position = Position._createAt( this.boundaries[ this.direction == 'backward' ? 'end' : 'start' ] );
 		}
 
 		// Reset position stickiness in case it was set to other value, as the stickiness is kept after cloning.
@@ -208,7 +208,7 @@ export default class TreeWalker {
 	 */
 	_next() {
 		const previousPosition = this.position;
-		const position = Position.createFromPosition( this.position );
+		const position = this.position.clone();
 		const parent = this._visitedParent;
 
 		// We are at the end of the root.
@@ -282,7 +282,7 @@ export default class TreeWalker {
 	 */
 	_previous() {
 		const previousPosition = this.position;
-		const position = Position.createFromPosition( this.position );
+		const position = this.position.clone();
 		const parent = this._visitedParent;
 
 		// We are at the beginning of the root.
@@ -379,10 +379,7 @@ function formatReturnValue( type, item, previousPosition, nextPosition, length )
  * @property {module:engine/model/item~Item} item Item between old and new positions of {@link module:engine/model/treewalker~TreeWalker}.
  * @property {module:engine/model/position~Position} previousPosition Previous position of the iterator.
  * * 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 module:engine/model/position~Position.createBefore}. It is also more efficient to get the
- * position after node by shifting `previousPosition` by `length`, using {@link module:engine/model/position~Position#getShiftedBy},
- * then calculate the position using {@link module:engine/model/position~Position.createAfter}.
+ * position before the item.
  * * Backward iteration: For `'elementStart'` it is the first position inside the element. For all other types it is
  * the position after item.
  * @property {module:engine/model/position~Position} nextPosition Next position of the iterator.
@@ -391,7 +388,7 @@ function formatReturnValue( type, item, previousPosition, nextPosition, length )
  * * 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 `'elementStart'` and `'character'` it is 1. For `'text'` it is
- * the length of the text. For `'elementEnd'` it is undefined.
+ * the length of the text. For `'elementEnd'` it is `undefined`.
  */
 
 /**

+ 8 - 12
packages/ckeditor5-engine/src/model/utils/deletecontent.js

@@ -8,7 +8,6 @@
  */
 
 import LivePosition from '../liveposition';
-import Position from '../position';
 import Range from '../range';
 import DocumentSelection from '../documentselection';
 
@@ -62,8 +61,7 @@ export default function deleteContent( model, selection, options = {} ) {
 
 		const selRange = selection.getFirstRange();
 		const startPos = selRange.start;
-		const endPos = LivePosition.createFromPosition( selRange.end );
-		endPos.stickiness = 'toNext';
+		const endPos = LivePosition.fromPosition( selRange.end, 'toNext' );
 
 		// 2. Remove the content if there is any.
 		if ( !selRange.start.isTouching( selRange.end ) ) {
@@ -118,10 +116,8 @@ function mergeBranches( writer, startPos, endPos ) {
 		return;
 	}
 
-	// If one of the positions is a root, then there's nothing more to merge (at least in the current state of implementation).
-	// Theoretically in this case we could unwrap the <p>: <$root>x[]<p>{}y</p></$root>, but we don't need to support it yet
-	// so let's just abort.
-	if ( !startParent.parent || !endParent.parent ) {
+	// If one of the positions is a limit element, then there's nothing to merge because we don't want to cross the limit boundaries.
+	if ( writer.model.schema.isLimit( startParent ) || writer.model.schema.isLimit( endParent ) ) {
 		return;
 	}
 
@@ -136,8 +132,8 @@ function mergeBranches( writer, startPos, endPos ) {
 	// <a><b>x[]</b></a><c><d>{}y</d></c>
 	// will become:
 	// <a><b>xy</b>[]</a><c>{}</c>
-	startPos = Position.createAfter( startParent );
-	endPos = Position.createBefore( endParent );
+	startPos = writer.createPositionAfter( startParent );
+	endPos = writer.createPositionBefore( endParent );
 
 	if ( !endPos.isEqual( startPos ) ) {
 		// In this case, before we merge, we need to move `endParent` to the `startPos`:
@@ -160,7 +156,7 @@ function mergeBranches( writer, startPos, endPos ) {
 	while ( endPos.parent.isEmpty ) {
 		const parentToRemove = endPos.parent;
 
-		endPos = Position.createBefore( parentToRemove );
+		endPos = writer.createPositionBefore( parentToRemove );
 
 		writer.remove( parentToRemove );
 	}
@@ -209,8 +205,8 @@ function insertParagraph( writer, position, selection ) {
 function replaceEntireContentWithParagraph( writer, selection ) {
 	const limitElement = writer.model.schema.getLimitElement( selection );
 
-	writer.remove( Range.createIn( limitElement ) );
-	insertParagraph( writer, Position.createAt( limitElement ), selection );
+	writer.remove( writer.createRangeIn( limitElement ) );
+	insertParagraph( writer, writer.createPositionAt( limitElement, 0 ), selection );
 }
 
 // We want to replace the entire content with a paragraph when:

+ 8 - 11
packages/ckeditor5-engine/src/model/utils/getselectedcontent.js

@@ -7,9 +7,6 @@
  * @module engine/model/utils/getselectedcontent
  */
 
-import Range from '../range';
-import Position from '../position';
-
 /**
  * Gets a clone of the selected content.
  *
@@ -63,9 +60,9 @@ export default function getSelectedContent( model, selection ) {
 			// The original range is flat, so take it.
 			flatSubtreeRange = range;
 		} else {
-			flatSubtreeRange = Range.createFromParentsAndOffsets(
-				commonParent, range.start.path[ commonPath.length ],
-				commonParent, range.end.path[ commonPath.length ] + 1
+			flatSubtreeRange = writer.createRange(
+				writer.createPositionAt( commonParent, range.start.path[ commonPath.length ] ),
+				writer.createPositionAt( commonParent, range.end.path[ commonPath.length ] + 1 )
 			);
 		}
 
@@ -97,10 +94,10 @@ export default function getSelectedContent( model, selection ) {
 		// [<quote><p>y</p><h>fir]st</h></quote><p>se[cond</p>]
 		if ( flatSubtreeRange != range ) {
 			// Find the position of the original range in the cloned fragment.
-			const newRange = range._getTransformedByMove( flatSubtreeRange.start, Position.createAt( frag, 0 ), howMany )[ 0 ];
+			const newRange = range._getTransformedByMove( flatSubtreeRange.start, writer.createPositionAt( frag, 0 ), howMany )[ 0 ];
 
-			const leftExcessRange = new Range( Position.createAt( frag ), newRange.start );
-			const rightExcessRange = new Range( newRange.end, Position.createAt( frag, 'end' ) );
+			const leftExcessRange = writer.createRange( writer.createPositionAt( frag, 0 ), newRange.start );
+			const rightExcessRange = writer.createRange( newRange.end, writer.createPositionAt( frag, 'end' ) );
 
 			removeRangeContent( rightExcessRange, writer );
 			removeRangeContent( leftExcessRange, writer );
@@ -118,7 +115,7 @@ function removeRangeContent( range, writer ) {
 	Array.from( range.getItems( { direction: 'backward' } ) )
 		// We should better store ranges because text proxies will lose integrity
 		// with the text nodes when we'll start removing content.
-		.map( item => Range.createOn( item ) )
+		.map( item => writer.createRangeOn( item ) )
 		// Filter only these items which are fully contained in the passed range.
 		//
 		// E.g. for the following range: [<quote><p>y</p><h>fir]st</h>
@@ -143,7 +140,7 @@ function removeRangeContent( range, writer ) {
 		let parent = parentToCheck;
 
 		while ( parent.parent && parent.isEmpty ) {
-			const removeRange = Range.createOn( parent );
+			const removeRange = writer.createRangeOn( parent );
 
 			parent = parent.parent;
 

+ 36 - 21
packages/ckeditor5-engine/src/model/utils/insertcontent.js

@@ -13,24 +13,40 @@ import Element from '../element';
 import Range from '../range';
 import log from '@ckeditor/ckeditor5-utils/src/log';
 import DocumentSelection from '../documentselection';
+import Selection from '../selection';
 
 /**
  * Inserts content into the editor (specified selection) as one would expect the paste
  * functionality to work.
  *
+ * If an instance of {@link module:engine/model/selection~Selection} is passed as `selectable` it will be modified
+ * to the insertion selection (equal to a range to be selected after insertion).
+ *
  * **Note:** Use {@link module:engine/model/model~Model#insertContent} instead of this function.
- * This function is only exposed to be reusable in algorithms
- * which change the {@link module:engine/model/model~Model#insertContent}
+ * This function is only exposed to be reusable in algorithms which change the {@link module:engine/model/model~Model#insertContent}
  * method's behavior.
  *
  * @param {module:engine/model/model~Model} model The model in context of which the insertion
  * should be performed.
  * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
- * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
+ * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+ * module:engine/model/position~Position|module:engine/model/element~Element|
+ * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} [selectable=model.document.selection]
  * Selection into which the content should be inserted.
+ * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
  */
-export default function insertContent( model, content, selection ) {
+export default function insertContent( model, content, selectable, placeOrOffset ) {
 	model.change( writer => {
+		let selection;
+
+		if ( !selectable ) {
+			selection = model.document.selection;
+		} else if ( selectable instanceof Selection || selectable instanceof DocumentSelection ) {
+			selection = selectable;
+		} else {
+			selection = writer.createSelection( selectable, placeOrOffset );
+		}
+
 		if ( !selection.isCollapsed ) {
 			model.deleteContent( selection );
 		}
@@ -158,7 +174,7 @@ class Insertion {
 	 */
 	getSelectionRange() {
 		if ( this.nodeToSelect ) {
-			return Range.createOn( this.nodeToSelect );
+			return Range._createOn( this.nodeToSelect );
 		}
 
 		return this.model.schema.getNearestSelectionRange( this.position );
@@ -260,12 +276,11 @@ class Insertion {
 			return;
 		}
 
-		const livePos = LivePosition.createFromPosition( this.position );
-		livePos.stickiness = 'toNext';
+		const livePos = LivePosition.fromPosition( this.position, 'toNext' );
 
 		this.writer.insert( node, this.position );
 
-		this.position = Position.createFromPosition( livePos );
+		this.position = livePos.toPosition();
 		livePos.detach();
 
 		// The last inserted object should be selected because we can't put a collapsed selection after it.
@@ -290,19 +305,19 @@ class Insertion {
 
 		const mergeLeft = this._canMergeLeft( node, context );
 		const mergeRight = this._canMergeRight( node, context );
-		const mergePosLeft = LivePosition.createBefore( node );
+		const mergePosLeft = LivePosition._createBefore( node );
 		mergePosLeft.stickiness = 'toNext';
-		const mergePosRight = LivePosition.createAfter( node );
+		const mergePosRight = LivePosition._createAfter( node );
 		mergePosRight.stickiness = 'toNext';
 
 		if ( mergeLeft ) {
-			const position = LivePosition.createFromPosition( this.position );
-			position.stickiness = 'toNext';
+			const livePosition = LivePosition.fromPosition( this.position );
+			livePosition.stickiness = 'toNext';
 
 			this.writer.merge( mergePosLeft );
 
-			this.position = Position.createFromPosition( position );
-			position.detach();
+			this.position = livePosition.toPosition();
+			livePosition.detach();
 		}
 
 		if ( mergeRight ) {
@@ -316,16 +331,16 @@ class Insertion {
 
 			// Move the position to the previous node, so it isn't moved to the graveyard on merge.
 			// <p>x</p>[]<p>y</p> => <p>x[]</p><p>y</p>
-			this.position = Position.createAt( mergePosRight.nodeBefore, 'end' );
+			this.position = Position._createAt( mergePosRight.nodeBefore, 'end' );
 
 			// OK:  <p>xx[]</p> + <p>yy</p> => <p>xx[]yy</p> (when sticks to previous)
 			// NOK: <p>xx[]</p> + <p>yy</p> => <p>xxyy[]</p> (when sticks to next)
-			const position = new LivePosition( this.position.root, this.position.path, 'toPrevious' );
+			const livePosition = new LivePosition( this.position.root, this.position.path, 'toPrevious' );
 
 			this.writer.merge( mergePosRight );
 
-			this.position = Position.createFromPosition( position );
-			position.detach();
+			this.position = livePosition.toPosition();
+			livePosition.detach();
 		}
 
 		if ( mergeLeft || mergeRight ) {
@@ -412,7 +427,7 @@ class Insertion {
 
 			if ( this.position.isAtStart ) {
 				const parent = this.position.parent;
-				this.position = Position.createBefore( parent );
+				this.position = this.writer.createPositionBefore( parent );
 
 				// Special case – parent is empty (<p>^</p>) so isAtStart == isAtEnd == true.
 				// We can remove the element after moving selection out of it.
@@ -420,9 +435,9 @@ class Insertion {
 					this.writer.remove( parent );
 				}
 			} else if ( this.position.isAtEnd ) {
-				this.position = Position.createAfter( this.position.parent );
+				this.position = this.writer.createPositionAfter( this.position.parent );
 			} else {
-				const tempPos = Position.createAfter( this.position.parent );
+				const tempPos = this.writer.createPositionAfter( this.position.parent );
 
 				this.writer.split( this.position );
 

+ 2 - 2
packages/ckeditor5-engine/src/model/utils/modifyselection.js

@@ -106,7 +106,7 @@ function tryExtendingTo( data, value ) {
 	if ( value.type == ( data.isForward ? 'elementStart' : 'elementEnd' ) ) {
 		// If it's an object, we can select it now.
 		if ( data.schema.isObject( value.item ) ) {
-			return Position.createAt( value.item, data.isForward ? 'after' : 'before' );
+			return Position._createAt( value.item, data.isForward ? 'after' : 'before' );
 		}
 
 		// If text allowed on this position, extend to this place.
@@ -195,7 +195,7 @@ function getCorrectWordBreakPosition( walker, isForward ) {
 
 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 );

+ 7 - 4
packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js

@@ -153,7 +153,7 @@ function tryFixingCollapsedRange( range, schema ) {
 
 	// Check single node selection (happens in tables).
 	if ( fixedPosition.nodeAfter && schema.isLimit( fixedPosition.nodeAfter ) ) {
-		return new Range( fixedPosition, Position.createAfter( fixedPosition.nodeAfter ) );
+		return new Range( fixedPosition, Position._createAfter( fixedPosition.nodeAfter ) );
 	}
 
 	return new Range( fixedPosition );
@@ -211,8 +211,11 @@ function tryFixingNonCollapsedRage( range, schema ) {
 
 		// Although we've already found limit element on start/end positions we must find the outer-most limit element.
 		// as limit elements might be nested directly inside (ie table > tableRow > tableCell).
-		const fixedStart = expandStart ? expandSelectionOnIsLimitNode( Position.createAt( startLimitElement ), schema, 'start' ) : start;
-		const fixedEnd = expandEnd ? expandSelectionOnIsLimitNode( Position.createAt( endLimitElement ), schema, 'end' ) : end;
+		const startPosition = Position._createAt( startLimitElement, 0 );
+		const endPosition = Position._createAt( endLimitElement, 0 );
+
+		const fixedStart = expandStart ? expandSelectionOnIsLimitNode( startPosition, schema, 'start' ) : start;
+		const fixedEnd = expandEnd ? expandSelectionOnIsLimitNode( endPosition, schema, 'end' ) : end;
 
 		return new Range( fixedStart, fixedEnd );
 	}
@@ -238,7 +241,7 @@ function expandSelectionOnIsLimitNode( position, schema, expandToDirection ) {
 	}
 
 	// Depending on direction of expanding selection return position before or after found node.
-	return expandToDirection === 'start' ? Position.createBefore( node ) : Position.createAfter( node );
+	return expandToDirection === 'start' ? Position._createBefore( node ) : Position._createAfter( node );
 }
 
 // Checks whether both range ends are placed around non-limit elements.

+ 149 - 71
packages/ckeditor5-engine/src/model/writer.js

@@ -16,8 +16,6 @@ import RenameOperation from './operation/renameoperation';
 import RootAttributeOperation from './operation/rootattributeoperation';
 import SplitOperation from './operation/splitoperation';
 import MergeOperation from './operation/mergeoperation';
-import WrapOperation from './operation/wrapoperation';
-import UnwrapOperation from './operation/unwrapoperation';
 
 import DocumentFragment from './documentfragment';
 import Text from './text';
@@ -45,6 +43,10 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  * Note that the writer should never be stored and used outside of the `change()` and
  * `enqueueChange()` blocks.
  *
+ * Note that writer's methods do not check the {@link module:engine/model/schema~Schema}. It is possible
+ * to create incorrect model structures by using the writer. Read more about in
+ * {@glink framework/guides/deep-dive/schema#who-checks-the-schema "Who checks the schema?"}.
+ *
  * @see module:engine/model/model~Model#change
  * @see module:engine/model/model~Model#enqueueChange
  */
@@ -135,7 +137,7 @@ export default class Writer {
 	 *		const paragraph = writer.createElement( 'paragraph' );
 	 *		writer.insert( paragraph, anotherParagraph, 'after' );
 	 *
-	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt `Position.createAt()`}.
+	 * These parameters works the same way as {@link #createPositionAt `writer.createPositionAt()`}.
 	 *
 	 * Note that if the item already has parent it will be removed from the previous parent.
 	 *
@@ -145,23 +147,26 @@ export default class Writer {
 	 * If you want to move {@link module:engine/model/range~Range range} instead of an
 	 * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move `Writer#move()`}.
 	 *
+	 * **Note:** For a paste-like content insertion mechanism see
+	 * {@link module:engine/model/model~Model#insertContent `model.insertContent()`}.
+	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment} item Item or document
 	 * fragment to insert.
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * second parameter is a {@link module:engine/model/item~Item model item}.
 	 */
-	insert( item, itemOrPosition, offset ) {
+	insert( item, itemOrPosition, offset = 0 ) {
 		this._assertWriterUsedCorrectly();
 
-		const position = Position.createAt( itemOrPosition, offset );
+		const position = Position._createAt( itemOrPosition, offset );
 
 		// If item has a parent already.
 		if ( item.parent ) {
 			// We need to check if item is going to be inserted within the same document.
 			if ( isSameTree( item.root, position.root ) ) {
 				// If it's we just need to move it.
-				this.move( Range.createOn( item ), position );
+				this.move( Range._createOn( item ), position );
 
 				return;
 			}
@@ -193,13 +198,13 @@ export default class Writer {
 		if ( item instanceof DocumentFragment ) {
 			for ( const [ markerName, markerRange ] of item.markers ) {
 				// We need to migrate marker range from DocumentFragment to Document.
-				const rangeRootPosition = Position.createAt( markerRange.root );
+				const rangeRootPosition = Position._createAt( markerRange.root, 0 );
 				const range = new Range(
 					markerRange.start._getCombined( rangeRootPosition, position ),
 					markerRange.end._getCombined( rangeRootPosition, position )
 				);
 
-				this.addMarker( markerName, { range, usingOperation: true } );
+				this.addMarker( markerName, { range, usingOperation: true, affectsData: true } );
 			}
 		}
 	}
@@ -220,12 +225,12 @@ export default class Writer {
 	 *		// Inserts 'foo' after an image:
 	 *		writer.insertText( 'foo', image, 'after' );
 	 *
-	 * These parameters work in the same way as {@link module:engine/model/position~Position.createAt `Position.createAt()`}.
+	 * These parameters work in the same way as {@link #createPositionAt `writer.createPositionAt()`}.
 	 *
 	 * @param {String} data Text data.
 	 * @param {Object} [attributes] Text attributes.
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * third parameter is a {@link module:engine/model/item~Item model item}.
 	 */
 	insertText( text, attributes, itemOrPosition, offset ) {
@@ -252,12 +257,12 @@ export default class Writer {
 	 *		// Inserts after an image:
 	 *		writer.insertElement( 'paragraph', image, 'after' );
 	 *
-	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt `Position.createAt()`}.
+	 * These parameters works the same way as {@link #createPositionAt `writer.createPositionAt()`}.
 	 *
 	 * @param {String} name Name of the element.
 	 * @param {Object} [attributes] Elements attributes.
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * third parameter is a {@link module:engine/model/item~Item model item}.
 	 */
 	insertElement( name, attributes, itemOrPosition, offset ) {
@@ -426,7 +431,7 @@ export default class Writer {
 	 *		// Moves all items in the range to a position after an image:
 	 *		writer.move( sourceRange, image, 'after' );
 	 *
-	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt `Position.createAt()`}.
+	 * These parameters works the same way as {@link #createPositionAt `writer.createPositionAt()`}.
 	 *
 	 * Note that items can be moved only within the same tree. It means that you can move items within the same root
 	 * (element or document fragment) or between {@link module:engine/model/document~Document#roots documents roots},
@@ -435,7 +440,7 @@ export default class Writer {
 	 *
 	 * @param {module:engine/model/range~Range} range Source range.
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * second parameter is a {@link module:engine/model/item~Item model item}.
 	 */
 	move( range, itemOrPosition, offset ) {
@@ -459,7 +464,7 @@ export default class Writer {
 			throw new CKEditorError( 'writer-move-range-not-flat: Range to move is not flat.' );
 		}
 
-		const position = Position.createAt( itemOrPosition, offset );
+		const position = Position._createAt( itemOrPosition, offset );
 
 		if ( !isSameTree( range.root, position.root ) ) {
 			/**
@@ -496,7 +501,7 @@ export default class Writer {
 		} else {
 			const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1;
 
-			applyRemoveOperation( Position.createBefore( itemOrRange ), howMany, this.batch, this.model );
+			applyRemoveOperation( Position._createBefore( itemOrRange ), howMany, this.batch, this.model );
 		}
 	}
 
@@ -539,6 +544,97 @@ export default class Writer {
 		}
 	}
 
+	/**
+	 * Shortcut for {@link module:engine/model/model~Model#createPositionFromPath `Model#createPositionFromPath()`}.
+	 *
+	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} root Root of the position.
+	 * @param {Array.<Number>} path Position path. See {@link module:engine/model/position~Position#path}.
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
+	 * See {@link module:engine/model/position~PositionStickiness}.
+	 * @returns {module:engine/model/position~Position}
+	 */
+	createPositionFromPath( root, path, stickiness ) {
+		return this.model.createPositionFromPath( root, path, stickiness );
+	}
+
+	/**
+	 * Shortcut for {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
+	 * first parameter is a {@link module:engine/model/item~Item model item}.
+	 * @returns {module:engine/model/position~Position}
+	 */
+	createPositionAt( itemOrPosition, offset ) {
+		return this.model.createPositionAt( itemOrPosition, offset );
+	}
+
+	/**
+	 * Shortcut for {@link module:engine/model/model~Model#createPositionAfter `Model#createPositionAfter()`}.
+	 *
+	 * @param {module:engine/model/item~Item} item Item after which the position should be placed.
+	 * @returns {module:engine/model/position~Position}
+	 */
+	createPositionAfter( item ) {
+		return this.model.createPositionAfter( item );
+	}
+
+	/**
+	 * Shortcut for {@link module:engine/model/model~Model#createPositionBefore `Model#createPositionBefore()`}.
+	 *
+	 * @param {module:engine/model/item~Item} item Item after which the position should be placed.
+	 * @returns {module:engine/model/position~Position}
+	 */
+	createPositionBefore( item ) {
+		return this.model.createPositionBefore( item );
+	}
+
+	/**
+	 * Shortcut for {@link module:engine/model/model~Model#createRange `Model#createRange()`}.
+	 *
+	 * @param {module:engine/model/position~Position} start Start position.
+	 * @param {module:engine/model/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
+	 * @returns {module:engine/model/range~Range}
+	 */
+	createRange( start, end ) {
+		return this.model.createRange( start, end );
+	}
+
+	/**
+	 * Shortcut for {@link module:engine/model/model~Model#createRangeIn `Model#createRangeIn()`}.
+	 *
+	 * @param {module:engine/model/element~Element} element Element which is a parent for the range.
+	 * @returns {module:engine/model/range~Range}
+	 */
+	createRangeIn( element ) {
+		return this.model.createRangeIn( element );
+	}
+
+	/**
+	 * Shortcut for {@link module:engine/model/model~Model#createRangeOn `Model#createRangeOn()`}.
+	 *
+	 * @param {module:engine/model/element~Element} element Element which is a parent for the range.
+	 * @returns {module:engine/model/range~Range}
+	 */
+	createRangeOn( element ) {
+		return this.model.createRangeOn( element );
+	}
+
+	/**
+	 * Shortcut for {@link module:engine/model/model~Model#createSelection `Model#createSelection()`}.
+	 *
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+	 * module:engine/model/position~Position|module:engine/model/element~Element|
+	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
+	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
+	 * @param {Object} [options]
+	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
+	 * @returns {module:engine/model/selection~Selection}
+	 */
+	createSelection( selectable, placeOrOffset, options ) {
+		return this.model.createSelection( selectable, placeOrOffset, options );
+	}
+
 	/**
 	 * Performs merge action in a detached tree.
 	 *
@@ -549,7 +645,7 @@ export default class Writer {
 		const nodeBefore = position.nodeBefore;
 		const nodeAfter = position.nodeAfter;
 
-		this.move( Range.createIn( nodeAfter ), Position.createAt( nodeBefore, 'end' ) );
+		this.move( Range._createIn( nodeAfter ), Position._createAt( nodeBefore, 'end' ) );
 		this.remove( nodeAfter );
 	}
 
@@ -560,8 +656,8 @@ export default class Writer {
 	 * @param {module:engine/model/position~Position} position Position between merged elements.
 	 */
 	_merge( position ) {
-		const targetPosition = Position.createAt( position.nodeBefore, 'end' );
-		const sourcePosition = Position.createAt( position.nodeAfter, 0 );
+		const targetPosition = Position._createAt( position.nodeBefore, 'end' );
+		const sourcePosition = Position._createAt( position.nodeAfter, 0 );
 
 		const graveyard = position.root.document.graveyard;
 		const graveyardPosition = new Position( graveyard, [ 0 ] );
@@ -595,7 +691,7 @@ export default class Writer {
 		}
 
 		const version = element.root.document ? element.root.document.version : null;
-		const renameOperation = new RenameOperation( Position.createBefore( element ), element.name, newName, version );
+		const renameOperation = new RenameOperation( Position._createBefore( element ), element.name, newName, version );
 
 		this.batch.addOperation( renameOperation );
 		this.model.applyOperation( renameOperation );
@@ -657,13 +753,13 @@ export default class Writer {
 				firstCopyElement = position.parent.nextSibling;
 			}
 
-			position = Position.createAfter( position.parent );
+			position = this.createPositionAfter( position.parent );
 			splitElement = position.parent;
 		} while ( splitElement !== limitElement );
 
 		return {
 			position,
-			range: new Range( Position.createAt( firstSplitElement, 'end' ), Position.createAt( firstCopyElement ) )
+			range: new Range( Position._createAt( firstSplitElement, 'end' ), Position._createAt( firstCopyElement, 0 ) )
 		};
 	}
 
@@ -709,10 +805,21 @@ export default class Writer {
 		}
 
 		const version = range.root.document ? range.root.document.version : null;
-		const wrap = new WrapOperation( range.start, range.end.offset - range.start.offset, element, version );
 
-		this.batch.addOperation( wrap );
-		this.model.applyOperation( wrap );
+		// Has to be `range.start` not `range.end` for better transformations.
+		const insert = new InsertOperation( range.start, element, version );
+		this.batch.addOperation( insert );
+		this.model.applyOperation( insert );
+
+		const move = new MoveOperation(
+			range.start.getShiftedBy( 1 ),
+			range.end.offset - range.start.offset,
+			Position._createAt( element, 0 ),
+			version === null ? null : version + 1
+		);
+
+		this.batch.addOperation( move );
+		this.model.applyOperation( move );
 	}
 
 	/**
@@ -733,43 +840,10 @@ export default class Writer {
 			throw new CKEditorError( 'writer-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
 		}
 
-		if ( !element.root.document ) {
-			this._unwrapDetached( element );
-		} else {
-			this._unwrap( element );
-		}
-	}
-
-	/**
-	 * Performs unwrap action in a detached tree.
-	 *
-	 * @private
-	 * @param {module:engine/model/element~Element} element Element to unwrap.
-	 */
-	_unwrapDetached( element ) {
-		this.move( Range.createIn( element ), Position.createAfter( element ) );
+		this.move( Range._createIn( element ), this.createPositionAfter( element ) );
 		this.remove( element );
 	}
 
-	/**
-	 * Performs unwrap action in a non-detached tree.
-	 *
-	 * @private
-	 * @param {module:engine/model/element~Element} element Element to unwrap.
-	 */
-	_unwrap( element ) {
-		const position = Position.createAt( element, 0 );
-		const version = position.root.document.version;
-
-		const graveyard = position.root.document.graveyard;
-		const graveyardPosition = new Position( graveyard, [ 0 ] );
-
-		const unwrap = new UnwrapOperation( position, element.maxOffset, graveyardPosition, version );
-
-		this.batch.addOperation( unwrap );
-		this.model.applyOperation( unwrap );
-	}
-
 	/**
 	 * Adds a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
 	 * changes in the document and updates its range automatically, when model tree changes.
@@ -993,29 +1067,33 @@ export default class Writer {
 	}
 
 	/**
-	 * Sets this selection's ranges and direction to the specified location based on the given
-	 * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
-	 * {@link module:engine/model/node~Node node}, {@link module:engine/model/position~Position position},
-	 * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
+	 * Sets the document's selection (ranges and direction) to the specified location based on:
+	 *
+	 * * the given {@link module:engine/model/selection~Selection selection},
+	 * * or the given {@link module:engine/model/position~Position position},
+	 * * or the given {@link module:engine/model/range~Range range},
+	 * * or the given iterable of {@link module:engine/model/range~Range ranges},
+	 * * or the given {@link module:engine/model/node~Node node},
+	 * * or `null`.
 	 *
 	 *		// Sets selection to the given range.
-	 *		const range = new Range( start, end );
+	 *		const range = writer.createRange( start, end );
 	 *		writer.setSelection( range );
 	 *
 	 *		// Sets selection to given ranges.
-	 *		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
 	 *		writer.setSelection( range );
 	 *
 	 *		// Sets selection to other selection.
-	 *		const otherSelection = new Selection();
+	 *		const otherSelection = writer.createSelection();
 	 *		writer.setSelection( otherSelection );
 	 *
 	 *		// Sets selection to the given document selection.
-	 *		const documentSelection = new DocumentSelection( doc );
+	 *		const documentSelection = model.document.selection;
 	 *		writer.setSelection( documentSelection );
 	 *
 	 *		// Sets collapsed selection at the given position.
-	 *		const position = new Position( root, path );
+	 *		const position = writer.createPosition( root, path );
 	 *		writer.setSelection( position );
 	 *
 	 *		// Sets collapsed selection at the position of the given node and an offset.
@@ -1057,7 +1135,7 @@ export default class Writer {
 	 * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
 	 *
 	 * The location can be specified in the same form as
-	 * {@link module:engine/model/position~Position.createAt `Position.createAt()`} parameters.
+	 * {@link #createPositionAt `writer.createPositionAt()`} parameters.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
 	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
@@ -1303,7 +1381,7 @@ function setAttributeOnItem( writer, key, value, item ) {
 
 			operation = new RootAttributeOperation( item, key, previousValue, value, version );
 		} else {
-			range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
+			range = new Range( Position._createBefore( item ), writer.createPositionAfter( item ) );
 
 			const version = range.root.document ? doc.version : null;
 

+ 10 - 7
packages/ckeditor5-engine/src/view/attributeelement.js

@@ -14,22 +14,25 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 const DEFAULT_PRIORITY = 10;
 
 /**
- * Attributes are elements which define document presentation. They are mostly elements like `<b>` or `<span>`.
- * Attributes can be broken and merged by the {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}.
+ * Attribute elements are used to represent formatting elements in the view (think – `<b>`, `<span style="font-size: 2em">`, etc.).
+ * Most often they are created when downcasting model text attributes.
  *
- * Editing engine does not define fixed HTML DTD. This is why the type of the {@link module:engine/view/element~Element} need to
- * be defined by the feature developer. Creating an element you should use {@link module:engine/view/containerelement~ContainerElement}
- * class or `AttributeElement`.
+ * Editing engine does not define a fixed HTML DTD. This is why a feature developer needs to choose between various
+ * types (container element, {@link module:engine/view/attributeelement~AttributeElement attribute element},
+ * {@link module:engine/view/emptyelement~EmptyElement empty element}, etc) when developing a feature.
+ *
+ * To create a new attribute element instance use the
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement `DowncastWriter#createAttributeElement()`} method.
  *
  * @extends module:engine/view/element~Element
  */
 export default class AttributeElement extends Element {
 	/**
-	 * Creates a attribute element.
+	 * Creates an attribute element.
 	 *
 	 * @see module:engine/view/downcastwriter~DowncastWriter#createAttributeElement
-	 * @protected
 	 * @see module:engine/view/element~Element
+	 * @protected
 	 */
 	constructor( name, attrs, children ) {
 		super( name, attrs, children );

+ 19 - 30
packages/ckeditor5-engine/src/view/containerelement.js

@@ -11,35 +11,22 @@ import Element from './element';
 
 /**
  * Containers are elements which define document structure. They define boundaries for
- * {@link module:engine/view/attributeelement~AttributeElement attributes}. They are mostly use for block elements like `<p>` or `<div>`.
+ * {@link module:engine/view/attributeelement~AttributeElement attributes}. They are mostly used for block elements like `<p>` or `<div>`.
  *
- * Editing engine does not define fixed HTML DTD. This is why the type of the {@link module:engine/view/element~Element} need to
- * be defined by the feature developer.
+ * Editing engine does not define a fixed HTML DTD. This is why a feature developer needs to choose between various
+ * types (container element, {@link module:engine/view/attributeelement~AttributeElement attribute element},
+ * {@link module:engine/view/emptyelement~EmptyElement empty element}, etc) when developing a feature.
  *
- * Creating an element you should use `ContainerElement` class or {@link module:engine/view/attributeelement~AttributeElement}. This is
- * important to define the type of the element because of two reasons:
+ * The container element should be your default choice when writing a converter, unless:
  *
- * Firstly, {@link module:engine/view/domconverter~DomConverter} needs the information what is an editable block to convert elements to
- * DOM properly. {@link module:engine/view/domconverter~DomConverter} will ensure that `ContainerElement` is editable and it is possible
- * to put caret inside it, even if the container is empty.
+ * * this element represents a model text attribute (then use {@link module:engine/view/attributeelement~AttributeElement}),
+ * * this is an empty element like `<img>` (then use {@link module:engine/view/emptyelement~EmptyElement}),
+ * * this is a root element,
+ * * this is a nested editable element (then use  {@link module:engine/view/editableelement~EditableElement}).
  *
- * Secondly, {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} uses this information.
- * Nodes {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breaking} and
- * {@link module:engine/view/downcastwriter~DowncastWriter#mergeAttributes merging} is performed only in a bounds of a container nodes.
- *
- * For instance if `<p>` is an container and `<b>` is attribute:
- *
- *		<p><b>fo^o</b></p>
- *
- * {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breakAttributes} will create:
- *
- *		<p><b>fo</b><b>o</b></p>
- *
- * There might be a need to mark `<span>` element as a container node, for example in situation when it will be a
- * container of an inline widget:
- *
- *		<span color="red">foobar</span>		// attribute
- *		<span data-widget>foobar</span>		// container
+ * To create a new container element instance use the
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement `DowncastWriter#createContainerElement()`}
+ * method.
  *
  * @extends module:engine/view/element~Element
  */
@@ -47,8 +34,8 @@ export default class ContainerElement extends Element {
 	/**
 	 * Creates a container element.
 	 *
-	 * @see module:engine/view/element~Element
 	 * @see module:engine/view/downcastwriter~DowncastWriter#createContainerElement
+	 * @see module:engine/view/element~Element
 	 * @protected
 	 */
 	constructor( name, attrs, children ) {
@@ -75,10 +62,12 @@ export default class ContainerElement extends Element {
 	}
 }
 
-// Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed.
-//
-// @returns {Number|null} Block filler offset or `null` if block filler is not needed.
-function getFillerOffset() {
+/**
+ * Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed.
+ *
+ * @returns {Number|null} Block filler offset or `null` if block filler is not needed.
+ */
+export function getFillerOffset() {
 	const children = [ ...this.getChildren() ];
 	const lastChild = children[ this.childCount - 1 ];
 

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

@@ -14,15 +14,19 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 
 /**
- * DocumentFragment class.
+ * Document fragment.
+ *
+ * To create a new document fragment instance use the
+ * {@link module:engine/view/upcastwriter~UpcastWriter#createDocumentFragment `UpcastWriter#createDocumentFragment()`}
+ * method.
  */
 export default class DocumentFragment {
 	/**
 	 * Creates new DocumentFragment instance.
 	 *
 	 * @protected
-	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children] List of nodes to be inserted into
-	 * created document fragment.
+	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
+	 * A list of nodes to be inserted into the created document fragment.
 	 */
 	constructor( children ) {
 		/**

+ 18 - 23
packages/ckeditor5-engine/src/view/documentselection.js

@@ -12,20 +12,14 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 
 /**
- * Class representing document selection in tree view. It's instance is stored at
- * {@link module:engine/view/document~Document#selection}. It is similar to {@link module:engine/view/selection~Selection} but
- * it has read-only API and can be modified only by writer obtained from {@link module:engine/view/view~View#change} method.
+ * Class representing the document selection in the view.
  *
- * Selection can consist of {@link module:engine/view/range~Range ranges}.
- * Selection's ranges can be obtained via {@link module:engine/view/documentselection~DocumentSelection#getRanges getRanges},
- * {@link module:engine/view/documentselection~DocumentSelection#getFirstRange getFirstRange}
- * and {@link module:engine/view/documentselection~DocumentSelection#getLastRange getLastRange}
- * methods, which return copies of ranges stored inside selection. Modifications made on these copies will not change
- * selection's state. Similar situation occurs when getting {@link module:engine/view/documentselection~DocumentSelection#anchor anchor},
- * {@link module:engine/view/documentselection~DocumentSelection#focus focus},
- * {@link module:engine/view/documentselection~DocumentSelection#getFirstPosition first} and
- * {@link module:engine/view/documentselection~DocumentSelection#getLastPosition last} positions - all will return
- * copies of requested positions.
+ * Its instance is available in {@link module:engine/view/document~Document#selection `Document#selection`}.
+ *
+ * It is similar to {@link module:engine/view/selection~Selection} but
+ * it has a read-only API and can be modified only by the writer available in
+ * the {@link module:engine/view/view~View#change `View#change()`} block
+ * (so via {@link module:engine/view/downcastwriter~DowncastWriter#setSelection `DowncastWriter#setSelection()`}).
  */
 export default class DocumentSelection {
 	/**
@@ -35,19 +29,19 @@ export default class DocumentSelection {
 	 *		const selection = new DocumentSelection();
 	 *
 	 *		// Creates selection at the given range.
-	 *		const range = new Range( start, end );
+	 *		const range = writer.createRange( start, end );
 	 *		const selection = new DocumentSelection( range );
 	 *
 	 *		// Creates selection at the given ranges
-	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( start2, end2 ) ];
 	 *		const selection = new DocumentSelection( ranges );
 	 *
 	 *		// Creates selection from the other selection.
-	 *		const otherSelection = new Selection();
+	 *		const otherSelection = writer.createSelection();
 	 *		const selection = new DocumentSelection( otherSelection );
 	 *
 	 * 		// Creates selection at the given position.
-	 *		const position = new Position( root, path );
+	 *		const position = writer.createPositionAt( root, offset );
 	 *		const selection = new DocumentSelection( position );
 	 *
 	 *		// Creates collapsed selection at the position of given item and offset.
@@ -290,19 +284,19 @@ export default class DocumentSelection {
 	 * an iterable of {@link module:engine/view/range~Range ranges} or null.
 	 *
 	 *		// Sets selection to the given range.
-	 *		const range = new Range( start, end );
+	 *		const range = writer.createRange( start, end );
 	 *		documentSelection._setTo( range );
 	 *
 	 *		// Sets selection to given ranges.
-	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( start2, end2 ) ];
 	 *		documentSelection._setTo( range );
 	 *
 	 *		// Sets selection to the other selection.
-	 *		const otherSelection = new Selection();
+	 *		const otherSelection = writer.createSelection();
 	 *		documentSelection._setTo( otherSelection );
 	 *
 	 * 		// Sets collapsed selection at the given position.
-	 *		const position = new Position( root, path );
+	 *		const position = writer.createPositionAt( root, offset );
 	 *		documentSelection._setTo( position );
 	 *
 	 * 		// Sets collapsed selection at the position of given item and offset.
@@ -352,12 +346,13 @@ export default class DocumentSelection {
 	/**
 	 * Moves {@link #focus} to the specified location.
 	 *
-	 * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
+	 * The location can be specified in the same form as {@link module:engine/view/view~View#createPositionAt view.createPositionAt()}
+	 * parameters.
 	 *
 	 * @protected
 	 * @fires change
 	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/view/item~Item view item}.
 	 */
 	_setFocus( itemOrPosition, offset ) {

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

@@ -534,7 +534,7 @@ export default class DomConverter {
 		const viewElement = this.mapDomToView( domParent );
 
 		if ( viewElement && viewElement.is( 'uiElement' ) ) {
-			return ViewPosition.createBefore( viewElement );
+			return ViewPosition._createBefore( viewElement );
 		}
 
 		if ( isText( domParent ) ) {
@@ -1066,7 +1066,7 @@ export default class DomConverter {
 	 */
 	_getTouchingViewTextNode( node, getNext ) {
 		const treeWalker = new ViewTreeWalker( {
-			startPosition: getNext ? ViewPosition.createAfter( node ) : ViewPosition.createBefore( node ),
+			startPosition: getNext ? ViewPosition._createAfter( node ) : ViewPosition._createBefore( node ),
 			direction: getNext ? 'forward' : 'backward'
 		} );
 

+ 218 - 51
packages/ckeditor5-engine/src/view/downcastwriter.js

@@ -8,11 +8,12 @@
  */
 
 import Position from './position';
+import Range from './range';
+import Selection from './selection';
 import ContainerElement from './containerelement';
 import AttributeElement from './attributeelement';
 import EmptyElement from './emptyelement';
 import UIElement from './uielement';
-import Range from './range';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import DocumentFragment from './documentfragment';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
@@ -21,11 +22,19 @@ import EditableElement from './editableelement';
 import { isPlainObject } from 'lodash-es';
 
 /**
- * View downcast writer class. Provides set of methods used to properly manipulate nodes attached to
- * {@link module:engine/view/document~Document view document}. It is not recommended to use it directly. To get an instance
- * of view writer associated with the document use {@link module:engine/view/view~View#change view.change()) method.
- * The `DowncastWriter` is designed to work with semantic view which is the view downcasted from model. For working with
- * ordinary view (e.g. parsed from string) {@link module:engine/view/upcastwriter~UpcastWriter upcast writer} should be used.
+ * View downcast writer.
+ *
+ * It provides a set of methods used to manipulate view nodes.
+ *
+ * Do not create an instance of this writer manually. To modify a view structure, use
+ * the {@link module:engine/view/view~View#change `View#change()`} block.
+ *
+ * The `DowncastWriter` is designed to work with semantic views which are the views that were/are being downcasted from the model.
+ * To work with ordinary views (e.g. parsed from a pasted content) use the
+ * {@link module:engine/view/upcastwriter~UpcastWriter upcast writer}.
+ *
+ * Read more about changing the view in the {@glink framework/guides/architecture/editing-engine#changing-the-view Changing the view}
+ * section of the {@glink framework/guides/architecture/editing-engine Editing engine architecture} guide.
  */
 export default class DowncastWriter {
 	constructor( document ) {
@@ -40,7 +49,7 @@ export default class DowncastWriter {
 		 * The keys are `id`s, the values are `Set`s holding {@link module:engine/view/attributeelement~AttributeElement}s.
 		 *
 		 * @private
-		 * @type {Map}
+		 * @type {Map.<String,Set>}
 		 */
 		this._cloneGroups = new Map();
 	}
@@ -52,26 +61,26 @@ export default class DowncastWriter {
 	 * {@link module:engine/view/item~Item item}, {@link module:engine/view/range~Range range},
 	 * an iterable of {@link module:engine/view/range~Range ranges} or null.
 	 *
-	 * ### Usage:
+	 * Usage:
 	 *
 	 *		// Sets selection to the given range.
-	 *		const range = new Range( start, end );
+	 *		const range = writer.createRange( start, end );
 	 *		writer.setSelection( range );
 	 *
 	 *		// Sets backward selection to the given range.
-	 *		const range = new Range( start, end );
+	 *		const range = writer.createRange( start, end );
 	 *		writer.setSelection( range );
 	 *
 	 *		// Sets selection to given ranges.
-	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
 	 *		writer.setSelection( range );
 	 *
 	 *		// Sets selection to the other selection.
-	 *		const otherSelection = new Selection();
+	 *		const otherSelection = writer.createSelection();
 	 *		writer.setSelection( otherSelection );
 	 *
 	 * 		// Sets collapsed selection at the given position.
-	 *		const position = new Position( root, path );
+	 *		const position = writer.createPositionFromPath( root, path );
 	 *		writer.setSelection( position );
 	 *
 	 * 		// Sets collapsed selection at the position of given item and offset.
@@ -120,10 +129,11 @@ export default class DowncastWriter {
 	/**
 	 * Moves {@link module:engine/view/documentselection~DocumentSelection#focus selection's focus} to the specified location.
 	 *
-	 * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
+	 * The location can be specified in the same form as {@link module:engine/view/view~View#createPositionAt view.createPositionAt()}
+	 * parameters.
 	 *
 	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/view/item~Item view item}.
 	 */
 	setSelectionFocus( itemOrPosition, offset ) {
@@ -135,8 +145,8 @@ export default class DowncastWriter {
 	 *
 	 *		writer.createText( 'foo' );
 	 *
-	 * @param {String} data Text data.
-	 * @returns {module:engine/view/text~Text} Created text node.
+	 * @param {String} data The text's data.
+	 * @returns {module:engine/view/text~Text} The created text node.
 	 */
 	createText( data ) {
 		return new Text( data );
@@ -146,7 +156,7 @@ export default class DowncastWriter {
 	 * Creates new {@link module:engine/view/attributeelement~AttributeElement}.
 	 *
 	 *		writer.createAttributeElement( 'strong' );
-	 *		writer.createAttributeElement( 'strong', { 'alignment': 'center' } );
+	 *		writer.createAttributeElement( 'strong', { alignment: 'center' } );
 	 *
 	 *		// Make `<a>` element contain other attributes element so the `<a>` element is not broken.
 	 *		writer.createAttributeElement( 'a', { href: 'foo.bar' }, { priority: 5 } );
@@ -179,7 +189,7 @@ export default class DowncastWriter {
 	 * Creates new {@link module:engine/view/containerelement~ContainerElement}.
 	 *
 	 *		writer.createContainerElement( 'paragraph' );
-	 *		writer.createContainerElement( 'paragraph', { 'alignment': 'center' } );
+	 *		writer.createContainerElement( 'paragraph', { alignment: 'center' } );
 	 *
 	 * @param {String} name Name of the element.
 	 * @param {Object} [attributes] Elements attributes.
@@ -193,7 +203,7 @@ export default class DowncastWriter {
 	 * Creates new {@link module:engine/view/editableelement~EditableElement}.
 	 *
 	 *		writer.createEditableElement( 'div' );
-	 *		writer.createEditableElement( 'div', { 'alignment': 'center' } );
+	 *		writer.createEditableElement( 'div', { alignment: 'center' } );
 	 *
 	 * @param {String} name Name of the element.
 	 * @param {Object} [attributes] Elements attributes.
@@ -210,7 +220,7 @@ export default class DowncastWriter {
 	 * Creates new {@link module:engine/view/emptyelement~EmptyElement}.
 	 *
 	 *		writer.createEmptyElement( 'img' );
-	 *		writer.createEmptyElement( 'img', { 'alignment': 'center' } );
+	 *		writer.createEmptyElement( 'img', { alignment: 'center' } );
 	 *
 	 * @param {String} name Name of the element.
 	 * @param {Object} [attributes] Elements attributes.
@@ -224,7 +234,7 @@ export default class DowncastWriter {
 	 * Creates new {@link module:engine/view/uielement~UIElement}.
 	 *
 	 *		writer.createUIElement( 'span' );
-	 *		writer.createUIElement( 'span', { 'alignment': 'center' } );
+	 *		writer.createUIElement( 'span', { alignment: 'center' } );
 	 *
 	 * Custom render function can be provided as third parameter:
 	 *
@@ -452,19 +462,19 @@ export default class DowncastWriter {
 		}
 
 		if ( position.isAtStart ) {
-			return Position.createBefore( element );
+			return Position._createBefore( element );
 		} else if ( !position.isAtEnd ) {
 			const newElement = element._clone( false );
 
-			this.insert( Position.createAfter( element ), newElement );
+			this.insert( Position._createAfter( element ), newElement );
 
-			const sourceRange = new Range( position, Position.createAt( element, 'end' ) );
+			const sourceRange = new Range( position, Position._createAt( element, 'end' ) );
 			const targetPosition = new Position( newElement, 0 );
 
 			this.move( sourceRange, targetPosition );
 		}
 
-		return Position.createAfter( element );
+		return Position._createAfter( element );
 	}
 
 	/**
@@ -575,10 +585,10 @@ export default class DowncastWriter {
 		}
 
 		const lastChild = prev.getChild( prev.childCount - 1 );
-		const newPosition = lastChild instanceof Text ? Position.createAt( lastChild, 'end' ) : Position.createAt( prev, 'end' );
+		const newPosition = lastChild instanceof Text ? Position._createAt( lastChild, 'end' ) : Position._createAt( prev, 'end' );
 
-		this.move( Range.createIn( next ), Position.createAt( prev, 'end' ) );
-		this.remove( Range.createOn( next ) );
+		this.move( Range._createIn( next ), Position._createAt( prev, 'end' ) );
+		this.remove( Range._createOn( next ) );
 
 		return newPosition;
 	}
@@ -651,11 +661,14 @@ export default class DowncastWriter {
 	 * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
 	 * same parent container.
 	 *
-	 * @param {module:engine/view/range~Range} range Range to remove from container. After removing, it will be updated
+	 * @param {module:engine/view/range~Range|module:engine/view/item~Item} rangeOrItem Range to remove from container
+	 * or an {@link module:engine/view/item~Item item} to remove. If range is provided, after removing, it will be updated
 	 * to a collapsed range showing the new position.
 	 * @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
 	 */
-	remove( range ) {
+	remove( rangeOrItem ) {
+		const range = rangeOrItem instanceof Range ? rangeOrItem : Range._createOn( rangeOrItem );
+
 		validateRangeContainer( range );
 
 		// If range is collapsed - nothing to remove.
@@ -679,7 +692,7 @@ export default class DowncastWriter {
 		// Merge after removing.
 		const mergePosition = this.mergeAttributes( breakStart );
 		range.start = mergePosition;
-		range.end = Position.createFromPosition( mergePosition );
+		range.end = mergePosition.clone();
 
 		// Return removed nodes.
 		return new DocumentFragment( removed );
@@ -713,7 +726,7 @@ export default class DowncastWriter {
 			// When current item matches to the given element.
 			if ( item.is( 'element' ) && element.isSimilar( item ) ) {
 				// Create range on this element.
-				rangeToRemove = Range.createOn( item );
+				rangeToRemove = Range._createOn( item );
 				// When range starts inside Text or TextProxy element.
 			} else if ( !current.nextPosition.isAfter( range.start ) && item.is( 'textProxy' ) ) {
 				// We need to check if parent of this text matches to given element.
@@ -723,7 +736,7 @@ export default class DowncastWriter {
 
 				// If it is then create range inside this element.
 				if ( parentElement ) {
-					rangeToRemove = Range.createIn( parentElement );
+					rangeToRemove = Range._createIn( parentElement );
 				}
 			}
 
@@ -905,19 +918,178 @@ export default class DowncastWriter {
 	 *
 	 * Since this function creates a new element and removes the given one, the new element is returned to keep reference.
 	 *
-	 * @param {module:engine/view/containerelement~ContainerElement} viewElement Element to be renamed.
 	 * @param {String} newName New name for element.
+	 * @param {module:engine/view/containerelement~ContainerElement} viewElement Element to be renamed.
 	 */
-	rename( viewElement, newName ) {
+	rename( newName, viewElement ) {
 		const newElement = new ContainerElement( newName, viewElement.getAttributes() );
 
-		this.insert( Position.createAfter( viewElement ), newElement );
-		this.move( Range.createIn( viewElement ), Position.createAt( newElement ) );
-		this.remove( Range.createOn( viewElement ) );
+		this.insert( Position._createAfter( viewElement ), newElement );
+		this.move( Range._createIn( viewElement ), Position._createAt( newElement, 0 ) );
+		this.remove( Range._createOn( viewElement ) );
 
 		return newElement;
 	}
 
+	/**
+	 * Cleans up memory by removing obsolete cloned elements group from the writer.
+	 *
+	 * Should be used whenever all {@link module:engine/view/attributeelement~AttributeElement attribute elements}
+	 * with the same {@link module:engine/view/attributeelement~AttributeElement#id id} are going to be removed from the view and
+	 * the group will no longer be needed.
+	 *
+	 * Cloned elements group are not removed automatically in case if the group is still needed after all its elements
+	 * were removed from the view.
+	 *
+	 * Keep in mind that group names are equal to the `id` property of the attribute element.
+	 *
+	 * @param {String} groupName Name of the group to clear.
+	 */
+	clearClonedElementsGroup( groupName ) {
+		this._cloneGroups.delete( groupName );
+	}
+
+	/**
+	 * Creates position at the given location. The location can be specified as:
+	 *
+	 * * a {@link module:engine/view/position~Position position},
+	 * * parent element and offset (offset defaults to `0`),
+	 * * parent element and `'end'` (sets position at the end of that element),
+	 * * {@link module:engine/view/item~Item view item} and `'before'` or `'after'` (sets position before or after given view item).
+	 *
+	 * This method is a shortcut to other constructors such as:
+	 *
+	 * * {@link #createPositionBefore},
+	 * * {@link #createPositionAfter},
+	 *
+	 * @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
+	 * first parameter is a {@link module:engine/view/item~Item view item}.
+	 */
+	createPositionAt( itemOrPosition, offset ) {
+		return Position._createAt( itemOrPosition, offset );
+	}
+
+	/**
+	 * Creates a new position after given view item.
+	 *
+	 * @param {module:engine/view/item~Item} item View item after which the position should be located.
+	 * @returns {module:engine/view/position~Position}
+	 */
+	createPositionAfter( item ) {
+		return Position._createAfter( item );
+	}
+
+	/**
+	 * Creates a new position before given view item.
+	 *
+	 * @param {module:engine/view/item~Item} item View item before which the position should be located.
+	 * @returns {module:engine/view/position~Position}
+	 */
+	createPositionBefore( item ) {
+		return Position._createBefore( item );
+	}
+
+	/**
+	 * Creates a range spanning from `start` position to `end` position.
+	 *
+	 * **Note:** This factory method creates it's own {@link module:engine/view/position~Position} instances basing on passed values.
+	 *
+	 * @param {module:engine/view/position~Position} start Start position.
+	 * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
+	 * @returns {module:engine/view/range~Range}
+	 */
+	createRange( start, end ) {
+		return new Range( start, end );
+	}
+
+	/**
+	 * Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
+	 *
+	 * @param {module:engine/view/item~Item} item
+	 * @returns {module:engine/view/range~Range}
+	 */
+	createRangeOn( item ) {
+		return Range._createOn( item );
+	}
+
+	/**
+	 * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
+	 * that element and ends after the last child of that element.
+	 *
+	 * @param {module:engine/view/element~Element} element Element which is a parent for the range.
+	 * @returns {module:engine/view/range~Range}
+	 */
+	createRangeIn( element ) {
+		return Range._createIn( element );
+	}
+
+	/**
+	 Creates new {@link module:engine/view/selection~Selection} instance.
+	 *
+	 * 		// Creates empty selection without ranges.
+	 *		const selection = writer.createSelection();
+	 *
+	 *		// Creates selection at the given range.
+	 *		const range = writer.createRange( start, end );
+	 *		const selection = writer.createSelection( range );
+	 *
+	 *		// Creates selection at the given ranges
+	 * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
+	 *		const selection = writer.createSelection( ranges );
+	 *
+	 *		// Creates selection from the other selection.
+	 *		const otherSelection = writer.createSelection();
+	 *		const selection = writer.createSelection( otherSelection );
+	 *
+	 *		// Creates selection from the document selection.
+	 *		const selection = writer.createSelection( editor.editing.view.document.selection );
+	 *
+	 * 		// Creates selection at the given position.
+	 *		const position = writer.createPositionFromPath( root, path );
+	 *		const selection = writer.createSelection( position );
+	 *
+	 *		// Creates collapsed selection at the position of given item and offset.
+	 *		const paragraph = writer.createContainerElement( 'paragraph' );
+	 *		const selection = writer.createSelection( paragraph, offset );
+	 *
+	 *		// Creates a range inside an {@link module:engine/view/element~Element element} which starts before the
+	 *		// first child of that element and ends after the last child of that element.
+	 *		const selection = writer.createSelection( paragraph, 'in' );
+	 *
+	 *		// Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
+	 *		// just after the item.
+	 *		const selection = writer.createSelection( paragraph, 'on' );
+	 *
+	 * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
+	 *
+	 *		// Creates backward selection.
+	 *		const selection = writer.createSelection( range, { backward: true } );
+	 *
+	 * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
+	 * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
+	 * represented in other way, for example by applying proper CSS class.
+	 *
+	 * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
+	 * (and be  properly handled by screen readers).
+	 *
+	 *		// Creates fake selection with label.
+	 *		const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
+	 *
+	 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection|
+	 * module:engine/view/position~Position|Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|
+	 * module:engine/view/item~Item|null} [selectable=null]
+	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
+	 * @param {Object} [options]
+	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
+	 * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
+	 * @param {String} [options.label] Label for the fake selection.
+	 * @returns {module:engine/view/selection~Selection}
+	 */
+	createSelection( selectable, placeOrOffset, options ) {
+		return new Selection( selectable, placeOrOffset, options );
+	}
+
 	/**
 	 * Wraps children with provided `attribute`. Only children contained in `parent` element between
 	 * `startOffset` and `endOffset` will be wrapped.
@@ -981,7 +1153,7 @@ export default class DowncastWriter {
 			}
 		}
 
-		return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
+		return Range._createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
 	}
 
 	/**
@@ -1053,7 +1225,7 @@ export default class DowncastWriter {
 			}
 		}
 
-		return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
+		return Range._createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
 	}
 
 	/**
@@ -1073,8 +1245,8 @@ export default class DowncastWriter {
 		if ( rangeSpansOnAllChildren( range ) && this._wrapAttributeElement( attribute, range.start.parent ) ) {
 			const parent = range.start.parent;
 
-			const end = this.mergeAttributes( Position.createAfter( parent ) );
-			const start = this.mergeAttributes( Position.createBefore( parent ) );
+			const end = this.mergeAttributes( Position._createAfter( parent ) );
+			const start = this.mergeAttributes( Position._createBefore( parent ) );
 
 			return new Range( start, end );
 		}
@@ -1134,7 +1306,7 @@ export default class DowncastWriter {
 	_wrapPosition( position, attribute ) {
 		// Return same position when trying to wrap with attribute similar to position parent.
 		if ( attribute.isSimilar( position.parent ) ) {
-			return movePositionToTextNode( Position.createFromPosition( position ) );
+			return movePositionToTextNode( position.clone() );
 		}
 
 		// When position is inside text node - break it and place new position between two text nodes.
@@ -1378,12 +1550,12 @@ export default class DowncastWriter {
 
 		// There are no attributes to break and text nodes breaking is not forced.
 		if ( !forceSplitText && positionParent.is( 'text' ) && isContainerOrFragment( positionParent.parent ) ) {
-			return Position.createFromPosition( position );
+			return position.clone();
 		}
 
 		// Position's parent is container, so no attributes to break.
 		if ( isContainerOrFragment( positionParent ) ) {
-			return Position.createFromPosition( position );
+			return position.clone();
 		}
 
 		// Break text and start again in new position.
@@ -1516,11 +1688,6 @@ export default class DowncastWriter {
 		group.delete( element );
 		// Not removing group from element on purpose!
 		// If other parts of code have reference to this element, they will be able to get references to other elements from the group.
-		// If all other elements are removed from the set, everything will be garbage collected.
-
-		if ( group.size === 0 ) {
-			this._cloneGroups.delete( id );
-		}
 	}
 }
 

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

@@ -20,6 +20,9 @@ const documentSymbol = Symbol( 'document' );
  *
  * Editable is automatically read-only when its {@link module:engine/view/document~Document Document} is read-only.
  *
+ * The constructor of this class shouldn't be used directly. To create new `EditableElement` use the
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createEditableElement `downcastWriter#createEditableElement()`} method.
+ *
  * @extends module:engine/view/containerelement~ContainerElement
  * @mixes module:utils/observablemixin~ObservableMixin
  */
@@ -63,6 +66,17 @@ export default class EditableElement extends ContainerElement {
 		 */
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	is( type, name = null ) {
+		if ( !name ) {
+			return type == 'editableElement' || super.is( type );
+		} else {
+			return ( type == 'editableElement' && name == this.name ) || super.is( type, name );
+		}
+	}
+
 	/**
 	 * Returns document associated with the editable.
 	 *

+ 19 - 23
packages/ckeditor5-engine/src/view/element.js

@@ -22,20 +22,21 @@ import { isPlainObject } from 'lodash-es';
  * This is why the type of the {@link module:engine/view/element~Element} need to
  * be defined by the feature developer. When creating an element you should use one of the following methods:
  *
- * * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement `writer.createContainerElement()`} in order to create
- * a {@link module:engine/view/containerelement~ContainerElement},
- * * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement `writer.createAttributeElement()`} in order to create
- * a {@link module:engine/view/attributeelement~AttributeElement},
- * * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement `writer.createEmptyElement()`} in order to create
- * a {@link module:engine/view/emptyelement~EmptyElement}.
- * * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement `writer.createUIElement()`} in order to create
- * a {@link module:engine/view/uielement~UIElement}.
- * * {@link module:engine/view/downcastwriter~DowncastWriter#createEditableElement `writer.createEditableElement()`} in order to create
- * a {@link module:engine/view/editableelement~EditableElement}.
+ * * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement `downcastWriter#createContainerElement()`}
+ * in order to create a {@link module:engine/view/containerelement~ContainerElement},
+ * * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement `downcastWriter#createAttributeElement()`}
+ * in order to create a {@link module:engine/view/attributeelement~AttributeElement},
+ * * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement `downcastWriter#createEmptyElement()`}
+ * in order to create a {@link module:engine/view/emptyelement~EmptyElement}.
+ * * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement `downcastWriter#createUIElement()`}
+ * in order to create a {@link module:engine/view/uielement~UIElement}.
+ * * {@link module:engine/view/downcastwriter~DowncastWriter#createEditableElement `downcastWriter#createEditableElement()`}
+ * in order to create a {@link module:engine/view/editableelement~EditableElement}.
  *
  * Note that for view elements which are not created from the model, like elements from mutations, paste or
- * {@link module:engine/controller/datacontroller~DataController#set data.set} it is not possible to define the type of the element, so
- * these will be instances of the {@link module:engine/view/element~Element}.
+ * {@link module:engine/controller/datacontroller~DataController#set data.set} it is not possible to define the type of the element.
+ * In such cases the {@link module:engine/view/upcastwriter~UpcastWriter#createElement `UpcastWriter#createElement()`} method
+ * should be used to create generic view elements.
  *
  * @extends module:engine/view/node~Node
  */
@@ -45,22 +46,15 @@ export default class Element extends Node {
 	 *
 	 * Attributes can be passed in various formats:
 	 *
-	 *		new Element( 'div', { 'class': 'editor', 'contentEditable': 'true' } ); // object
+	 *		new Element( 'div', { class: 'editor', contentEditable: 'true' } ); // object
 	 *		new Element( 'div', [ [ 'class', 'editor' ], [ 'contentEditable', 'true' ] ] ); // map-like iterator
 	 *		new Element( 'div', mapOfAttributes ); // map
 	 *
-	 * **Note:** Constructor of this class shouldn't be used directly in the code. Use the
-	 * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement} for inline element,
-	 * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement} for block element,
-	 * {@link module:engine/view/downcastwriter~DowncastWriter#createEditableElement} for editable element,
-	 * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement} for empty element or
-	 * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement} for UI element instead.
-	 *
 	 * @protected
 	 * @param {String} name Node name.
 	 * @param {Object|Iterable} [attrs] Collection of attributes.
 	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
-	 * List of nodes to be inserted into created element.
+	 * A list of nodes to be inserted into created element.
 	 */
 	constructor( name, attrs, children ) {
 		super();
@@ -574,6 +568,7 @@ export default class Element extends Node {
 	 * Removes number of child nodes starting at the given index and set the parent of these nodes to `null`.
 	 *
 	 * @see module:engine/view/downcastwriter~DowncastWriter#remove
+	 * @protected
 	 * @param {Number} index Number of the first node to remove.
 	 * @param {Number} [howMany=1] Number of nodes to remove.
 	 * @fires module:engine/view/node~Node#change
@@ -675,6 +670,7 @@ export default class Element extends Node {
 	 *		element._removeClass( [ 'foo', 'bar' ] ); // Removes both 'foo' and 'bar' classes.
 	 *
 	 * @see module:engine/view/downcastwriter~DowncastWriter#removeClass
+	 * @protected
 	 * @param {Array.<String>|String} className
 	 * @fires module:engine/view/node~Node#change
 	 */
@@ -840,8 +836,7 @@ function parseInlineStyles( stylesMap, stylesString ) {
 
 					break;
 
-				// eslint-disable-next-line no-case-declarations
-				case ';':
+				case ';': {
 					// Property value just ended.
 					// Use previously stored property value start to obtain property value.
 					const propertyValue = stylesString.substr( propertyValueStart, i - propertyValueStart );
@@ -857,6 +852,7 @@ function parseInlineStyles( stylesMap, stylesString ) {
 					propertyNameStart = i + 1;
 
 					break;
+				}
 			}
 		} else if ( char === quoteType ) {
 			// If a quote char is found and it is a closing quote, mark this fact by `null`-ing `quoteType`.

+ 4 - 1
packages/ckeditor5-engine/src/view/emptyelement.js

@@ -12,7 +12,10 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import Node from './node';
 
 /**
- * EmptyElement class. It is used to represent elements that cannot contain any child nodes.
+ * Empty element class. It is used to represent elements that cannot contain any child nodes (for example `<img>` elements).
+ *
+ * To create a new empty element use the
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement `downcastWriter#createEmptyElement()`} method.
  */
 export default class EmptyElement extends Element {
 	/**

+ 10 - 7
packages/ckeditor5-engine/src/view/node.js

@@ -16,13 +16,15 @@ import { clone } from 'lodash-es';
 /**
  * Abstract tree view node class.
  *
+ * This is an abstract class. Its constructor should not be used directly.
+ * Use the {@link module:engine/view/element~Element} class to create view elements
+ * or {@link module:engine/view/text~Text} class to create view text nodes.
+ *
  * @abstract
  */
 export default class Node {
 	/**
 	 * Creates a tree view node.
-	 *
-	 * This is an abstract class, so this constructor should not be used directly.
 	 */
 	constructor() {
 		/**
@@ -123,11 +125,11 @@ export default class Node {
 	 * Gets a path to the node. The path is an array containing indices of consecutive ancestors of this node,
 	 * beginning from {@link module:engine/view/node~Node#root root}, down to this node's index.
 	 *
-	 *		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 ] );
+	 *		const abc = downcastWriter.createText( 'abc' );
+	 *		const foo = downcastWriter.createText( 'foo' );
+	 *		const h1 = downcastWriter.createElement( 'h1', null, downcastWriter.createText( 'header' ) );
+	 *		const p = downcastWriter.createElement( 'p', null, [ abc, foo ] );
+	 *		const div = downcastWriter.createElement( '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 [].
@@ -257,6 +259,7 @@ export default class Node {
 	}
 
 	/**
+	 * @protected
 	 * @param {module:engine/view/document~ChangeType} type Type of the change.
 	 * @param {module:engine/view/node~Node} node Changed node.
 	 * @fires change

+ 50 - 25
packages/ckeditor5-engine/src/view/position.js

@@ -14,7 +14,13 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import EditableElement from './editableelement';
 
 /**
- * Position in the tree. Position is always located before or after a node.
+ * Position in the view tree. Position is represented by its parent node and an offset in this parent.
+ *
+ * In order to create a new position instance use the `createPosition*()` factory methods available in:
+ *
+ * * {@module:engine/view/view~View}
+ * * {@module:engine/view/downcastwriter~DowncastWriter}
+ * * {@module:engine/view/upcastwriter~UpcastWriter}
  */
 export default class Position {
 	/**
@@ -131,7 +137,7 @@ export default class Position {
 	 * @returns {module:engine/view/position~Position} Shifted position.
 	 */
 	getShiftedBy( shift ) {
-		const shifted = Position.createFromPosition( this );
+		const shifted = Position._createAt( this );
 
 		const offset = shifted.offset + shift;
 		shifted.offset = offset < 0 ? 0 : offset;
@@ -274,6 +280,25 @@ export default class Position {
 		}
 	}
 
+	/**
+	 * Creates a {@link module:engine/view/treewalker~TreeWalker TreeWalker} instance with this positions as a start position.
+	 *
+	 * @param {Object} options Object with configuration options. See {@link module:engine/view/treewalker~TreeWalker}
+	 * @param {module:engine/view/range~Range} [options.boundaries=null] Range to define boundaries of the iterator.
+	 * @param {Boolean} [options.singleCharacters=false]
+	 * @param {Boolean} [options.shallow=false]
+	 * @param {Boolean} [options.ignoreElementEnd=false]
+	 */
+	getWalker( options = {} ) {
+		options.startPosition = this;
+
+		return new TreeWalker( options );
+	}
+
+	clone() {
+		return new Position( this.parent, this.offset );
+	}
+
 	/**
 	 * Creates position at the given location. The location can be specified as:
 	 *
@@ -284,28 +309,36 @@ export default class Position {
 	 *
 	 * This method is a shortcut to other constructors such as:
 	 *
-	 * * {@link module:engine/view/position~Position.createBefore},
-	 * * {@link module:engine/view/position~Position.createAfter},
-	 * * {@link module:engine/view/position~Position.createFromPosition}.
+	 * * {@link module:engine/view/position~Position._createBefore},
+	 * * {@link module:engine/view/position~Position._createAfter}.
 	 *
-	 * @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @protected
+	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/view/item~Item view item}.
 	 */
-	static createAt( itemOrPosition, offset ) {
+	static _createAt( itemOrPosition, offset ) {
 		if ( itemOrPosition instanceof Position ) {
-			return this.createFromPosition( itemOrPosition );
+			return new this( itemOrPosition.parent, itemOrPosition.offset );
 		} else {
 			const node = itemOrPosition;
 
 			if ( offset == 'end' ) {
 				offset = node.is( 'text' ) ? node.data.length : node.childCount;
 			} else if ( offset == 'before' ) {
-				return this.createBefore( node );
+				return this._createBefore( node );
 			} else if ( offset == 'after' ) {
-				return this.createAfter( node );
-			} else if ( !offset ) {
-				offset = 0;
+				return this._createAfter( node );
+			} else if ( offset !== 0 && !offset ) {
+				/**
+				 * {@link module:engine/view/view~View#createPositionAt `View#createPositionAt()`}
+				 * requires the offset to be specified when the first parameter is a view item.
+				 *
+				 * @error view-createPositionAt-offset-required
+				 */
+				throw new CKEditorError(
+					'view-createPositionAt-offset-required: ' +
+					'View#createPositionAt() requires the offset when the first parameter is a view item.' );
 			}
 
 			return new Position( node, offset );
@@ -315,10 +348,11 @@ export default class Position {
 	/**
 	 * Creates a new position after given view item.
 	 *
+	 * @protected
 	 * @param {module:engine/view/item~Item} item View item after which the position should be located.
 	 * @returns {module:engine/view/position~Position}
 	 */
-	static createAfter( item ) {
+	static _createAfter( item ) {
 		// TextProxy is not a instance of Node so we need do handle it in specific way.
 		if ( item.is( 'textProxy' ) ) {
 			return new Position( item.textNode, item.offsetInText + item.data.length );
@@ -340,10 +374,11 @@ export default class Position {
 	/**
 	 * Creates a new position before given view item.
 	 *
+	 * @protected
 	 * @param {module:engine/view/item~Item} item View item before which the position should be located.
 	 * @returns {module:engine/view/position~Position}
 	 */
-	static createBefore( item ) {
+	static _createBefore( item ) {
 		// TextProxy is not a instance of Node so we need do handle it in specific way.
 		if ( item.is( 'textProxy' ) ) {
 			return new Position( item.textNode, item.offsetInText );
@@ -361,16 +396,6 @@ export default class Position {
 
 		return new Position( item.parent, item.index );
 	}
-
-	/**
-	 * Creates and returns a new instance of `Position`, which is equal to the passed position.
-	 *
-	 * @param {module:engine/view/position~Position} position Position to be cloned.
-	 * @returns {module:engine/view/position~Position}
-	 */
-	static createFromPosition( position ) {
-		return new this( position.parent, position.offset );
-	}
 }
 
 /**

+ 65 - 65
packages/ckeditor5-engine/src/view/range.js

@@ -11,7 +11,13 @@ import Position from './position';
 import TreeWalker from './treewalker';
 
 /**
- * Tree view range.
+ * Range in the view tree. A range is represented by its start and end {@link module:engine/view/position~Position positions}.
+ *
+ * In order to create a new position instance use the `createPosition*()` factory methods available in:
+ *
+ * * {@module:engine/view/view~View}
+ * * {@module:engine/view/downcastwriter~DowncastWriter}
+ * * {@module:engine/view/upcastwriter~UpcastWriter}
  */
 export default class Range {
 	/**
@@ -20,7 +26,7 @@ export default class Range {
 	 * **Note:** Constructor creates it's own {@link module:engine/view/position~Position} instances basing on passed values.
 	 *
 	 * @param {module:engine/view/position~Position} start Start position.
-	 * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
+	 * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at the `start` position.
 	 */
 	constructor( start, end = null ) {
 		/**
@@ -29,7 +35,7 @@ export default class Range {
 		 * @readonly
 		 * @member {module:engine/view/position~Position}
 		 */
-		this.start = Position.createFromPosition( start );
+		this.start = start.clone();
 
 		/**
 		 * End position.
@@ -37,7 +43,7 @@ export default class Range {
 		 * @readonly
 		 * @member {module:engine/view/position~Position}
 		 */
-		this.end = end ? Position.createFromPosition( end ) : Position.createFromPosition( start );
+		this.end = end ? end.clone() : start.clone();
 	}
 
 	/**
@@ -91,13 +97,14 @@ export default class Range {
 	 *
 	 * For example:
 	 *
-	 * 		<p>Foo</p><p><b>{Bar}</b></p> -> <p>Foo</p>[<p><b>Bar</b>]</p>
-	 * 		<p><b>foo</b>{bar}<span></span></p> -> <p><b>foo[</b>bar<span></span>]</p>
+	 *		<p>Foo</p><p><b>{Bar}</b></p> -> <p>Foo</p>[<p><b>Bar</b>]</p>
+	 *		<p><b>foo</b>{bar}<span></span></p> -> <p><b>foo[</b>bar<span></span>]</p>
 	 *
 	 * Note that in the sample above:
-	 *  - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
-	 *  - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
-	 *  - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
+	 *
+	 * - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
+	 * - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
+	 * - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
 	 *
 	 * @returns {module:engine/view/range~Range} Enlarged range.
 	 */
@@ -107,11 +114,11 @@ export default class Range {
 
 		// Fix positions, in case if they are in Text node.
 		if ( start.parent.is( 'text' ) && start.isAtStart ) {
-			start = Position.createBefore( start.parent );
+			start = Position._createBefore( start.parent );
 		}
 
 		if ( end.parent.is( 'text' ) && end.isAtEnd ) {
-			end = Position.createAfter( end.parent );
+			end = Position._createAfter( end.parent );
 		}
 
 		return new Range( start, end );
@@ -123,13 +130,14 @@ export default class Range {
 	 *
 	 * For example:
 	 *
-	 * 		<p>Foo</p>[<p><b>Bar</b>]</p> -> <p>Foo</p><p><b>{Bar}</b></p>
-	 * 		<p><b>foo[</b>bar<span></span>]</p> -> <p><b>foo</b>{bar}<span></span></p>
+	 *		<p>Foo</p>[<p><b>Bar</b>]</p> -> <p>Foo</p><p><b>{Bar}</b></p>
+	 *		<p><b>foo[</b>bar<span></span>]</p> -> <p><b>foo</b>{bar}<span></span></p>
 	 *
 	 * Note that in the sample above:
-	 *  - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
-	 *  - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
-	 *  - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
+	 *
+	 * - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
+	 * - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
+	 * - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
 	 *
 	 * @returns {module:engine/view/range~Range} Shrink range.
 	 */
@@ -205,21 +213,24 @@ export default class Range {
 	 *
 	 * Examples:
 	 *
-	 *		let foo = new Text( 'foo' );
-	 *		let img = new ContainerElement( 'img' );
-	 *		let bar = new Text( 'bar' );
-	 *		let p = new ContainerElement( 'p', null, [ foo, img, bar ] );
+	 *		let foo = downcastWriter.createText( 'foo' );
+	 *		let img = downcastWriter.createContainerElement( 'img' );
+	 *		let bar = downcastWriter.createText( 'bar' );
+	 *		let p = downcastWriter.createContainerElement( 'p', null, [ foo, img, bar ] );
 	 *
-	 *		let range = new Range( new Position( foo, 2 ), new Position( bar, 1 ); // "o", img, "b" are in range.
-	 *		let otherRange = new Range( new Position( foo, 1 ), new Position( bar, 2 ); "oo", img, "ba" are in range.
+	 *		let range = view.createRange( view.createPositionAt( foo, 2 ), view.createPositionAt( bar, 1 ); // "o", img, "b" are in range.
+	 *		let otherRange = view.createRange( // "oo", img, "ba" are in range.
+	 *			view.createPositionAt( foo, 1 ),
+	 *			view.createPositionAt( bar, 2 )
+	 *		);
 	 *		let transformed = range.getDifference( otherRange );
 	 *		// transformed array has no ranges because `otherRange` contains `range`
 	 *
-	 *		otherRange = new Range( new Position( foo, 1 ), new Position( p, 2 ); // "oo", img are in range.
+	 *		otherRange = view.createRange( view.createPositionAt( foo, 1 ), view.createPositionAt( p, 2 ); // "oo", img are in range.
 	 *		transformed = range.getDifference( otherRange );
 	 *		// transformed array has one range: from ( p, 2 ) to ( bar, 1 )
 	 *
-	 *		otherRange = new Range( new Position( p, 1 ), new Position( p, 2 ) ); // img is in range.
+	 *		otherRange = view.createRange( view.createPositionAt( p, 1 ), view.createPositionAt( p, 2 ) ); // img is in range.
 	 *		transformed = range.getDifference( otherRange );
 	 *		// transformed array has two ranges: from ( foo, 1 ) to ( p, 1 ) and from ( p, 2 ) to ( bar, 1 )
 	 *
@@ -245,7 +256,7 @@ export default class Range {
 			}
 		} else {
 			// Ranges do not intersect, return the original range.
-			ranges.push( Range.createFromRange( this ) );
+			ranges.push( this.clone() );
 		}
 
 		return ranges;
@@ -257,16 +268,16 @@ export default class Range {
 	 *
 	 * Examples:
 	 *
-	 *		let foo = new Text( 'foo' );
-	 *		let img = new ContainerElement( 'img' );
-	 *		let bar = new Text( 'bar' );
-	 *		let p = new ContainerElement( 'p', null, [ foo, img, bar ] );
+	 *		let foo = downcastWriter.createText( 'foo' );
+	 *		let img = downcastWriter.createContainerElement( 'img' );
+	 *		let bar = downcastWriter.createText( 'bar' );
+	 *		let p = downcastWriter.createContainerElement( 'p', null, [ foo, img, bar ] );
 	 *
-	 *		let range = new Range( new Position( foo, 2 ), new Position( bar, 1 ); // "o", img, "b" are in range.
-	 *		let otherRange = new Range( new Position( foo, 1 ), new Position( p, 2 ); // "oo", img are in range.
+	 *		let range = view.createRange( view.createPositionAt( foo, 2 ), view.createPositionAt( bar, 1 ); // "o", img, "b" are in range.
+	 *		let otherRange = view.createRange( view.createPositionAt( foo, 1 ), view.createPositionAt( p, 2 ); // "oo", img are in range.
 	 *		let transformed = range.getIntersection( otherRange ); // range from ( foo, 1 ) to ( p, 2 ).
 	 *
-	 *		otherRange = new Range( new Position( bar, 1 ), new Position( bar, 3 ); "ar" is in range.
+	 *		otherRange = view.createRange( view.createPositionAt( bar, 1 ), view.createPositionAt( bar, 3 ); "ar" is in range.
 	 *		transformed = range.getIntersection( otherRange ); // null - no common part.
 	 *
 	 * @param {module:engine/view/range~Range} otherRange Range to check for intersection.
@@ -306,6 +317,7 @@ export default class Range {
 	 * @param {Boolean} [options.singleCharacters=false]
 	 * @param {Boolean} [options.shallow=false]
 	 * @param {Boolean} [options.ignoreElementEnd=false]
+	 * @returns {module:engine/view/treewalker~TreeWalker}
 	 */
 	getWalker( options = {} ) {
 		options.boundaries = this;
@@ -323,6 +335,15 @@ export default class Range {
 		return this.start.getCommonAncestor( this.end );
 	}
 
+	/**
+	 * Clones this range.
+	 *
+	 * @returns {module:engine/view/range~Range}
+	 */
+	clone() {
+		return new Range( this.start, this.end );
+	}
+
 	/**
 	 * Returns an iterator that iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns
 	 * them.
@@ -374,7 +395,7 @@ export default class Range {
 	}
 
 	/**
-	 * Checks and returns whether this range intersects with given range.
+	 * Checks and returns whether this range intersects with the given range.
 	 *
 	 * @param {module:engine/view/range~Range} otherRange Range to compare with.
 	 * @returns {Boolean} True if ranges intersect.
@@ -384,8 +405,9 @@ export default class Range {
 	}
 
 	/**
-	 * Creates a range from given parents and offsets.
+	 * Creates a range from the given parents and offsets.
 	 *
+	 * @protected
 	 * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} startElement Start position
 	 * parent element.
 	 * @param {Number} startOffset Start position offset.
@@ -394,32 +416,23 @@ export default class Range {
 	 * @param {Number} endOffset End position offset.
 	 * @returns {module:engine/view/range~Range} Created range.
 	 */
-	static createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
+	static _createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
 		return new this(
 			new Position( startElement, startOffset ),
 			new Position( endElement, endOffset )
 		);
 	}
 
-	/**
-	 * Creates and returns a new instance of Range which is equal to passed range.
-	 *
-	 * @param {module:engine/view/range~Range} range Range to clone.
-	 * @returns {module:engine/view/range~Range}
-	 */
-	static createFromRange( range ) {
-		return new this( range.start, range.end );
-	}
-
 	/**
 	 * Creates a new range, spreading from specified {@link module:engine/view/position~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.
 	 *
+	 * @protected
 	 * @param {module:engine/view/position~Position} position Beginning of the range.
 	 * @param {Number} shift How long the range should be.
 	 * @returns {module:engine/view/range~Range}
 	 */
-	static createFromPositionAndShift( position, shift ) {
+	static _createFromPositionAndShift( position, shift ) {
 		const start = position;
 		const end = position.getShiftedBy( shift );
 
@@ -430,38 +443,25 @@ export default class Range {
 	 * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
 	 * that element and ends after the last child of that element.
 	 *
+	 * @protected
 	 * @param {module:engine/view/element~Element} element Element which is a parent for the range.
 	 * @returns {module:engine/view/range~Range}
 	 */
-	static createIn( element ) {
-		return this.createFromParentsAndOffsets( element, 0, element, element.childCount );
+	static _createIn( element ) {
+		return this._createFromParentsAndOffsets( element, 0, element, element.childCount );
 	}
 
 	/**
 	 * Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
 	 *
+	 * @protected
 	 * @param {module:engine/view/item~Item} item
 	 * @returns {module:engine/view/range~Range}
 	 */
-	static createOn( item ) {
+	static _createOn( item ) {
 		const size = item.is( 'textProxy' ) ? item.offsetSize : 1;
 
-		return this.createFromPositionAndShift( Position.createBefore( item ), size );
-	}
-
-	/**
-	 * Creates a collapsed range at given {@link module:engine/view/position~Position position}
-	 * or on the given {@link module:engine/view/item~Item item}.
-	 *
-	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
-	 * first parameter is a {@link module:engine/view/item~Item view item}.
-	 */
-	static createCollapsedAt( itemOrPosition, offset ) {
-		const start = Position.createAt( itemOrPosition, offset );
-		const end = Position.createFromPosition( start );
-
-		return new Range( start, end );
+		return this._createFromPositionAndShift( Position._createBefore( item ), size );
 	}
 }
 

+ 45 - 17
packages/ckeditor5-engine/src/view/renderer.js

@@ -3,6 +3,8 @@
  * For licensing, see LICENSE.md.
  */
 
+/* globals Node */
+
 /**
  * @module engine/view/renderer
  */
@@ -20,6 +22,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import isText from '@ckeditor/ckeditor5-utils/src/dom/istext';
 import isNode from '@ckeditor/ckeditor5-utils/src/dom/isnode';
 import fastDiff from '@ckeditor/ckeditor5-utils/src/fastdiff';
+import env from '@ckeditor/ckeditor5-utils/src/env';
 
 /**
  * Renderer is responsible for updating the DOM structure and the DOM selection based on
@@ -299,28 +302,23 @@ export default class Renderer {
 	 * @param {Node} domElement The DOM element representing the given view element.
 	 */
 	_updateElementMappings( viewElement, domElement ) {
-		// Because we replace new view element mapping with the existing one, the corresponding DOM element
-		// will not be rerendered. The new view element may have different attributes than the previous one.
-		// Since its corresponding DOM element will not be rerendered, new attributes will not be added
-		// to the DOM, so we need to mark it here to make sure its attributes gets updated.
-		// Such situations may happen if only new view element was added to `this.markedAttributes`
-		// or none of the elements were added (relying on 'this._updateChildren()' which by rerendering the element
-		// also rerenders its attributes). See #1427 for more detailed case study.
-		const newViewChild = this.domConverter.mapDomToView( domElement );
-
-		// It may also happen that 'newViewChild' mapping is not present since its parent mapping
-		// was already removed (the 'domConverter.unbindDomElement()' method also unbinds children
-		// mappings) so we also check for '!newViewChild'.
-		if ( !newViewChild || newViewChild && !newViewChild.isSimilar( viewElement ) ) {
-			this.markedAttributes.add( viewElement );
-		}
-
 		// Remap 'DomConverter' bindings.
 		this.domConverter.unbindDomElement( domElement );
 		this.domConverter.bindElements( domElement, viewElement );
 
 		// View element may have children which needs to be updated, but are not marked, mark them to update.
 		this.markedChildren.add( viewElement );
+
+		// Because we replace new view element mapping with the existing one, the corresponding DOM element
+		// will not be rerendered. The new view element may have different attributes than the previous one.
+		// Since its corresponding DOM element will not be rerendered, new attributes will not be added
+		// to the DOM, so we need to mark it here to make sure its attributes gets updated. See #1427 for more
+		// detailed case study.
+		// Also there are cases where replaced element is removed from the view structure and then has
+		// its attributes changed or removed. In such cases the element will not be present in `markedAttributes`
+		// and also may be the same (`element.isSimilar()`) as the reused element not having its attributes updated.
+		// To prevent such situations we always mark reused element to have its attributes rerenderd (#1560).
+		this.markedAttributes.add( viewElement );
 	}
 
 	/**
@@ -340,7 +338,7 @@ export default class Renderer {
 		const firstPos = this.selection.getFirstPosition();
 
 		if ( firstPos.parent.is( 'text' ) ) {
-			return ViewPosition.createBefore( this.selection.getFirstPosition().parent );
+			return ViewPosition._createBefore( this.selection.getFirstPosition().parent );
 		} else {
 			return firstPos;
 		}
@@ -762,6 +760,11 @@ export default class Renderer {
 
 		domSelection.collapse( anchor.parent, anchor.offset );
 		domSelection.extend( focus.parent, focus.offset );
+
+		// Firefox–specific hack (https://github.com/ckeditor/ckeditor5-engine/issues/1439).
+		if ( env.isGecko ) {
+			fixGeckoSelectionAfterBr( focus, domSelection );
+		}
 	}
 
 	/**
@@ -933,3 +936,28 @@ function sameNodes( blockFiller, actualDomChild, expectedDomChild ) {
 	// Not matching types.
 	return false;
 }
+
+// The following is a Firefox–specific hack (https://github.com/ckeditor/ckeditor5-engine/issues/1439).
+// When the native DOM selection is at the end of the block and preceded by <br /> e.g.
+//
+//		<p>foo<br/>[]</p>
+//
+// which happens a lot when using the soft line break, the browser fails to (visually) move the
+// caret to the new line. A quick fix is as simple as force–refreshing the selection with the same range.
+function fixGeckoSelectionAfterBr( focus, domSelection ) {
+	const parent = focus.parent;
+
+	// This fix works only when the focus point is at the very end of an element.
+	// There is no point in running it in cases unrelated to the browser bug.
+	if ( parent.nodeType != Node.ELEMENT_NODE || focus.offset != parent.childNodes.length - 1 ) {
+		return;
+	}
+
+	const childAtOffset = parent.childNodes[ focus.offset ];
+
+	// To stay on the safe side, the fix being as specific as possible, it targets only the
+	// selection which is at the very end of the element and preceded by <br />.
+	if ( childAtOffset && childAtOffset.tagName == 'BR' ) {
+		domSelection.addRange( domSelection.getRangeAt( 0 ) );
+	}
+}

+ 48 - 46
packages/ckeditor5-engine/src/view/selection.js

@@ -19,63 +19,64 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 import DocumentSelection from './documentselection';
 
 /**
- * Class representing selection in tree view.
+ * Class representing an arbirtary selection in the view.
+ * See also {@link module:engine/view/documentselection~DocumentSelection}.
  *
- * Selection can consist of {@link module:engine/view/range~Range ranges} that can be set using
- * {@link module:engine/view/selection~Selection#setTo setTo} method.
- * That method create copies of provided ranges and store those copies internally. Further modifications to passed
- * ranges will not change selection's state.
- * Selection's ranges can be obtained via {@link module:engine/view/selection~Selection#getRanges getRanges},
- * {@link module:engine/view/selection~Selection#getFirstRange getFirstRange} and
- * {@link module:engine/view/selection~Selection#getLastRange getLastRange} methods, which return copies of ranges
- * stored inside selection. Modifications made on these copies will not change selection's state. Similar situation
- * occurs when getting {@link module:engine/view/selection~Selection#anchor anchor},
- * {@link module:engine/view/selection~Selection#focus focus}, {@link module:engine/view/selection~Selection#getFirstPosition first}
- * and {@link module:engine/view/selection~Selection#getLastPosition last} positions - all will return
- * copies of requested positions.
+ * New selection instances can be created via the constructor or one these methods:
+ *
+ * * {@link module:engine/view/view~View#createSelection `View#createSelection()`},
+ * * {@link module:engine/view/upcastwriter~UpcastWriter#createSelection `UpcastWriter#createSelection()`}.
+ *
+ * A selection can consist of {@link module:engine/view/range~Range ranges} that can be set by using
+ * the {@link module:engine/view/selection~Selection#setTo `Selection#setTo()`} method.
  */
 export default class Selection {
 	/**
 	 * Creates new selection instance.
 	 *
+	 * **Note**: The selection constructor is available as a factory method:
+	 *
+	 * * {@link module:engine/view/view~View#createSelection `View#createSelection()`},
+	 * * {@link module:engine/view/upcastwriter~UpcastWriter#createSelection `UpcastWriter#createSelection()`}.
+	 *
 	 * 		// Creates empty selection without ranges.
-	 *		const selection = new Selection();
+	 *		const selection = writer.createSelection();
 	 *
 	 *		// Creates selection at the given range.
-	 *		const range = new Range( start, end );
-	 *		const selection = new Selection( range );
+	 *		const range = writer.createRange( start, end );
+	 *		const selection = writer.createSelection( range );
 	 *
 	 *		// Creates selection at the given ranges
-	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
-	 *		const selection = new Selection( ranges );
+	 * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
+	 *		const selection = writer.createSelection( ranges );
 	 *
 	 *		// Creates selection from the other selection.
-	 *		const otherSelection = new Selection();
-	 *		const selection = new Selection( otherSelection );
+	 *		const otherSelection = writer.createSelection();
+	 *		const selection = writer.createSelection( otherSelection );
 	 *
 	 *		// Creates selection from the document selection.
-	 *		const selection = new Selection( editor.editing.view.document.selection );
+	 *		const selection = writer.createSelection( editor.editing.view.document.selection );
 	 *
 	 * 		// Creates selection at the given position.
-	 *		const position = new Position( root, path );
-	 *		const selection = new Selection( position );
+	 *		const position = writer.createPositionFromPath( root, path );
+	 *		const selection = writer.createSelection( position );
 	 *
 	 *		// Creates collapsed selection at the position of given item and offset.
 	 *		const paragraph = writer.createContainerElement( 'paragraph' );
-	 *		const selection = new Selection( paragraph, offset );
+	 *		const selection = writer.createSelection( paragraph, offset );
 	 *
 	 *		// Creates a range inside an {@link module:engine/view/element~Element element} which starts before the
 	 *		// first child of that element and ends after the last child of that element.
-	 *		const selection = new Selection( paragraph, 'in' );
+	 *		const selection = writer.createSelection( paragraph, 'in' );
 	 *
 	 *		// Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
 	 *		// just after the item.
-	 *		const selection = new Selection( paragraph, 'on' );
+	 *		const selection = writer.createSelection( paragraph, 'on' );
 	 *
 	 * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
 	 *
 	 *		// Creates backward selection.
-	 *		const selection = new Selection( range, { backward: true } );
+	 *		const selection = writer.createSelection( range, { backward: true } );
 	 *
 	 * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
 	 * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
@@ -85,7 +86,7 @@ export default class Selection {
 	 * (and be  properly handled by screen readers).
 	 *
 	 *		// Creates fake selection with label.
-	 *		const selection = new Selection( range, { fake: true, label: 'foo' } );
+	 *		const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
 	 *
 	 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection|
 	 * module:engine/view/position~Position|Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|
@@ -168,7 +169,7 @@ export default class Selection {
 		const range = this._ranges[ this._ranges.length - 1 ];
 		const anchor = this._lastRangeBackward ? range.end : range.start;
 
-		return Position.createFromPosition( anchor );
+		return anchor.clone();
 	}
 
 	/**
@@ -184,7 +185,7 @@ export default class Selection {
 		const range = this._ranges[ this._ranges.length - 1 ];
 		const focus = this._lastRangeBackward ? range.start : range.end;
 
-		return Position.createFromPosition( focus );
+		return focus.clone();
 	}
 
 	/**
@@ -236,7 +237,7 @@ export default class Selection {
 	 */
 	* getRanges() {
 		for ( const range of this._ranges ) {
-			yield Range.createFromRange( range );
+			yield range.clone();
 		}
 	}
 
@@ -257,7 +258,7 @@ export default class Selection {
 			}
 		}
 
-		return first ? Range.createFromRange( first ) : null;
+		return first ? first.clone() : null;
 	}
 
 	/**
@@ -276,7 +277,7 @@ export default class Selection {
 			}
 		}
 
-		return last ? Range.createFromRange( last ) : null;
+		return last ? last.clone() : null;
 	}
 
 	/**
@@ -289,7 +290,7 @@ export default class Selection {
 	getFirstPosition() {
 		const firstRange = this.getFirstRange();
 
-		return firstRange ? Position.createFromPosition( firstRange.start ) : null;
+		return firstRange ? firstRange.start.clone() : null;
 	}
 
 	/**
@@ -302,7 +303,7 @@ export default class Selection {
 	getLastPosition() {
 		const lastRange = this.getLastRange();
 
-		return lastRange ? Position.createFromPosition( lastRange.end ) : null;
+		return lastRange ? lastRange.end.clone() : null;
 	}
 
 	/**
@@ -429,22 +430,22 @@ export default class Selection {
 	 * an iterable of {@link module:engine/view/range~Range ranges} or null.
 	 *
 	 *		// Sets selection to the given range.
-	 *		const range = new Range( start, end );
+	 *		const range = writer.createRange( start, end );
 	 *		selection.setTo( range );
 	 *
 	 *		// Sets selection to given ranges.
-	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
 	 *		selection.setTo( range );
 	 *
 	 *		// Sets selection to the other selection.
-	 *		const otherSelection = new Selection();
+	 *		const otherSelection = writer.createSelection();
 	 *		selection.setTo( otherSelection );
 	 *
 	 *	 	// Sets selection to contents of DocumentSelection.
 	 *		selection.setTo( editor.editing.view.document.selection );
 	 *
 	 * 		// Sets collapsed selection at the given position.
-	 *		const position = new Position( root, path );
+	 *		const position = writer.createPositionAt( root, path );
 	 *		selection.setTo( position );
 	 *
 	 * 		// Sets collapsed selection at the position of given item and offset.
@@ -515,11 +516,11 @@ export default class Selection {
 					'selection.setTo requires the second parameter when the first parameter is a node.'
 				);
 			} else if ( placeOrOffset == 'in' ) {
-				range = Range.createIn( selectable );
+				range = Range._createIn( selectable );
 			} else if ( placeOrOffset == 'on' ) {
-				range = Range.createOn( selectable );
+				range = Range._createOn( selectable );
 			} else {
-				range = Range.createCollapsedAt( selectable, placeOrOffset );
+				range = new Range( Position._createAt( selectable, placeOrOffset ) );
 			}
 
 			this._setRanges( [ range ], backward );
@@ -544,11 +545,12 @@ export default class Selection {
 	/**
 	 * Moves {@link #focus} to the specified location.
 	 *
-	 * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
+	 * The location can be specified in the same form as {@link module:engine/view/view~View#createPositionAt view.createPositionAt()}
+	 * parameters.
 	 *
 	 * @fires change
 	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/view/item~Item view item}.
 	 */
 	setFocus( itemOrPosition, offset ) {
@@ -563,7 +565,7 @@ export default class Selection {
 			);
 		}
 
-		const newFocus = Position.createAt( itemOrPosition, offset );
+		const newFocus = Position._createAt( itemOrPosition, offset );
 
 		if ( newFocus.compareWith( this.focus ) == 'same' ) {
 			return;
@@ -684,7 +686,7 @@ export default class Selection {
 			}
 		}
 
-		this._ranges.push( Range.createFromRange( range ) );
+		this._ranges.push( new Range( range.start, range.end ) );
 	}
 
 	/**

+ 9 - 6
packages/ckeditor5-engine/src/view/text.js

@@ -12,17 +12,20 @@ import Node from './node';
 /**
  * Tree view text node.
  *
+ * The constructor of this class shouldn't be used directly. To create new Text instances
+ * use the {@link module:engine/view/downcastwriter~DowncastWriter#createText `DowncastWriter#createText()`}
+ * method when working on data downcasted from the model or the
+ * {@link module:engine/view/upcastwriter~UpcastWriter#createText `UpcastWriter#createText()`}
+ * method when working on non-semantic views.
+ *
  * @extends module:engine/view/node~Node
  */
 export default class Text extends Node {
 	/**
 	 * Creates a tree view text node.
 	 *
-	 * **Note:** Constructor of this class shouldn't be used directly in the code.
-	 * Use the {@link module:engine/view/downcastwriter~DowncastWriter#createText} method instead.
-	 *
 	 * @protected
-	 * @param {String} data Text.
+	 * @param {String} data The text's data.
 	 */
 	constructor( data ) {
 		super();
@@ -57,8 +60,8 @@ export default class Text extends Node {
 	/**
 	 * This getter is required when using the addition assignment operator on protected property:
 	 *
-	 *		const foo = new Text( 'foo' );
-	 *		const bar = new Text( 'bar' );
+	 *		const foo = downcastWriter.createText( 'foo' );
+	 *		const bar = downcastWriter.createText( 'bar' );
 	 *
 	 *		foo._data += bar.data;   // executes: `foo._data = foo._data + bar.data`
 	 *		console.log( foo.data ); // prints: 'foobar'

+ 21 - 21
packages/ckeditor5-engine/src/view/treewalker.js

@@ -73,9 +73,9 @@ export default class TreeWalker {
 		 * @member {module:engine/view/position~Position} module:engine/view/treewalker~TreeWalker#position
 		 */
 		if ( options.startPosition ) {
-			this.position = Position.createFromPosition( options.startPosition );
+			this.position = Position._createAt( options.startPosition );
 		} else {
-			this.position = Position.createFromPosition( options.boundaries[ options.direction == 'backward' ? 'end' : 'start' ] );
+			this.position = Position._createAt( options.boundaries[ options.direction == 'backward' ? 'end' : 'start' ] );
 		}
 
 		/**
@@ -189,7 +189,7 @@ export default class TreeWalker {
 	 * @returns {module:engine/view/treewalker~TreeWalkerValue} return.value Information about taken step.
 	 */
 	_next() {
-		let position = Position.createFromPosition( this.position );
+		let position = this.position.clone();
 		const previousPosition = this.position;
 		const parent = position.parent;
 
@@ -210,7 +210,7 @@ export default class TreeWalker {
 		if ( parent instanceof Text ) {
 			if ( position.isAtEnd ) {
 				// Prevent returning "elementEnd" for Text node. Skip that value and return the next walker step.
-				this.position = Position.createAfter( parent );
+				this.position = Position._createAfter( parent );
 
 				return this._next();
 			}
@@ -244,7 +244,7 @@ export default class TreeWalker {
 				if ( node == this._boundaryEndParent ) {
 					charactersCount = this.boundaries.end.offset;
 					item = new TextProxy( node, 0, charactersCount );
-					position = Position.createAfter( item );
+					position = Position._createAfter( item );
 				} else {
 					item = new TextProxy( node, 0, node.data.length );
 					// If not just keep moving forward.
@@ -275,7 +275,7 @@ export default class TreeWalker {
 			return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength );
 		} else {
 			// `node` is not set, we reached the end of current `parent`.
-			position = Position.createAfter( parent );
+			position = Position._createAfter( parent );
 			this.position = position;
 
 			if ( this.ignoreElementEnd ) {
@@ -295,7 +295,7 @@ export default class TreeWalker {
 	 * @returns {module:engine/view/treewalker~TreeWalkerValue} return.value Information about taken step.
 	 */
 	_previous() {
-		let position = Position.createFromPosition( this.position );
+		let position = this.position.clone();
 		const previousPosition = this.position;
 		const parent = position.parent;
 
@@ -316,7 +316,7 @@ export default class TreeWalker {
 		if ( parent instanceof Text ) {
 			if ( position.isAtStart ) {
 				// Prevent returning "elementStart" for Text node. Skip that value and return the next walker step.
-				this.position = Position.createBefore( parent );
+				this.position = Position._createBefore( parent );
 
 				return this._previous();
 			}
@@ -358,7 +358,7 @@ export default class TreeWalker {
 
 					item = new TextProxy( node, offset, node.data.length - offset );
 					charactersCount = item.data.length;
-					position = Position.createBefore( item );
+					position = Position._createBefore( item );
 				} else {
 					item = new TextProxy( node, 0, node.data.length );
 					// If not just keep moving backward.
@@ -390,7 +390,7 @@ export default class TreeWalker {
 			return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength );
 		} else {
 			// `node` is not set, we reached the beginning of current `parent`.
-			position = Position.createBefore( parent );
+			position = Position._createBefore( parent );
 			this.position = position;
 
 			return this._formatReturnValue( 'elementStart', parent, previousPosition, position, 1 );
@@ -417,22 +417,22 @@ export default class TreeWalker {
 			// Position is at the end of Text.
 			if ( item.offsetInText + item.data.length == item.textNode.data.length ) {
 				if ( this.direction == 'forward' && !( this.boundaries && this.boundaries.end.isEqual( this.position ) ) ) {
-					nextPosition = Position.createAfter( item.textNode );
+					nextPosition = Position._createAfter( item.textNode );
 					// When we change nextPosition of returned value we need also update walker current position.
 					this.position = nextPosition;
 				} else {
-					previousPosition = Position.createAfter( item.textNode );
+					previousPosition = Position._createAfter( item.textNode );
 				}
 			}
 
 			// Position is at the begining ot the text.
 			if ( item.offsetInText === 0 ) {
 				if ( this.direction == 'backward' && !( this.boundaries && this.boundaries.start.isEqual( this.position ) ) ) {
-					nextPosition = Position.createBefore( item.textNode );
+					nextPosition = Position._createBefore( item.textNode );
 					// When we change nextPosition of returned value we need also update walker current position.
 					this.position = nextPosition;
 				} else {
-					previousPosition = Position.createBefore( item.textNode );
+					previousPosition = Position._createBefore( item.textNode );
 				}
 			}
 		}
@@ -464,24 +464,24 @@ export default class TreeWalker {
  *
  * @typedef {Object} module:engine/view/treewalker~TreeWalkerValue
  * @property {module:engine/view/treewalker~TreeWalkerValueType} type
- * @property {module:engine/view/item~Item} item Item between old and new positions of {@link module:engine/view/treewalker~TreeWalker}.
+ * @property {module:engine/view/item~Item} item Item between the old and the new positions
+ * of the tree walker.
  * @property {module:engine/view/position~Position} previousPosition Previous position of the iterator.
  * * 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 module:engine/view/position~Position.createBefore}.
+ * position before the item.
  * * 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 module:engine/view/text~Text} it is always moved from the
- * inside of the Text to its parent just before or just after Text.
+ * inside of the text to its parent just before or just after that text.
  * @property {module:engine/view/position~Position} nextPosition Next position of the iterator.
  * * 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 `'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 module:engine/view/text~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 `'elementStart'` it is 1. For `'text'` it is
- * the length of the text. For `'elementEnd'` it is undefined.
+ * inside of the text to its parent just before or just after that text.
+ * @property {Number} [length] Length of the item. For `'elementStart'` it is `1`. For `'text'` it is
+ * the length of that text. For `'elementEnd'` it is `undefined`.
  */
 
 /**

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

@@ -13,8 +13,22 @@ import Node from './node';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
 /**
- * UIElement class. It is used to represent UI not a content of the document.
- * This element can't be split and selection can't be placed inside this element.
+ * UI element class. It should be used to represent editing UI which needs to be injected into the editing view
+ * If possible, you should keep your UI outside the editing view. However, if that is not possible,
+ * UI elements can be used.
+ *
+ * How a UI element is rendered is in your control (you pass a callback to
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement `downcastWriter#createUIElement()`}).
+ * The editor will ignore your UI element – the selection cannot be placed in it, it is skipped (invisible) when
+ * the user modifies the selection by using arrow keys and the editor does not listen to any mutations which
+ * happen inside your UI elements.
+ *
+ * The limitation is that you cannot convert a model element to a UI element. UI elements need to be
+ * created for {@link module:engine/model/markercollection~Marker markers} or as additinal elements
+ * inside normal {@link module:engine/view/containerelement~ContainerElement container elements}.
+ *
+ * To create a new UI element use the
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement `downcastWriter#createUIElement()`} method.
  */
 export default class UIElement extends Element {
 	/**
@@ -72,7 +86,7 @@ export default class UIElement extends Element {
 	 * {@link module:engine/view/domconverter~DomConverter}.
 	 * Do not use inheritance to create custom rendering method, replace `render()` method instead:
 	 *
-	 *		const myUIElement = new UIElement( 'span' );
+	 *		const myUIElement = downcastWriter.createUIElement( 'span' );
 	 *		myUIElement.render = function( domDocument ) {
 	 *			const domElement = this.toDomElement( domDocument );
 	 *			domElement.innerHTML = '<b>this is ui element</b>';

+ 205 - 6
packages/ckeditor5-engine/src/view/upcastwriter.js

@@ -7,18 +7,76 @@
  * @module module:engine/view/upcastwriter
  */
 
+import DocumentFragment from './documentfragment';
 import Element from './element';
+import Text from './text';
 import { isPlainObject } from 'lodash-es';
+import Position from './position';
+import Range from './range';
+import Selection from './selection';
 
 /**
- * View upcast writer class. Provides set of methods used to properly manipulate nodes attached to
- * {@link module:engine/view/view~View view instance}. It should be only used to manipulate non-semantic view
- * (view created from HTML string). For view which was downcasted from the {@link module:engine/model/model~Model model}
- * see {@link module:engine/view/downcastwriter~DowncastWriter writer}.
+ * View upcast writer. It provides a set of methods used to manipulate non-semantic view trees.
+ *
+ * It should be used only while working on a non-semantic view
+ * (e.g. a view created from HTML string on paste).
+ * To manipulate a view which was or is being downcasted from the the model use the
+ * {@link module:engine/view/downcastwriter~DowncastWriter downcast writer}.
+ *
+ * Read more about changing the view in the {@glink framework/guides/architecture/editing-engine#changing-the-view Changing the view}
+ * section of the {@glink framework/guides/architecture/editing-engine Editing engine architecture} guide.
+ *
+ * Unlike `DowncastWriter`, which is available in the {@link module:engine/view/view~View#change `View#change()`} block,
+ * `UpcastWriter` can wherever you need it:
+ *
+ *		const writer = new UpcastWriter();
+ *		const text = writer.createText( 'foo!' );
+ *
+ *		writer.appendChild( text, someViewElement );
  */
 export default class UpcastWriter {
 	/**
-	 * Clones provided element.
+	 * Creates a new {@link module:engine/view/documentfragment~DocumentFragment} instance.
+	 *
+	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
+	 * A list of nodes to be inserted into the created document fragment.
+	 * @returns {module:engine/view/documentfragment~DocumentFragment} The created document fragment.
+	 */
+	createDocumentFragment( children ) {
+		return new DocumentFragment( children );
+	}
+
+	/**
+	 * Creates a new {@link module:engine/view/element~Element} instance.
+	 *
+	 * Attributes can be passed in various formats:
+	 *
+	 *		upcastWriter.createElement( 'div', { class: 'editor', contentEditable: 'true' } ); // object
+	 *		upcastWriter.createElement( 'div', [ [ 'class', 'editor' ], [ 'contentEditable', 'true' ] ] ); // map-like iterator
+	 *		upcastWriter.createElement( 'div', mapOfAttributes ); // map
+	 *
+	 * @param {String} name Node name.
+	 * @param {Object|Iterable} [attrs] Collection of attributes.
+	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
+	 * A list of nodes to be inserted into created element.
+	 * @returns {module:engine/view/element~Element} Created element.
+	 */
+	createElement( name, attrs, children ) {
+		return new Element( name, attrs, children );
+	}
+
+	/**
+	 * Creates a new {@link module:engine/view/text~Text} instance.
+	 *
+	 * @param {String} data The text's data.
+	 * @returns {module:engine/view/text~Text} The created text node.
+	 */
+	createText( data ) {
+		return new Text( data );
+	}
+
+	/**
+	 * Clones the provided element.
 	 *
 	 * @see module:engine/view/element~Element#_clone
 	 * @param {module:engine/view/element~Element} element Element to be cloned.
@@ -62,7 +120,7 @@ export default class UpcastWriter {
 	}
 
 	/**
-	 * Removes number of child nodes starting at the given index and set the parent of these nodes to `null`.
+	 * Removes the given number of child nodes starting at the given index and set the parent of these nodes to `null`.
 	 *
 	 * @see module:engine/view/element~Element#_removeChildren
 	 * @param {Number} index Offset from which nodes will be removed.
@@ -245,4 +303,145 @@ export default class UpcastWriter {
 	removeCustomProperty( key, element ) {
 		return element._removeCustomProperty( key );
 	}
+
+	/**
+	 * Creates position at the given location. The location can be specified as:
+	 *
+	 * * a {@link module:engine/view/position~Position position},
+	 * * parent element and offset (offset defaults to `0`),
+	 * * parent element and `'end'` (sets position at the end of that element),
+	 * * {@link module:engine/view/item~Item view item} and `'before'` or `'after'` (sets position before or after given view item).
+	 *
+	 * This method is a shortcut to other constructors such as:
+	 *
+	 * * {@link #createPositionBefore},
+	 * * {@link #createPositionAfter},
+	 *
+	 * @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
+	 * first parameter is a {@link module:engine/view/item~Item view item}.
+	 */
+	createPositionAt( itemOrPosition, offset ) {
+		return Position._createAt( itemOrPosition, offset );
+	}
+
+	/**
+	 * Creates a new position after given view item.
+	 *
+	 * @param {module:engine/view/item~Item} item View item after which the position should be located.
+	 * @returns {module:engine/view/position~Position}
+	 */
+	createPositionAfter( item ) {
+		return Position._createAfter( item );
+	}
+
+	/**
+	 * Creates a new position before given view item.
+	 *
+	 * @param {module:engine/view/item~Item} item View item before which the position should be located.
+	 * @returns {module:engine/view/position~Position}
+	 */
+	createPositionBefore( item ) {
+		return Position._createBefore( item );
+	}
+
+	/**
+	 * Creates a range spanning from `start` position to `end` position.
+	 *
+	 * **Note:** This factory method creates it's own {@link module:engine/view/position~Position} instances basing on passed values.
+	 *
+	 * @param {module:engine/view/position~Position} start Start position.
+	 * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
+	 * @returns {module:engine/view/range~Range}
+	 */
+	createRange( start, end ) {
+		return new Range( start, end );
+	}
+
+	/**
+	 * Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
+	 *
+	 * @param {module:engine/view/item~Item} item
+	 * @returns {module:engine/view/range~Range}
+	 */
+	createRangeOn( item ) {
+		return Range._createOn( item );
+	}
+
+	/**
+	 * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
+	 * that element and ends after the last child of that element.
+	 *
+	 * @param {module:engine/view/element~Element} element Element which is a parent for the range.
+	 * @returns {module:engine/view/range~Range}
+	 */
+	createRangeIn( element ) {
+		return Range._createIn( element );
+	}
+
+	/**
+	 * Creates a new {@link module:engine/view/selection~Selection} instance.
+	 *
+	 * 		// Creates empty selection without ranges.
+	 *		const selection = writer.createSelection();
+	 *
+	 *		// Creates selection at the given range.
+	 *		const range = writer.createRange( start, end );
+	 *		const selection = writer.createSelection( range );
+	 *
+	 *		// Creates selection at the given ranges
+	 * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
+	 *		const selection = writer.createSelection( ranges );
+	 *
+	 *		// Creates selection from the other selection.
+	 *		const otherSelection = writer.createSelection();
+	 *		const selection = writer.createSelection( otherSelection );
+	 *
+	 *		// Creates selection from the document selection.
+	 *		const selection = writer.createSelection( editor.editing.view.document.selection );
+	 *
+	 * 		// Creates selection at the given position.
+	 *		const position = writer.createPositionFromPath( root, path );
+	 *		const selection = writer.createSelection( position );
+	 *
+	 *		// Creates collapsed selection at the position of given item and offset.
+	 *		const paragraph = writer.createContainerElement( 'paragraph' );
+	 *		const selection = writer.createSelection( paragraph, offset );
+	 *
+	 *		// Creates a range inside an {@link module:engine/view/element~Element element} which starts before the
+	 *		// first child of that element and ends after the last child of that element.
+	 *		const selection = writer.createSelection( paragraph, 'in' );
+	 *
+	 *		// Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
+	 *		// just after the item.
+	 *		const selection = writer.createSelection( paragraph, 'on' );
+	 *
+	 * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
+	 *
+	 *		// Creates backward selection.
+	 *		const selection = writer.createSelection( range, { backward: true } );
+	 *
+	 * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
+	 * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
+	 * represented in other way, for example by applying proper CSS class.
+	 *
+	 * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
+	 * (and be  properly handled by screen readers).
+	 *
+	 *		// Creates fake selection with label.
+	 *		const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
+	 *
+	 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection|
+	 * module:engine/view/position~Position|Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|
+	 * module:engine/view/item~Item|null} [selectable=null]
+	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
+	 * @param {Object} [options]
+	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
+	 * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
+	 * @param {String} [options.label] Label for the fake selection.
+	 * @returns {module:engine/view/selection~Selection}
+	 */
+	createSelection( selectable, placeOrOffset, options ) {
+		return new Selection( selectable, placeOrOffset, options );
+	}
 }

+ 185 - 26
packages/ckeditor5-engine/src/view/view.js

@@ -11,6 +11,9 @@ import Document from './document';
 import DowncastWriter from './downcastwriter';
 import Renderer from './renderer';
 import DomConverter from './domconverter';
+import Position from './position';
+import Range from './range';
+import Selection from './selection';
 
 import MutationObserver from './observer/mutationobserver';
 import KeyObserver from './observer/keyobserver';
@@ -51,10 +54,10 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  * * {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}.
  * * {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
  *
- * This class also {@link module:engine/view/view~View#attachDomRoot bind DOM and View elements}.
+ * This class also {@link module:engine/view/view~View#attachDomRoot binds the DOM and the view elements}.
  *
- * If you do not need full DOM - View management, and want to only transform the tree of view elements to the DOM
- * elements you do not need this controller, you can use the {@link module:engine/view/domconverter~DomConverter DomConverter}.
+ * If you do not need full a DOM - view management, and only want to transform a tree of view elements to a tree of DOM
+ * elements you do not need this controller. You can use the {@link module:engine/view/domconverter~DomConverter DomConverter} instead.
  *
  * @mixes module:utils/observablemixin~ObservableMixin
  */
@@ -127,6 +130,14 @@ export default class View {
 		 */
 		this._postFixersInProgress = false;
 
+		/**
+		 * Internal flag to temporary disable rendering. See usage in the editing controller.
+		 *
+		 * @protected
+		 * @member {Boolean} module:engine/view/view~View#_renderingDisabled
+		 */
+		this._renderingDisabled = false;
+
 		/**
 		 * DowncastWriter instance used in {@link #change change method) callbacks.
 		 *
@@ -299,30 +310,32 @@ export default class View {
 	}
 
 	/**
-	 * Change method is the primary way of changing the view. You should use it to modify any node in the view tree.
-	 * It makes sure that after all changes are made view is rendered to DOM. It prevents situations when DOM is updated
-	 * when view state is not yet correct. It allows to nest calls one inside another and still perform single rendering
-	 * after all changes are applied.
+	 * The `change()` method is the primary way of changing the view. You should use it to modify any node in the view tree.
+	 * It makes sure that after all changes are made the view is rendered to the DOM. It prevents situations when the DOM is updated
+	 * when the view state is not yet correct. It allows to nest calls one inside another and still performs a single rendering
+	 * after all those changes are made. It also returns the return value of its callback.
 	 *
-	 *		view.change( writer => {
-	 *			writer.insert( position1, writer.createText( 'foo' ) );
+	 *		const text = view.change( writer => {
+	 *			const newText = writer.createText( 'foo' );
+	 *			writer.insert( position1, newText );
 	 *
 	 *			view.change( writer => {
 	 *				writer.insert( position2, writer.createText( 'bar' ) );
 	 *			} );
 	 *
 	 * 			writer.remove( range );
-	 *		} );
 	 *
-	 * Change block is executed immediately.
+	 * 			return newText;
+	 *		} );
 	 *
-	 * When the outermost change block is done and rendering to DOM is over it fires
-	 * {@link module:engine/view/view~View#event:render} event.
+	 * When the outermost change block is done and rendering to the DOM is over the
+	 * {@link module:engine/view/view~View#event:render `View#render`} event is fired.
 	 *
-	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `applying-view-changes-on-rendering` when
-	 * change block is used after rendering to DOM has started.
+	 * This method throws a `applying-view-changes-on-rendering` error when
+	 * the change block is used after rendering to the DOM has started.
 	 *
 	 * @param {Function} callback Callback function which may modify the view.
+	 * @returns {*} Value returned by the callback.
 	 */
 	change( callback ) {
 		if ( this._renderingInProgress || this._postFixersInProgress ) {
@@ -334,33 +347,38 @@ export default class View {
 			 * * calling {@link #change} or {@link #render} during rendering process,
 			 * * calling {@link #change} or {@link #render} inside of
 			 *   {@link module:engine/view/document~Document#registerPostFixer post-fixer function}.
+			 *
+			 * @error cannot-change-view-tree
 			 */
 			throw new CKEditorError(
 				'cannot-change-view-tree: ' +
-				'Attempting to make changes to the view when it is in incorrect state: rendering or post-fixers are in progress. ' +
-				'This may cause some unexpected behaviour and inconsistency between the DOM and the view.'
+				'Attempting to make changes to the view when it is in an incorrect state: rendering or post-fixers are in progress. ' +
+				'This may cause some unexpected behavior and inconsistency between the DOM and the view.'
 			);
 		}
 
 		// Recursive call to view.change() method - execute listener immediately.
 		if ( this._ongoingChange ) {
-			callback( this._writer );
-
-			return;
+			return callback( this._writer );
 		}
 
 		// This lock will assure that all recursive calls to view.change() will end up in same block - one "render"
 		// event for all nested calls.
 		this._ongoingChange = true;
-		callback( this._writer );
+		const callbackResult = callback( this._writer );
 		this._ongoingChange = false;
 
-		// Execute all document post-fixers after the change.
-		this._postFixersInProgress = true;
-		this.document._callPostFixers( this._writer );
-		this._postFixersInProgress = false;
+		// This lock is used by editing controller to render changes from outer most model.change() once. As plugins might call
+		// view.change() inside model.change() block - this will ensures that postfixers and rendering are called once after all changes.
+		if ( !this._renderingDisabled ) {
+			this._postFixersInProgress = true;
+			this.document._callPostFixers( this._writer );
+			this._postFixersInProgress = false;
+
+			this.fire( 'render' );
+		}
 
-		this.fire( 'render' );
+		return callbackResult;
 	}
 
 	/**
@@ -385,6 +403,147 @@ export default class View {
 		this.stopListening();
 	}
 
+	/**
+	 * Creates position at the given location. The location can be specified as:
+	 *
+	 * * a {@link module:engine/view/position~Position position},
+	 * * parent element and offset (offset defaults to `0`),
+	 * * parent element and `'end'` (sets position at the end of that element),
+	 * * {@link module:engine/view/item~Item view item} and `'before'` or `'after'` (sets position before or after given view item).
+	 *
+	 * This method is a shortcut to other constructors such as:
+	 *
+	 * * {@link #createPositionBefore},
+	 * * {@link #createPositionAfter},
+	 *
+	 * @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
+	 * first parameter is a {@link module:engine/view/item~Item view item}.
+	 */
+	createPositionAt( itemOrPosition, offset ) {
+		return Position._createAt( itemOrPosition, offset );
+	}
+
+	/**
+	 * Creates a new position after given view item.
+	 *
+	 * @param {module:engine/view/item~Item} item View item after which the position should be located.
+	 * @returns {module:engine/view/position~Position}
+	 */
+	createPositionAfter( item ) {
+		return Position._createAfter( item );
+	}
+
+	/**
+	 * Creates a new position before given view item.
+	 *
+	 * @param {module:engine/view/item~Item} item View item before which the position should be located.
+	 * @returns {module:engine/view/position~Position}
+	 */
+	createPositionBefore( item ) {
+		return Position._createBefore( item );
+	}
+
+	/**
+	 * Creates a range spanning from `start` position to `end` position.
+	 *
+	 * **Note:** This factory method creates it's own {@link module:engine/view/position~Position} instances basing on passed values.
+	 *
+	 * @param {module:engine/view/position~Position} start Start position.
+	 * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
+	 * @returns {module:engine/view/range~Range}
+	 */
+	createRange( start, end ) {
+		return new Range( start, end );
+	}
+
+	/**
+	 * Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
+	 *
+	 * @param {module:engine/view/item~Item} item
+	 * @returns {module:engine/view/range~Range}
+	 */
+	createRangeOn( item ) {
+		return Range._createOn( item );
+	}
+
+	/**
+	 * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
+	 * that element and ends after the last child of that element.
+	 *
+	 * @param {module:engine/view/element~Element} element Element which is a parent for the range.
+	 * @returns {module:engine/view/range~Range}
+	 */
+	createRangeIn( element ) {
+		return Range._createIn( element );
+	}
+
+	/**
+	 Creates new {@link module:engine/view/selection~Selection} instance.
+	 *
+	 * 		// Creates empty selection without ranges.
+	 *		const selection = view.createSelection();
+	 *
+	 *		// Creates selection at the given range.
+	 *		const range = view.createRange( start, end );
+	 *		const selection = view.createSelection( range );
+	 *
+	 *		// Creates selection at the given ranges
+	 * 		const ranges = [ view.createRange( start1, end2 ), view.createRange( star2, end2 ) ];
+	 *		const selection = view.createSelection( ranges );
+	 *
+	 *		// Creates selection from the other selection.
+	 *		const otherSelection = view.createSelection();
+	 *		const selection = view.createSelection( otherSelection );
+	 *
+	 *		// Creates selection from the document selection.
+	 *		const selection = view.createSelection( editor.editing.view.document.selection );
+	 *
+	 * 		// Creates selection at the given position.
+	 *		const position = view.createPositionFromPath( root, path );
+	 *		const selection = view.createSelection( position );
+	 *
+	 *		// Creates collapsed selection at the position of given item and offset.
+	 *		const paragraph = view.createContainerElement( 'paragraph' );
+	 *		const selection = view.createSelection( paragraph, offset );
+	 *
+	 *		// Creates a range inside an {@link module:engine/view/element~Element element} which starts before the
+	 *		// first child of that element and ends after the last child of that element.
+	 *		const selection = view.createSelection( paragraph, 'in' );
+	 *
+	 *		// Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
+	 *		// just after the item.
+	 *		const selection = view.createSelection( paragraph, 'on' );
+	 *
+	 * `Selection`'s factory method allow passing additional options (`backward`, `fake` and `label`) as the last argument.
+	 *
+	 *		// Creates backward selection.
+	 *		const selection = view.createSelection( range, { backward: true } );
+	 *
+	 * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
+	 * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
+	 * represented in other way, for example by applying proper CSS class.
+	 *
+	 * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
+	 * (and be  properly handled by screen readers).
+	 *
+	 *		// Creates fake selection with label.
+	 *		const selection = view.createSelection( range, { fake: true, label: 'foo' } );
+	 *
+	 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection|
+	 * module:engine/view/position~Position|Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|
+	 * module:engine/view/item~Item|null} [selectable=null]
+	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
+	 * @param {Object} [options]
+	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
+	 * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
+	 * @param {String} [options.label] Label for the fake selection.
+	 * @returns {module:engine/view/selection~Selection}
+	 */
+	createSelection( selectable, placeOrOffset, options ) {
+		return new Selection( selectable, placeOrOffset, options );
+	}
+
 	/**
 	 * Renders all changes. In order to avoid triggering the observers (e.g. mutations) all observers are disabled
 	 * before rendering and re-enabled after that.

+ 5 - 5
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -407,7 +407,7 @@ describe( 'DataController', () => {
 
 			model.change( writer => {
 				writer.insert( modelElement, modelRoot, 0 );
-				const range = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 1 );
+				const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 1 ) );
 				writer.addMarker( 'marker:a', { range, usingOperation: true } );
 			} );
 
@@ -430,8 +430,8 @@ describe( 'DataController', () => {
 			model.change( writer => {
 				writer.insert( modelElement, modelRoot, 0 );
 
-				const rangeA = ModelRange.createFromParentsAndOffsets( modelP1, 1, modelP1, 3 );
-				const rangeB = ModelRange.createFromParentsAndOffsets( modelP2, 0, modelP2, 2 );
+				const rangeA = writer.createRange( writer.createPositionAt( modelP1, 1 ), writer.createPositionAt( modelP1, 3 ) );
+				const rangeB = writer.createRange( writer.createPositionAt( modelP2, 0 ), writer.createPositionAt( modelP2, 2 ) );
 
 				writer.addMarker( 'marker:a', { range: rangeA, usingOperation: true } );
 				writer.addMarker( 'marker:b', { range: rangeB, usingOperation: true } );
@@ -463,8 +463,8 @@ describe( 'DataController', () => {
 			const firstModelElement = modelDocumentFragment.getChild( 0 );
 			const firstViewElement = viewDocumentFragment.getChild( 0 );
 
-			const modelRange = ModelRange.createOn( firstModelElement );
-			const viewRange = ViewRange.createOn( firstViewElement );
+			const modelRange = ModelRange._createOn( firstModelElement );
+			const viewRange = ViewRange._createOn( firstViewElement );
 
 			const mappedModelRange = data.mapper.toModelRange( viewRange );
 			const mappedViewRange = data.mapper.toViewRange( modelRange );

+ 151 - 22
packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -114,9 +114,10 @@ describe( 'EditingController', () => {
 			model.change( writer => {
 				writer.insert( modelData, model.document.getRoot() );
 
-				writer.setSelection( ModelRange.createFromParentsAndOffsets(
-					modelRoot.getChild( 0 ), 1, modelRoot.getChild( 0 ), 1 )
-				);
+				writer.setSelection( writer.createRange(
+					writer.createPositionAt( modelRoot.getChild( 0 ), 1 ),
+					writer.createPositionAt( modelRoot.getChild( 0 ), 1 )
+				) );
 			} );
 		} );
 
@@ -136,9 +137,10 @@ describe( 'EditingController', () => {
 			model.change( writer => {
 				writer.split( model.document.selection.getFirstPosition() );
 
-				writer.setSelection(
-					ModelRange.createFromParentsAndOffsets(	modelRoot.getChild( 1 ), 0, modelRoot.getChild( 1 ), 0 )
-				);
+				writer.setSelection( writer.createRange(
+					writer.createPositionAt( modelRoot.getChild( 1 ), 0 ),
+					writer.createPositionAt( modelRoot.getChild( 1 ), 0 )
+				) );
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<p>f</p><p>{}oo</p><p></p><p>bar</p>' );
@@ -157,12 +159,13 @@ describe( 'EditingController', () => {
 		it( 'should convert delete', () => {
 			model.change( writer => {
 				writer.remove(
-					ModelRange.createFromPositionAndShift( model.document.selection.getFirstPosition(), 1 )
+					ModelRange._createFromPositionAndShift( model.document.selection.getFirstPosition(), 1 )
 				);
 
-				writer.setSelection(
-					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 1, modelRoot.getChild( 0 ), 1 )
-				);
+				writer.setSelection( writer.createRange(
+					writer.createPositionAt( modelRoot.getChild( 0 ), 1 ),
+					writer.createPositionAt( modelRoot.getChild( 0 ), 1 )
+				) );
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<p>f{}o</p><p></p><p>bar</p>' );
@@ -195,9 +198,10 @@ describe( 'EditingController', () => {
 
 		it( 'should convert collapsed selection', () => {
 			model.change( writer => {
-				writer.setSelection(
-					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 1, modelRoot.getChild( 2 ), 1 )
-				);
+				writer.setSelection( writer.createRange(
+					writer.createPositionAt( modelRoot.getChild( 2 ), 1 ),
+					writer.createPositionAt( modelRoot.getChild( 2 ), 1 )
+				) );
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<p>foo</p><p></p><p>b{}ar</p>' );
@@ -205,9 +209,10 @@ describe( 'EditingController', () => {
 
 		it( 'should convert not collapsed selection', () => {
 			model.change( writer => {
-				writer.setSelection(
-					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 1, modelRoot.getChild( 2 ), 2 )
-				);
+				writer.setSelection( writer.createRange(
+					writer.createPositionAt( modelRoot.getChild( 2 ), 1 ),
+					writer.createPositionAt( modelRoot.getChild( 2 ), 2 )
+				) );
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<p>foo</p><p></p><p>b{a}r</p>' );
@@ -215,17 +220,19 @@ describe( 'EditingController', () => {
 
 		it( 'should clear previous selection', () => {
 			model.change( writer => {
-				writer.setSelection(
-					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 1, modelRoot.getChild( 2 ), 1 )
-				);
+				writer.setSelection( writer.createRange(
+					writer.createPositionAt( modelRoot.getChild( 2 ), 1 ),
+					writer.createPositionAt( modelRoot.getChild( 2 ), 1 )
+				) );
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<p>foo</p><p></p><p>b{}ar</p>' );
 
 			model.change( writer => {
-				writer.setSelection(
-					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 2, modelRoot.getChild( 2 ), 2 )
-				);
+				writer.setSelection( writer.createRange(
+					writer.createPositionAt( modelRoot.getChild( 2 ), 2 ),
+					writer.createPositionAt( modelRoot.getChild( 2 ), 2 )
+				) );
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<p>foo</p><p></p><p>ba{}r</p>' );
@@ -339,6 +346,128 @@ describe( 'EditingController', () => {
 			expect( getViewData( editing.view, { withoutSelection: true } ) )
 				.to.equal( '<p></p><p>f<span>oo</span></p><p>bar</p>' );
 		} );
+
+		describe( 'preventing rendering while in the model.change() block', () => {
+			let renderSpy;
+
+			beforeEach( () => {
+				renderSpy = sinon.spy();
+
+				editing.view.on( 'render', renderSpy );
+			} );
+
+			it( 'should not call render in the model.change() block', () => {
+				model.change( writer => {
+					executeSomeModelChange( writer );
+
+					expect( renderSpy.called ).to.be.false;
+				} );
+
+				expect( renderSpy.called ).to.be.true;
+			} );
+
+			it( 'should not call render in the model.change() block even if view.change() was called', () => {
+				model.change( writer => {
+					executeSomeModelChange( writer );
+
+					editing.view.change( writer => executeSomeViewChange( writer ) );
+
+					expect( renderSpy.called ).to.be.false;
+				} );
+
+				expect( renderSpy.called ).to.be.true;
+			} );
+
+			it( 'should not call render in enqueued changes', () => {
+				model.enqueueChange( writer => {
+					executeSomeModelChange( writer );
+
+					expect( renderSpy.called ).to.be.false;
+
+					model.enqueueChange( writer => {
+						executeSomeOtherModelChange( writer );
+
+						expect( renderSpy.called ).to.be.false;
+					} );
+
+					expect( renderSpy.called ).to.be.false;
+				} );
+
+				expect( renderSpy.called ).to.be.true;
+			} );
+
+			it( 'should not call render if some model changes were executed in the post fixer', () => {
+				const postfixerSpy = sinon.spy();
+
+				model.document.registerPostFixer( () => {
+					model.change( writer => executeSomeOtherModelChange( writer ) );
+
+					expect( renderSpy.called ).to.be.false;
+
+					postfixerSpy();
+				} );
+
+				model.change( writer => {
+					executeSomeModelChange( writer );
+
+					expect( renderSpy.called ).to.be.false;
+				} );
+
+				expect( renderSpy.called ).to.be.true;
+				expect( postfixerSpy.calledOnce ).to.be.true;
+			} );
+
+			it( 'should not call render if some view changes were executed in the change listener', () => {
+				const changeListenerSpy = sinon.spy();
+
+				model.document.on( 'change', () => {
+					editing.view.change( writer => executeSomeViewChange( writer ) );
+
+					expect( renderSpy.called ).to.be.false;
+
+					changeListenerSpy();
+				} );
+
+				model.change( writer => {
+					executeSomeModelChange( writer );
+
+					expect( renderSpy.called ).to.be.false;
+				} );
+
+				expect( renderSpy.called ).to.be.true;
+				expect( changeListenerSpy.calledOnce ).to.be.true;
+			} );
+
+			it( 'should call view post-fixers once for model.change() block', () => {
+				const postfixerSpy = sinon.spy();
+
+				editing.view.document.registerPostFixer( postfixerSpy );
+
+				model.change( writer => {
+					executeSomeModelChange( writer );
+
+					editing.view.change( writer => {
+						executeSomeViewChange( writer );
+					} );
+				} );
+
+				sinon.assert.calledOnce( postfixerSpy );
+			} );
+
+			function executeSomeModelChange( writer ) {
+				const range = new ModelRange( new ModelPosition( modelRoot, [ 0, 1 ] ), new ModelPosition( modelRoot, [ 2, 2 ] ) );
+				writer.addMarker( 'marker1', { range, usingOperation: true } );
+			}
+
+			function executeSomeOtherModelChange( writer ) {
+				const range = new ModelRange( new ModelPosition( modelRoot, [ 0, 1 ] ), new ModelPosition( modelRoot, [ 2, 2 ] ) );
+				writer.addMarker( 'marker2', { range, usingOperation: true } );
+			}
+
+			function executeSomeViewChange( writer ) {
+				writer.addClass( 'foo', editing.view.document.getRoot() );
+			}
+		} );
 	} );
 
 	describe( 'destroy()', () => {

+ 4 - 2
packages/ckeditor5-engine/tests/conversion/conversion.js

@@ -13,7 +13,6 @@ import { convertText, convertToModelFragment } from '../../src/conversion/upcast
 import EditingController from '../../src/controller/editingcontroller';
 
 import Model from '../../src/model/model';
-import ModelRange from '../../src/model/range';
 
 import { stringify as viewStringify, parse as viewParse } from '../../src/dev-utils/view';
 import { stringify as modelStringify } from '../../src/dev-utils/model';
@@ -644,7 +643,10 @@ describe( 'Conversion', () => {
 			} );
 
 			model.change( writer => {
-				writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, modelRoot.maxOffset ) );
+				writer.remove( writer.createRange(
+					writer.createPositionAt( modelRoot, 0 ),
+					writer.createPositionAt( modelRoot, modelRoot.maxOffset ) )
+				);
 				writer.insert( convertedModel, modelRoot, 0 );
 			} );
 		}

+ 40 - 30
packages/ckeditor5-engine/tests/conversion/downcast-converters.js

@@ -10,8 +10,6 @@ import Conversion from '../../src/conversion/conversion';
 import Model from '../../src/model/model';
 import ModelElement from '../../src/model/element';
 import ModelText from '../../src/model/text';
-import ModelRange from '../../src/model/range';
-import ModelPosition from '../../src/model/position';
 
 import ViewElement from '../../src/view/element';
 import ViewAttributeElement from '../../src/view/attributeelement';
@@ -473,7 +471,7 @@ describe( 'downcast-helpers', () => {
 			model.change( writer => {
 				writer.insertText( 'foo', modelRoot, 0 );
 
-				const range = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 );
+				const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
 				writer.addMarker( 'search', { range, usingOperation: false } );
 			} );
 
@@ -488,7 +486,7 @@ describe( 'downcast-helpers', () => {
 
 			model.change( writer => {
 				writer.insertText( 'foo', modelRoot, 0 );
-				const range = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 );
+				const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
 				writer.addMarker( 'search', { range, usingOperation: false } );
 			} );
 
@@ -510,7 +508,7 @@ describe( 'downcast-helpers', () => {
 
 			model.change( writer => {
 				writer.insertText( 'foo', modelRoot, 0 );
-				const range = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 );
+				const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
 				writer.addMarker( 'search', { range, usingOperation: false } );
 			} );
 
@@ -529,7 +527,7 @@ describe( 'downcast-helpers', () => {
 
 			model.change( writer => {
 				writer.insertText( 'foo', modelRoot, 0 );
-				const range = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 );
+				const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) );
 				writer.addMarker( 'search', { range, usingOperation: false } );
 			} );
 
@@ -545,7 +543,7 @@ describe( 'downcast-helpers', () => {
 
 			model.change( writer => {
 				writer.insertText( 'foo', modelRoot, 0 );
-				const range = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 );
+				const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 3 ) );
 				writer.addMarker( 'comment', { range, usingOperation: false } );
 			} );
 
@@ -560,7 +558,7 @@ describe( 'downcast-helpers', () => {
 
 			model.change( writer => {
 				writer.insertText( 'foo', modelRoot, 0 );
-				const range = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 );
+				const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 3 ) );
 				writer.addMarker( 'comment', { range, usingOperation: false } );
 			} );
 
@@ -583,7 +581,7 @@ describe( 'downcast-helpers', () => {
 
 			model.change( writer => {
 				writer.insertText( 'foo', modelRoot, 0 );
-				const range = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 );
+				const range = writer.createRange( writer.createPositionAt( modelRoot, 0 ), writer.createPositionAt( modelRoot, 3 ) );
 				writer.addMarker( 'comment:abc', { range, usingOperation: false } );
 			} );
 
@@ -622,7 +620,7 @@ describe( 'downcast-converters', () => {
 
 		dispatcher.on( 'attribute:class', changeAttribute() );
 
-		modelRootStart = ModelPosition.createAt( modelRoot, 0 );
+		modelRootStart = model.createPositionAt( modelRoot, 0 );
 	} );
 
 	function viewAttributesToString( item ) {
@@ -815,7 +813,7 @@ describe( 'downcast-converters', () => {
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
 
 			model.change( writer => {
-				writer.removeAttribute( 'bold', ModelRange.createIn( modelElement ) );
+				writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) );
 			} );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
@@ -839,7 +837,7 @@ describe( 'downcast-converters', () => {
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p><b>foobar</b></p></div>' );
 
 			model.change( writer => {
-				writer.removeAttribute( 'style', ModelRange.createIn( modelElement ) );
+				writer.removeAttribute( 'style', writer.createRangeIn( modelElement ) );
 			} );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
@@ -866,7 +864,7 @@ describe( 'downcast-converters', () => {
 
 			// Set new attribute on old link but also on non-linked characters.
 			model.change( writer => {
-				writer.setAttribute( 'link', 'http://foobar.com', ModelRange.createIn( modelElement ) );
+				writer.setAttribute( 'link', 'http://foobar.com', writer.createRangeIn( modelElement ) );
 			} );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p><a href="http://foobar.com">xfoox</a></p></div>' );
@@ -885,7 +883,7 @@ describe( 'downcast-converters', () => {
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>நி<b>லைக்</b>கு</p></div>' );
 
 			model.change( writer => {
-				writer.removeAttribute( 'bold', ModelRange.createIn( modelElement ) );
+				writer.removeAttribute( 'bold', writer.createRangeIn( modelElement ) );
 			} );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>நிலைக்கு</p></div>' );
@@ -945,7 +943,7 @@ describe( 'downcast-converters', () => {
 
 		describe( 'collapsed range', () => {
 			beforeEach( () => {
-				range = ModelRange.createFromParentsAndOffsets( modelElement, 3, modelElement, 3 );
+				range = model.createRange( model.createPositionAt( modelElement, 3 ), model.createPositionAt( modelElement, 3 ) );
 			} );
 
 			it( 'should insert and remove ui element', () => {
@@ -1006,7 +1004,7 @@ describe( 'downcast-converters', () => {
 
 		describe( 'non-collapsed range', () => {
 			beforeEach( () => {
-				range = ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 5 );
+				range = model.createRange( model.createPositionAt( modelElement, 2 ), model.createPositionAt( modelElement, 5 ) );
 			} );
 
 			it( 'should insert and remove ui element - element as a creator', () => {
@@ -1106,7 +1104,9 @@ describe( 'downcast-converters', () => {
 			} );
 
 			model.change( writer => {
-				writer.remove( ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
+				writer.remove(
+					writer.createRange( writer.createPositionAt( modelElement, 2 ), writer.createPositionAt( modelElement, 4 ) )
+				);
 			} );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foar</p></div>' );
@@ -1122,7 +1122,9 @@ describe( 'downcast-converters', () => {
 			} );
 
 			model.change( writer => {
-				writer.remove( ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
+				writer.remove(
+					writer.createRange( writer.createPositionAt( modelElement, 2 ), writer.createPositionAt( modelElement, 4 ) )
+				);
 			} );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
@@ -1136,7 +1138,9 @@ describe( 'downcast-converters', () => {
 			} );
 
 			model.change( writer => {
-				writer.remove( ModelRange.createFromParentsAndOffsets( modelElement, 0, modelElement, 6 ) );
+				writer.remove(
+					writer.createRange( writer.createPositionAt( modelElement, 0 ), writer.createPositionAt( modelElement, 6 ) )
+				);
 			} );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>கு</p></div>' );
@@ -1152,14 +1156,18 @@ describe( 'downcast-converters', () => {
 
 			// Remove 'b'.
 			model.change( writer => {
-				writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 3, modelRoot, 4 ) );
+				writer.remove(
+					writer.createRange( writer.createPositionAt( modelRoot, 3 ), writer.createPositionAt( modelRoot, 4 ) )
+				);
 			} );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div>foz<span></span>ar</div>' );
 
 			// Remove 'z'.
 			model.change( writer => {
-				writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 2, modelRoot, 3 ) );
+				writer.remove(
+					writer.createRange( writer.createPositionAt( modelRoot, 2 ), writer.createPositionAt( modelRoot, 3 ) )
+				);
 			} );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div>fo<span></span>ar</div>' );
@@ -1175,7 +1183,9 @@ describe( 'downcast-converters', () => {
 
 			// Remove 'z<span></span>b'.
 			model.change( writer => {
-				writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 2, modelRoot, 4 ) );
+				writer.remove(
+					writer.createRange( writer.createPositionAt( modelRoot, 2 ), writer.createPositionAt( modelRoot, 4 ) )
+				);
 			} );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div>foar</div>' );
@@ -1250,7 +1260,7 @@ describe( 'downcast-converters', () => {
 
 			// Remove second paragraph element.
 			model.change( writer => {
-				writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) );
+				writer.remove( writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 2 ) ) );
 			} );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p></p><span></span><span></span></div>' );
@@ -1290,7 +1300,7 @@ describe( 'downcast-converters', () => {
 					writer.insert( [ modelElement1, modelElement2 ], modelRootStart );
 				} );
 
-				markerRange = ModelRange.createIn( modelRoot );
+				markerRange = model.createRangeIn( modelRoot );
 			} );
 
 			it( 'should wrap and unwrap text nodes', () => {
@@ -1378,7 +1388,7 @@ describe( 'downcast-converters', () => {
 				dispatcher.on( 'addMarker:marker', highlightElement( descriptor ), { priority: 'high' } );
 				dispatcher.on( 'removeMarker:marker', removeHighlight( descriptor ), { priority: 'high' } );
 
-				markerRange = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 0 );
+				markerRange = model.createRange( model.createPositionAt( modelRoot, 0 ), model.createPositionAt( modelRoot, 0 ) );
 
 				model.change( writer => {
 					writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
@@ -1410,7 +1420,7 @@ describe( 'downcast-converters', () => {
 				const p2 = modelRoot.getChild( 1 );
 
 				model.change( writer => {
-					const range = ModelRange.createFromParentsAndOffsets( p1, 0, p1, 3 );
+					const range = writer.createRange( writer.createPositionAt( p1, 0 ), writer.createPositionAt( p1, 3 ) );
 					writer.addMarker( 'markerFoo', { range, usingOperation: false } );
 				} );
 
@@ -1424,7 +1434,7 @@ describe( 'downcast-converters', () => {
 				);
 
 				model.change( writer => {
-					const range = ModelRange.createFromParentsAndOffsets( p1, 1, p2, 2 );
+					const range = writer.createRange( writer.createPositionAt( p1, 1 ), writer.createPositionAt( p2, 2 ) );
 					writer.addMarker( 'markerBar', { range, usingOperation: false } );
 				} );
 
@@ -1444,7 +1454,7 @@ describe( 'downcast-converters', () => {
 				);
 
 				model.change( writer => {
-					const range = ModelRange.createFromParentsAndOffsets( p1, 2, p2, 3 );
+					const range = writer.createRange( writer.createPositionAt( p1, 2 ), writer.createPositionAt( p2, 3 ) );
 					writer.addMarker( 'markerXyz', { range, usingOperation: false } );
 				} );
 
@@ -1516,7 +1526,7 @@ describe( 'downcast-converters', () => {
 				const p1 = modelRoot.getChild( 0 );
 				const p2 = modelRoot.getChild( 1 );
 
-				const markerRange = ModelRange.createFromParentsAndOffsets( p1, 3, p2, 0 );
+				const markerRange = model.createRange( model.createPositionAt( p1, 3 ), model.createPositionAt( p2, 0 ) );
 
 				model.change( writer => {
 					writer.addMarker( 'marker', { range: markerRange, usingOperation: false } );
@@ -1564,7 +1574,7 @@ describe( 'downcast-converters', () => {
 					writer.insert( modelElement, modelRootStart );
 				} );
 
-				markerRange = ModelRange.createOn( modelElement );
+				markerRange = model.createRangeOn( modelElement );
 
 				dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
 				dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );

+ 25 - 28
packages/ckeditor5-engine/tests/conversion/downcast-selection-converters.js

@@ -4,9 +4,6 @@
  */
 
 import Model from '../../src/model/model';
-import ModelElement from '../../src/model/element';
-import ModelRange from '../../src/model/range';
-import ModelPosition from '../../src/model/position';
 
 import View from '../../src/view/view';
 import ViewUIElement from '../../src/view/uielement';
@@ -197,9 +194,9 @@ describe( 'downcast-selection-converters', () => {
 				setModelData( model, 'fo<$text bold="true">ob</$text>ar' );
 
 				model.change( writer => {
-					const range = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 );
+					const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) );
 					marker = writer.addMarker( 'marker', { range, usingOperation: false } );
-					writer.setSelection( new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) );
+					writer.setSelection( modelRoot, 3 );
 				} );
 
 				// Remove view children manually (without firing additional conversion).
@@ -207,7 +204,7 @@ describe( 'downcast-selection-converters', () => {
 
 				// Convert model to view.
 				view.change( writer => {
-					dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
+					dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
 					dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
 					dispatcher.convertSelection( docSelection, model.markers, writer );
 				} );
@@ -222,9 +219,9 @@ describe( 'downcast-selection-converters', () => {
 				setModelData( model, 'fo<$text bold="true">ob</$text>ar' );
 
 				model.change( writer => {
-					const range = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 );
+					const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) );
 					marker = writer.addMarker( 'marker', { range, usingOperation: false } );
-					writer.setSelection( new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) );
+					writer.setSelection( modelRoot, 3 );
 					writer.removeSelectionAttribute( 'bold' );
 				} );
 
@@ -233,7 +230,7 @@ describe( 'downcast-selection-converters', () => {
 
 				// Convert model to view.
 				view.change( writer => {
-					dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
+					dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
 					dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
 					dispatcher.convertSelection( docSelection, model.markers, writer );
 				} );
@@ -251,9 +248,9 @@ describe( 'downcast-selection-converters', () => {
 				setModelData( model, 'foobar' );
 
 				model.change( writer => {
-					const range = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 );
+					const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) );
 					marker = writer.addMarker( 'marker2', { range, usingOperation: false } );
-					writer.setSelection( new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) );
+					writer.setSelection( modelRoot, 3 );
 				} );
 
 				// Remove view children manually (without firing additional conversion).
@@ -261,7 +258,7 @@ describe( 'downcast-selection-converters', () => {
 
 				// Convert model to view.
 				view.change( writer => {
-					dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
+					dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
 					dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
 					dispatcher.convertSelection( docSelection, model.markers, writer );
 				} );
@@ -277,9 +274,9 @@ describe( 'downcast-selection-converters', () => {
 				setModelData( model, 'foobar' );
 
 				model.change( writer => {
-					const range = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 );
+					const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) );
 					marker = writer.addMarker( 'marker3', { range, usingOperation: false } );
-					writer.setSelection( new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) );
+					writer.setSelection( modelRoot, 3 );
 				} );
 
 				// Remove view children manually (without firing additional conversion).
@@ -287,7 +284,7 @@ describe( 'downcast-selection-converters', () => {
 
 				// Convert model to view.
 				view.change( writer => {
-					dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
+					dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
 					dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
 					dispatcher.convertSelection( docSelection, model.markers, writer );
 				} );
@@ -308,7 +305,7 @@ describe( 'downcast-selection-converters', () => {
 				] );
 
 				model.change( writer => {
-					writer.setSelection( new ModelRange( new ModelPosition( modelRoot, [ 0 ] ) ) );
+					writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 0 ] ) ) );
 					writer.setSelectionAttribute( 'bold', true );
 				} );
 
@@ -327,13 +324,13 @@ describe( 'downcast-selection-converters', () => {
 				setModelData( model, 'x' );
 
 				model.change( writer => {
-					writer.setSelection( new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) );
+					writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) );
 					writer.setSelectionAttribute( 'bold', true );
 				} );
 
 				// Convert model to view.
 				view.change( writer => {
-					dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
+					dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
 
 					// Add ui element to view.
 					const uiElement = new ViewUIElement( 'span' );
@@ -352,13 +349,13 @@ describe( 'downcast-selection-converters', () => {
 				setModelData( model, '<$text bold="true">x</$text>y' );
 
 				model.change( writer => {
-					writer.setSelection( new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) );
+					writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) );
 					writer.setSelectionAttribute( 'bold', true );
 				} );
 
 				// Convert model to view.
 				view.change( writer => {
-					dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
+					dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
 
 					// Add ui element to view.
 					const uiElement = new ViewUIElement( 'span' );
@@ -439,7 +436,7 @@ describe( 'downcast-selection-converters', () => {
 				);
 
 				view.change( writer => {
-					const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
+					const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) );
 					model.change( writer => {
 						writer.setSelection( modelRange );
 					} );
@@ -465,7 +462,7 @@ describe( 'downcast-selection-converters', () => {
 					// Remove <strong></strong> manually.
 					writer.mergeAttributes( viewSelection.getFirstPosition() );
 
-					const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
+					const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) );
 					model.change( writer => {
 						writer.setSelection( modelRange );
 					} );
@@ -480,7 +477,7 @@ describe( 'downcast-selection-converters', () => {
 			} );
 
 			it( 'should clear fake selection', () => {
-				const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
+				const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) );
 
 				view.change( writer => {
 					writer.setSelection( modelRange, { fake: true } );
@@ -521,7 +518,7 @@ describe( 'downcast-selection-converters', () => {
 				for ( const range of selection.getRanges() ) {
 					const node = range.start.parent;
 
-					if ( node instanceof ModelElement && node.name == 'td' ) {
+					if ( !!node && node.is( 'td' ) ) {
 						conversionApi.consumable.consume( selection, 'selection' );
 
 						const viewNode = conversionApi.mapper.toViewElement( node );
@@ -571,12 +568,12 @@ describe( 'downcast-selection-converters', () => {
 		const startPath = typeof selectionPaths[ 0 ] == 'number' ? [ selectionPaths[ 0 ] ] : selectionPaths[ 0 ];
 		const endPath = typeof selectionPaths[ 1 ] == 'number' ? [ selectionPaths[ 1 ] ] : selectionPaths[ 1 ];
 
-		const startPos = new ModelPosition( modelRoot, startPath );
-		const endPos = new ModelPosition( modelRoot, endPath );
+		const startPos = model.createPositionFromPath( modelRoot, startPath );
+		const endPos = model.createPositionFromPath( modelRoot, endPath );
 
 		const isBackward = selectionPaths[ 2 ] === 'backward';
 		model.change( writer => {
-			writer.setSelection( new ModelRange( startPos, endPos ), { backward: isBackward } );
+			writer.setSelection( writer.createRange( startPos, endPos ), { backward: isBackward } );
 
 			// And add or remove passed attributes.
 			for ( const key in selectionAttributes ) {
@@ -595,7 +592,7 @@ describe( 'downcast-selection-converters', () => {
 
 		// Convert model to view.
 		view.change( writer => {
-			dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
+			dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer );
 			dispatcher.convertSelection( docSelection, model.markers, writer );
 		} );
 

+ 48 - 43
packages/ckeditor5-engine/tests/conversion/downcastdispatcher.js

@@ -8,7 +8,6 @@ import Model from '../../src/model/model';
 import ModelText from '../../src/model/text';
 import ModelElement from '../../src/model/element';
 import ModelRange from '../../src/model/range';
-import ModelPosition from '../../src/model/position';
 
 import View from '../../src/view/view';
 import ViewContainerElement from '../../src/view/containerelement';
@@ -43,8 +42,8 @@ describe( 'DowncastDispatcher', () => {
 		it( 'should call convertInsert for insert change', () => {
 			sinon.stub( dispatcher, 'convertInsert' );
 
-			const position = new ModelPosition( root, [ 0 ] );
-			const range = ModelRange.createFromPositionAndShift( position, 1 );
+			const position = model.createPositionFromPath( root, [ 0 ] );
+			const range = ModelRange._createFromPositionAndShift( position, 1 );
 
 			differStub.getChanges = () => [ { type: 'insert', position, length: 1 } ];
 
@@ -59,7 +58,7 @@ describe( 'DowncastDispatcher', () => {
 		it( 'should call convertRemove for remove change', () => {
 			sinon.stub( dispatcher, 'convertRemove' );
 
-			const position = new ModelPosition( root, [ 0 ] );
+			const position = model.createPositionFromPath( root, [ 0 ] );
 
 			differStub.getChanges = () => [ { type: 'remove', position, length: 2, name: '$text' } ];
 
@@ -73,8 +72,8 @@ describe( 'DowncastDispatcher', () => {
 		it( 'should call convertAttribute for attribute change', () => {
 			sinon.stub( dispatcher, 'convertAttribute' );
 
-			const position = new ModelPosition( root, [ 0 ] );
-			const range = ModelRange.createFromPositionAndShift( position, 1 );
+			const position = model.createPositionFromPath( root, [ 0 ] );
+			const range = ModelRange._createFromPositionAndShift( position, 1 );
 
 			differStub.getChanges = () => [
 				{ type: 'attribute', position, range, attributeKey: 'key', attributeOldValue: null, attributeNewValue: 'foo' }
@@ -92,8 +91,8 @@ describe( 'DowncastDispatcher', () => {
 			sinon.stub( dispatcher, 'convertRemove' );
 			sinon.stub( dispatcher, 'convertAttribute' );
 
-			const position = new ModelPosition( root, [ 0 ] );
-			const range = ModelRange.createFromPositionAndShift( position, 1 );
+			const position = model.createPositionFromPath( root, [ 0 ] );
+			const range = ModelRange._createFromPositionAndShift( position, 1 );
 
 			differStub.getChanges = () => [
 				{ type: 'insert', position, length: 1 },
@@ -114,8 +113,8 @@ describe( 'DowncastDispatcher', () => {
 		it( 'should call convertMarkerAdd when markers are added', () => {
 			sinon.stub( dispatcher, 'convertMarkerAdd' );
 
-			const fooRange = ModelRange.createFromParentsAndOffsets( root, 0, root, 1 );
-			const barRange = ModelRange.createFromParentsAndOffsets( root, 3, root, 6 );
+			const fooRange = model.createRange( model.createPositionAt( root, 0 ), model.createPositionAt( root, 1 ) );
+			const barRange = model.createRange( model.createPositionAt( root, 3 ), model.createPositionAt( root, 6 ) );
 
 			differStub.getMarkersToAdd = () => [
 				{ name: 'foo', range: fooRange },
@@ -133,8 +132,8 @@ describe( 'DowncastDispatcher', () => {
 		it( 'should call convertMarkerRemove when markers are removed', () => {
 			sinon.stub( dispatcher, 'convertMarkerRemove' );
 
-			const fooRange = ModelRange.createFromParentsAndOffsets( root, 0, root, 1 );
-			const barRange = ModelRange.createFromParentsAndOffsets( root, 3, root, 6 );
+			const fooRange = model.createRange( model.createPositionAt( root, 0 ), model.createPositionAt( root, 1 ) );
+			const barRange = model.createRange( model.createPositionAt( root, 3 ), model.createPositionAt( root, 6 ) );
 
 			differStub.getMarkersToRemove = () => [
 				{ name: 'foo', range: fooRange },
@@ -159,7 +158,7 @@ describe( 'DowncastDispatcher', () => {
 				new ModelElement( 'paragraph', { class: 'nice' }, new ModelText( 'xx', { italic: true } ) )
 			] );
 
-			const range = ModelRange.createIn( root );
+			const range = model.createRangeIn( root );
 			const loggedEvents = [];
 
 			// We will check everything connected with insert event:
@@ -219,7 +218,7 @@ describe( 'DowncastDispatcher', () => {
 				conversionApi.consumable.consume( data.item, 'attribute:bold' );
 			} );
 
-			const range = ModelRange.createIn( root );
+			const range = model.createRangeIn( root );
 
 			dispatcher.convertInsert( range );
 
@@ -242,7 +241,7 @@ describe( 'DowncastDispatcher', () => {
 				loggedEvents.push( log );
 			} );
 
-			dispatcher.convertRemove( ModelPosition.createFromParentAndOffset( root, 3 ), 3, '$text' );
+			dispatcher.convertRemove( model.createPositionAt( root, 3 ), 3, '$text' );
 
 			expect( loggedEvents ).to.deep.equal( [ 'remove:3:3' ] );
 		} );
@@ -255,8 +254,8 @@ describe( 'DowncastDispatcher', () => {
 			root._appendChild( new ModelText( 'foobar' ) );
 			model.change( writer => {
 				writer.setSelection( [
-					new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 3 ] ) ),
-					new ModelRange( new ModelPosition( root, [ 4 ] ), new ModelPosition( root, [ 5 ] ) )
+					writer.createRange( writer.createPositionFromPath( root, [ 1 ] ), writer.createPositionFromPath( root, [ 3 ] ) ),
+					writer.createRange( writer.createPositionFromPath( root, [ 4 ] ), writer.createPositionFromPath( root, [ 5 ] ) )
 				] );
 			} );
 		} );
@@ -274,8 +273,10 @@ describe( 'DowncastDispatcher', () => {
 
 		it( 'should prepare correct list of consumable values', () => {
 			model.change( writer => {
-				writer.setAttribute( 'bold', true, ModelRange.createIn( root ) );
-				writer.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
+				writer.setAttribute( 'bold', true, writer.createRangeIn( root ) );
+				writer.setAttribute( 'italic', true,
+					writer.createRange( writer.createPositionAt( root, 4 ), writer.createPositionAt( root, 5 ) )
+				);
 			} );
 
 			dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
@@ -289,8 +290,10 @@ describe( 'DowncastDispatcher', () => {
 
 		it( 'should not fire attributes events for non-collapsed selection', () => {
 			model.change( writer => {
-				writer.setAttribute( 'bold', true, ModelRange.createIn( root ) );
-				writer.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
+				writer.setAttribute( 'bold', true, writer.createRangeIn( root ) );
+				writer.setAttribute( 'italic', true,
+					writer.createRange( writer.createPositionAt( root, 4 ), writer.createPositionAt( root, 5 ) )
+				);
 			} );
 
 			sinon.spy( dispatcher, 'fire' );
@@ -304,12 +307,12 @@ describe( 'DowncastDispatcher', () => {
 		it( 'should fire attributes events for collapsed selection', () => {
 			model.change( writer => {
 				writer.setSelection(
-					new ModelRange( new ModelPosition( root, [ 2 ] ), new ModelPosition( root, [ 2 ] ) )
+					writer.createRange( writer.createPositionFromPath( root, [ 2 ] ), writer.createPositionFromPath( root, [ 2 ] ) )
 				);
 			} );
 
 			model.change( writer => {
-				writer.setAttribute( 'bold', true, ModelRange.createIn( root ) );
+				writer.setAttribute( 'bold', true, writer.createRangeIn( root ) );
 			} );
 
 			sinon.spy( dispatcher, 'fire' );
@@ -322,13 +325,15 @@ describe( 'DowncastDispatcher', () => {
 		it( 'should not fire attributes events if attribute has been consumed', () => {
 			model.change( writer => {
 				writer.setSelection(
-					new ModelRange( new ModelPosition( root, [ 2 ] ), new ModelPosition( root, [ 2 ] ) )
+					writer.createRange( writer.createPositionFromPath( root, [ 2 ] ), writer.createPositionFromPath( root, [ 2 ] ) )
 				);
 			} );
 
 			model.change( writer => {
-				writer.setAttribute( 'bold', true, ModelRange.createIn( root ) );
-				writer.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) );
+				writer.setAttribute( 'bold', true, writer.createRangeIn( root ) );
+				writer.setAttribute( 'italic', true,
+					writer.createRange( writer.createPositionAt( root, 4 ), writer.createPositionAt( root, 5 ) )
+				);
 			} );
 
 			dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
@@ -345,9 +350,9 @@ describe( 'DowncastDispatcher', () => {
 		it( 'should fire events for markers for collapsed selection', () => {
 			model.change( writer => {
 				writer.setSelection(
-					new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 1 ] ) )
+					writer.createRange( writer.createPositionFromPath( root, [ 1 ] ), writer.createPositionFromPath( root, [ 1 ] ) )
 				);
-				const range = ModelRange.createFromParentsAndOffsets( root, 0, root, 2 );
+				const range = writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 2 ) );
 				writer.addMarker( 'name', { range, usingOperation: false } );
 			} );
 
@@ -361,7 +366,7 @@ describe( 'DowncastDispatcher', () => {
 
 		it( 'should not fire events for markers for non-collapsed selection', () => {
 			model.change( writer => {
-				const range = ModelRange.createFromParentsAndOffsets( root, 0, root, 2 );
+				const range = writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 2 ) );
 				writer.addMarker( 'name', { range, usingOperation: false } );
 			} );
 
@@ -402,9 +407,9 @@ describe( 'DowncastDispatcher', () => {
 			};
 
 			model.change( writer => {
-				const range = ModelRange.createFromParentsAndOffsets( root, 0, root, 1 );
+				const range = writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 1 ) );
 				writer.addMarker( 'name', { range, usingOperation: false } );
-				writer.setSelection( ModelRange.createFromParentsAndOffsets( caption, 1, caption, 1 ) );
+				writer.setSelection( caption, 1 );
 			} );
 			sinon.spy( dispatcher, 'fire' );
 
@@ -418,10 +423,10 @@ describe( 'DowncastDispatcher', () => {
 		it( 'should not fire events if information about marker has been consumed', () => {
 			model.change( writer => {
 				writer.setSelection(
-					new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 1 ] ) )
+					writer.createRange( writer.createPositionFromPath( root, [ 1 ] ), writer.createPositionFromPath( root, [ 1 ] ) )
 				);
 
-				const range = ModelRange.createFromParentsAndOffsets( root, 0, root, 2 );
+				const range = writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 2 ) );
 				writer.addMarker( 'foo', { range, usingOperation: false } );
 				writer.addMarker( 'bar', { range, usingOperation: false } );
 			} );
@@ -448,7 +453,7 @@ describe( 'DowncastDispatcher', () => {
 			element = new ModelElement( 'paragraph', null, [ text ] );
 			root._appendChild( [ element ] );
 
-			range = ModelRange.createFromParentsAndOffsets( element, 0, element, 4 );
+			range = model.createRange( model.createPositionAt( element, 0 ), model.createPositionAt( element, 4 ) );
 		} );
 
 		it( 'should fire addMarker event', () => {
@@ -460,7 +465,7 @@ describe( 'DowncastDispatcher', () => {
 		} );
 
 		it( 'should not convert marker if it is in graveyard', () => {
-			const gyRange = ModelRange.createFromParentsAndOffsets( doc.graveyard, 0, doc.graveyard, 0 );
+			const gyRange = model.createRange( model.createPositionAt( doc.graveyard, 0 ), model.createPositionAt( doc.graveyard, 0 ) );
 			sinon.spy( dispatcher, 'fire' );
 
 			dispatcher.convertMarkerAdd( 'name', gyRange );
@@ -470,7 +475,7 @@ describe( 'DowncastDispatcher', () => {
 
 		it( 'should not convert marker if it is not in model root', () => {
 			const element = new ModelElement( 'element', null, new ModelText( 'foo' ) );
-			const eleRange = ModelRange.createFromParentsAndOffsets( element, 1, element, 2 );
+			const eleRange = model.createRange( model.createPositionAt( element, 1 ), model.createPositionAt( element, 2 ) );
 			sinon.spy( dispatcher, 'fire' );
 
 			dispatcher.convertMarkerAdd( 'name', eleRange );
@@ -479,7 +484,7 @@ describe( 'DowncastDispatcher', () => {
 		} );
 
 		it( 'should fire conversion for each item in the range', () => {
-			range = ModelRange.createIn( root );
+			range = model.createRangeIn( root );
 
 			const items = [];
 
@@ -498,7 +503,7 @@ describe( 'DowncastDispatcher', () => {
 		} );
 
 		it( 'should be possible to override', () => {
-			range = ModelRange.createIn( root );
+			range = model.createRangeIn( root );
 
 			const addMarkerSpy = sinon.spy();
 			const highAddMarkerSpy = sinon.spy();
@@ -528,7 +533,7 @@ describe( 'DowncastDispatcher', () => {
 			element = new ModelElement( 'paragraph', null, [ text ] );
 			root._appendChild( [ element ] );
 
-			range = ModelRange.createFromParentsAndOffsets( element, 0, element, 4 );
+			range = model.createRange( model.createPositionAt( element, 0 ), model.createPositionAt( element, 4 ) );
 		} );
 
 		it( 'should fire removeMarker event', () => {
@@ -540,7 +545,7 @@ describe( 'DowncastDispatcher', () => {
 		} );
 
 		it( 'should not convert marker if it is in graveyard', () => {
-			const gyRange = ModelRange.createFromParentsAndOffsets( doc.graveyard, 0, doc.graveyard, 0 );
+			const gyRange = model.createRange( model.createPositionAt( doc.graveyard, 0 ), model.createPositionAt( doc.graveyard, 0 ) );
 			sinon.spy( dispatcher, 'fire' );
 
 			dispatcher.convertMarkerRemove( 'name', gyRange );
@@ -550,7 +555,7 @@ describe( 'DowncastDispatcher', () => {
 
 		it( 'should not convert marker if it is not in model root', () => {
 			const element = new ModelElement( 'element', null, new ModelText( 'foo' ) );
-			const eleRange = ModelRange.createFromParentsAndOffsets( element, 1, element, 2 );
+			const eleRange = model.createRange( model.createPositionAt( element, 1 ), model.createPositionAt( element, 2 ) );
 			sinon.spy( dispatcher, 'fire' );
 
 			dispatcher.convertMarkerRemove( 'name', eleRange );
@@ -559,7 +564,7 @@ describe( 'DowncastDispatcher', () => {
 		} );
 
 		it( 'should fire conversion for the range', () => {
-			range = ModelRange.createIn( root );
+			range = model.createRangeIn( root );
 
 			dispatcher.on( 'addMarker:name', ( evt, data ) => {
 				expect( data.markerName ).to.equal( 'name' );
@@ -570,7 +575,7 @@ describe( 'DowncastDispatcher', () => {
 		} );
 
 		it( 'should be possible to override', () => {
-			range = ModelRange.createIn( root );
+			range = model.createRangeIn( root );
 
 			const removeMarkerSpy = sinon.spy();
 			const highRemoveMarkerSpy = sinon.spy();

+ 4 - 4
packages/ckeditor5-engine/tests/conversion/mapper.js

@@ -379,7 +379,7 @@ describe( 'Mapper', () => {
 
 		describe( 'toModelRange', () => {
 			it( 'should transform range', () => {
-				const viewRange = ViewRange.createFromParentsAndOffsets( viewDiv, 0, viewTextFOO, 2 );
+				const viewRange = ViewRange._createFromParentsAndOffsets( viewDiv, 0, viewTextFOO, 2 );
 				const modelRange = mapper.toModelRange( viewRange );
 				expect( modelRange.start.parent ).to.equal( modelDiv );
 				expect( modelRange.start.offset ).to.equal( 0 );
@@ -390,7 +390,7 @@ describe( 'Mapper', () => {
 
 		describe( 'toViewRange', () => {
 			it( 'should transform range', () => {
-				const modelRange = ModelRange.createFromParentsAndOffsets( modelDiv, 0, modelP, 3 );
+				const modelRange = new ModelRange( ModelPosition._createAt( modelDiv, 0 ), ModelPosition._createAt( modelP, 3 ) );
 				const viewRange = mapper.toViewRange( modelRange );
 				expect( viewRange.start.parent ).to.equal( viewTextX );
 				expect( viewRange.start.offset ).to.equal( 0 );
@@ -400,7 +400,7 @@ describe( 'Mapper', () => {
 		} );
 
 		function createToViewTest( modelElement, modelOffset, viewElement, viewOffset ) {
-			const modelPosition = ModelPosition.createFromParentAndOffset( modelElement, modelOffset );
+			const modelPosition = ModelPosition._createAt( modelElement, modelOffset );
 			const viewPosition = mapper.toViewPosition( modelPosition );
 			expect( viewPosition.parent ).to.equal( viewElement );
 			expect( viewPosition.offset ).to.equal( viewOffset );
@@ -530,7 +530,7 @@ describe( 'Mapper', () => {
 		} );
 
 		function createToViewTest( modelElement, modelOffset, viewElement, viewOffset ) {
-			const modelPosition = ModelPosition.createFromParentAndOffset( modelElement, modelOffset );
+			const modelPosition = ModelPosition._createAt( modelElement, modelOffset );
 			const viewPosition = mapper.toViewPosition( modelPosition );
 			expect( viewPosition.parent ).to.equal( viewElement );
 			expect( viewPosition.offset ).to.equal( viewOffset );

+ 4 - 4
packages/ckeditor5-engine/tests/conversion/upcast-converters.js

@@ -761,7 +761,7 @@ describe( 'upcast-converters', () => {
 				if ( conversionApi.consumable.consume( data.viewItem ) ) {
 					const text = conversionApi.writer.createText( data.viewItem.data.replace( /fuck/gi, '****' ) );
 					conversionApi.writer.insert( text, data.modelCursor );
-					data.modelRange = ModelRange.createFromPositionAndShift( data.modelCursor, text.offsetSize );
+					data.modelRange = ModelRange._createFromPositionAndShift( data.modelCursor, text.offsetSize );
 					data.modelCursor = data.modelRange.end;
 				}
 			} );
@@ -841,9 +841,9 @@ describe( 'upcast-converters', () => {
 					const paragraph = conversionApi.writer.createElement( 'paragraph' );
 
 					conversionApi.writer.insert( paragraph, data.modelCursor );
-					conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( paragraph ) );
+					conversionApi.convertChildren( data.viewItem, ModelPosition._createAt( paragraph, 0 ) );
 
-					data.modelRange = ModelRange.createOn( paragraph );
+					data.modelRange = ModelRange._createOn( paragraph );
 					data.modelCursor = data.modelRange.end;
 				}
 			} );
@@ -863,7 +863,7 @@ describe( 'upcast-converters', () => {
 				new ViewContainerElement( 'div', null, [ new ViewText( 'abc' ), new ViewContainerElement( 'foo' ) ] ),
 				new ViewContainerElement( 'bar' )
 			] );
-			const position = ModelPosition.createAt( new ModelElement( 'element' ) );
+			const position = ModelPosition._createAt( new ModelElement( 'element' ), 0 );
 
 			dispatcher.on( 'documentFragment', convertToModelFragment() );
 			dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );

+ 7 - 7
packages/ckeditor5-engine/tests/conversion/upcast-selection-converters.js

@@ -46,7 +46,7 @@ describe( 'convertSelectionChange', () => {
 
 	it( 'should convert collapsed selection', () => {
 		const viewSelection = new ViewSelection();
-		viewSelection.setTo( ViewRange.createFromParentsAndOffsets(
+		viewSelection.setTo( ViewRange._createFromParentsAndOffsets(
 			viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) );
 
 		convertSelection( null, { newSelection: viewSelection } );
@@ -63,7 +63,7 @@ describe( 'convertSelectionChange', () => {
 		mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) );
 
 		const viewSelection = new ViewSelection( [
-			ViewRange.createFromParentsAndOffsets( viewRoot.getChild( 0 ).getChild( 0 ), 2, viewRoot.getChild( 0 ).getChild( 0 ), 6 )
+			ViewRange._createFromParentsAndOffsets( viewRoot.getChild( 0 ).getChild( 0 ), 2, viewRoot.getChild( 0 ).getChild( 0 ), 6 )
 		] );
 
 		convertSelection( null, { newSelection: viewSelection } );
@@ -73,9 +73,9 @@ describe( 'convertSelectionChange', () => {
 
 	it( 'should convert multi ranges selection', () => {
 		const viewSelection = new ViewSelection( [
-			ViewRange.createFromParentsAndOffsets(
+			ViewRange._createFromParentsAndOffsets(
 				viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ),
-			ViewRange.createFromParentsAndOffsets(
+			ViewRange._createFromParentsAndOffsets(
 				viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 )
 		] );
 
@@ -100,9 +100,9 @@ describe( 'convertSelectionChange', () => {
 
 	it( 'should convert reverse selection', () => {
 		const viewSelection = new ViewSelection( [
-			ViewRange.createFromParentsAndOffsets(
+			ViewRange._createFromParentsAndOffsets(
 				viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ),
-			ViewRange.createFromParentsAndOffsets(
+			ViewRange._createFromParentsAndOffsets(
 				viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 )
 		], { backward: true } );
 
@@ -114,7 +114,7 @@ describe( 'convertSelectionChange', () => {
 
 	it( 'should not enqueue changes if selection has not changed', () => {
 		const viewSelection = new ViewSelection( [
-			ViewRange.createFromParentsAndOffsets(
+			ViewRange._createFromParentsAndOffsets(
 				viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 )
 		] );
 

+ 15 - 15
packages/ckeditor5-engine/tests/conversion/upcastdispatcher.js

@@ -162,7 +162,7 @@ describe( 'UpcastDispatcher', () => {
 
 				// Set conversion result to `modelRange` property of `data`.
 				// Later we will check if it was returned by `convert` method.
-				data.modelRange = ModelRange.createOn( text );
+				data.modelRange = ModelRange._createOn( text );
 			} );
 
 			const conversionResult = model.change( writer => dispatcher.convert( viewText, writer ) );
@@ -199,7 +199,7 @@ describe( 'UpcastDispatcher', () => {
 
 				// Set conversion result to `modelRange` property of `data`.
 				// Later we will check if it was returned by `convert` method.
-				data.modelRange = ModelRange.createOn( paragraph );
+				data.modelRange = ModelRange._createOn( paragraph );
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
@@ -236,7 +236,7 @@ describe( 'UpcastDispatcher', () => {
 
 				// Set conversion result to `modelRange` property of `data`.
 				// Later we will check if it was returned by `convert` method.
-				data.modelRange = ModelRange.createOn( text );
+				data.modelRange = ModelRange._createOn( text );
 			} );
 
 			const conversionResult = model.change( writer => dispatcher.convert( viewFragment, writer ) );
@@ -262,25 +262,25 @@ describe( 'UpcastDispatcher', () => {
 
 				// Create and insert empty split element before target element.
 				const emptySplit = conversionApi.writer.createElement( 'paragraph' );
-				conversionApi.writer.insert( emptySplit, ModelPosition.createAfter( paragraph ) );
+				conversionApi.writer.insert( emptySplit, ModelPosition._createAfter( paragraph ) );
 
 				// Create and insert not empty split after target element.
 				const notEmptySplit = conversionApi.writer.createElement( 'paragraph' );
 				conversionApi.writer.appendText( 'foo', notEmptySplit );
-				conversionApi.writer.insert( notEmptySplit, ModelPosition.createAfter( emptySplit ) );
+				conversionApi.writer.insert( notEmptySplit, ModelPosition._createAfter( emptySplit ) );
 
 				// Create and insert split with other split inside (both should be removed)
 				const outerSplit = conversionApi.writer.createElement( 'paragraph' );
 				const innerSplit = conversionApi.writer.createElement( 'paragraph' );
 				conversionApi.writer.append( innerSplit, outerSplit );
-				conversionApi.writer.insert( outerSplit, ModelPosition.createBefore( paragraph ) );
+				conversionApi.writer.insert( outerSplit, ModelPosition._createBefore( paragraph ) );
 
 				dispatcher._removeIfEmpty.add( emptySplit );
 				dispatcher._removeIfEmpty.add( notEmptySplit );
 				dispatcher._removeIfEmpty.add( outerSplit );
 				dispatcher._removeIfEmpty.add( innerSplit );
 
-				data.modelRange = ModelRange.createOn( paragraph );
+				data.modelRange = ModelRange._createOn( paragraph );
 				data.modelCursor = data.modelRange.end;
 
 				// We have the following result:
@@ -321,7 +321,7 @@ describe( 'UpcastDispatcher', () => {
 				conversionApi.writer.insert( fragment, data.modelCursor );
 
 				// Create range on this fragment as a conversion result.
-				data.modelRange = ModelRange.createIn( data.modelCursor.parent );
+				data.modelRange = ModelRange._createIn( data.modelCursor.parent );
 			} );
 
 			const conversionResult = model.change( writer => dispatcher.convert( viewFragment, writer ) );
@@ -404,14 +404,14 @@ describe( 'UpcastDispatcher', () => {
 			dispatcher.on( 'element:p', ( evt, data ) => {
 				spyP();
 
-				data.modelRange = ModelRange.createOn( modelP );
+				data.modelRange = ModelRange._createOn( modelP );
 				data.modelCursor = data.modelRange.end;
 			} );
 
 			dispatcher.on( 'text', ( evt, data ) => {
 				spyText();
 
-				data.modelRange = ModelRange.createOn( modelText );
+				data.modelRange = ModelRange._createOn( modelText );
 				data.modelCursor = data.modelRange.end;
 			} );
 
@@ -501,7 +501,7 @@ describe( 'UpcastDispatcher', () => {
 				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
 					spy();
 
-					const result = conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( rootMock ) );
+					const result = conversionApi.convertChildren( data.viewItem, ModelPosition._createAt( rootMock, 0 ) );
 
 					expect( result.modelRange ).to.be.instanceof( ModelRange );
 					expect( result.modelRange.start.path ).to.deep.equal( [ 0 ] );
@@ -540,7 +540,7 @@ describe( 'UpcastDispatcher', () => {
 				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
 					const paragraph = conversionApi.writer.createElement( 'paragraph' );
 					const span = conversionApi.writer.createElement( 'span' );
-					const position = ModelPosition.createAt( paragraph );
+					const position = ModelPosition._createAt( paragraph, 0 );
 
 					const result = conversionApi.splitToAllowedParent( span, position );
 
@@ -572,13 +572,13 @@ describe( 'UpcastDispatcher', () => {
 					conversionApi.writer.insert( paragraph, section );
 					conversionApi.writer.insert( span, paragraph );
 
-					const position = ModelPosition.createAt( span );
+					const position = ModelPosition._createAt( span, 0 );
 
 					const paragraph2 = conversionApi.writer.createElement( 'paragraph' );
 					const result = conversionApi.splitToAllowedParent( paragraph2, position );
 
 					expect( result ).to.deep.equal( {
-						position: ModelPosition.createAfter( paragraph ),
+						position: ModelPosition._createAfter( paragraph ),
 						cursorParent: paragraph.parent.getChild( 1 ).getChild( 0 )
 					} );
 
@@ -597,7 +597,7 @@ describe( 'UpcastDispatcher', () => {
 				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
 					const paragraph = conversionApi.writer.createElement( 'paragraph' );
 					const span = conversionApi.writer.createElement( 'span' );
-					const position = ModelPosition.createAt( paragraph );
+					const position = ModelPosition._createAt( paragraph, 0 );
 
 					const result = conversionApi.splitToAllowedParent( span, position );
 

+ 25 - 83
packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js

@@ -23,8 +23,6 @@ import RenameOperation from '../../src/model/operation/renameoperation';
 import RootAttributeOperation from '../../src/model/operation/rootattributeoperation';
 import MergeOperation from '../../src/model/operation/mergeoperation';
 import SplitOperation from '../../src/model/operation/splitoperation';
-import WrapOperation from '../../src/model/operation/wrapoperation';
-import UnwrapOperation from '../../src/model/operation/unwrapoperation';
 import Model from '../../src/model/model';
 import ModelDocumentFragment from '../../src/model/documentfragment';
 
@@ -185,9 +183,9 @@ describe( 'debug tools', () => {
 		} );
 
 		it( 'for ModelRange', () => {
-			const rangeInRoot = ModelRange.createIn( modelRoot );
-			const rangeInElement = ModelRange.createIn( modelElement );
-			const rangeInDocFrag = ModelRange.createIn( modelDocFrag );
+			const rangeInRoot = ModelRange._createIn( modelRoot );
+			const rangeInElement = ModelRange._createIn( modelElement );
+			const rangeInDocFrag = ModelRange._createIn( modelDocFrag );
 
 			expect( rangeInRoot.toString() ).to.equal( 'main [ 0 ] - [ 0 ]' );
 			expect( rangeInElement.toString() ).to.equal( '<paragraph> [ 0 ] - [ 3 ]' );
@@ -228,7 +226,7 @@ describe( 'debug tools', () => {
 			} );
 
 			it( 'AttributeOperation', () => {
-				const op = new AttributeOperation( ModelRange.createIn( modelRoot ), 'key', null, { foo: 'bar' }, 0 );
+				const op = new AttributeOperation( ModelRange._createIn( modelRoot ), 'key', null, { foo: 'bar' }, 0 );
 
 				expect( op.toString() ).to.equal( 'AttributeOperation( 0 ): "key": null -> {"foo":"bar"}, main [ 0 ] - [ 6 ]' );
 
@@ -237,7 +235,7 @@ describe( 'debug tools', () => {
 			} );
 
 			it( 'DetachOperation (text node)', () => {
-				const op = new DetachOperation( ModelPosition.createAt( modelRoot, 0 ), 3 );
+				const op = new DetachOperation( ModelPosition._createAt( modelRoot, 0 ), 3 );
 
 				expect( op.toString() ).to.equal( 'DetachOperation( null ): #foo -> main [ 0 ] - [ 3 ]' );
 
@@ -249,7 +247,7 @@ describe( 'debug tools', () => {
 				const element = new ModelElement( 'element' );
 				modelRoot._insertChild( 0, element );
 
-				const op = new DetachOperation( ModelPosition.createBefore( element ), 1 );
+				const op = new DetachOperation( ModelPosition._createBefore( element ), 1 );
 
 				expect( op.toString() ).to.equal( 'DetachOperation( null ): <element> -> main [ 0 ] - [ 1 ]' );
 
@@ -261,7 +259,7 @@ describe( 'debug tools', () => {
 				const element = new ModelElement( 'element' );
 				modelRoot._insertChild( 0, element );
 
-				const op = new DetachOperation( ModelPosition.createBefore( element ), 2 );
+				const op = new DetachOperation( ModelPosition._createBefore( element ), 2 );
 
 				expect( op.toString() ).to.equal( 'DetachOperation( null ): [ 2 ] -> main [ 0 ] - [ 2 ]' );
 
@@ -270,7 +268,7 @@ describe( 'debug tools', () => {
 			} );
 
 			it( 'InsertOperation (text node)', () => {
-				const op = new InsertOperation( ModelPosition.createAt( modelRoot, 3 ), [ new ModelText( 'abc' ) ], 0 );
+				const op = new InsertOperation( ModelPosition._createAt( modelRoot, 3 ), [ new ModelText( 'abc' ) ], 0 );
 
 				expect( op.toString() ).to.equal( 'InsertOperation( 0 ): #abc -> main [ 3 ]' );
 
@@ -279,7 +277,7 @@ describe( 'debug tools', () => {
 			} );
 
 			it( 'InsertOperation (element)', () => {
-				const op = new InsertOperation( ModelPosition.createAt( modelRoot, 3 ), [ new ModelElement( 'paragraph' ) ], 0 );
+				const op = new InsertOperation( ModelPosition._createAt( modelRoot, 3 ), [ new ModelElement( 'paragraph' ) ], 0 );
 
 				expect( op.toString() ).to.equal( 'InsertOperation( 0 ): <paragraph> -> main [ 3 ]' );
 
@@ -289,7 +287,7 @@ describe( 'debug tools', () => {
 
 			it( 'InsertOperation (multiple nodes)', () => {
 				const nodes = [ new ModelText( 'x' ), new ModelElement( 'y' ), new ModelText( 'z' ) ];
-				const op = new InsertOperation( ModelPosition.createAt( modelRoot, 3 ), nodes, 0 );
+				const op = new InsertOperation( ModelPosition._createAt( modelRoot, 3 ), nodes, 0 );
 
 				expect( op.toString() ).to.equal( 'InsertOperation( 0 ): [ 3 ] -> main [ 3 ]' );
 
@@ -298,7 +296,7 @@ describe( 'debug tools', () => {
 			} );
 
 			it( 'MarkerOperation', () => {
-				const op = new MarkerOperation( 'marker', null, ModelRange.createIn( modelRoot ), modelDoc.markers, false, 0 );
+				const op = new MarkerOperation( 'marker', null, ModelRange._createIn( modelRoot ), modelDoc.markers, false, 0 );
 
 				expect( op.toString() ).to.equal( 'MarkerOperation( 0 ): "marker": null -> main [ 0 ] - [ 6 ]' );
 
@@ -307,7 +305,7 @@ describe( 'debug tools', () => {
 			} );
 
 			it( 'MoveOperation', () => {
-				const op = new MoveOperation( ModelPosition.createAt( modelRoot, 1 ), 2, ModelPosition.createAt( modelRoot, 6 ), 0 );
+				const op = new MoveOperation( ModelPosition._createAt( modelRoot, 1 ), 2, ModelPosition._createAt( modelRoot, 6 ), 0 );
 
 				expect( op.toString() ).to.equal( 'MoveOperation( 0 ): main [ 1 ] - [ 3 ] -> main [ 6 ]' );
 
@@ -325,7 +323,7 @@ describe( 'debug tools', () => {
 			} );
 
 			it( 'RenameOperation', () => {
-				const op = new RenameOperation( ModelPosition.createAt( modelRoot, 1 ), 'old', 'new', 0 );
+				const op = new RenameOperation( ModelPosition._createAt( modelRoot, 1 ), 'old', 'new', 0 );
 
 				expect( op.toString() ).to.equal( 'RenameOperation( 0 ): main [ 1 ]: "old" -> "new"' );
 
@@ -360,15 +358,11 @@ describe( 'debug tools', () => {
 			} );
 
 			it( 'SplitOperation without graveyard position', () => {
-				const op = new SplitOperation(
-					new ModelPosition( modelRoot, [ 1, 4 ] ),
-					6,
-					null,
-					0
-				);
+				const position = new ModelPosition( modelRoot, [ 1, 4 ] );
+				const op = new SplitOperation( position, 6, null, 0 );
 
 				expect( op.toString() ).to.equal(
-					'SplitOperation( 0 ): main [ 1, 4 ] ( 6 )'
+					'SplitOperation( 0 ): main [ 1, 4 ] ( 6 ) -> main [ 2 ]'
 				);
 
 				op.log();
@@ -376,63 +370,11 @@ describe( 'debug tools', () => {
 			} );
 
 			it( 'SplitOperation with graveyard position', () => {
-				const op = new SplitOperation(
-					new ModelPosition( modelRoot, [ 1, 4 ] ),
-					6,
-					new ModelPosition( modelDoc.graveyard, [ 0 ] ),
-					0
-				);
-
-				expect( op.toString() ).to.equal(
-					'SplitOperation( 0 ): main [ 1, 4 ] ( 6 ), $graveyard [ 0 ]'
-				);
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'WrapOperation with element', () => {
-				const op = new WrapOperation(
-					new ModelPosition( modelRoot, [ 3 ] ),
-					2,
-					new ModelElement( 'blockQuote' ),
-					0
-				);
-
-				expect( op.toString() ).to.equal(
-					'WrapOperation( 0 ): main [ 3 ] - [ 5 ] with <blockQuote>'
-				);
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'WrapOperation with graveyard position', () => {
-				const op = new WrapOperation(
-					new ModelPosition( modelRoot, [ 3 ] ),
-					2,
-					new ModelPosition( modelDoc.graveyard, [ 0 ] ),
-					0
-				);
-
-				expect( op.toString() ).to.equal(
-					'WrapOperation( 0 ): main [ 3 ] - [ 5 ] with $graveyard [ 0 ]'
-				);
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'UnwrapOperation', () => {
-				const op = new UnwrapOperation(
-					new ModelPosition( modelRoot, [ 1, 0 ] ),
-					2,
-					new ModelPosition( modelDoc.graveyard, [ 0 ] ),
-					0
-				);
+				const position = new ModelPosition( modelRoot, [ 1, 4 ] );
+				const op = new SplitOperation( position, 6, new ModelPosition( modelDoc.graveyard, [ 0 ] ), 0 );
 
 				expect( op.toString() ).to.equal(
-					'UnwrapOperation( 0 ): main [ 1, 0 ] ( 2 ), $graveyard [ 0 ]'
+					'SplitOperation( 0 ): main [ 1, 4 ] ( 6 ) -> main [ 2 ] with $graveyard [ 0 ]'
 				);
 
 				op.log();
@@ -441,7 +383,7 @@ describe( 'debug tools', () => {
 		} );
 
 		it( 'for applied operations', () => {
-			const op = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), [ new ModelText( 'foo' ) ], 0 );
+			const op = new InsertOperation( ModelPosition._createAt( modelRoot, 0 ), [ new ModelText( 'foo' ) ], 0 );
 
 			model.applyOperation( op );
 
@@ -617,11 +559,11 @@ describe( 'debug tools', () => {
 			const viewDoc = view.document;
 
 			model.change( () => {
-				const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), new ModelText( 'foobar' ), 0 );
+				const insert = new InsertOperation( ModelPosition._createAt( modelRoot, 0 ), new ModelText( 'foobar' ), 0 );
 				model.applyOperation( insert );
 
 				const graveyard = modelDoc.graveyard;
-				const move = new MoveOperation( ModelPosition.createAt( modelRoot, 1 ), 2, ModelPosition.createAt( graveyard, 0 ), 1 );
+				const move = new MoveOperation( ModelPosition._createAt( modelRoot, 1 ), 2, ModelPosition._createAt( graveyard, 0 ), 1 );
 				model.applyOperation( move );
 			} );
 
@@ -709,7 +651,7 @@ describe( 'debug tools', () => {
 			const modelRoot = model.document.getRoot();
 
 			for ( let i = 0; i < 25; i++ ) {
-				const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), new ModelText( 'foobar' ), modelDoc.version );
+				const insert = new InsertOperation( ModelPosition._createAt( modelRoot, 0 ), new ModelText( 'foobar' ), modelDoc.version );
 				model.applyOperation( insert );
 			}
 
@@ -730,7 +672,7 @@ describe( 'debug tools', () => {
 
 			otherRoot._appendChild( element );
 
-			const insert = new InsertOperation( ModelPosition.createAt( element, 0 ), new ModelText( 'foo' ), 0 );
+			const insert = new InsertOperation( ModelPosition._createAt( element, 0 ), new ModelText( 'foo' ), 0 );
 			model.applyOperation( insert );
 
 			const stringifiedOperations = model.getAppliedOperations();
@@ -747,7 +689,7 @@ describe( 'debug tools', () => {
 
 			otherRoot._appendChild( element );
 
-			const insert = new InsertOperation( ModelPosition.createAt( element, 0 ), new ModelText( 'foo' ), 0 );
+			const insert = new InsertOperation( ModelPosition._createAt( element, 0 ), new ModelText( 'foo' ), 0 );
 			model.applyOperation( insert );
 
 			const stringifiedOperations = model.getAppliedOperations();

+ 10 - 8
packages/ckeditor5-engine/tests/dev-utils/model.js

@@ -67,7 +67,7 @@ describe( 'model test utils', () => {
 			const stringifySpy = sinon.spy( getData, '_stringify' );
 			root._appendChild( new Element( 'b', null, new Text( 'btext' ) ) );
 			model.change( writer => {
-				writer.setSelection( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
+				writer.setSelection( new Range( Position._createAt( root, 0 ), Position._createAt( root, 1 ) ) );
 			} );
 			expect( getData( model ) ).to.equal( '[<b>btext</b>]' );
 			sinon.assert.calledOnce( stringifySpy );
@@ -355,7 +355,7 @@ describe( 'model test utils', () => {
 
 			it( 'writes flat selection containing couple of nodes', () => {
 				model.change( writer => {
-					writer.setSelection( Range.createFromParentsAndOffsets( root, 0, root, 4 ) );
+					writer.setSelection( new Range( Position._createAt( root, 0 ), Position._createAt( root, 4 ) ) );
 				} );
 
 				expect( stringify( root, selection ) ).to.equal(
@@ -365,7 +365,7 @@ describe( 'model test utils', () => {
 
 			it( 'writes flat selection within text', () => {
 				model.change( writer => {
-					writer.setSelection( Range.createFromParentsAndOffsets( root, 2, root, 3 ) );
+					writer.setSelection( new Range( Position._createAt( root, 2 ), Position._createAt( root, 3 ) ) );
 				} );
 
 				expect( stringify( root, selection ) ).to.equal(
@@ -375,7 +375,7 @@ describe( 'model test utils', () => {
 
 			it( 'writes multi-level selection', () => {
 				model.change( writer => {
-					writer.setSelection( Range.createFromParentsAndOffsets( elA, 0, elB, 0 ) );
+					writer.setSelection( new Range( Position._createAt( elA, 0 ), Position._createAt( elB, 0 ) ) );
 				} );
 
 				expect( stringify( root, selection ) ).to.equal(
@@ -385,7 +385,7 @@ describe( 'model test utils', () => {
 
 			it( 'writes selection when is backward', () => {
 				model.change( writer => {
-					writer.setSelection( Range.createFromParentsAndOffsets( elA, 0, elB, 0 ), { backward: true } );
+					writer.setSelection( new Range( Position._createAt( elA, 0 ), Position._createAt( elB, 0 ) ), { backward: true } );
 				} );
 
 				expect( stringify( root, selection ) ).to.equal(
@@ -398,14 +398,14 @@ describe( 'model test utils', () => {
 
 				root._appendChild( new Text( 'நிலைக்கு' ) );
 				model.change( writer => {
-					writer.setSelection( Range.createFromParentsAndOffsets( root, 2, root, 6 ) );
+					writer.setSelection( new Range( Position._createAt( root, 2 ), Position._createAt( root, 6 ) ) );
 				} );
 
 				expect( stringify( root, selection ) ).to.equal( 'நி[லைக்]கு' );
 			} );
 
 			it( 'uses range and coverts it to selection', () => {
-				const range = Range.createFromParentsAndOffsets( elA, 0, elB, 0 );
+				const range = new Range( Position._createAt( elA, 0 ), Position._createAt( elB, 0 ) );
 
 				expect( stringify( root, range ) ).to.equal(
 					'<a>[</a>foo<$text bold="true">bar</$text><b>]</b>'
@@ -441,7 +441,9 @@ describe( 'model test utils', () => {
 				expect( el ).to.be.instanceOf( Element );
 				expect( fragment ).to.be.instanceOf( DocumentFragment );
 				expect( selection.rangeCount ).to.equal( 1 );
-				expect( selection.getFirstRange().isEqual( Range.createFromParentsAndOffsets( fragment, 0, fragment, 1 ) ) ).to.be.true;
+
+				const range = new Range( Position._createAt( fragment, 0 ), Position._createAt( fragment, 1 ) );
+				expect( selection.getFirstRange().isEqual( range ) ).to.be.true;
 			}
 		} );
 

+ 34 - 34
packages/ckeditor5-engine/tests/dev-utils/view.js

@@ -64,7 +64,7 @@ describe( 'view test utils', () => {
 				root._appendChild( new Element( 'p' ) );
 
 				view.change( writer => {
-					writer.setSelection( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
+					writer.setSelection( Range._createFromParentsAndOffsets( root, 0, root, 1 ) );
 				} );
 
 				expect( getData( view, options ) ).to.equal( '[<p></p>]' );
@@ -166,7 +166,7 @@ describe( 'view test utils', () => {
 			const b1 = new Element( 'b', null, text1 );
 			const b2 = new Element( 'b', null, text2 );
 			const p = new Element( 'p', null, [ b1, b2 ] );
-			const range = Range.createFromParentsAndOffsets( p, 1, p, 2 );
+			const range = Range._createFromParentsAndOffsets( p, 1, p, 2 );
 			const selection = new DocumentSelection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p><b>foobar</b>[<b>bazqux</b>]</p>' );
 		} );
@@ -175,7 +175,7 @@ describe( 'view test utils', () => {
 			const text = new Text( 'நிலைக்கு' );
 			const b = new Element( 'b', null, text );
 			const p = new Element( 'p', null, b );
-			const range = Range.createFromParentsAndOffsets( p, 0, text, 4 );
+			const range = Range._createFromParentsAndOffsets( p, 0, text, 4 );
 			const selection = new DocumentSelection( [ range ] );
 
 			expect( stringify( p, selection ) ).to.equal( '<p>[<b>நிலை}க்கு</b></p>' );
@@ -184,7 +184,7 @@ describe( 'view test utils', () => {
 		it( 'should write collapsed selection ranges inside elements', () => {
 			const text = new Text( 'foobar' );
 			const p = new Element( 'p', null, text );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 0 );
+			const range = Range._createFromParentsAndOffsets( p, 0, p, 0 );
 			const selection = new DocumentSelection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p>[]foobar</p>' );
 		} );
@@ -195,7 +195,7 @@ describe( 'view test utils', () => {
 			const b1 = new Element( 'b', null, text1 );
 			const b2 = new Element( 'b', null, text2 );
 			const p = new Element( 'p', null, [ b1, b2 ] );
-			const range = Range.createFromParentsAndOffsets( text1, 1, text1, 5 );
+			const range = Range._createFromParentsAndOffsets( text1, 1, text1, 5 );
 			const selection = new DocumentSelection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p><b>f{ooba}r</b><b>bazqux</b></p>' );
 		} );
@@ -206,7 +206,7 @@ describe( 'view test utils', () => {
 			const b1 = new Element( 'b', null, text1 );
 			const b2 = new Element( 'b', null, text2 );
 			const p = new Element( 'p', null, [ b1, b2 ] );
-			const range = Range.createFromParentsAndOffsets( text1, 1, text1, 5 );
+			const range = Range._createFromParentsAndOffsets( text1, 1, text1, 5 );
 			const selection = new DocumentSelection( [ range ] );
 			expect( stringify( p, selection, { sameSelectionCharacters: true } ) )
 				.to.equal( '<p><b>f[ooba]r</b><b>bazqux</b></p>' );
@@ -215,7 +215,7 @@ describe( 'view test utils', () => {
 		it( 'should write collapsed selection ranges inside texts', () => {
 			const text = new Text( 'foobar' );
 			const p = new Element( 'p', null, text );
-			const range = Range.createFromParentsAndOffsets( text, 0, text, 0 );
+			const range = Range._createFromParentsAndOffsets( text, 0, text, 0 );
 			const selection = new DocumentSelection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p>{}foobar</p>' );
 		} );
@@ -226,7 +226,7 @@ describe( 'view test utils', () => {
 			const b1 = new Element( 'b', null, text1 );
 			const b2 = new Element( 'b', null, text2 );
 			const p = new Element( 'p', null, [ b1, b2 ] );
-			const range = Range.createFromParentsAndOffsets( p, 0, text2, 5 );
+			const range = Range._createFromParentsAndOffsets( p, 0, text2, 5 );
 			const selection = new DocumentSelection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b><b>bazqu}x</b></p>' );
 		} );
@@ -277,7 +277,7 @@ describe( 'view test utils', () => {
 			const text = new Text( 'foobar' );
 			const b = new Element( 'b', null, text );
 			const p = new Element( 'p', null, b );
-			const range = Range.createFromParentsAndOffsets( p, 0, p, 5 );
+			const range = Range._createFromParentsAndOffsets( p, 0, p, 5 );
 
 			expect( stringify( p, range ) ).to.equal( '<p>[<b>foobar</b></p>' );
 		} );
@@ -286,7 +286,7 @@ describe( 'view test utils', () => {
 			const text = new Text( 'foobar' );
 			const b = new Element( 'b', null, text );
 			const p = new Element( 'p', null, b );
-			const range = Range.createFromParentsAndOffsets( p, -1, p, 1 );
+			const range = Range._createFromParentsAndOffsets( p, -1, p, 1 );
 
 			expect( stringify( p, range ) ).to.equal( '<p><b>foobar</b>]</p>' );
 		} );
@@ -295,7 +295,7 @@ describe( 'view test utils', () => {
 			const text = new Text( 'foobar' );
 			const b = new Element( 'b', null, text );
 			const p = new Element( 'p', null, b );
-			const range = Range.createFromParentsAndOffsets( text, 0, text, 7 );
+			const range = Range._createFromParentsAndOffsets( text, 0, text, 7 );
 
 			expect( stringify( p, range ) ).to.equal( '<p><b>{foobar</b></p>' );
 		} );
@@ -304,7 +304,7 @@ describe( 'view test utils', () => {
 			const text = new Text( 'foobar' );
 			const b = new Element( 'b', null, text );
 			const p = new Element( 'p', null, b );
-			const range = Range.createFromParentsAndOffsets( text, -1, text, 2 );
+			const range = Range._createFromParentsAndOffsets( text, -1, text, 2 );
 
 			expect( stringify( p, range ) ).to.equal( '<p><b>fo}obar</b></p>' );
 		} );
@@ -315,8 +315,8 @@ describe( 'view test utils', () => {
 			const b1 = new Element( 'b', null, text1 );
 			const b2 = new Element( 'b', null, text2 );
 			const p = new Element( 'p', null, [ b1, b2 ] );
-			const range1 = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-			const range2 = Range.createFromParentsAndOffsets( p, 1, p, 1 );
+			const range1 = Range._createFromParentsAndOffsets( p, 0, p, 1 );
+			const range2 = Range._createFromParentsAndOffsets( p, 1, p, 1 );
 			const selection = new DocumentSelection( [ range2, range1 ] );
 
 			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b>][]<b>bazqux</b></p>' );
@@ -327,10 +327,10 @@ describe( 'view test utils', () => {
 			const text2 = new Text( 'bazqux' );
 			const b = new Element( 'b', null, text1 );
 			const p = new Element( 'p', null, [ b, text2 ] );
-			const range1 = Range.createFromParentsAndOffsets( p, 0, p, 1 );
-			const range2 = Range.createFromParentsAndOffsets( text2, 0, text2, 3 );
-			const range3 = Range.createFromParentsAndOffsets( text2, 3, text2, 4 );
-			const range4 = Range.createFromParentsAndOffsets( p, 1, p, 1 );
+			const range1 = Range._createFromParentsAndOffsets( p, 0, p, 1 );
+			const range2 = Range._createFromParentsAndOffsets( text2, 0, text2, 3 );
+			const range3 = Range._createFromParentsAndOffsets( text2, 3, text2, 4 );
+			const range4 = Range._createFromParentsAndOffsets( p, 1, p, 1 );
 			const selection = new DocumentSelection( [ range1, range2, range3, range4 ] );
 
 			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b>][]{baz}{q}ux</p>' );
@@ -345,7 +345,7 @@ describe( 'view test utils', () => {
 
 		it( 'should use Range instance instead of Selection', () => {
 			const text = new Text( 'foobar' );
-			const range = Range.createFromParentsAndOffsets( text, 3, text, 4 );
+			const range = Range._createFromParentsAndOffsets( text, 3, text, 4 );
 			const string = stringify( text, range );
 			expect( string ).to.equal( 'foo{b}ar' );
 		} );
@@ -428,7 +428,7 @@ describe( 'view test utils', () => {
 
 			expect( view ).to.be.instanceOf( DocumentFragment );
 			expect( selection.rangeCount ).to.equal( 1 );
-			expect( selection.getFirstRange().isEqual( Range.createFromParentsAndOffsets( view, 0, view, 0 ) ) ).to.be.true;
+			expect( selection.getFirstRange().isEqual( Range._createFromParentsAndOffsets( view, 0, view, 0 ) ) ).to.be.true;
 		} );
 
 		it( 'should return Element if range is around single element', () => {
@@ -438,7 +438,7 @@ describe( 'view test utils', () => {
 			expect( view ).to.be.instanceOf( Element );
 			expect( parent ).to.be.instanceOf( DocumentFragment );
 			expect( selection.rangeCount ).to.equal( 1 );
-			expect( selection.getFirstRange().isEqual( Range.createFromParentsAndOffsets( parent, 0, parent, 1 ) ) ).to.be.true;
+			expect( selection.getFirstRange().isEqual( Range._createFromParentsAndOffsets( parent, 0, parent, 1 ) ) ).to.be.true;
 		} );
 
 		it( 'should create DocumentFragment when multiple elements on root', () => {
@@ -538,8 +538,8 @@ describe( 'view test utils', () => {
 			expect( selection.rangeCount ).to.equal( 2 );
 			const ranges = [ ...selection.getRanges() ];
 
-			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 1, view, 3 ) ) ).to.be.true;
-			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( view, 4, view, 4 ) ) ).to.be.true;
+			expect( ranges[ 0 ].isEqual( Range._createFromParentsAndOffsets( view, 1, view, 3 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range._createFromParentsAndOffsets( view, 4, view, 4 ) ) ).to.be.true;
 		} );
 
 		it( 'should parse selection range between elements', () => {
@@ -555,8 +555,8 @@ describe( 'view test utils', () => {
 			expect( text.data ).to.equal( 'foobar' );
 			expect( selection.rangeCount ).to.equal( 2 );
 			const ranges = [ ...selection.getRanges() ];
-			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 0, b, 1 ) ) ).to.be.true;
-			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( view, 1, view, 1 ) ) ).to.be.true;
+			expect( ranges[ 0 ].isEqual( Range._createFromParentsAndOffsets( view, 0, b, 1 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range._createFromParentsAndOffsets( view, 1, view, 1 ) ) ).to.be.true;
 		} );
 
 		it( 'should support unicode', () => {
@@ -590,7 +590,7 @@ describe( 'view test utils', () => {
 			expect( text ).to.be.instanceof( Text );
 			expect( text.data ).to.equal( 'foobar' );
 			expect( selection.rangeCount ).to.equal( 1 );
-			expect( selection.getFirstRange().isEqual( Range.createFromParentsAndOffsets( text, 3, view, 1 ) ) ).to.be.true;
+			expect( selection.getFirstRange().isEqual( Range._createFromParentsAndOffsets( text, 3, view, 1 ) ) ).to.be.true;
 		} );
 
 		it( 'should parse ranges #2', () => {
@@ -608,17 +608,17 @@ describe( 'view test utils', () => {
 			expect( text2.data ).to.equal( 'baz' );
 			expect( selection.rangeCount ).to.equal( 2 );
 			const ranges = [ ...selection.getRanges() ];
-			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 0, text1, 4 ) ) ).to.be.true;
-			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( text2, 0, view, 2 ) ) ).to.be.true;
+			expect( ranges[ 0 ].isEqual( Range._createFromParentsAndOffsets( view, 0, text1, 4 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range._createFromParentsAndOffsets( text2, 0, view, 2 ) ) ).to.be.true;
 		} );
 
 		it( 'should use ranges order when provided', () => {
 			const { view, selection } = parse( '{f}oo{b}arb{a}z', { order: [ 3, 1, 2 ] } );
 			expect( selection.rangeCount ).to.equal( 3 );
 			const ranges = [ ...selection.getRanges() ];
-			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 3, view, 4 ) ) ).to.be.true;
-			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( view, 7, view, 8 ) ) ).to.be.true;
-			expect( ranges[ 2 ].isEqual( Range.createFromParentsAndOffsets( view, 0, view, 1 ) ) ).to.be.true;
+			expect( ranges[ 0 ].isEqual( Range._createFromParentsAndOffsets( view, 3, view, 4 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range._createFromParentsAndOffsets( view, 7, view, 8 ) ) ).to.be.true;
+			expect( ranges[ 2 ].isEqual( Range._createFromParentsAndOffsets( view, 0, view, 1 ) ) ).to.be.true;
 			expect( selection.anchor.isEqual( ranges[ 2 ].start ) ).to.be.true;
 			expect( selection.focus.isEqual( ranges[ 2 ].end ) ).to.be.true;
 		} );
@@ -627,9 +627,9 @@ describe( 'view test utils', () => {
 			const { view, selection } = parse( '{f}oo{b}arb{a}z', { order: [ 3, 1, 2 ], lastRangeBackward: true } );
 			expect( selection.rangeCount ).to.equal( 3 );
 			const ranges = [ ...selection.getRanges() ];
-			expect( ranges[ 0 ].isEqual( Range.createFromParentsAndOffsets( view, 3, view, 4 ) ) ).to.be.true;
-			expect( ranges[ 1 ].isEqual( Range.createFromParentsAndOffsets( view, 7, view, 8 ) ) ).to.be.true;
-			expect( ranges[ 2 ].isEqual( Range.createFromParentsAndOffsets( view, 0, view, 1 ) ) ).to.be.true;
+			expect( ranges[ 0 ].isEqual( Range._createFromParentsAndOffsets( view, 3, view, 4 ) ) ).to.be.true;
+			expect( ranges[ 1 ].isEqual( Range._createFromParentsAndOffsets( view, 7, view, 8 ) ) ).to.be.true;
+			expect( ranges[ 2 ].isEqual( Range._createFromParentsAndOffsets( view, 0, view, 1 ) ) ).to.be.true;
 			expect( selection.anchor.isEqual( ranges[ 2 ].end ) ).to.be.true;
 			expect( selection.focus.isEqual( ranges[ 2 ].start ) ).to.be.true;
 		} );

+ 2 - 5
packages/ckeditor5-engine/tests/manual/highlight.js

@@ -5,9 +5,6 @@
 
 /* global console, window, document */
 
-import ModelRange from '../../src/model/range';
-import ViewPosition from '../../src/view/position';
-
 import {
 	upcastElementToElement,
 } from '../../src/conversion/upcast-converters';
@@ -51,7 +48,7 @@ class FancyWidget extends Plugin {
 			model: 'fancywidget',
 			view: ( modelItem, viewWriter ) => {
 				const widgetElement = viewWriter.createContainerElement( 'figure', { class: 'fancy-widget' } );
-				viewWriter.insert( ViewPosition.createAt( widgetElement ), viewWriter.createText( 'widget' ) );
+				viewWriter.insert( viewWriter.createPositionAt( widgetElement, 0 ), viewWriter.createText( 'widget' ) );
 
 				return toWidget( widgetElement, viewWriter );
 			}
@@ -127,7 +124,7 @@ ClassicEditor.create( global.document.querySelector( '#editor' ), {
 
 function addMarker( editor, color ) {
 	editor.model.change( writer => {
-		const range = ModelRange.createFromRange( editor.model.document.selection.getFirstRange() );
+		const range = editor.model.document.selection.getFirstRange();
 		writer.addMarker( 'marker:' + color, { range, usingOperation: false } );
 	} );
 }

+ 1 - 1
packages/ckeditor5-engine/tests/manual/markers.js

@@ -108,7 +108,7 @@ function uid() {
 
 function addHighlight( color ) {
 	model.change( writer => {
-		const range = Range.createFromRange( model.document.selection.getFirstRange() );
+		const range = model.document.selection.getFirstRange();
 		const name = 'highlight:' + color + ':' + uid();
 
 		markerNames.push( name );

+ 0 - 3
packages/ckeditor5-engine/tests/manual/markers.md

@@ -1,6 +1,3 @@
-@bender-ui: collapsed
-@bender-tags: ticket, 643, iteration5
-
 ### Markers integration [#643](https://github.com/ckeditor/ckeditor5-engine/issues/643)
 
 1. Write before, after and inside marked text.

+ 1 - 1
packages/ckeditor5-engine/tests/manual/selection.html

@@ -45,4 +45,4 @@
 	<p>baz</p>
 </div>
 <h2>Model contents:</h2>
-<div id="model"></div>
+<pre id="model"></pre>

+ 1 - 0
packages/ckeditor5-engine/tests/manual/tickets/1439/1.html

@@ -0,0 +1 @@
+<div id="editor"></div>

+ 33 - 0
packages/ckeditor5-engine/tests/manual/tickets/1439/1.js

@@ -0,0 +1,33 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ ArticlePluginSet ],
+		toolbar: {
+			items: [
+				'heading',
+				'bold',
+				'italic',
+				'link',
+				'bulletedList',
+				'numberedList',
+				'blockQuote',
+				'undo',
+				'redo'
+			]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 11 - 0
packages/ckeditor5-engine/tests/manual/tickets/1439/1.md

@@ -0,0 +1,11 @@
+## Firefox should visually move the caret to the new line after a soft break [#1439](https://github.com/ckeditor/ckeditor5-engine/issues/1439)
+
+
+1. Open Firefox,
+2. In an empty editor type "foo",
+3. Press <kbd>Shift</kbd>+<kbd>Enter</kbd>.
+
+**Expected**
+
+1. The soft break should be created,
+2. The caret should be **in the new line and blinking**.

+ 2 - 0
packages/ckeditor5-engine/tests/manual/tickets/462/1.md

@@ -1,5 +1,7 @@
 ### Selection attributes conversion test [#462](https://github.com/ckeditor/ckeditor5-engine/issues/462)
 
+Open the console. Watch the logged data.
+
 #### Test 1
 
 1. Put caret inside unstyled word.

+ 2 - 2
packages/ckeditor5-engine/tests/manual/tickets/475/1.js

@@ -70,9 +70,9 @@ class AutoLinker extends Plugin {
 					const length = url.length;
 
 					if ( entry.position.offset + entry.length == index + length ) {
-						const livePos = LivePosition.createFromParentAndOffset( parent, index );
+						const livePos = LivePosition._createAt( parent, index );
 						this.editor.model.enqueueChange( writer => {
-							const urlRange = Range.createFromPositionAndShift( livePos, length );
+							const urlRange = Range._createFromPositionAndShift( livePos, length );
 							writer.setAttribute( 'link', url, urlRange );
 						} );
 						return;

+ 1 - 1
packages/ckeditor5-engine/tests/manual/tickets/ckeditor5-721/1.js

@@ -47,7 +47,7 @@ ClassicEditor
 					const b = writer.createAttributeElement( 'b' );
 					const div = writer.createContainerElement( 'div' );
 
-					writer.insert( ViewPosition.createAt( div ), b );
+					writer.insert( ViewPosition._createAt( div, 0 ), b );
 
 					return toWidget( div, writer, { label: 'element label' } );
 				}

+ 2 - 1
packages/ckeditor5-engine/tests/model/_utils/utils.js

@@ -4,6 +4,7 @@
  */
 
 import Range from '../../../src/model/range';
+import Position from '../../../src/model/position';
 import TreeWalker from '../../../src/model/treewalker';
 import Text from '../../../src/model/text';
 import TextProxy from '../../../src/model/textproxy';
@@ -85,5 +86,5 @@ export function getText( element ) {
  * @returns {engine.model.Range}
  */
 export function createRangeOnElementOnly( element ) {
-	return Range.createFromParentsAndOffsets( element.parent, element.startOffset, element, 0 );
+	return new Range( Position._createAt( element.parent, element.startOffset ), Position._createAt( element, 0 ) );
 }

+ 110 - 190
packages/ckeditor5-engine/tests/model/differ.js

@@ -15,8 +15,6 @@ import RenameOperation from '../../src/model/operation/renameoperation';
 import AttributeOperation from '../../src/model/operation/attributeoperation';
 import SplitOperation from '../../src/model/operation/splitoperation';
 import MergeOperation from '../../src/model/operation/mergeoperation';
-import WrapOperation from '../../src/model/operation/wrapoperation';
-import UnwrapOperation from '../../src/model/operation/unwrapoperation';
 
 describe( 'Differ', () => {
 	let doc, differ, root, model;
@@ -141,9 +139,9 @@ describe( 'Differ', () => {
 				insert( image, position );
 
 				const caption = new Element( 'caption' );
-				insert( caption, Position.createAt( image, 0 ) );
+				insert( caption, Position._createAt( image, 0 ) );
 
-				insert( new Text( 'foo' ), Position.createAt( caption, 0 ) );
+				insert( new Text( 'foo' ), Position._createAt( caption, 0 ) );
 
 				expectChanges( [
 					{ type: 'insert', name: 'image', length: 1, position }
@@ -170,13 +168,13 @@ describe( 'Differ', () => {
 		it( 'node in a element with changed attribute', () => {
 			const text = new Text( 'xyz', { bold: true } );
 			const position = new Position( root, [ 0, 3 ] );
-			const range = Range.createFromParentsAndOffsets( root, 0, root, 1 );
+			const range = new Range( Position._createAt( root, 0 ), Position._createAt( root, 1 ) );
 
 			model.change( () => {
 				insert( text, position );
 				attribute( range, 'align', null, 'center' );
 
-				const diffRange = Range.createFromParentsAndOffsets( root, 0, root.getChild( 0 ), 0 );
+				const diffRange = new Range( Position._createAt( root, 0 ), Position._createAt( root.getChild( 0 ), 0 ) );
 
 				// Compare to scenario above, this time there is only an attribute change on parent element,
 				// so there is also a diff for text.
@@ -200,14 +198,14 @@ describe( 'Differ', () => {
 
 		it( 'nodes before nodes with changed attributes', () => {
 			const p1 = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( p1, 1, p1, 3 );
+			const range = new Range( Position._createAt( p1, 1 ), Position._createAt( p1, 3 ) );
 			const position = new Position( root, [ 0, 0 ] );
 
 			model.change( () => {
 				attribute( range, 'bold', null, true );
 				insert( new Text( 'xx' ), position );
 
-				const rangeAfter = Range.createFromParentsAndOffsets( p1, 3, p1, 5 );
+				const rangeAfter = new Range( Position._createAt( p1, 3 ), Position._createAt( p1, 5 ) );
 
 				expectChanges( [
 					{ type: 'insert', name: '$text', length: 2, position },
@@ -218,15 +216,15 @@ describe( 'Differ', () => {
 
 		it( 'nodes between nodes with changed attributes', () => {
 			const p1 = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( p1, 1, p1, 3 );
+			const range = new Range( Position._createAt( p1, 1 ), Position._createAt( p1, 3 ) );
 			const position = new Position( root, [ 0, 2 ] );
 
 			model.change( () => {
 				attribute( range, 'bold', null, true );
 				insert( new Text( 'xx' ), position );
 
-				const rangeBefore = Range.createFromParentsAndOffsets( p1, 1, p1, 2 );
-				const rangeAfter = Range.createFromParentsAndOffsets( p1, 4, p1, 5 );
+				const rangeBefore = new Range( Position._createAt( p1, 1 ), Position._createAt( p1, 2 ) );
+				const rangeAfter = new Range( Position._createAt( p1, 4 ), Position._createAt( p1, 5 ) );
 
 				expectChanges( [
 					{
@@ -250,7 +248,7 @@ describe( 'Differ', () => {
 
 		it( 'nodes after nodes with changed attributes', () => {
 			const p1 = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( p1, 1, p1, 3 );
+			const range = new Range( Position._createAt( p1, 1 ), Position._createAt( p1, 3 ) );
 			const position = new Position( root, [ 0, 3 ] );
 
 			model.change( () => {
@@ -442,13 +440,13 @@ describe( 'Differ', () => {
 			const position = new Position( root, [ 0, 0 ] );
 
 			const p1 = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( p1, 2, p1, 3 );
+			const range = new Range( Position._createAt( p1, 2 ), Position._createAt( p1, 3 ) );
 
 			model.change( () => {
 				attribute( range, 'bold', null, true );
 				remove( position, 1 );
 
-				const newRange = Range.createFromParentsAndOffsets( p1, 1, p1, 2 );
+				const newRange = new Range( Position._createAt( p1, 1 ), Position._createAt( p1, 2 ) );
 
 				expectChanges( [
 					{ type: 'remove', name: '$text', length: 1, position },
@@ -467,13 +465,13 @@ describe( 'Differ', () => {
 			const position = new Position( root, [ 0, 0 ] );
 
 			const p1 = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( p1, 1, p1, 3 );
+			const range = new Range( Position._createAt( p1, 1 ), Position._createAt( p1, 3 ) );
 
 			model.change( () => {
 				attribute( range, 'bold', null, true );
 				remove( position, 2 );
 
-				const newRange = Range.createFromParentsAndOffsets( p1, 0, p1, 1 );
+				const newRange = new Range( Position._createAt( p1, 0 ), Position._createAt( p1, 1 ) );
 
 				expectChanges( [
 					{ type: 'remove', name: '$text', length: 2, position },
@@ -492,14 +490,14 @@ describe( 'Differ', () => {
 			const position = new Position( root, [ 0, 1 ] );
 
 			const p1 = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( p1, 0, p1, 3 );
+			const range = new Range( Position._createAt( p1, 0 ), Position._createAt( p1, 3 ) );
 
 			model.change( () => {
 				attribute( range, 'bold', null, true );
 				remove( position, 1 );
 
-				const rangeBefore = Range.createFromParentsAndOffsets( p1, 0, p1, 1 );
-				const rangeAfter = Range.createFromParentsAndOffsets( p1, 1, p1, 2 );
+				const rangeBefore = new Range( Position._createAt( p1, 0 ), Position._createAt( p1, 1 ) );
+				const rangeAfter = new Range( Position._createAt( p1, 1 ), Position._createAt( p1, 2 ) );
 
 				expectChanges( [
 					{
@@ -525,13 +523,13 @@ describe( 'Differ', () => {
 			const position = new Position( root, [ 0, 1 ] );
 
 			const p1 = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( p1, 0, p1, 2 );
+			const range = new Range( Position._createAt( p1, 0 ), Position._createAt( p1, 2 ) );
 
 			model.change( () => {
 				attribute( range, 'bold', null, true );
 				remove( position, 2 );
 
-				const newRange = Range.createFromParentsAndOffsets( p1, 0, p1, 1 );
+				const newRange = new Range( Position._createAt( p1, 0 ), Position._createAt( p1, 1 ) );
 
 				expectChanges( [
 					{
@@ -550,7 +548,7 @@ describe( 'Differ', () => {
 			const position = new Position( root, [ 0, 2 ] );
 
 			const p1 = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( p1, 0, p1, 1 );
+			const range = new Range( Position._createAt( p1, 0 ), Position._createAt( p1, 1 ) );
 
 			model.change( () => {
 				attribute( range, 'bold', null, true );
@@ -700,12 +698,12 @@ describe( 'Differ', () => {
 		const attributeNewValue = 'foo';
 
 		it( 'on an element', () => {
-			const range = Range.createFromParentsAndOffsets( root, 0, root, 1 );
+			const range = new Range( Position._createAt( root, 0 ), Position._createAt( root, 1 ) );
 
 			model.change( () => {
 				attribute( range, attributeKey, attributeOldValue, attributeNewValue );
 
-				const diffRange = Range.createFromParentsAndOffsets( root, 0, root.getChild( 0 ), 0 );
+				const diffRange = new Range( Position._createAt( root, 0 ), Position._createAt( root.getChild( 0 ), 0 ) );
 
 				expectChanges( [
 					{ type: 'attribute', range: diffRange, attributeKey, attributeOldValue, attributeNewValue }
@@ -714,7 +712,7 @@ describe( 'Differ', () => {
 		} );
 
 		it( 'on an element - only one of many attributes changes', () => {
-			const range = Range.createFromParentsAndOffsets( root, 0, root, 1 );
+			const range = new Range( Position._createAt( root, 0 ), Position._createAt( root, 1 ) );
 
 			model.change( () => {
 				// Set an attribute on an element. It won't change afterwards.
@@ -724,7 +722,7 @@ describe( 'Differ', () => {
 			model.change( () => {
 				attribute( range, attributeKey, attributeOldValue, attributeNewValue );
 
-				const diffRange = Range.createFromParentsAndOffsets( root, 0, root.getChild( 0 ), 0 );
+				const diffRange = new Range( Position._createAt( root, 0 ), Position._createAt( root.getChild( 0 ), 0 ) );
 
 				expectChanges( [
 					{ type: 'attribute', range: diffRange, attributeKey, attributeOldValue, attributeNewValue }
@@ -734,7 +732,7 @@ describe( 'Differ', () => {
 
 		it( 'on a character', () => {
 			const parent = root.getChild( 1 );
-			const range = Range.createFromParentsAndOffsets( parent, 1, parent, 2 );
+			const range = new Range( Position._createAt( parent, 1 ), Position._createAt( parent, 2 ) );
 
 			model.change( () => {
 				attribute( range, attributeKey, attributeOldValue, attributeNewValue );
@@ -747,7 +745,7 @@ describe( 'Differ', () => {
 
 		it( 'on a character - case with same characters next to each other', () => {
 			const parent = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( parent, 1, parent, 2 );
+			const range = new Range( Position._createAt( parent, 1 ), Position._createAt( parent, 2 ) );
 
 			model.change( () => {
 				attribute( range, attributeKey, attributeOldValue, attributeNewValue );
@@ -760,7 +758,7 @@ describe( 'Differ', () => {
 
 		it( 'on multiple characters', () => {
 			const parent = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( parent, 0, parent, 3 );
+			const range = new Range( Position._createAt( parent, 0 ), Position._createAt( parent, 3 ) );
 
 			model.change( () => {
 				attribute( range, attributeKey, attributeOldValue, attributeNewValue );
@@ -774,14 +772,14 @@ describe( 'Differ', () => {
 		it( 'on multiple consecutive characters in multiple operations', () => {
 			const parent = root.getChild( 0 );
 
-			const range1 = Range.createFromParentsAndOffsets( parent, 1, parent, 2 );
-			const range2 = Range.createFromParentsAndOffsets( parent, 2, parent, 3 );
+			const range1 = new Range( Position._createAt( parent, 1 ), Position._createAt( parent, 2 ) );
+			const range2 = new Range( Position._createAt( parent, 2 ), Position._createAt( parent, 3 ) );
 
 			model.change( () => {
 				attribute( range1, attributeKey, attributeOldValue, attributeNewValue );
 				attribute( range2, attributeKey, attributeOldValue, attributeNewValue );
 
-				const range = Range.createFromParentsAndOffsets( parent, 1, parent, 3 );
+				const range = new Range( Position._createAt( parent, 1 ), Position._createAt( parent, 3 ) );
 
 				expectChanges( [
 					{ type: 'attribute', range, attributeKey, attributeOldValue, attributeNewValue }
@@ -801,7 +799,7 @@ describe( 'Differ', () => {
 
 			model.change( () => {
 				for ( const item of ranges ) {
-					const range = Range.createFromParentsAndOffsets( parent, item[ 0 ], parent, item[ 1 ] );
+					const range = new Range( Position._createAt( parent, item[ 0 ] ), Position._createAt( parent, item[ 1 ] ) );
 
 					attribute( range, item[ 4 ], item[ 2 ], item[ 3 ] );
 				}
@@ -809,14 +807,14 @@ describe( 'Differ', () => {
 				expectChanges( [
 					{
 						type: 'attribute',
-						range: Range.createFromParentsAndOffsets( parent, 0, parent, 2 ),
+						range: new Range( Position._createAt( parent, 0 ), Position._createAt( parent, 2 ) ),
 						attributeKey: 'foo',
 						attributeOldValue: null,
 						attributeNewValue: true
 					},
 					{
 						type: 'attribute',
-						range: Range.createFromParentsAndOffsets( parent, 1, parent, 3 ),
+						range: new Range( Position._createAt( parent, 1 ), Position._createAt( parent, 3 ) ),
 						attributeKey: 'bar',
 						attributeOldValue: null,
 						attributeNewValue: true
@@ -837,7 +835,7 @@ describe( 'Differ', () => {
 
 			model.change( () => {
 				for ( const item of ranges ) {
-					const range = Range.createFromParentsAndOffsets( parent, item[ 0 ], parent, item[ 1 ] );
+					const range = new Range( Position._createAt( parent, item[ 0 ] ), Position._createAt( parent, item[ 1 ] ) );
 
 					attribute( range, item[ 4 ], item[ 2 ], item[ 3 ] );
 				}
@@ -845,28 +843,28 @@ describe( 'Differ', () => {
 				expectChanges( [
 					{
 						type: 'attribute',
-						range: Range.createFromParentsAndOffsets( parent, 0, parent, 1 ),
+						range: new Range( Position._createAt( parent, 0 ), Position._createAt( parent, 1 ) ),
 						attributeKey: 'bar',
 						attributeOldValue: null,
 						attributeNewValue: true
 					},
 					{
 						type: 'attribute',
-						range: Range.createFromParentsAndOffsets( parent, 1, parent, 2 ),
+						range: new Range( Position._createAt( parent, 1 ), Position._createAt( parent, 2 ) ),
 						attributeKey: 'foo',
 						attributeOldValue: null,
 						attributeNewValue: true
 					},
 					{
 						type: 'attribute',
-						range: Range.createFromParentsAndOffsets( parent, 1, parent, 2 ),
+						range: new Range( Position._createAt( parent, 1 ), Position._createAt( parent, 2 ) ),
 						attributeKey: 'bar',
 						attributeOldValue: null,
 						attributeNewValue: true
 					},
 					{
 						type: 'attribute',
-						range: Range.createFromParentsAndOffsets( parent, 2, parent, 3 ),
+						range: new Range( Position._createAt( parent, 2 ), Position._createAt( parent, 3 ) ),
 						attributeKey: 'foo',
 						attributeOldValue: null,
 						attributeNewValue: true
@@ -886,7 +884,7 @@ describe( 'Differ', () => {
 
 			model.change( () => {
 				for ( const item of ranges ) {
-					const range = Range.createFromParentsAndOffsets( parent, item[ 0 ], parent, item[ 1 ] );
+					const range = new Range( Position._createAt( parent, item[ 0 ] ), Position._createAt( parent, item[ 1 ] ) );
 
 					attribute( range, attributeKey, item[ 2 ], item[ 3 ] );
 				}
@@ -908,14 +906,14 @@ describe( 'Differ', () => {
 
 			model.change( () => {
 				for ( const item of ranges ) {
-					const range = Range.createFromParentsAndOffsets( parent, item[ 0 ], parent, item[ 1 ] );
+					const range = new Range( Position._createAt( parent, item[ 0 ] ), Position._createAt( parent, item[ 1 ] ) );
 
 					attribute( range, attributeKey, item[ 2 ], item[ 3 ] );
 				}
 
 				expectChanges( [ {
 					type: 'attribute',
-					range: Range.createFromParentsAndOffsets( parent, 0, parent, 2 ),
+					range: new Range( Position._createAt( parent, 0 ), Position._createAt( parent, 2 ) ),
 					attributeKey,
 					attributeOldValue: null,
 					attributeNewValue: true
@@ -926,8 +924,8 @@ describe( 'Differ', () => {
 		it( 'on multiple non-consecutive characters in multiple operations', () => {
 			const parent = root.getChild( 0 );
 
-			const range1 = Range.createFromParentsAndOffsets( parent, 0, parent, 1 );
-			const range2 = Range.createFromParentsAndOffsets( parent, 2, parent, 3 );
+			const range1 = new Range( Position._createAt( parent, 0 ), Position._createAt( parent, 1 ) );
+			const range2 = new Range( Position._createAt( parent, 2 ), Position._createAt( parent, 3 ) );
 
 			model.change( () => {
 				// Note "reversed" order of ranges. Further range is changed first.
@@ -943,7 +941,7 @@ describe( 'Differ', () => {
 		} );
 
 		it( 'on range containing various nodes', () => {
-			const range = Range.createFromParentsAndOffsets( root, 0, root, 2 );
+			const range = new Range( Position._createAt( root, 0 ), Position._createAt( root, 2 ) );
 
 			model.change( () => {
 				attribute( range, attributeKey, attributeOldValue, attributeNewValue );
@@ -955,14 +953,14 @@ describe( 'Differ', () => {
 				expectChanges( [
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( root, 0, p1, 0 ),
+						range: new Range( Position._createAt( root, 0 ), Position._createAt( p1, 0 ) ),
 						attributeKey,
 						attributeOldValue,
 						attributeNewValue
 					},
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( root, 1, p2, 0 ),
+						range: new Range( Position._createAt( root, 1 ), Position._createAt( p2, 0 ) ),
 						attributeKey,
 						attributeOldValue,
 						attributeNewValue
@@ -976,14 +974,14 @@ describe( 'Differ', () => {
 
 			p.getChild( 0 )._setAttribute( 'bold', true );
 
-			const range = Range.createFromParentsAndOffsets( p, 1, p, 3 );
+			const range = new Range( Position._createAt( p, 1 ), Position._createAt( p, 3 ) );
 
 			model.change( () => {
 				attribute( range, 'bold', true, null );
 				attribute( range, 'italic', null, true );
 
-				const range1 = Range.createFromParentsAndOffsets( p, 1, p, 2 );
-				const range2 = Range.createFromParentsAndOffsets( p, 2, p, 3 );
+				const range1 = new Range( Position._createAt( p, 1 ), Position._createAt( p, 2 ) );
+				const range2 = new Range( Position._createAt( p, 2 ), Position._createAt( p, 3 ) );
 
 				// Attribute change glueing does not work 100% correct.
 				expectChanges( [
@@ -1023,13 +1021,13 @@ describe( 'Differ', () => {
 			const position = new Position( root, [ 0, 1 ] );
 
 			const p1 = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( p1, 0, p1, 2 );
+			const range = new Range( Position._createAt( p1, 0 ), Position._createAt( p1, 2 ) );
 
 			model.change( () => {
 				insert( new Text( 'xx' ), position );
 				attribute( range, attributeKey, attributeOldValue, attributeNewValue );
 
-				const rangeBefore = Range.createFromParentsAndOffsets( p1, 0, p1, 1 );
+				const rangeBefore = new Range( Position._createAt( p1, 0 ), Position._createAt( p1, 1 ) );
 
 				expectChanges( [
 					{ type: 'attribute', range: rangeBefore, attributeKey, attributeOldValue, attributeNewValue },
@@ -1042,7 +1040,7 @@ describe( 'Differ', () => {
 			const position = new Position( root, [ 0, 1 ] );
 
 			const p1 = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( p1, 2, p1, 3 );
+			const range = new Range( Position._createAt( p1, 2 ), Position._createAt( p1, 3 ) );
 
 			model.change( () => {
 				insert( new Text( 'xxx' ), position );
@@ -1058,13 +1056,13 @@ describe( 'Differ', () => {
 			const position = new Position( root, [ 0, 1 ] );
 
 			const p1 = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( p1, 2, p1, 4 );
+			const range = new Range( Position._createAt( p1, 2 ), Position._createAt( p1, 4 ) );
 
 			model.change( () => {
 				insert( new Text( 'xx' ), position );
 				attribute( range, attributeKey, attributeOldValue, attributeNewValue );
 
-				const rangeAfter = Range.createFromParentsAndOffsets( p1, 3, p1, 4 );
+				const rangeAfter = new Range( Position._createAt( p1, 3 ), Position._createAt( p1, 4 ) );
 
 				expectChanges( [
 					{ type: 'insert', name: '$text', length: 2, position },
@@ -1077,14 +1075,14 @@ describe( 'Differ', () => {
 			const position = new Position( root, [ 0, 1 ] );
 
 			const p1 = root.getChild( 0 );
-			const range = Range.createFromParentsAndOffsets( p1, 0, p1, 4 );
+			const range = new Range( Position._createAt( p1, 0 ), Position._createAt( p1, 4 ) );
 
 			model.change( () => {
 				insert( new Text( 'xx' ), position );
 				attribute( range, attributeKey, attributeOldValue, attributeNewValue );
 
-				const rangeBefore = Range.createFromParentsAndOffsets( p1, 0, p1, 1 );
-				const rangeAfter = Range.createFromParentsAndOffsets( p1, 3, p1, 4 );
+				const rangeBefore = new Range( Position._createAt( p1, 0 ), Position._createAt( p1, 1 ) );
+				const rangeAfter = new Range( Position._createAt( p1, 3 ), Position._createAt( p1, 4 ) );
 
 				expectChanges( [
 					{ type: 'attribute', range: rangeBefore, attributeKey, attributeOldValue, attributeNewValue },
@@ -1097,8 +1095,8 @@ describe( 'Differ', () => {
 		it( 'on some not changed and some changed nodes', () => {
 			const p = root.getChild( 0 );
 
-			const rangeA = Range.createFromParentsAndOffsets( p, 1, p, 3 );
-			const rangeB = Range.createFromParentsAndOffsets( p, 0, p, 2 );
+			const rangeA = new Range( Position._createAt( p, 1 ), Position._createAt( p, 3 ) );
+			const rangeB = new Range( Position._createAt( p, 0 ), Position._createAt( p, 2 ) );
 
 			model.change( () => {
 				attribute( rangeA, 'a', null, true );
@@ -1112,28 +1110,28 @@ describe( 'Differ', () => {
 				expectChanges( [
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( p, 0, p, 1 ),
+						range: new Range( Position._createAt( p, 0 ), Position._createAt( p, 1 ) ),
 						attributeKey: 'b',
 						attributeOldValue,
 						attributeNewValue
 					},
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( p, 1, p, 2 ),
+						range: new Range( Position._createAt( p, 1 ), Position._createAt( p, 2 ) ),
 						attributeKey: 'a',
 						attributeOldValue,
 						attributeNewValue
 					},
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( p, 1, p, 2 ),
+						range: new Range( Position._createAt( p, 1 ), Position._createAt( p, 2 ) ),
 						attributeKey: 'b',
 						attributeOldValue,
 						attributeNewValue
 					},
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( p, 2, p, 3 ),
+						range: new Range( Position._createAt( p, 2 ), Position._createAt( p, 3 ) ),
 						attributeKey: 'a',
 						attributeOldValue,
 						attributeNewValue
@@ -1145,8 +1143,8 @@ describe( 'Differ', () => {
 		it( 'on already changed nodes', () => {
 			const p = root.getChild( 1 );
 
-			const rangeA = Range.createFromParentsAndOffsets( p, 0, p, 3 );
-			const rangeB = Range.createFromParentsAndOffsets( p, 1, p, 2 );
+			const rangeA = new Range( Position._createAt( p, 0 ), Position._createAt( p, 3 ) );
+			const rangeB = new Range( Position._createAt( p, 1 ), Position._createAt( p, 2 ) );
 
 			model.change( () => {
 				attribute( rangeA, 'a', null, true );
@@ -1160,21 +1158,21 @@ describe( 'Differ', () => {
 				expectChanges( [
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( p, 0, p, 2 ),
+						range: new Range( Position._createAt( p, 0 ), Position._createAt( p, 2 ) ),
 						attributeKey: 'a',
 						attributeOldValue,
 						attributeNewValue
 					},
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( p, 1, p, 2 ),
+						range: new Range( Position._createAt( p, 1 ), Position._createAt( p, 2 ) ),
 						attributeKey: 'b',
 						attributeOldValue,
 						attributeNewValue
 					},
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( p, 2, p, 3 ),
+						range: new Range( Position._createAt( p, 2 ), Position._createAt( p, 3 ) ),
 						attributeKey: 'a',
 						attributeOldValue,
 						attributeNewValue
@@ -1186,8 +1184,8 @@ describe( 'Differ', () => {
 		it( 'on some changed and some not changed nodes', () => {
 			const p = root.getChild( 1 );
 
-			const rangeA = Range.createFromParentsAndOffsets( p, 0, p, 2 );
-			const rangeB = Range.createFromParentsAndOffsets( p, 1, p, 3 );
+			const rangeA = new Range( Position._createAt( p, 0 ), Position._createAt( p, 2 ) );
+			const rangeB = new Range( Position._createAt( p, 1 ), Position._createAt( p, 3 ) );
 
 			model.change( () => {
 				attribute( rangeA, 'a', null, true );
@@ -1200,14 +1198,14 @@ describe( 'Differ', () => {
 				expectChanges( [
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( p, 0, p, 2 ),
+						range: new Range( Position._createAt( p, 0 ), Position._createAt( p, 2 ) ),
 						attributeKey: 'a',
 						attributeOldValue,
 						attributeNewValue
 					},
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( p, 1, p, 3 ),
+						range: new Range( Position._createAt( p, 1 ), Position._createAt( p, 3 ) ),
 						attributeKey: 'b',
 						attributeOldValue,
 						attributeNewValue
@@ -1219,8 +1217,8 @@ describe( 'Differ', () => {
 		it( 'over all changed nodes and some not changed nodes', () => {
 			const p = root.getChild( 0 );
 
-			const rangeA = Range.createFromParentsAndOffsets( p, 1, p, 2 );
-			const rangeB = Range.createFromParentsAndOffsets( p, 0, p, 3 );
+			const rangeA = new Range( Position._createAt( p, 1 ), Position._createAt( p, 2 ) );
+			const rangeB = new Range( Position._createAt( p, 0 ), Position._createAt( p, 3 ) );
 
 			model.change( () => {
 				attribute( rangeA, 'a', null, true );
@@ -1234,21 +1232,21 @@ describe( 'Differ', () => {
 				expectChanges( [
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( p, 0, p, 1 ),
+						range: new Range( Position._createAt( p, 0 ), Position._createAt( p, 1 ) ),
 						attributeKey: 'b',
 						attributeOldValue,
 						attributeNewValue
 					},
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( p, 1, p, 2 ),
+						range: new Range( Position._createAt( p, 1 ), Position._createAt( p, 2 ) ),
 						attributeKey: 'a',
 						attributeOldValue,
 						attributeNewValue
 					},
 					{
 						type,
-						range: Range.createFromParentsAndOffsets( p, 1, p, 3 ),
+						range: new Range( Position._createAt( p, 1 ), Position._createAt( p, 3 ) ),
 						attributeKey: 'b',
 						attributeOldValue,
 						attributeNewValue
@@ -1302,6 +1300,25 @@ describe( 'Differ', () => {
 				] );
 			} );
 		} );
+
+		it( 'should correctly mark a change in graveyard', () => {
+			model.change( () => {
+				merge( new Position( root, [ 1, 0 ] ), new Position( root, [ 0, 3 ] ) );
+			} );
+
+			model.change( () => {
+				const position = new Position( root, [ 0, 3 ] );
+				const operation = new SplitOperation( position, 3, new Position( doc.graveyard, [ 0 ] ), doc.version );
+
+				model.applyOperation( operation );
+
+				expectChanges( [
+					{ type: 'remove', name: 'paragraph', length: 1, position: new Position( doc.graveyard, [ 0 ] ) },
+					{ type: 'remove', name: '$text', length: 3, position: new Position( root, [ 0, 3 ] ) },
+					{ type: 'insert', name: 'paragraph', length: 1, position: new Position( root, [ 1 ] ) }
+				], true );
+			} );
+		} );
 	} );
 
 	describe( 'merge', () => {
@@ -1356,100 +1373,16 @@ describe( 'Differ', () => {
 				] );
 			} );
 		} );
-	} );
-
-	describe( 'wrap', () => {
-		it( 'wrap elements', () => {
-			model.change( () => {
-				wrap( new Position( root, [ 0 ] ), 2, new Element( 'blockQuote' ) );
-
-				expectChanges( [
-					{ type: 'remove', name: 'paragraph', length: 1, position: new Position( root, [ 0 ] ) },
-					{ type: 'remove', name: 'paragraph', length: 1, position: new Position( root, [ 0 ] ) },
-					{ type: 'insert', name: 'blockQuote', length: 1, position: new Position( root, [ 0 ] ) }
-				] );
-			} );
-		} );
-
-		it( 'wrap old and new elements', () => {
-			model.change( () => {
-				insert( new Element( 'paragraph' ), new Position( root, [ 0 ] ) );
-				wrap( new Position( root, [ 0 ] ), 2, new Element( 'blockQuote' ) );
-
-				expectChanges( [
-					{ type: 'remove', name: 'paragraph', length: 1, position: new Position( root, [ 0 ] ) },
-					{ type: 'insert', name: 'blockQuote', length: 1, position: new Position( root, [ 0 ] ) }
-				] );
-			} );
-		} );
-
-		it( 'wrap inside a new element', () => {
-			model.change( () => {
-				insert(
-					new Element( 'div', null, [
-						new Element( 'paragraph' ),
-						new Element( 'paragraph' )
-					] ),
-					new Position( root, [ 0 ] )
-				);
-				wrap( new Position( root, [ 0, 0 ] ), 2, new Element( 'blockQuote' ) );
-
-				expectChanges( [
-					{ type: 'insert', name: 'div', length: 1, position: new Position( root, [ 0 ] ) }
-				] );
-			} );
-		} );
-	} );
-
-	describe( 'unwrap', () => {
-		it( 'unwrap elements', () => {
-			model.change( () => {
-				unwrap( new Position( root, [ 0, 0 ] ) );
-
-				expectChanges( [
-					{ type: 'remove', name: 'paragraph', length: 1, position: new Position( root, [ 0 ] ) },
-					{ type: 'insert', name: '$text', length: 3, position: new Position( root, [ 0 ] ) }
-				] );
-			} );
-		} );
-
-		it( 'unwrap a new element', () => {
-			model.change( () => {
-				insert( new Element( 'paragraph', null, new Text( 'Ab' ) ), new Position( root, [ 0 ] ) );
-				unwrap( new Position( root, [ 0, 0 ] ) );
-
-				expectChanges( [
-					{ type: 'insert', name: '$text', length: 2, position: new Position( root, [ 0 ] ) }
-				] );
-			} );
-		} );
-
-		it( 'unwrap element with new nodes', () => {
-			model.change( () => {
-				insert( new Text( 'Ab' ), new Position( root, [ 0, 1 ] ) );
-				unwrap( new Position( root, [ 0, 0 ] ) );
-
-				expectChanges( [
-					{ type: 'remove', name: 'paragraph', length: 1, position: new Position( root, [ 0 ] ) },
-					{ type: 'insert', name: '$text', length: 5, position: new Position( root, [ 0 ] ) }
-				] );
-			} );
-		} );
 
-		it( 'unwrap element inside a new element', () => {
+		it( 'should correctly mark a change in graveyard', () => {
 			model.change( () => {
-				insert(
-					new Element( 'blockQuote', null, [
-						new Element( 'paragraph', null, new Text( 'Ab' ) )
-					] ),
-					new Position( root, [ 0 ] )
-				);
-
-				unwrap( new Position( root, [ 0, 0, 0 ] ) );
+				merge( new Position( root, [ 1, 0 ] ), new Position( root, [ 0, 3 ] ) );
 
 				expectChanges( [
-					{ type: 'insert', name: 'blockQuote', length: 1, position: new Position( root, [ 0 ] ) }
-				] );
+					{ type: 'insert', name: 'paragraph', length: 1, position: new Position( doc.graveyard, [ 0 ] ) },
+					{ type: 'insert', name: '$text', length: 3, position: new Position( root, [ 0, 3 ] ) },
+					{ type: 'remove', name: 'paragraph', length: 1, position: new Position( root, [ 1 ] ) }
+				], true );
 			} );
 		} );
 	} );
@@ -1458,8 +1391,8 @@ describe( 'Differ', () => {
 		let range, rangeB;
 
 		beforeEach( () => {
-			range = Range.createFromParentsAndOffsets( root, 0, root, 1 );
-			rangeB = Range.createFromParentsAndOffsets( root, 1, root, 2 );
+			range = new Range( Position._createAt( root, 0 ), Position._createAt( root, 1 ) );
+			rangeB = new Range( Position._createAt( root, 1 ), Position._createAt( root, 2 ) );
 		} );
 
 		it( 'add marker', () => {
@@ -1670,10 +1603,10 @@ describe( 'Differ', () => {
 		it( 'remove is correctly transformed by multiple affecting changes', () => {
 			root._appendChild( new Element( 'paragraph', null, new Text( 'xyz' ) ) );
 
-			model.change( () => {
+			model.change( writer => {
 				rename( root.getChild( 1 ), 'heading' );
 				rename( root.getChild( 2 ), 'heading' );
-				remove( Position.createAt( root, 0 ), 3 );
+				remove( writer.createPositionAt( root, 0 ), 3 );
 
 				expectChanges( [
 					{ type: 'remove', name: 'paragraph', length: 1, position: new Position( root, [ 0 ] ) },
@@ -1691,8 +1624,8 @@ describe( 'Differ', () => {
 			position = new Position( root, [ 0, 1 ] );
 			p1 = root.getChild( 0 );
 
-			range = Range.createFromParentsAndOffsets( p1, 2, p1, 4 );
-			rangeAttrChange = Range.createFromParentsAndOffsets( p1, 3, p1, 4 );
+			range = new Range( Position._createAt( p1, 2 ), Position._createAt( p1, 4 ) );
+			rangeAttrChange = new Range( Position._createAt( p1, 3 ), Position._createAt( p1, 4 ) );
 		} );
 
 		it( 'should return changes in graveyard if a flag was set up', () => {
@@ -1828,21 +1761,8 @@ describe( 'Differ', () => {
 		model.applyOperation( operation );
 	}
 
-	function wrap( position, howMany, element ) {
-		const operation = new WrapOperation( position, howMany, element, doc.version );
-
-		model.applyOperation( operation );
-	}
-
-	function unwrap( position ) {
-		const howMany = position.parent.maxOffset;
-		const operation = new UnwrapOperation( position, howMany, new Position( doc.graveyard, [ 0 ] ), doc.version );
-
-		model.applyOperation( operation );
-	}
-
 	function remove( sourcePosition, howMany ) {
-		const targetPosition = Position.createAt( doc.graveyard, doc.graveyard.maxOffset );
+		const targetPosition = Position._createAt( doc.graveyard, doc.graveyard.maxOffset );
 		const operation = new MoveOperation( sourcePosition, howMany, targetPosition, doc.version );
 
 		model.applyOperation( operation );
@@ -1855,7 +1775,7 @@ describe( 'Differ', () => {
 	}
 
 	function rename( element, newName ) {
-		const operation = new RenameOperation( Position.createBefore( element ), element.name, newName, doc.version );
+		const operation = new RenameOperation( Position._createBefore( element ), element.name, newName, doc.version );
 
 		model.applyOperation( operation );
 	}

+ 8 - 9
packages/ckeditor5-engine/tests/model/document.js

@@ -8,7 +8,6 @@ import Document from '../../src/model/document';
 import RootElement from '../../src/model/rootelement';
 import Text from '../../src/model/text';
 import Batch from '../../src/model/batch';
-import Range from '../../src/model/range';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import count from '@ckeditor/ckeditor5-utils/src/count';
@@ -228,7 +227,7 @@ describe( 'Document', () => {
 			sinon.spy( doc.differ, 'bufferMarkerChange' );
 
 			model.change( writer => {
-				const range = Range.createCollapsedAt( doc.getRoot(), 0 );
+				const range = writer.createRange( writer.createPositionAt( doc.getRoot(), 0 ) );
 				writer.addMarker( 'marker', { range, usingOperation: false } );
 			} );
 
@@ -324,7 +323,7 @@ describe( 'Document', () => {
 			doc.on( 'change', spy );
 
 			model.change( writer => {
-				writer.setSelection( Range.createFromParentsAndOffsets( root, 2, root, 2 ) );
+				writer.setSelection( root, 2 );
 			} );
 
 			sinon.assert.calledOnce( spy );
@@ -373,7 +372,7 @@ describe( 'Document', () => {
 			doc.on( 'change:data', spy );
 
 			model.change( writer => {
-				writer.setSelection( Range.createFromParentsAndOffsets( root, 2, root, 2 ) );
+				writer.setSelection( root, 2 );
 			} );
 
 			sinon.assert.notCalled( spy );
@@ -388,7 +387,7 @@ describe( 'Document', () => {
 			doc.on( 'change:data', spy );
 
 			model.change( writer => {
-				const range = Range.createFromParentsAndOffsets( root, 2, root, 4 );
+				const range = writer.createRange( writer.createPositionAt( root, 2 ), writer.createPositionAt( root, 4 ) );
 				writer.addMarker( 'name', { range, usingOperation: true, affectsData: true } );
 			} );
 
@@ -404,7 +403,7 @@ describe( 'Document', () => {
 			doc.on( 'change:data', spy );
 
 			model.change( writer => {
-				const range = Range.createFromParentsAndOffsets( root, 2, root, 4 );
+				const range = writer.createRange( writer.createPositionAt( root, 2 ), writer.createPositionAt( root, 4 ) );
 				writer.addMarker( 'name', { range, usingOperation: true } );
 			} );
 
@@ -420,7 +419,7 @@ describe( 'Document', () => {
 			doc.on( 'change:data', spy );
 
 			model.change( writer => {
-				const range = Range.createFromParentsAndOffsets( root, 2, root, 4 );
+				const range = writer.createRange( writer.createPositionAt( root, 2 ), writer.createPositionAt( root, 4 ) );
 				writer.addMarker( 'name', { range, usingOperation: false, affectsData: true } );
 			} );
 
@@ -436,7 +435,7 @@ describe( 'Document', () => {
 			doc.on( 'change:data', spy );
 
 			model.change( writer => {
-				const range = Range.createFromParentsAndOffsets( root, 2, root, 4 );
+				const range = writer.createRange( writer.createPositionAt( root, 2 ), writer.createPositionAt( root, 4 ) );
 				writer.addMarker( 'name', { range, usingOperation: false } );
 			} );
 
@@ -471,7 +470,7 @@ describe( 'Document', () => {
 			doc.on( 'change', changeSpy );
 
 			model.change( writer => {
-				const range = Range.createFromParentsAndOffsets( root, 2, root, 4 );
+				const range = writer.createRange( writer.createPositionAt( root, 2 ), writer.createPositionAt( root, 4 ) );
 				writer.addMarker( 'name', { range, usingOperation: false } );
 			} );
 

+ 20 - 20
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -257,7 +257,7 @@ describe( 'DocumentSelection', () => {
 	describe( '_setFocus()', () => {
 		it( 'modifies default range', () => {
 			const startPos = selection.getFirstPosition();
-			const endPos = Position.createAt( root, 'end' );
+			const endPos = Position._createAt( root, 'end' );
 
 			selection._setFocus( endPos );
 
@@ -266,9 +266,9 @@ describe( 'DocumentSelection', () => {
 		} );
 
 		it( 'detaches the range it replaces', () => {
-			const startPos = Position.createAt( root, 1 );
-			const endPos = Position.createAt( root, 2 );
-			const newEndPos = Position.createAt( root, 4 );
+			const startPos = Position._createAt( root, 1 );
+			const endPos = Position._createAt( root, 2 );
+			const newEndPos = Position._createAt( root, 4 );
 			const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
 
 			selection._setTo( new Range( startPos, endPos ) );
@@ -281,7 +281,7 @@ describe( 'DocumentSelection', () => {
 		it( 'refreshes attributes', () => {
 			const spy = sinon.spy( selection._selection, '_updateAttributes' );
 
-			selection._setFocus( Position.createAt( root, 1 ) );
+			selection._setFocus( Position._createAt( root, 1 ) );
 
 			expect( spy.called ).to.be.true;
 		} );
@@ -379,7 +379,7 @@ describe( 'DocumentSelection', () => {
 
 			setData( model, 'f<$text italic="true">[o</$text><$text bold="true">ob]a</$text>r' );
 
-			selection._setTo( [ Range.createFromPositionAndShift( selection.getLastRange().end, 0 ) ] );
+			selection._setTo( [ Range._createFromPositionAndShift( selection.getLastRange().end, 0 ) ] );
 
 			expect( selection.getAttribute( 'bold' ) ).to.equal( true );
 			expect( selection.hasAttribute( 'italic' ) ).to.equal( false );
@@ -447,7 +447,7 @@ describe( 'DocumentSelection', () => {
 
 				// Trigger selecton auto update on document change. It should not get attribute from surrounding text;
 				model.change( writer => {
-					writer.setAttribute( 'foo', 'bar', Range.createIn( fullP ) );
+					writer.setAttribute( 'foo', 'bar', Range._createIn( fullP ) );
 				} );
 
 				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
@@ -656,7 +656,7 @@ describe( 'DocumentSelection', () => {
 				selection._setAttribute( 'foo', 'bar' );
 
 				model.change( writer => {
-					writer.move( Range.createOn( fullP.getChild( 0 ) ), rangeInEmptyP.start );
+					writer.move( writer.createRangeOn( fullP.getChild( 0 ) ), rangeInEmptyP.start );
 				} );
 
 				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
@@ -671,7 +671,7 @@ describe( 'DocumentSelection', () => {
 
 				model.change( writer => {
 					// <emptyP>{}<emptyP2>
-					writer.merge( Position.createAfter( emptyP ) );
+					writer.merge( writer.createPositionAfter( emptyP ) );
 				} );
 
 				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
@@ -717,7 +717,7 @@ describe( 'DocumentSelection', () => {
 
 				model.change( writer => {
 					// <emptyP>{}<emptyP2>
-					writer.merge( Position.createAfter( emptyP ) );
+					writer.merge( writer.createPositionAfter( emptyP ) );
 				} );
 
 				expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
@@ -1210,8 +1210,8 @@ describe( 'DocumentSelection', () => {
 			] );
 
 			selection._setTo( [
-				Range.createIn( root.getNodeByPath( [ 0 ] ) ),
-				Range.createIn( root.getNodeByPath( [ 1 ] ) )
+				Range._createIn( root.getNodeByPath( [ 0 ] ) ),
+				Range._createIn( root.getNodeByPath( [ 1 ] ) )
 			] );
 
 			spyRange = sinon.spy();
@@ -1234,11 +1234,11 @@ describe( 'DocumentSelection', () => {
 		root._appendChild( '\uD83D\uDCA9' );
 
 		expect( () => {
-			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
+			doc.selection._setTo( new Range( Position._createAt( root, 0 ), Position._createAt( root, 1 ) ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 
 		expect( () => {
-			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 1, root, 2 ) );
+			doc.selection._setTo( new Range( Position._createAt( root, 1 ), Position._createAt( root, 2 ) ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 	} );
 
@@ -1247,27 +1247,27 @@ describe( 'DocumentSelection', () => {
 		root._appendChild( 'foo̻̐ͩbar' );
 
 		expect( () => {
-			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 3, root, 9 ) );
+			doc.selection._setTo( new Range( Position._createAt( root, 3 ), Position._createAt( root, 9 ) ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 
 		expect( () => {
-			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 4, root, 9 ) );
+			doc.selection._setTo( new Range( Position._createAt( root, 4 ), Position._createAt( root, 9 ) ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 
 		expect( () => {
-			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 5, root, 9 ) );
+			doc.selection._setTo( new Range( Position._createAt( root, 5 ), Position._createAt( root, 9 ) ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 
 		expect( () => {
-			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 1, root, 3 ) );
+			doc.selection._setTo( new Range( Position._createAt( root, 1 ), Position._createAt( root, 3 ) ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 
 		expect( () => {
-			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 1, root, 4 ) );
+			doc.selection._setTo( new Range( Position._createAt( root, 1 ), Position._createAt( root, 4 ) ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 
 		expect( () => {
-			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 1, root, 5 ) );
+			doc.selection._setTo( new Range( Position._createAt( root, 1 ), Position._createAt( root, 5 ) ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 	} );
 } );

+ 25 - 28
packages/ckeditor5-engine/tests/model/liveposition.js

@@ -12,7 +12,8 @@ import LivePosition from '../../src/model/liveposition';
 import Range from '../../src/model/range';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
-describe( 'LivePosition', () => {
+describe( 'LivePosition', () =>
+{
 	let model, doc, root, ul, p, li1, li2;
 
 	beforeEach( () => {
@@ -68,26 +69,22 @@ describe( 'LivePosition', () => {
 		LivePosition.prototype.stopListening.restore();
 	} );
 
-	it( 'createFromPosition should return LivePosition', () => {
-		const position = LivePosition.createFromPosition( new Position( root, [ 0 ] ) );
-		expect( position ).to.be.instanceof( LivePosition );
-		position.detach();
-	} );
-
-	it( 'createFromParentAndOffset should return LivePosition', () => {
-		const position = LivePosition.createFromParentAndOffset( ul, 0 );
-		expect( position ).to.be.instanceof( LivePosition );
-		position.detach();
+	describe( 'fromPosition()', () => {
+		it( 'should return LivePosition', () => {
+			const position = LivePosition.fromPosition( new Position( root, [ 0 ] ) );
+			expect( position ).to.be.instanceof( LivePosition );
+			position.detach();
+		} );
 	} );
 
-	it( 'createBefore should return LivePosition', () => {
-		const position = LivePosition.createBefore( ul );
+	it( '_createBefore should return LivePosition', () => {
+		const position = LivePosition._createBefore( ul );
 		expect( position ).to.be.instanceof( LivePosition );
 		position.detach();
 	} );
 
-	it( 'createAfter should return LivePosition', () => {
-		const position = LivePosition.createAfter( ul );
+	it( '_createAfter should return LivePosition', () => {
+		const position = LivePosition._createAfter( ul );
 		expect( position ).to.be.instanceof( LivePosition );
 		position.detach();
 	} );
@@ -140,7 +137,7 @@ describe( 'LivePosition', () => {
 			it( 'is at the same parent and closer offset', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 1, 0, 1 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 3 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 3 );
 					const targetPosition = new Position( root, [ 1, 1, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -154,7 +151,7 @@ describe( 'LivePosition', () => {
 				live.stickiness = 'toNext';
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 1, 0, 1 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 3 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 3 );
 					const targetPosition = new Position( root, [ 1, 1, 3 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -167,7 +164,7 @@ describe( 'LivePosition', () => {
 			it( 'is at a position before a node from the live position path', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 1, 0, 1 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 2 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 2 );
 					const targetPosition = new Position( root, [ 1, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -180,7 +177,7 @@ describe( 'LivePosition', () => {
 			it( 'is from the same parent and closer offset', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 1, 1, 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 2 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 2 );
 					const targetPosition = new Position( root, [ 1, 0, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -193,7 +190,7 @@ describe( 'LivePosition', () => {
 			it( 'is from a position before a node from the live position path', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 1, 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 1 );
 					const targetPosition = new Position( root, [ 1, 2 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -206,7 +203,7 @@ describe( 'LivePosition', () => {
 			it( 'contains live position (same level)', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 1, 1, 2 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 2 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 2 );
 					const targetPosition = new Position( root, [ 1, 0, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -219,7 +216,7 @@ describe( 'LivePosition', () => {
 			it( 'contains live position (deep)', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 1, 1 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 1 );
 					const targetPosition = new Position( root, [ 1, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -295,7 +292,7 @@ describe( 'LivePosition', () => {
 			it( 'is at the same parent and further offset', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 1, 0, 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 3 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 3 );
 					const targetPosition = new Position( root, [ 1, 1, 6 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -312,7 +309,7 @@ describe( 'LivePosition', () => {
 
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 1, 0, 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 3 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 3 );
 					const targetPosition = new Position( root, [ 1, 1, 3 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -327,7 +324,7 @@ describe( 'LivePosition', () => {
 			it( 'is at a position after a node from the live position path', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 1, 0, 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 3 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 3 );
 					const targetPosition = new Position( root, [ 2 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -340,7 +337,7 @@ describe( 'LivePosition', () => {
 			it( 'is from the same parent and further offset', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 1, 1, 4 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 2 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 2 );
 					const targetPosition = new Position( otherRoot, [ 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -357,7 +354,7 @@ describe( 'LivePosition', () => {
 
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 1, 1 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 1 );
 					const targetPosition = new Position( otherRoot, [ 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -374,7 +371,7 @@ describe( 'LivePosition', () => {
 					writer.insertText( 'foo', new Position( otherRoot, [ 0 ] ) );
 
 					const sourcePosition = new Position( otherRoot, [ 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 1 );
 					const targetPosition = new Position( otherRoot, [ 3 ] );
 
 					writer.move( sourceRange, targetPosition );

+ 42 - 50
packages/ckeditor5-engine/tests/model/liverange.js

@@ -67,33 +67,21 @@ describe( 'LiveRange', () => {
 		LiveRange.prototype.stopListening.restore();
 	} );
 
-	it( 'createIn should return LiveRange', () => {
-		const range = LiveRange.createIn( p );
+	it( '_createIn should return LiveRange', () => {
+		const range = LiveRange._createIn( p );
 		expect( range ).to.be.instanceof( LiveRange );
 		range.detach();
 	} );
 
-	it( 'createFromParentsAndOffsets should return LiveRange', () => {
-		const range = LiveRange.createFromParentsAndOffsets( root, 0, p, 2 );
-		expect( range ).to.be.instanceof( LiveRange );
-		range.detach();
-	} );
-
-	it( 'createFromPositionAndShift should return LiveRange', () => {
-		const range = LiveRange.createFromPositionAndShift( new Position( root, [ 0, 1 ] ), 4 );
-		expect( range ).to.be.instanceof( LiveRange );
-		range.detach();
-	} );
-
-	it( 'createFromRange should return LiveRange', () => {
-		const range = LiveRange.createFromRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ) );
+	it( '_createFromPositionAndShift should return LiveRange', () => {
+		const range = LiveRange._createFromPositionAndShift( new Position( root, [ 0, 1 ] ), 4 );
 		expect( range ).to.be.instanceof( LiveRange );
 		range.detach();
 	} );
 
 	it( 'should fire change:range event with when its boundaries are changed', () => {
 		const live = new LiveRange( new Position( root, [ 0, 1, 4 ] ), new Position( root, [ 0, 2, 2 ] ) );
-		const copy = Range.createFromRange( live );
+		const copy = live.toRange();
 
 		const spy = sinon.spy();
 		live.on( 'change:range', spy );
@@ -102,7 +90,7 @@ describe( 'LiveRange', () => {
 		const targetPosition = new Position( root, [ 0 ] );
 
 		model.change( writer => {
-			const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
+			const sourceRange = Range._createFromPositionAndShift( sourcePosition, 1 );
 
 			writer.move( sourceRange, targetPosition );
 		} );
@@ -126,7 +114,7 @@ describe( 'LiveRange', () => {
 		const targetPosition = new Position( root, [ 0, 4, 0 ] );
 
 		model.change( writer => {
-			const sourceRange = Range.createFromPositionAndShift( sourcePosition, 2 );
+			const sourceRange = Range._createFromPositionAndShift( sourcePosition, 2 );
 
 			writer.move( sourceRange, targetPosition );
 		} );
@@ -149,7 +137,7 @@ describe( 'LiveRange', () => {
 		const sourcePosition = new Position( root, [ 0, 0 ] );
 
 		model.change( writer => {
-			writer.remove( Range.createFromPositionAndShift( sourcePosition, 6 ) );
+			writer.remove( Range._createFromPositionAndShift( sourcePosition, 6 ) );
 		} );
 
 		// Second parameter is deletion position.
@@ -267,7 +255,7 @@ describe( 'LiveRange', () => {
 			it( 'is to the same parent as range start and before it', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 4, 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 4 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 4 );
 					const targetPosition = new Position( root, [ 0, 1, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -281,7 +269,7 @@ describe( 'LiveRange', () => {
 			it( 'is to the same parent as range end and before it', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 4, 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 4 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 4 );
 					const targetPosition = new Position( root, [ 0, 2, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -295,7 +283,7 @@ describe( 'LiveRange', () => {
 			it( 'is to a position before a node from range start path', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 4 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 2 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 2 );
 					const targetPosition = new Position( root, [ 0, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -309,7 +297,7 @@ describe( 'LiveRange', () => {
 			it( 'is to a position before a node from range end path', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 4 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 1 );
 					const targetPosition = new Position( root, [ 0, 2 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -323,7 +311,7 @@ describe( 'LiveRange', () => {
 			it( 'is from the same parent as range start and before it', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 1, 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 3 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 3 );
 					const targetPosition = new Position( root, [ 0, 4, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -337,7 +325,7 @@ describe( 'LiveRange', () => {
 			it( 'is from the same parent as range end and before it - #1', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 2, 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 1 );
 					const targetPosition = new Position( root, [ 0, 4, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -351,7 +339,7 @@ describe( 'LiveRange', () => {
 			it( 'is from the same parent as range end and before it - #2', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 2, 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 2 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 2 );
 					const targetPosition = new Position( root, [ 0, 4, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -365,7 +353,7 @@ describe( 'LiveRange', () => {
 			it( 'is from a position before a node from range start path', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 1 );
 					const targetPosition = new Position( root, [ 0, 4 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -379,7 +367,7 @@ describe( 'LiveRange', () => {
 			it( 'intersects on live range left side', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 1, 2 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 4 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 4 );
 					const targetPosition = new Position( root, [ 0, 4, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -393,7 +381,7 @@ describe( 'LiveRange', () => {
 			it( 'intersects on live range right side', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 2, 1 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 4 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 4 );
 					const targetPosition = new Position( root, [ 0, 4, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -409,7 +397,7 @@ describe( 'LiveRange', () => {
 
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 1, 4 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 3 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 3 );
 					const targetPosition = new Position( root, [ 0, 4, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -425,7 +413,7 @@ describe( 'LiveRange', () => {
 
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 1, 3 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 5 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 5 );
 					const targetPosition = new Position( root, [ 0, 4, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -441,7 +429,7 @@ describe( 'LiveRange', () => {
 
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 1, 2 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 3 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 3 );
 					const targetPosition = new Position( root, [ 0, 1, 8 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -455,7 +443,7 @@ describe( 'LiveRange', () => {
 			it( 'is intersecting with live range on right and is moved into live range', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 2, 1 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 5 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 5 );
 					const targetPosition = new Position( root, [ 0, 2, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -507,7 +495,8 @@ describe( 'LiveRange', () => {
 					writer.wrap( new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ), 'w' );
 				} );
 
-				expect( stringify( root, live ) ).to.equal( '<w><p>a[b</p></w><p>x</p><p>c]d</p>' );
+				// Should be '<w><p>a[b</p></w><p>x</p><p>c]d</p>' but the range is trimmed.
+				expect( stringify( root, live ) ).to.equal( '<w><p>ab</p></w>[<p>x</p><p>c]d</p>' );
 			} );
 
 			it( 'its end is intersecting with the wrapped range', () => {
@@ -533,7 +522,8 @@ describe( 'LiveRange', () => {
 					writer.wrap( new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) ), 'w' );
 				} );
 
-				expect( stringify( root, live ) ).to.equal( '<w><p>a[b</p><p>x</p></w><p>c]d</p>' );
+				// Should be '<w><p>a[b</p><p>x</p></w><p>c]d</p>' but the range is trimmed.
+				expect( stringify( root, live ) ).to.equal( '<w><p>ab</p><p>x</p></w>[<p>c]d</p>' );
 			} );
 
 			it( 'its end is intersecting with the wrapped range (multiple elements)', () => {
@@ -613,7 +603,8 @@ describe( 'LiveRange', () => {
 					writer.unwrap( root.getChild( 1 ) );
 				} );
 
-				expect( stringify( root, live ) ).to.equal( '<p>a[b</p><p>c]d</p>' );
+				// Should be '<p>a[b</p><p>c]d</p>' but the range is trimmed.
+				expect( stringify( root, live ) ).to.equal( '<p>a[b</p>]<p>cd</p>' );
 			} );
 
 			it( 'its start is intersecting with the wrapper to remove (multiple elements)', () => {
@@ -637,7 +628,8 @@ describe( 'LiveRange', () => {
 					writer.unwrap( root.getChild( 1 ) );
 				} );
 
-				expect( stringify( root, live ) ).to.equal( '<p>a[b</p><p>x</p><p>c]d</p>' );
+				// Should be '<p>a[b</p><p>x</p><p>c]d</p>' but the range is trimmed.
+				expect( stringify( root, live ) ).to.equal( '<p>a[b</p>]<p>x</p><p>cd</p>' );
 			} );
 
 			it( 'contains wrapped element', () => {
@@ -659,7 +651,7 @@ describe( 'LiveRange', () => {
 
 		beforeEach( () => {
 			live = new LiveRange( new Position( root, [ 0, 1, 4 ] ), new Position( root, [ 0, 2, 2 ] ) );
-			clone = Range.createFromRange( live );
+			clone = live.toRange();
 
 			spy = sinon.spy();
 			live.on( 'change:content', spy );
@@ -684,7 +676,7 @@ describe( 'LiveRange', () => {
 			it( 'inside the range', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 4, 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 3 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 3 );
 					const targetPosition = new Position( root, [ 0, 1, 5 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -697,7 +689,7 @@ describe( 'LiveRange', () => {
 			it( 'from the range', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 1, 5 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 2 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 2 );
 					const targetPosition = new Position( root, [ 0, 4, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -710,7 +702,7 @@ describe( 'LiveRange', () => {
 			it( 'from the beginning of range', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 1, 4 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 2 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 2 );
 					const targetPosition = new Position( root, [ 0, 4, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -725,7 +717,7 @@ describe( 'LiveRange', () => {
 
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 1, 5 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 1 );
 					const targetPosition = new Position( root, [ 0, 1, 7 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -744,7 +736,7 @@ describe( 'LiveRange', () => {
 		beforeEach( () => {
 			otherRoot = doc.createRoot( '$root', 'otherRoot' );
 			live = new LiveRange( new Position( root, [ 0, 1, 4 ] ), new Position( root, [ 0, 2, 2 ] ) );
-			clone = Range.createFromRange( live );
+			clone = live.toRange();
 
 			spy = sinon.spy();
 			live.on( 'change', spy );
@@ -787,7 +779,7 @@ describe( 'LiveRange', () => {
 			it( 'is to the same parent as range end and after it', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 4, 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 3 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 3 );
 					const targetPosition = new Position( root, [ 0, 2, 4 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -800,7 +792,7 @@ describe( 'LiveRange', () => {
 			it( 'is to a position after a node from range end path', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 5 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 1 );
 					const targetPosition = new Position( root, [ 0, 4 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -813,7 +805,7 @@ describe( 'LiveRange', () => {
 			it( 'is from the same parent as range end and after it', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 2, 4 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 3 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 3 );
 					const targetPosition = new Position( root, [ 0, 4, 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -826,7 +818,7 @@ describe( 'LiveRange', () => {
 			it( 'is from a position after a node from range end path', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 4 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 1 );
 					const targetPosition = new Position( root, [ 0, 5 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -839,7 +831,7 @@ describe( 'LiveRange', () => {
 			it( 'is to different root', () => {
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 0, 4 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 1 );
 					const targetPosition = new Position( otherRoot, [ 0 ] );
 
 					writer.move( sourceRange, targetPosition );
@@ -854,7 +846,7 @@ describe( 'LiveRange', () => {
 					writer.insertText( 'foo', new Position( otherRoot, [ 0 ] ) );
 
 					const sourcePosition = new Position( otherRoot, [ 0 ] );
-					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
+					const sourceRange = Range._createFromPositionAndShift( sourcePosition, 1 );
 					const targetPosition = new Position( root, [ 0, 4 ] );
 
 					writer.move( sourceRange, targetPosition );

+ 12 - 12
packages/ckeditor5-engine/tests/model/markercollection.js

@@ -21,8 +21,8 @@ describe( 'MarkerCollection', () => {
 		markers = new MarkerCollection();
 
 		root = doc.createRoot();
-		range = Range.createFromParentsAndOffsets( root, 0, root, 1 );
-		range2 = Range.createFromParentsAndOffsets( root, 0, root, 2 );
+		range = new Range( Position._createAt( root, 0 ), Position._createAt( root, 1 ) );
+		range2 = new Range( Position._createAt( root, 0 ), Position._createAt( root, 2 ) );
 	} );
 
 	describe( 'iterator', () => {
@@ -212,7 +212,7 @@ describe( 'MarkerCollection', () => {
 			markers._set( 'a', range );
 			const markerB = markers._set( 'b', range2 );
 
-			const result = Array.from( markers.getMarkersAtPosition( Position.createAt( root, 1 ) ) );
+			const result = Array.from( markers.getMarkersAtPosition( Position._createAt( root, 1 ) ) );
 
 			expect( result ).to.deep.equal( [ markerB ] );
 		} );
@@ -250,7 +250,7 @@ describe( 'Marker', () => {
 	it( 'should provide API that returns up-to-date marker range parameters', () => {
 		root._appendChild( new Text( 'foo' ) );
 
-		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
+		const range = new Range( Position._createAt( root, 1 ), Position._createAt( root, 2 ) );
 		const marker = model.markers._set( 'name', range );
 
 		expect( marker.getRange().isEqual( range ) ).to.be.true;
@@ -261,7 +261,7 @@ describe( 'Marker', () => {
 			writer.insertText( 'abc', root );
 		} );
 
-		const updatedRange = Range.createFromParentsAndOffsets( root, 4, root, 5 );
+		const updatedRange = new Range( Position._createAt( root, 4 ), Position._createAt( root, 5 ) );
 
 		expect( marker.getRange().isEqual( updatedRange ) ).to.be.true;
 		expect( marker.getStart().isEqual( updatedRange.start ) ).to.be.true;
@@ -269,7 +269,7 @@ describe( 'Marker', () => {
 	} );
 
 	it( 'should throw when using the API if marker was removed from markers collection', () => {
-		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
+		const range = new Range( Position._createAt( root, 1 ), Position._createAt( root, 2 ) );
 		const marker = model.markers._set( 'name', range );
 
 		model.markers._remove( 'name' );
@@ -296,7 +296,7 @@ describe( 'Marker', () => {
 	} );
 
 	it( 'should attach live range to marker', () => {
-		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
+		const range = new Range( Position._createAt( root, 1 ), Position._createAt( root, 2 ) );
 		const marker = model.markers._set( 'name', range );
 
 		const eventRange = sinon.spy();
@@ -313,7 +313,7 @@ describe( 'Marker', () => {
 	} );
 
 	it( 'should detach live range from marker', () => {
-		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
+		const range = new Range( Position._createAt( root, 1 ), Position._createAt( root, 2 ) );
 		const marker = model.markers._set( 'name', range );
 		const liveRange = marker._liveRange;
 
@@ -335,10 +335,10 @@ describe( 'Marker', () => {
 	} );
 
 	it( 'should reattach live range to marker', () => {
-		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
+		const range = new Range( Position._createAt( root, 1 ), Position._createAt( root, 2 ) );
 		const marker = model.markers._set( 'name', range );
 		const oldLiveRange = marker._liveRange;
-		const newLiveRange = LiveRange.createFromParentsAndOffsets( root, 0, root, 1 );
+		const newLiveRange = new LiveRange( Position._createAt( root, 0 ), Position._createAt( root, 1 ) );
 
 		const eventRange = sinon.spy();
 		const eventContent = sinon.spy();
@@ -364,7 +364,7 @@ describe( 'Marker', () => {
 	} );
 
 	it( 'should change managedUsingOperations flag', () => {
-		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
+		const range = new Range( Position._createAt( root, 1 ), Position._createAt( root, 2 ) );
 		const marker = model.markers._set( 'name', range, false );
 
 		expect( marker.managedUsingOperations ).to.be.false;
@@ -379,7 +379,7 @@ describe( 'Marker', () => {
 	} );
 
 	it( 'should change affectsData flag', () => {
-		const range = Range.createFromParentsAndOffsets( root, 1, root, 2 );
+		const range = new Range( Position._createAt( root, 1 ), Position._createAt( root, 2 ) );
 		const marker = model.markers._set( 'name', range, false, false );
 
 		expect( marker.affectsData ).to.be.false;

+ 90 - 9
packages/ckeditor5-engine/tests/model/model.js

@@ -7,8 +7,10 @@ import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import Model from '../../src/model/model';
 import ModelText from '../../src/model/text';
 import ModelRange from '../../src/model/range';
+import ModelPosition from '../../src/model/position';
 import ModelSelection from '../../src/model/selection';
 import ModelDocumentFragment from '../../src/model/documentfragment';
+import Batch from '../../src/model/batch';
 import { getData, setData, stringify } from '../../src/dev-utils/model';
 
 describe( 'Model', () => {
@@ -347,7 +349,7 @@ describe( 'Model', () => {
 
 			model.on( 'insertContent', spy );
 
-			model.insertContent( new ModelText( 'a' ), model.document.selection );
+			model.insertContent( new ModelText( 'a' ) );
 
 			expect( spy.calledOnce ).to.be.true;
 		} );
@@ -357,7 +359,7 @@ describe( 'Model', () => {
 
 			setData( model, '<paragraph>fo[]ar</paragraph>' );
 
-			model.insertContent( new ModelText( 'ob' ), model.document.selection );
+			model.insertContent( new ModelText( 'ob' ) );
 
 			expect( getData( model ) ).to.equal( '<paragraph>foob[]ar</paragraph>' );
 		} );
@@ -367,7 +369,17 @@ describe( 'Model', () => {
 
 			setData( model, '<paragraph>fo[]ar</paragraph>' );
 
-			model.insertContent( new ModelDocumentFragment( [ new ModelText( 'ob' ) ] ), model.document.selection );
+			model.insertContent( new ModelDocumentFragment( [ new ModelText( 'ob' ) ] ) );
+
+			expect( getData( model ) ).to.equal( '<paragraph>foob[]ar</paragraph>' );
+		} );
+
+		it( 'should use current model selection if no selectable passed', () => {
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+
+			setData( model, '<paragraph>fo[]ar</paragraph>' );
+
+			model.insertContent( new ModelText( 'ob' ) );
 
 			expect( getData( model ) ).to.equal( '<paragraph>foob[]ar</paragraph>' );
 		} );
@@ -377,7 +389,7 @@ describe( 'Model', () => {
 			setData( model, '<paragraph>[]</paragraph>' );
 
 			model.change( writer => {
-				model.insertContent( new ModelText( 'abc' ), model.document.selection );
+				model.insertContent( new ModelText( 'abc' ) );
 				expect( writer.batch.operations ).to.length( 1 );
 			} );
 		} );
@@ -529,38 +541,107 @@ describe( 'Model', () => {
 		} );
 
 		it( 'should return true if there is a text node in given range', () => {
-			const range = ModelRange.createFromParentsAndOffsets( root, 1, root, 2 );
+			const range = new ModelRange( ModelPosition._createAt( root, 1 ), ModelPosition._createAt( root, 2 ) );
 
 			expect( model.hasContent( range ) ).to.be.true;
 		} );
 
 		it( 'should return true if there is a part of text node in given range', () => {
 			const pFoo = root.getChild( 1 );
-			const range = ModelRange.createFromParentsAndOffsets( pFoo, 1, pFoo, 2 );
+			const range = new ModelRange( ModelPosition._createAt( pFoo, 1 ), ModelPosition._createAt( pFoo, 2 ) );
 
 			expect( model.hasContent( range ) ).to.be.true;
 		} );
 
 		it( 'should return true if there is element that is an object in given range', () => {
 			const divImg = root.getChild( 2 );
-			const range = ModelRange.createFromParentsAndOffsets( divImg, 0, divImg, 1 );
+			const range = new ModelRange( ModelPosition._createAt( divImg, 0 ), ModelPosition._createAt( divImg, 1 ) );
 
 			expect( model.hasContent( range ) ).to.be.true;
 		} );
 
 		it( 'should return false if range is collapsed', () => {
-			const range = ModelRange.createFromParentsAndOffsets( root, 1, root, 1 );
+			const range = new ModelRange( ModelPosition._createAt( root, 1 ), ModelPosition._createAt( root, 1 ) );
 
 			expect( model.hasContent( range ) ).to.be.false;
 		} );
 
 		it( 'should return false if range has only elements that are not objects', () => {
-			const range = ModelRange.createFromParentsAndOffsets( root, 0, root, 1 );
+			const range = new ModelRange( ModelPosition._createAt( root, 0 ), ModelPosition._createAt( root, 1 ) );
 
 			expect( model.hasContent( range ) ).to.be.false;
 		} );
 	} );
 
+	describe( 'createPositionFromPath()', () => {
+		it( 'should return instance of Position', () => {
+			expect( model.createPositionFromPath( model.document.getRoot(), [ 0 ] ) ).to.be.instanceof( ModelPosition );
+		} );
+	} );
+
+	describe( 'createPositionAt()', () => {
+		it( 'should return instance of Position', () => {
+			expect( model.createPositionAt( model.document.getRoot(), 0 ) ).to.be.instanceof( ModelPosition );
+		} );
+	} );
+
+	describe( 'createPositionAfter()', () => {
+		it( 'should return instance of Position', () => {
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			setData( model, '<paragraph>fo[]ar</paragraph>' );
+
+			expect( model.createPositionAfter( model.document.getRoot().getChild( 0 ) ) ).to.be.instanceof( ModelPosition );
+		} );
+	} );
+
+	describe( 'createPositionBefore()', () => {
+		it( 'should return instance of Position', () => {
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			setData( model, '<paragraph>fo[]ar</paragraph>' );
+
+			expect( model.createPositionBefore( model.document.getRoot().getChild( 0 ) ) ).to.be.instanceof( ModelPosition );
+		} );
+	} );
+
+	describe( 'createRange()', () => {
+		it( 'should return instance of Range', () => {
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			setData( model, '<paragraph>fo[]ar</paragraph>' );
+
+			expect( model.createRange( model.createPositionAt( model.document.getRoot(), 0 ) ) ).to.be.instanceof( ModelRange );
+		} );
+	} );
+
+	describe( 'createRangeIn()', () => {
+		it( 'should return instance of Range', () => {
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			setData( model, '<paragraph>fo[]ar</paragraph>' );
+
+			expect( model.createRangeIn( model.document.getRoot().getChild( 0 ) ) ).to.be.instanceof( ModelRange );
+		} );
+	} );
+
+	describe( 'createRangeOn()', () => {
+		it( 'should return instance of Range', () => {
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			setData( model, '<paragraph>fo[]ar</paragraph>' );
+
+			expect( model.createRangeOn( model.document.getRoot().getChild( 0 ) ) ).to.be.instanceof( ModelRange );
+		} );
+	} );
+
+	describe( 'createSelection()', () => {
+		it( 'should return instance of Selection', () => {
+			expect( model.createSelection() ).to.be.instanceof( ModelSelection );
+		} );
+	} );
+
+	describe( 'createBatch()', () => {
+		it( 'should return instance of Batch', () => {
+			expect( model.createBatch() ).to.be.instanceof( Batch );
+		} );
+	} );
+
 	describe( 'destroy()', () => {
 		it( 'should destroy document', () => {
 			sinon.spy( model.document, 'destroy' );

+ 5 - 5
packages/ckeditor5-engine/tests/model/operation/detachoperation.js

@@ -21,13 +21,13 @@ describe( 'DetachOperation', () => {
 	} );
 
 	it( 'should have type equal to detach', () => {
-		const op = new DetachOperation( Position.createBefore( element ), 1 );
+		const op = new DetachOperation( Position._createBefore( element ), 1 );
 
 		expect( op.type ).to.equal( 'detach' );
 	} );
 
 	it( 'should remove given element from parent', () => {
-		const op = new DetachOperation( Position.createBefore( element ), 1 );
+		const op = new DetachOperation( Position._createBefore( element ), 1 );
 
 		model.applyOperation( op );
 
@@ -41,7 +41,7 @@ describe( 'DetachOperation', () => {
 
 			root._appendChild( [ element ] );
 
-			const op = new DetachOperation( Position.createBefore( element ), 1 );
+			const op = new DetachOperation( Position._createBefore( element ), 1 );
 
 			expect( () => {
 				op._validate();
@@ -50,14 +50,14 @@ describe( 'DetachOperation', () => {
 	} );
 
 	it( 'should be not a document operation', () => {
-		const op = new DetachOperation( Position.createBefore( element ), 1 );
+		const op = new DetachOperation( Position._createBefore( element ), 1 );
 
 		expect( op.isDocumentOperation ).to.false;
 	} );
 
 	describe( 'toJSON', () => {
 		it( 'should create proper json object', () => {
-			const position = Position.createBefore( element );
+			const position = Position._createBefore( element );
 			const op = new DetachOperation( position, 1 );
 
 			const serialized = op.toJSON();

Некоторые файлы не были показаны из-за большого количества измененных файлов