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

Merge branch 'master' into t/ckeditor5/1514

Piotrek Koszuliński 6 лет назад
Родитель
Сommit
8816cde891
87 измененных файлов с 2032 добавлено и 581 удалено
  1. 30 0
      packages/ckeditor5-engine/CHANGELOG.md
  2. 1 1
      packages/ckeditor5-engine/docs/api/engine.md
  3. 17 17
      packages/ckeditor5-engine/package.json
  4. 1 1
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  5. 15 10
      packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
  6. 59 5
      packages/ckeditor5-engine/src/conversion/mapper.js
  7. 21 0
      packages/ckeditor5-engine/src/conversion/upcastdispatcher.js
  8. 18 15
      packages/ckeditor5-engine/src/conversion/upcasthelpers.js
  9. 1 1
      packages/ckeditor5-engine/src/model/differ.js
  10. 10 3
      packages/ckeditor5-engine/src/model/documentfragment.js
  11. 36 33
      packages/ckeditor5-engine/src/model/documentselection.js
  12. 20 12
      packages/ckeditor5-engine/src/model/element.js
  13. 20 0
      packages/ckeditor5-engine/src/model/liveposition.js
  14. 20 0
      packages/ckeditor5-engine/src/model/liverange.js
  15. 18 0
      packages/ckeditor5-engine/src/model/markercollection.js
  16. 47 23
      packages/ckeditor5-engine/src/model/node.js
  17. 4 4
      packages/ckeditor5-engine/src/model/operation/mergeoperation.js
  18. 1 10
      packages/ckeditor5-engine/src/model/operation/moveoperation.js
  19. 32 28
      packages/ckeditor5-engine/src/model/operation/transform.js
  20. 44 3
      packages/ckeditor5-engine/src/model/position.js
  21. 18 0
      packages/ckeditor5-engine/src/model/range.js
  22. 28 3
      packages/ckeditor5-engine/src/model/rootelement.js
  23. 61 47
      packages/ckeditor5-engine/src/model/selection.js
  24. 17 3
      packages/ckeditor5-engine/src/model/text.js
  25. 9 3
      packages/ckeditor5-engine/src/model/textproxy.js
  26. 1 1
      packages/ckeditor5-engine/src/model/writer.js
  27. 17 11
      packages/ckeditor5-engine/src/utils/bindtwostepcarettoattribute.js
  28. 29 3
      packages/ckeditor5-engine/src/view/attributeelement.js
  29. 28 3
      packages/ckeditor5-engine/src/view/containerelement.js
  30. 10 3
      packages/ckeditor5-engine/src/view/documentfragment.js
  31. 14 8
      packages/ckeditor5-engine/src/view/documentselection.js
  32. 21 21
      packages/ckeditor5-engine/src/view/downcastwriter.js
  33. 28 3
      packages/ckeditor5-engine/src/view/editableelement.js
  34. 21 10
      packages/ckeditor5-engine/src/view/element.js
  35. 28 3
      packages/ckeditor5-engine/src/view/emptyelement.js
  36. 40 13
      packages/ckeditor5-engine/src/view/node.js
  37. 1 1
      packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js
  38. 19 0
      packages/ckeditor5-engine/src/view/position.js
  39. 19 0
      packages/ckeditor5-engine/src/view/range.js
  40. 30 3
      packages/ckeditor5-engine/src/view/rooteditableelement.js
  41. 9 7
      packages/ckeditor5-engine/src/view/selection.js
  42. 16 2
      packages/ckeditor5-engine/src/view/text.js
  43. 10 3
      packages/ckeditor5-engine/src/view/textproxy.js
  44. 28 3
      packages/ckeditor5-engine/src/view/uielement.js
  45. 18 1
      packages/ckeditor5-engine/src/view/upcastwriter.js
  46. 25 9
      packages/ckeditor5-engine/tests/conversion/conversion.js
  47. 34 8
      packages/ckeditor5-engine/tests/conversion/downcastdispatcher.js
  48. 39 0
      packages/ckeditor5-engine/tests/conversion/mapper.js
  49. 7 1
      packages/ckeditor5-engine/tests/manual/tickets/1301/1.js
  50. 21 1
      packages/ckeditor5-engine/tests/manual/two-step-caret.html
  51. 46 3
      packages/ckeditor5-engine/tests/manual/two-step-caret.js
  52. 7 0
      packages/ckeditor5-engine/tests/manual/two-step-caret.md
  53. 30 0
      packages/ckeditor5-engine/tests/model/differ.js
  54. 4 2
      packages/ckeditor5-engine/tests/model/documentfragment.js
  55. 7 1
      packages/ckeditor5-engine/tests/model/documentselection.js
  56. 12 1
      packages/ckeditor5-engine/tests/model/element.js
  57. 23 0
      packages/ckeditor5-engine/tests/model/liveposition.js
  58. 23 0
      packages/ckeditor5-engine/tests/model/liverange.js
  59. 21 0
      packages/ckeditor5-engine/tests/model/markercollection.js
  60. 8 0
      packages/ckeditor5-engine/tests/model/node.js
  61. 19 2
      packages/ckeditor5-engine/tests/model/operation/mergeoperation.js
  62. 1 1
      packages/ckeditor5-engine/tests/model/operation/moveoperation.js
  63. 67 1
      packages/ckeditor5-engine/tests/model/operation/transform.js
  64. 192 85
      packages/ckeditor5-engine/tests/model/position.js
  65. 14 0
      packages/ckeditor5-engine/tests/model/range.js
  66. 11 1
      packages/ckeditor5-engine/tests/model/rootelement.js
  67. 110 114
      packages/ckeditor5-engine/tests/model/selection.js
  68. 4 1
      packages/ckeditor5-engine/tests/model/text.js
  69. 4 1
      packages/ckeditor5-engine/tests/model/textproxy.js
  70. 107 4
      packages/ckeditor5-engine/tests/utils/bindtwostepcarettoattribute.js
  71. 9 1
      packages/ckeditor5-engine/tests/view/attributeelement.js
  72. 9 1
      packages/ckeditor5-engine/tests/view/containerelement.js
  73. 4 1
      packages/ckeditor5-engine/tests/view/documentfragment.js
  74. 7 1
      packages/ckeditor5-engine/tests/view/documentselection.js
  75. 50 8
      packages/ckeditor5-engine/tests/view/editableelement.js
  76. 10 1
      packages/ckeditor5-engine/tests/view/element.js
  77. 10 1
      packages/ckeditor5-engine/tests/view/emptyelement.js
  78. 20 2
      packages/ckeditor5-engine/tests/view/node.js
  79. 30 3
      packages/ckeditor5-engine/tests/view/position.js
  80. 28 0
      packages/ckeditor5-engine/tests/view/range.js
  81. 16 1
      packages/ckeditor5-engine/tests/view/rooteditableelement.js
  82. 4 1
      packages/ckeditor5-engine/tests/view/selection.js
  83. 7 1
      packages/ckeditor5-engine/tests/view/text.js
  84. 5 1
      packages/ckeditor5-engine/tests/view/textproxy.js
  85. 13 0
      packages/ckeditor5-engine/tests/view/uielement.js
  86. 30 0
      packages/ckeditor5-engine/tests/view/upcastwriter.js
  87. 9 1
      packages/ckeditor5-engine/theme/placeholder.css

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

@@ -1,6 +1,36 @@
 Changelog
 Changelog
 =========
 =========
 
 
+## [14.0.0](https://github.com/ckeditor/ckeditor5-engine/compare/v13.2.1...v14.0.0) (2019-08-26)
+
+### Features
+
+* `transformSets()` will now return a `Map` instance linking transformed operations to the original operations. ([61da3ec](https://github.com/ckeditor/ckeditor5-engine/commit/61da3ec))
+* Brought support for RTL content in the `bindTwoStepCaretToAttribute()` helper. See [ckeditor/ckeditor5#1151](https://github.com/ckeditor/ckeditor5/issues/1151). ([d57ff5a](https://github.com/ckeditor/ckeditor5-engine/commit/d57ff5a))
+* Introduced `model.Differ#refreshItem()`. ([7dc8710](https://github.com/ckeditor/ckeditor5-engine/commit/7dc8710))
+* Introduced the `is()` method to additional objects from the model and view realms. Closes [#1667](https://github.com/ckeditor/ckeditor5-engine/issues/1667). ([89dbe43](https://github.com/ckeditor/ckeditor5-engine/commit/89dbe43))
+
+### Bug fixes
+
+* Fixed problem with handling very large text nodes. ([a7ae813](https://github.com/ckeditor/ckeditor5-engine/commit/a7ae813))
+* Prevented `Differ` crashing in some scenarios involving attribute changes on elements. Closes [#1764](https://github.com/ckeditor/ckeditor5-engine/issues/1764). ([482e55e](https://github.com/ckeditor/ckeditor5-engine/commit/482e55e))
+
+### Other changes
+
+* Add `unwrapElement()` method to UpcastWriter. ([9e97196](https://github.com/ckeditor/ckeditor5-engine/commit/9e97196))
+* Allowed for unbinding single elements from a marker name in `Mapper`. Closes [#1758](https://github.com/ckeditor/ckeditor5-engine/issues/1758). ([52e701d](https://github.com/ckeditor/ckeditor5-engine/commit/52e701d))
+* The issue tracker for this package was moved to https://github.com/ckeditor/ckeditor5/issues. See [ckeditor/ckeditor5#1988](https://github.com/ckeditor/ckeditor5/issues/1988). ([6ed94c6](https://github.com/ckeditor/ckeditor5-engine/commit/6ed94c6))
+* Introduced automatic marker re-rendering during conversion for markers which view element was unbound. Closes [#1780](https://github.com/ckeditor/ckeditor5-engine/issues/1780). ([5661fb6](https://github.com/ckeditor/ckeditor5-engine/commit/5661fb6))
+* Position getters (such as `#parent` or `#index`) will throw when position points at an incorrect place in its root. Closes [#1776](https://github.com/ckeditor/ckeditor5-engine/issues/1776). ([a359866](https://github.com/ckeditor/ckeditor5-engine/commit/a359866))
+
+### BREAKING CHANGES
+
+* New parameter introduced in `DowncastDispatcher#convertChanges()`. Now it is `convertChanges( differ, markers, writer )`.
+* Although it was rather impossible to use `DowncastDispatcher` without specifying any conversion API in the constructor, now it is a required parameter.
+* The `bindTwoStepCaretToAttribute()` helper arguments syntax has changed (replaced by an object). Please refer to the helper documentation to learn more.
+* `Mapper#unbindElementsFromMarkerName( markerName )` was replaced by `Mapper#unbindElementFromMarkerName( element, markerName )`.
+
+
 ## [13.2.1](https://github.com/ckeditor/ckeditor5-engine/compare/v13.2.0...v13.2.1) (2019-07-10)
 ## [13.2.1](https://github.com/ckeditor/ckeditor5-engine/compare/v13.2.0...v13.2.1) (2019-07-10)
 
 
 Internal changes only (updated dependencies, documentation, etc.).
 Internal changes only (updated dependencies, documentation, etc.).

+ 1 - 1
packages/ckeditor5-engine/docs/api/engine.md

@@ -30,5 +30,5 @@ The source code of this package is available on GitHub in https://github.com/cke
 
 
 * [`@ckeditor/ckeditor5-engine` on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)
 * [`@ckeditor/ckeditor5-engine` on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)
 * [`ckeditor/ckeditor5-engine` on GitHub](https://github.com/ckeditor/ckeditor5-engine)
 * [`ckeditor/ckeditor5-engine` on GitHub](https://github.com/ckeditor/ckeditor5-engine)
-* [Issue tracker](https://github.com/ckeditor/ckeditor5-engine/issues)
+* [Issue tracker](https://github.com/ckeditor/ckeditor5/issues)
 * [Changelog](https://github.com/ckeditor/ckeditor5-engine/blob/master/CHANGELOG.md)
 * [Changelog](https://github.com/ckeditor/ckeditor5-engine/blob/master/CHANGELOG.md)

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@ckeditor/ckeditor5-engine",
   "name": "@ckeditor/ckeditor5-engine",
-  "version": "13.2.1",
+  "version": "14.0.0",
   "description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
   "description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
   "keywords": [
   "keywords": [
     "wysiwyg",
     "wysiwyg",
@@ -21,24 +21,24 @@
     "ckeditor 5"
     "ckeditor 5"
   ],
   ],
   "dependencies": {
   "dependencies": {
-    "@ckeditor/ckeditor5-utils": "^13.0.1",
+    "@ckeditor/ckeditor5-utils": "^14.0.0",
     "lodash-es": "^4.17.10"
     "lodash-es": "^4.17.10"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^11.1.3",
-    "@ckeditor/ckeditor5-block-quote": "^11.1.2",
-    "@ckeditor/ckeditor5-core": "^12.2.1",
-    "@ckeditor/ckeditor5-editor-classic": "^12.1.3",
-    "@ckeditor/ckeditor5-enter": "^11.0.4",
-    "@ckeditor/ckeditor5-essentials": "^11.0.4",
-    "@ckeditor/ckeditor5-heading": "^11.0.4",
-    "@ckeditor/ckeditor5-link": "^11.1.1",
-    "@ckeditor/ckeditor5-list": "^12.0.4",
-    "@ckeditor/ckeditor5-paragraph": "^11.0.4",
-    "@ckeditor/ckeditor5-theme-lark": "^14.1.1",
-    "@ckeditor/ckeditor5-typing": "^12.1.1",
-    "@ckeditor/ckeditor5-undo": "^11.0.4",
-    "@ckeditor/ckeditor5-widget": "^11.0.4",
+    "@ckeditor/ckeditor5-basic-styles": "^11.1.4",
+    "@ckeditor/ckeditor5-block-quote": "^11.1.3",
+    "@ckeditor/ckeditor5-core": "^12.3.0",
+    "@ckeditor/ckeditor5-editor-classic": "^12.1.4",
+    "@ckeditor/ckeditor5-enter": "^11.1.0",
+    "@ckeditor/ckeditor5-essentials": "^11.0.5",
+    "@ckeditor/ckeditor5-heading": "^11.0.5",
+    "@ckeditor/ckeditor5-link": "^11.1.2",
+    "@ckeditor/ckeditor5-list": "^12.1.0",
+    "@ckeditor/ckeditor5-paragraph": "^11.0.5",
+    "@ckeditor/ckeditor5-theme-lark": "^14.2.0",
+    "@ckeditor/ckeditor5-typing": "^12.2.0",
+    "@ckeditor/ckeditor5-undo": "^11.0.5",
+    "@ckeditor/ckeditor5-widget": "^11.1.0",
     "eslint": "^5.5.0",
     "eslint": "^5.5.0",
     "eslint-config-ckeditor5": "^2.0.0",
     "eslint-config-ckeditor5": "^2.0.0",
     "husky": "^1.3.1",
     "husky": "^1.3.1",
@@ -51,7 +51,7 @@
   "author": "CKSource (http://cksource.com/)",
   "author": "CKSource (http://cksource.com/)",
   "license": "GPL-2.0-or-later",
   "license": "GPL-2.0-or-later",
   "homepage": "https://ckeditor.com/ckeditor-5",
   "homepage": "https://ckeditor.com/ckeditor-5",
-  "bugs": "https://github.com/ckeditor/ckeditor5-engine/issues",
+  "bugs": "https://github.com/ckeditor/ckeditor5/issues",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
     "url": "https://github.com/ckeditor/ckeditor5-engine.git"
     "url": "https://github.com/ckeditor/ckeditor5-engine.git"

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

@@ -87,7 +87,7 @@ export default class EditingController {
 		// Also convert model selection.
 		// Also convert model selection.
 		this.listenTo( doc, 'change', () => {
 		this.listenTo( doc, 'change', () => {
 			this.view.change( writer => {
 			this.view.change( writer => {
-				this.downcastDispatcher.convertChanges( doc.differ, writer );
+				this.downcastDispatcher.convertChanges( doc.differ, markers, writer );
 				this.downcastDispatcher.convertSelection( selection, markers, writer );
 				this.downcastDispatcher.convertSelection( selection, markers, writer );
 			} );
 			} );
 		}, { priority: 'low' } );
 		}, { priority: 'low' } );

+ 15 - 10
packages/ckeditor5-engine/src/conversion/downcastdispatcher.js

@@ -106,10 +106,10 @@ export default class DowncastDispatcher {
 	 * Creates a `DowncastDispatcher` instance.
 	 * Creates a `DowncastDispatcher` instance.
 	 *
 	 *
 	 * @see module:engine/conversion/downcastdispatcher~DowncastConversionApi
 	 * @see module:engine/conversion/downcastdispatcher~DowncastConversionApi
-	 * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
+	 * @param {Object} conversionApi Additional properties for interface that will be passed to events fired
 	 * by `DowncastDispatcher`.
 	 * by `DowncastDispatcher`.
 	 */
 	 */
-	constructor( conversionApi = {} ) {
+	constructor( conversionApi ) {
 		/**
 		/**
 		 * Interface passed by dispatcher to the events callbacks.
 		 * Interface passed by dispatcher to the events callbacks.
 		 *
 		 *
@@ -122,9 +122,10 @@ export default class DowncastDispatcher {
 	 * Takes {@link module:engine/model/differ~Differ model differ} object with buffered changes and fires conversion basing on it.
 	 * Takes {@link module:engine/model/differ~Differ model differ} object with buffered changes and fires conversion basing on it.
 	 *
 	 *
 	 * @param {module:engine/model/differ~Differ} differ Differ object with buffered changes.
 	 * @param {module:engine/model/differ~Differ} differ Differ object with buffered changes.
+	 * @param {module:engine/model/markercollection~MarkerCollection} markers Markers connected with converted model.
 	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
 	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
 	 */
 	 */
-	convertChanges( differ, writer ) {
+	convertChanges( differ, markers, writer ) {
 		// Before the view is updated, remove markers which have changed.
 		// Before the view is updated, remove markers which have changed.
 		for ( const change of differ.getMarkersToRemove() ) {
 		for ( const change of differ.getMarkersToRemove() ) {
 			this.convertMarkerRemove( change.name, change.range, writer );
 			this.convertMarkerRemove( change.name, change.range, writer );
@@ -142,6 +143,13 @@ export default class DowncastDispatcher {
 			}
 			}
 		}
 		}
 
 
+		for ( const markerName of this.conversionApi.mapper.flushUnboundMarkerNames() ) {
+			const markerRange = markers.get( markerName ).getRange();
+
+			this.convertMarkerRemove( markerName, markerRange, writer );
+			this.convertMarkerAdd( markerName, markerRange, writer );
+		}
+
 		// After the view is updated, convert markers which have changed.
 		// After the view is updated, convert markers which have changed.
 		for ( const change of differ.getMarkersToAdd() ) {
 		for ( const change of differ.getMarkersToAdd() ) {
 			this.convertMarkerAdd( change.name, change.range, writer );
 			this.convertMarkerAdd( change.name, change.range, writer );
@@ -252,7 +260,7 @@ export default class DowncastDispatcher {
 	 * @fires addMarker
 	 * @fires addMarker
 	 * @fires attribute
 	 * @fires attribute
 	 * @param {module:engine/model/selection~Selection} selection Selection to convert.
 	 * @param {module:engine/model/selection~Selection} selection Selection to convert.
-	 * @param {Array.<module:engine/model/markercollection~Marker>} markers Array of markers containing model markers.
+	 * @param {module:engine/model/markercollection~MarkerCollection} markers Markers connected with converted model.
 	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
 	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
 	 */
 	 */
 	convertSelection( selection, markers, writer ) {
 	convertSelection( selection, markers, writer ) {
@@ -532,7 +540,6 @@ export default class DowncastDispatcher {
 	 * @param {String} data.attributeKey Attribute key.
 	 * @param {String} data.attributeKey Attribute key.
 	 * @param {*} data.attributeOldValue Attribute value before the change. This is `null` when selection attribute is converted.
 	 * @param {*} data.attributeOldValue Attribute value before the change. This is `null` when selection attribute is converted.
 	 * @param {*} data.attributeNewValue New attribute value.
 	 * @param {*} data.attributeNewValue New attribute value.
-	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
 	 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
 	 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
 	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 	 */
@@ -542,7 +549,6 @@ export default class DowncastDispatcher {
 	 *
 	 *
 	 * @event selection
 	 * @event selection
 	 * @param {module:engine/model/selection~Selection} selection Selection that is converted.
 	 * @param {module:engine/model/selection~Selection} selection Selection that is converted.
-	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
 	 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
 	 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
 	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 	 */
@@ -558,17 +564,17 @@ export default class DowncastDispatcher {
 	 * If the marker range is not collapsed:
 	 * If the marker range is not collapsed:
 	 *
 	 *
 	 * * the event is fired for each item in the marker range one by one,
 	 * * the event is fired for each item in the marker range one by one,
-	 * * consumables object includes each item of the marker range and the consumable value is same as event name.
+	 * * `conversionApi.consumable` includes each item of the marker range and the consumable value is same as event name.
 	 *
 	 *
 	 * If the marker range is collapsed:
 	 * If the marker range is collapsed:
 	 *
 	 *
 	 * * there is only one event,
 	 * * there is only one event,
-	 * * consumables object includes marker range with event name.
+	 * * `conversionApi.consumable` includes marker range with event name.
 	 *
 	 *
 	 * If selection inside a marker is converted:
 	 * If selection inside a marker is converted:
 	 *
 	 *
 	 * * there is only one event,
 	 * * there is only one event,
-	 * * consumables object includes selection instance with event name.
+	 * * `conversionApi.consumable` includes selection instance with event name.
 	 *
 	 *
 	 * @event addMarker
 	 * @event addMarker
 	 * @param {Object} data Additional information about the change.
 	 * @param {Object} data Additional information about the change.
@@ -578,7 +584,6 @@ export default class DowncastDispatcher {
 	 * the marker range was not collapsed.
 	 * the marker range was not collapsed.
 	 * @param {module:engine/model/range~Range} data.markerRange Marker range.
 	 * @param {module:engine/model/range~Range} data.markerRange Marker range.
 	 * @param {String} data.markerName Marker name.
 	 * @param {String} data.markerName Marker name.
-	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
 	 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
 	 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
 	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 	 */

+ 59 - 5
packages/ckeditor5-engine/src/conversion/mapper.js

@@ -77,6 +77,25 @@ export default class Mapper {
 		 */
 		 */
 		this._markerNameToElements = new Map();
 		this._markerNameToElements = new Map();
 
 
+		/**
+		 * View element to model marker names mapping.
+		 *
+		 * This is reverse to {@link ~Mapper#_markerNameToElements} map.
+		 *
+		 * @private
+		 * @member {Map}
+		 */
+		this._elementToMarkerNames = new Map();
+
+		/**
+		 * Stores marker names of markers which has changed due to unbinding a view element (so it is assumed that the view element
+		 * has been removed, moved or renamed).
+		 *
+		 * @private
+		 * @member {Set.<module:engine/model/markercollection~Marker>}
+		 */
+		this._unboundMarkerNames = new Set();
+
 		// Default mapper algorithm for mapping model position to view position.
 		// Default mapper algorithm for mapping model position to view position.
 		this.on( 'modelToViewPosition', ( evt, data ) => {
 		this.on( 'modelToViewPosition', ( evt, data ) => {
 			if ( data.viewPosition ) {
 			if ( data.viewPosition ) {
@@ -132,6 +151,12 @@ export default class Mapper {
 
 
 		this._viewToModelMapping.delete( viewElement );
 		this._viewToModelMapping.delete( viewElement );
 
 
+		if ( this._elementToMarkerNames.has( viewElement ) ) {
+			for ( const markerName of this._elementToMarkerNames.get( viewElement ) ) {
+				this._unboundMarkerNames.add( markerName );
+			}
+		}
+
 		if ( this._modelToViewMapping.get( modelElement ) == viewElement ) {
 		if ( this._modelToViewMapping.get( modelElement ) == viewElement ) {
 			this._modelToViewMapping.delete( modelElement );
 			this._modelToViewMapping.delete( modelElement );
 		}
 		}
@@ -167,10 +192,13 @@ export default class Mapper {
 	 */
 	 */
 	bindElementToMarker( element, name ) {
 	bindElementToMarker( element, name ) {
 		const elements = this._markerNameToElements.get( name ) || new Set();
 		const elements = this._markerNameToElements.get( name ) || new Set();
-
 		elements.add( element );
 		elements.add( element );
 
 
+		const names = this._elementToMarkerNames.get( element ) || new Set();
+		names.add( name );
+
 		this._markerNameToElements.set( name, elements );
 		this._markerNameToElements.set( name, elements );
+		this._elementToMarkerNames.set( element, names );
 	}
 	}
 
 
 	/**
 	/**
@@ -180,15 +208,39 @@ export default class Mapper {
 	 * @param {String} name Marker name.
 	 * @param {String} name Marker name.
 	 */
 	 */
 	unbindElementFromMarkerName( element, name ) {
 	unbindElementFromMarkerName( element, name ) {
-		const elements = this._markerNameToElements.get( name );
+		const nameToElements = this._markerNameToElements.get( name );
 
 
-		if ( elements ) {
-			elements.delete( element );
+		if ( nameToElements ) {
+			nameToElements.delete( element );
 
 
-			if ( elements.size == 0 ) {
+			if ( nameToElements.size == 0 ) {
 				this._markerNameToElements.delete( name );
 				this._markerNameToElements.delete( name );
 			}
 			}
 		}
 		}
+
+		const elementToNames = this._elementToMarkerNames.get( element );
+
+		if ( elementToNames ) {
+			elementToNames.delete( name );
+
+			if ( elementToNames.size == 0 ) {
+				this._elementToMarkerNames.delete( element );
+			}
+		}
+	}
+
+	/**
+	 * Returns all marker names of markers which has changed due to unbinding a view element (so it is assumed that the view element
+	 * has been removed, moved or renamed) since the last flush. After returning, the marker names list is cleared.
+	 *
+	 * @returns {Array.<String>}
+	 */
+	flushUnboundMarkerNames() {
+		const markerNames = Array.from( this._unboundMarkerNames );
+
+		this._unboundMarkerNames.clear();
+
+		return markerNames;
 	}
 	}
 
 
 	/**
 	/**
@@ -198,6 +250,8 @@ export default class Mapper {
 		this._modelToViewMapping = new WeakMap();
 		this._modelToViewMapping = new WeakMap();
 		this._viewToModelMapping = new WeakMap();
 		this._viewToModelMapping = new WeakMap();
 		this._markerNameToElements = new Map();
 		this._markerNameToElements = new Map();
+		this._elementToMarkerNames = new Map();
+		this._unboundMarkerNames = new Set();
 	}
 	}
 
 
 	/**
 	/**

+ 21 - 0
packages/ckeditor5-engine/src/conversion/upcastdispatcher.js

@@ -55,6 +55,27 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
  *			}
  *			}
  *		} );
  *		} );
  *
  *
+ *		// Convert <p>'s font-size style.
+ *		// Note: You should use a low-priority observer in order to ensure that
+ *		// it's executed after the element-to-element converter.
+ *		editor.data.upcastDispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
+ *			const { consumable, schema, writer } = conversionApi;
+ *
+ *			if ( !consumable.consume( data.viewItem, { style: 'font-size' } ) ) {
+ *				return;
+ *			}
+ *
+ *			const fontSize = data.viewItem.getStyle( 'font-size' );
+ *
+ *			// Don't go for the model element after data.modelCursor because it might happen
+ *			// that a single view element was converted to multiple model elements. Get all of them.
+ *			for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
+ *				if ( schema.checkAttribute( item, 'fontSize' ) ) {
+ *					writer.setAttribute( 'fontSize', fontSize, item );
+ *				}
+ *			}
+ *		}, { priority: 'low' } );
+ *
  *		// Convert all elements which have no custom converter into paragraph (autoparagraphing).
  *		// Convert all elements which have no custom converter into paragraph (autoparagraphing).
  *  	editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
  *  	editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
  *  	 	// When element is already consumed by higher priority converters then do nothing.
  *  	 	// When element is already consumed by higher priority converters then do nothing.

+ 18 - 15
packages/ckeditor5-engine/src/conversion/upcasthelpers.js

@@ -156,7 +156,7 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
 	 * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
 	 * the model attribute. `value` property may be set as a function that takes a view element and returns the value.
 	 * the model attribute. `value` property may be set as a function that takes a view element and returns the value.
 	 * If `String` is given, the model attribute value will be set to `true`.
 	 * If `String` is given, the model attribute value will be set to `true`.
-	 * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
+	 * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
 	 * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
 	 * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
 	 */
 	 */
 	elementToAttribute( config ) {
 	elementToAttribute( config ) {
@@ -405,7 +405,7 @@ function upcastElementToElement( config ) {
 
 
 	const converter = prepareToElementConverter( config );
 	const converter = prepareToElementConverter( config );
 
 
-	const elementName = getViewElementNameFromConfig( config );
+	const elementName = getViewElementNameFromConfig( config.view );
 	const eventName = elementName ? 'element:' + elementName : 'element';
 	const eventName = elementName ? 'element:' + elementName : 'element';
 
 
 	return dispatcher => {
 	return dispatcher => {
@@ -422,7 +422,7 @@ function upcastElementToElement( config ) {
 // @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
 // @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
 // the model attribute. `value` property may be set as a function that takes a view element and returns the value.
 // the model attribute. `value` property may be set as a function that takes a view element and returns the value.
 // If `String` is given, the model attribute value will be set to `true`.
 // If `String` is given, the model attribute value will be set to `true`.
-// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
+// @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
 // @returns {Function} Conversion helper.
 // @returns {Function} Conversion helper.
 function upcastElementToAttribute( config ) {
 function upcastElementToAttribute( config ) {
 	config = cloneDeep( config );
 	config = cloneDeep( config );
@@ -431,7 +431,7 @@ function upcastElementToAttribute( config ) {
 
 
 	const converter = prepareToAttributeConverter( config, false );
 	const converter = prepareToAttributeConverter( config, false );
 
 
-	const elementName = getViewElementNameFromConfig( config );
+	const elementName = getViewElementNameFromConfig( config.view );
 	const eventName = elementName ? 'element:' + elementName : 'element';
 	const eventName = elementName ? 'element:' + elementName : 'element';
 
 
 	return dispatcher => {
 	return dispatcher => {
@@ -493,15 +493,15 @@ function upcastElementToMarker( config ) {
 // Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name
 // Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name
 // and if so, returns it.
 // and if so, returns it.
 //
 //
-// @param {Object} config Conversion config.
+// @param {Object} config Conversion view config.
 // @returns {String|null} View element name or `null` if name is not directly set.
 // @returns {String|null} View element name or `null` if name is not directly set.
-function getViewElementNameFromConfig( config ) {
-	if ( typeof config.view == 'string' ) {
-		return config.view;
+function getViewElementNameFromConfig( viewConfig ) {
+	if ( typeof viewConfig == 'string' ) {
+		return viewConfig;
 	}
 	}
 
 
-	if ( typeof config.view == 'object' && typeof config.view.name == 'string' ) {
-		return config.view.name;
+	if ( typeof viewConfig == 'object' && typeof viewConfig.name == 'string' ) {
+		return viewConfig.name;
 	}
 	}
 
 
 	return null;
 	return null;
@@ -684,7 +684,7 @@ function prepareToAttributeConverter( config, shallow ) {
 			return;
 			return;
 		}
 		}
 
 
-		if ( onlyViewNameIsDefined( config ) ) {
+		if ( onlyViewNameIsDefined( config.view, data.viewItem ) ) {
 			match.match.name = true;
 			match.match.name = true;
 		} else {
 		} else {
 			// Do not test or consume `name` consumable.
 			// Do not test or consume `name` consumable.
@@ -714,14 +714,17 @@ function prepareToAttributeConverter( config, shallow ) {
 
 
 // Helper function that checks if element name should be consumed in attribute converters.
 // Helper function that checks if element name should be consumed in attribute converters.
 //
 //
-// @param {Object} config Conversion config.
+// @param {Object} config Conversion view config.
 // @returns {Boolean}
 // @returns {Boolean}
-function onlyViewNameIsDefined( config ) {
-	if ( typeof config.view == 'object' && !getViewElementNameFromConfig( config ) ) {
+function onlyViewNameIsDefined( viewConfig, viewItem ) {
+	// https://github.com/ckeditor/ckeditor5-engine/issues/1786
+	const configToTest = typeof viewConfig == 'function' ? viewConfig( viewItem ) : viewConfig;
+
+	if ( typeof configToTest == 'object' && !getViewElementNameFromConfig( configToTest ) ) {
 		return false;
 		return false;
 	}
 	}
 
 
-	return !config.view.classes && !config.view.attributes && !config.view.styles;
+	return !configToTest.classes && !configToTest.attributes && !configToTest.styles;
 }
 }
 
 
 // Helper function for to-model-attribute converter. Sets model attribute on given range. Checks {@link module:engine/model/schema~Schema}
 // Helper function for to-model-attribute converter. Sets model attribute on given range. Checks {@link module:engine/model/schema~Schema}

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

@@ -162,7 +162,7 @@ export default class Differ {
 			case 'addAttribute':
 			case 'addAttribute':
 			case 'removeAttribute':
 			case 'removeAttribute':
 			case 'changeAttribute': {
 			case 'changeAttribute': {
-				for ( const item of operation.range.getItems() ) {
+				for ( const item of operation.range.getItems( { shallow: true } ) ) {
 					if ( this._isInInsertedElement( item.parent ) ) {
 					if ( this._isInInsertedElement( item.parent ) ) {
 						continue;
 						continue;
 					}
 					}

+ 10 - 3
packages/ckeditor5-engine/src/model/documentfragment.js

@@ -116,15 +116,22 @@ export default class DocumentFragment {
 	}
 	}
 
 
 	/**
 	/**
-	 * Checks whether given model tree object is of given type.
+	 * Checks whether this object is of the given type.
 	 *
 	 *
-	 * Read more in {@link module:engine/model/node~Node#is}.
+	 *		docFrag.is( 'documentFragment' ); // -> true
+	 *		docFrag.is( 'model:documentFragment' ); // -> true
+	 *
+	 *		docFrag.is( 'view:documentFragment' ); // -> false
+	 *		docFrag.is( 'element' ); // -> false
+	 *		docFrag.is( 'node' ); // -> false
+	 *
+	 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 	 *
 	 *
 	 * @param {String} type
 	 * @param {String} type
 	 * @returns {Boolean}
 	 * @returns {Boolean}
 	 */
 	 */
 	is( type ) {
 	is( type ) {
-		return type == 'documentFragment';
+		return type == 'documentFragment' || type == 'model:documentFragment';
 	}
 	}
 
 
 	/**
 	/**

+ 36 - 33
packages/ckeditor5-engine/src/model/documentselection.js

@@ -233,41 +233,26 @@ export default class DocumentSelection {
 	}
 	}
 
 
 	/**
 	/**
-	 * Gets elements of type "block" touched by the selection.
+	 * Gets elements of type {@link module:engine/model/schema~Schema#isBlock "block"} touched by the selection.
 	 *
 	 *
 	 * This method's result can be used for example to apply block styling to all blocks covered by this selection.
 	 * This method's result can be used for example to apply block styling to all blocks covered by this selection.
 	 *
 	 *
-	 * **Note:** `getSelectedBlocks()` always returns the deepest block.
+	 * **Note:** `getSelectedBlocks()` returns blocks that are nested in other non-block elements
+	 * but will not return blocks nested in other blocks.
 	 *
 	 *
-	 * In this case the function will return exactly all 3 paragraphs:
+	 * In this case the function will return exactly all 3 paragraphs (note: `<blockQuote>` is not a block itself):
 	 *
 	 *
 	 *		<paragraph>[a</paragraph>
 	 *		<paragraph>[a</paragraph>
-	 *		<quote>
+	 *		<blockQuote>
 	 *			<paragraph>b</paragraph>
 	 *			<paragraph>b</paragraph>
-	 *		</quote>
+	 *		</blockQuote>
 	 *		<paragraph>c]d</paragraph>
 	 *		<paragraph>c]d</paragraph>
 	 *
 	 *
 	 * In this case the paragraph will also be returned, despite the collapsed selection:
 	 * In this case the paragraph will also be returned, despite the collapsed selection:
 	 *
 	 *
 	 *		<paragraph>[]a</paragraph>
 	 *		<paragraph>[]a</paragraph>
 	 *
 	 *
-	 * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
-	 * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
-	 *
-	 *		<paragraph>[a</paragraph>
-	 *		<paragraph>b</paragraph>
-	 *		<paragraph>]c</paragraph> // this block will not be returned
-	 *
-	 * @returns {Iterable.<module:engine/model/element~Element>}
-	 */
-	getSelectedBlocks() {
-		return this._selection.getSelectedBlocks();
-	}
-
-	/**
-	 * Returns blocks that aren't nested in other selected blocks.
-	 *
-	 * In this case the method will return blocks A, B and E because C & D are children of block B:
+	 * In such a scenario, however, only blocks A, B & E will be returned as blocks C & D are nested in block B:
 	 *
 	 *
 	 *		[<blockA></blockA>
 	 *		[<blockA></blockA>
 	 *		<blockB>
 	 *		<blockB>
@@ -276,12 +261,24 @@ export default class DocumentSelection {
 	 *		</blockB>
 	 *		</blockB>
 	 *		<blockE></blockE>]
 	 *		<blockE></blockE>]
 	 *
 	 *
-	 * **Note:** To get all selected blocks use {@link #getSelectedBlocks `getSelectedBlocks()`}.
+	 * If the selection is inside a block all the inner blocks (A & B) are returned:
+	 *
+	 * 		<block>
+	 *			<blockA>[a</blockA>
+	 * 			<blockB>b]</blockB>
+	 * 		</block>
+	 *
+	 * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
+	 * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
+	 *
+	 *		<paragraph>[a</paragraph>
+	 *		<paragraph>b</paragraph>
+	 *		<paragraph>]c</paragraph> // this block will not be returned
 	 *
 	 *
 	 * @returns {Iterable.<module:engine/model/element~Element>}
 	 * @returns {Iterable.<module:engine/model/element~Element>}
 	 */
 	 */
-	getTopMostBlocks() {
-		return this._selection.getTopMostBlocks();
+	getSelectedBlocks() {
+		return this._selection.getSelectedBlocks();
 	}
 	}
 
 
 	/**
 	/**
@@ -367,21 +364,27 @@ export default class DocumentSelection {
 	}
 	}
 
 
 	/**
 	/**
-	 * Checks whether object is of given type following the convention set by
-	 * {@link module:engine/model/node~Node#is `Node#is()`}.
+	 * Checks whether this object is of the given type.
+	 *
+	 *		selection.is( 'selection' ); // -> true
+	 *		selection.is( 'documentSelection' ); // -> true
+	 *		selection.is( 'model:selection' ); // -> true
+	 *		selection.is( 'model:documentSelection' ); // -> true
 	 *
 	 *
-	 *		const selection = new DocumentSelection( ... );
+	 *		selection.is( 'view:selection' ); // -> false
+	 *		selection.is( 'element' ); // -> false
+	 *		selection.is( 'node' ); // -> false
 	 *
 	 *
-	 *		selection.is( 'selection' ); // true
-	 *		selection.is( 'documentSelection' ); // true
-	 *		selection.is( 'node' ); // false
-	 *		selection.is( 'element' ); // false
+	 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 	 *
 	 *
 	 * @param {String} type
 	 * @param {String} type
 	 * @returns {Boolean}
 	 * @returns {Boolean}
 	 */
 	 */
 	is( type ) {
 	is( type ) {
-		return type == 'selection' || type == 'documentSelection';
+		return type == 'selection' ||
+			type == 'model:selection' ||
+			type == 'documentSelection' ||
+			type == 'model:documentSelection';
 	}
 	}
 
 
 	/**
 	/**

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

@@ -19,7 +19,7 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  *
  *
  * **Important**: see {@link module:engine/model/node~Node} to read about restrictions using `Element` and `Node` API.
  * **Important**: see {@link module:engine/model/node~Node} to read about restrictions using `Element` and `Node` API.
  *
  *
- * @extends {module:engine/model/node~Node}
+ * @extends module:engine/model/node~Node
  */
  */
 export default class Element extends Node {
 export default class Element extends Node {
 	/**
 	/**
@@ -89,18 +89,24 @@ export default class Element extends Node {
 	}
 	}
 
 
 	/**
 	/**
-	 * Checks whether this model object is of the given type.
+	 * Checks whether this object is of the given.
 	 *
 	 *
-	 *		obj.name; // 'listItem'
-	 *		obj instanceof Element; // true
+	 *		element.is( 'element' ); // -> true
+	 *		element.is( 'node' ); // -> true
+	 *		element.is( 'model:element' ); // -> true
+	 *		element.is( 'model:node' ); // -> true
 	 *
 	 *
-	 *		obj.is( 'element' ); // true
-	 *		obj.is( 'listItem' ); // true
-	 *		obj.is( 'element', 'listItem' ); // true
-	 *		obj.is( 'text' ); // false
-	 *		obj.is( 'element', 'image' ); // false
+	 *		element.is( 'view:element' ); // -> false
+	 *		element.is( 'documentSelection' ); // -> false
 	 *
 	 *
-	 * Read more in {@link module:engine/model/node~Node#is `Node#is()`}.
+	 * Assuming that the object being checked is an element, you can also check its
+	 * {@link module:engine/model/element~Element#name name}:
+	 *
+	 *		element.is( 'image' ); // -> true if this is an <image> element
+	 *		element.is( 'element', 'image' ); // -> same as above
+	 *		text.is( 'image' ); -> false
+	 *
+	 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 	 *
 	 *
 	 * @param {String} type Type to check when `name` parameter is present.
 	 * @param {String} type Type to check when `name` parameter is present.
 	 * Otherwise, it acts like the `name` parameter.
 	 * Otherwise, it acts like the `name` parameter.
@@ -108,10 +114,12 @@ export default class Element extends Node {
 	 * @returns {Boolean}
 	 * @returns {Boolean}
 	 */
 	 */
 	is( type, name = null ) {
 	is( type, name = null ) {
+		const cutType = type.replace( /^model:/, '' );
+
 		if ( !name ) {
 		if ( !name ) {
-			return type == 'element' || type == this.name || super.is( type );
+			return cutType == 'element' || cutType == this.name || super.is( type );
 		} else {
 		} else {
-			return type == 'element' && name == this.name;
+			return cutType == 'element' && name == this.name;
 		}
 		}
 	}
 	}
 
 

+ 20 - 0
packages/ckeditor5-engine/src/model/liveposition.js

@@ -63,6 +63,26 @@ export default class LivePosition extends Position {
 		this.stopListening();
 		this.stopListening();
 	}
 	}
 
 
+	/**
+	 * Checks whether this object is of the given.
+	 *
+	 *		livePosition.is( 'position' ); // -> true
+	 *		livePosition.is( 'model:position' ); // -> true
+	 *		livePosition.is( 'liveposition' ); // -> true
+	 *		livePosition.is( 'model:livePosition' ); // -> true
+	 *
+	 *		livePosition.is( 'view:position' ); // -> false
+	 *		livePosition.is( 'documentSelection' ); // -> false
+	 *
+	 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'livePosition' || type == 'model:livePosition' || super.is( type );
+	}
+
 	/**
 	/**
 	 * Creates a {@link module:engine/model/position~Position position instance}, which is equal to this live position.
 	 * Creates a {@link module:engine/model/position~Position position instance}, which is equal to this live position.
 	 *
 	 *

+ 20 - 0
packages/ckeditor5-engine/src/model/liverange.js

@@ -40,6 +40,26 @@ export default class LiveRange extends Range {
 		this.stopListening();
 		this.stopListening();
 	}
 	}
 
 
+	/**
+	 * Checks whether this object is of the given.
+	 *
+	 *		liveRange.is( 'range' ); // -> true
+	 *		liveRange.is( 'model:range' ); // -> true
+	 *		liveRange.is( 'liveRange' ); // -> true
+	 *		liveRange.is( 'model:liveRange' ); // -> true
+	 *
+	 *		liveRange.is( 'view:range' ); // -> false
+	 *		liveRange.is( 'documentSelection' ); // -> false
+	 *
+	 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'liveRange' || type == 'model:liveRange' || super.is( type );
+	}
+
 	/**
 	/**
 	 * Creates a {@link module:engine/model/range~Range range instance} that is equal to this live range.
 	 * Creates a {@link module:engine/model/range~Range range instance} that is equal to this live range.
 	 *
 	 *

+ 18 - 0
packages/ckeditor5-engine/src/model/markercollection.js

@@ -448,6 +448,24 @@ class Marker {
 		return this._liveRange.toRange();
 		return this._liveRange.toRange();
 	}
 	}
 
 
+	/**
+	 * Checks whether this object is of the given.
+	 *
+	 *		marker.is( 'marker' ); // -> true
+	 *		marker.is( 'model:marker' ); // -> true
+	 *
+	 *		marker.is( 'view:element' ); // -> false
+	 *		marker.is( 'documentSelection' ); // -> false
+	 *
+	 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'marker' || type == 'model:marker';
+	}
+
 	/**
 	/**
 	 * Binds new live range to the marker and detach the old one if is attached.
 	 * Binds new live range to the marker and detach the old one if is attached.
 	 *
 	 *

+ 47 - 23
packages/ckeditor5-engine/src/model/node.js

@@ -396,6 +396,53 @@ export default class Node {
 		return json;
 		return json;
 	}
 	}
 
 
+	/**
+	 * Checks whether this object is of the given type.
+	 *
+	 * This method is useful when processing model objects that are of unknown type. For example, a function
+	 * may return a {@link module:engine/model/documentfragment~DocumentFragment} or a {@link module:engine/model/node~Node}
+	 * that can be either a text node or an element. This method can be used to check what kind of object is returned.
+	 *
+	 *		someObject.is( 'element' ); // -> true if this is an element
+	 *		someObject.is( 'node' ); // -> true if this is a node (a text node or an element)
+	 *		someObject.is( 'documentFragment' ); // -> true if this is a document fragment
+	 *
+	 * Since this method is also available on a range of view objects, you can prefix the type of the object with
+	 * `model:` or `view:` to check, for example, if this is the model's or view's element:
+	 *
+	 *		modelElement.is( 'model:element' ); // -> true
+	 *		modelElement.is( 'view:element' ); // -> false
+	 *
+	 * By using this method it is also possible to check a name of an element:
+	 *
+	 *		imageElement.is( 'image' ); // -> true
+	 *		imageElement.is( 'element', 'image' ); // -> same as above
+	 *		imageElement.is( 'model:element', 'image' ); // -> same as above, but more precise
+	 *
+	 * The list of model objects which implement the `is()` method:
+	 *
+	 * * {@link module:engine/model/node~Node#is `Node#is()`}
+	 * * {@link module:engine/model/text~Text#is `Text#is()`}
+	 * * {@link module:engine/model/element~Element#is `Element#is()`}
+	 * * {@link module:engine/model/rootelement~RootElement#is `RootElement#is()`}
+	 * * {@link module:engine/model/position~Position#is `Position#is()`}
+	 * * {@link module:engine/model/liveposition~LivePosition#is `LivePosition#is()`}
+	 * * {@link module:engine/model/range~Range#is `Range#is()`}
+	 * * {@link module:engine/model/liverange~LiveRange#is `LiveRange#is()`}
+	 * * {@link module:engine/model/documentfragment~DocumentFragment#is `DocumentFragment#is()`}
+	 * * {@link module:engine/model/selection~Selection#is `Selection#is()`}
+	 * * {@link module:engine/model/documentselection~DocumentSelection#is `DocumentSelection#is()`}
+	 * * {@link module:engine/model/markercollection~Marker#is `Marker#is()`}
+	 * * {@link module:engine/model/textproxy~TextProxy#is `TextProxy#is()`}
+	 *
+	 * @method #is
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'node' || type == 'model:node';
+	}
+
 	/**
 	/**
 	 * Creates a copy of this node, that is a node with exactly same attributes, and returns it.
 	 * Creates a copy of this node, that is a node with exactly same attributes, and returns it.
 	 *
 	 *
@@ -460,29 +507,6 @@ export default class Node {
 	_clearAttributes() {
 	_clearAttributes() {
 		this._attrs.clear();
 		this._attrs.clear();
 	}
 	}
-
-	/**
-	 * Checks whether given model tree object is of given type.
-	 *
-	 * This method is useful when processing model tree objects that are of unknown type. For example, a function
-	 * may return {@link module:engine/model/documentfragment~DocumentFragment} or {@link module:engine/model/node~Node}
-	 * that can be either text node or element. This method can be used to check what kind of object is returned.
-	 *
-	 *		obj.is( 'node' ); // true for any node, false for document fragment and text fragment
-	 *		obj.is( 'documentFragment' ); // true for document fragment, false for any node
-	 *		obj.is( 'element' ); // true for any element, false for text node or document fragment
-	 *		obj.is( 'element', 'paragraph' ); // true only for element which name is 'paragraph'
-	 *		obj.is( 'paragraph' ); // shortcut for obj.is( 'element', 'paragraph' )
-	 *		obj.is( 'text' ); // true for text node, false for element and document fragment
-	 *		obj.is( 'textProxy' ); // true for text proxy object
-	 *
-	 * @method #is
-	 * @param {'element'|'rootElement'|'text'|'textProxy'|'documentFragment'} type
-	 * @returns {Boolean}
-	 */
-	is( type ) {
-		return type == 'node';
-	}
 }
 }
 
 
 /**
 /**

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

@@ -141,16 +141,16 @@ export default class MergeOperation extends Operation {
 		const targetElement = this.targetPosition.parent;
 		const targetElement = this.targetPosition.parent;
 
 
 		// Validate whether merge operation has correct parameters.
 		// Validate whether merge operation has correct parameters.
-		if ( !sourceElement || !sourceElement.is( 'element' ) || !sourceElement.parent ) {
+		if ( !sourceElement.parent ) {
 			/**
 			/**
-			 * Merge source position is invalid.
+			 * Merge source position is invalid. The element to be merged must have a parent node.
 			 *
 			 *
 			 * @error merge-operation-source-position-invalid
 			 * @error merge-operation-source-position-invalid
 			 */
 			 */
 			throw new CKEditorError( 'merge-operation-source-position-invalid: Merge source position is invalid.', this );
 			throw new CKEditorError( 'merge-operation-source-position-invalid: Merge source position is invalid.', this );
-		} else if ( !targetElement || !targetElement.is( 'element' ) || !targetElement.parent ) {
+		} else if ( !targetElement.parent ) {
 			/**
 			/**
-			 * Merge target position is invalid.
+			 * Merge target position is invalid. The element to be merged must have a parent node.
 			 *
 			 *
 			 * @error merge-operation-target-position-invalid
 			 * @error merge-operation-target-position-invalid
 			 */
 			 */

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

@@ -123,16 +123,7 @@ export default class MoveOperation extends Operation {
 		// Validate whether move operation has correct parameters.
 		// Validate whether move operation has correct parameters.
 		// Validation is pretty complex but move operation is one of the core ways to manipulate the document state.
 		// Validation is pretty complex but move operation is one of the core ways to manipulate the document state.
 		// We expect that many errors might be connected with one of scenarios described below.
 		// We expect that many errors might be connected with one of scenarios described below.
-		if ( !sourceElement || !targetElement ) {
-			/**
-			 * Source position or target position is invalid.
-			 *
-			 * @error move-operation-position-invalid
-			 */
-			throw new CKEditorError(
-				'move-operation-position-invalid: Source position or target position is invalid.', this
-			);
-		} else if ( sourceOffset + this.howMany > sourceElement.maxOffset ) {
+		if ( sourceOffset + this.howMany > sourceElement.maxOffset ) {
 			/**
 			/**
 			 * The nodes which should be moved do not exist.
 			 * The nodes which should be moved do not exist.
 			 *
 			 *

+ 32 - 28
packages/ckeditor5-engine/src/model/operation/transform.js

@@ -101,14 +101,14 @@ export function transform( a, b, context = {} ) {
 
 
 		return transformationFunction( a, b, context );
 		return transformationFunction( a, b, context );
 	} catch ( e ) {
 	} catch ( e ) {
-		// @if CK_DEBUG // console.error( 'Error during operation transformation!', e.message );
-		// @if CK_DEBUG // console.error( 'Transformed operation', a );
-		// @if CK_DEBUG // console.error( 'Operation transformed by', b );
-		// @if CK_DEBUG // console.error( 'context.aIsStrong', context.aIsStrong );
-		// @if CK_DEBUG // console.error( 'context.aWasUndone', context.aWasUndone );
-		// @if CK_DEBUG // console.error( 'context.bWasUndone', context.bWasUndone );
-		// @if CK_DEBUG // console.error( 'context.abRelation', context.abRelation );
-		// @if CK_DEBUG // console.error( 'context.baRelation', context.baRelation );
+		// @if CK_DEBUG // console.warn( 'Error during operation transformation!', e.message );
+		// @if CK_DEBUG // console.warn( 'Transformed operation', a );
+		// @if CK_DEBUG // console.warn( 'Operation transformed by', b );
+		// @if CK_DEBUG // console.warn( 'context.aIsStrong', context.aIsStrong );
+		// @if CK_DEBUG // console.warn( 'context.aWasUndone', context.aWasUndone );
+		// @if CK_DEBUG // console.warn( 'context.bWasUndone', context.bWasUndone );
+		// @if CK_DEBUG // console.warn( 'context.abRelation', context.abRelation );
+		// @if CK_DEBUG // console.warn( 'context.baRelation', context.baRelation );
 
 
 		throw e;
 		throw e;
 	}
 	}
@@ -148,6 +148,8 @@ export function transform( a, b, context = {} ) {
  * @returns {Object} Transformation result.
  * @returns {Object} Transformation result.
  * @returns {Array.<module:engine/model/operation/operation~Operation>} return.operationsA Transformed `operationsA`.
  * @returns {Array.<module:engine/model/operation/operation~Operation>} return.operationsA Transformed `operationsA`.
  * @returns {Array.<module:engine/model/operation/operation~Operation>} return.operationsB Transformed `operationsB`.
  * @returns {Array.<module:engine/model/operation/operation~Operation>} return.operationsB Transformed `operationsB`.
+ * @returns {Map} return.originalOperations A map that links transformed operations to original operations. The keys are the transformed
+ * operations and the values are the original operations from the input (`operationsA` and `operationsB`).
  */
  */
 export function transformSets( operationsA, operationsB, options ) {
 export function transformSets( operationsA, operationsB, options ) {
 	// Create new arrays so the originally passed arguments are not changed.
 	// Create new arrays so the originally passed arguments are not changed.
@@ -155,9 +157,15 @@ export function transformSets( operationsA, operationsB, options ) {
 	operationsA = operationsA.slice();
 	operationsA = operationsA.slice();
 	operationsB = operationsB.slice();
 	operationsB = operationsB.slice();
 
 
+	const contextFactory = new ContextFactory( options.document, options.useRelations, options.forceWeakRemove );
+	contextFactory.setOriginalOperations( operationsA );
+	contextFactory.setOriginalOperations( operationsB );
+
+	const originalOperations = contextFactory.originalOperations;
+
 	// If one of sets is empty there is simply nothing to transform, so return sets as they are.
 	// If one of sets is empty there is simply nothing to transform, so return sets as they are.
 	if ( operationsA.length == 0 || operationsB.length == 0 ) {
 	if ( operationsA.length == 0 || operationsB.length == 0 ) {
-		return { operationsA, operationsB };
+		return { operationsA, operationsB, originalOperations };
 	}
 	}
 	//
 	//
 	// Following is a description of transformation process:
 	// Following is a description of transformation process:
@@ -305,10 +313,6 @@ export function transformSets( operationsA, operationsB, options ) {
 		originalOperationsBCount: operationsB.length
 		originalOperationsBCount: operationsB.length
 	};
 	};
 
 
-	const contextFactory = new ContextFactory( options.document, options.useRelations, options.forceWeakRemove );
-	contextFactory.setOriginalOperations( operationsA );
-	contextFactory.setOriginalOperations( operationsB );
-
 	// Index of currently transformed operation `a`.
 	// Index of currently transformed operation `a`.
 	let i = 0;
 	let i = 0;
 
 
@@ -374,7 +378,7 @@ export function transformSets( operationsA, operationsB, options ) {
 	updateBaseVersions( operationsA, data.nextBaseVersionB );
 	updateBaseVersions( operationsA, data.nextBaseVersionB );
 	updateBaseVersions( operationsB, data.nextBaseVersionA );
 	updateBaseVersions( operationsB, data.nextBaseVersionA );
 
 
-	return { operationsA, operationsB };
+	return { operationsA, operationsB, originalOperations };
 }
 }
 
 
 // Gathers additional data about operations processed during transformation. Can be used to obtain contextual information
 // Gathers additional data about operations processed during transformation. Can be used to obtain contextual information
@@ -388,6 +392,13 @@ class ContextFactory {
 	// @param {Boolean} [forceWeakRemove=false] If set to `false`, remove operation will be always stronger than move operation,
 	// @param {Boolean} [forceWeakRemove=false] If set to `false`, remove operation will be always stronger than move operation,
 	// so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
 	// so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
 	constructor( document, useRelations, forceWeakRemove = false ) {
 	constructor( document, useRelations, forceWeakRemove = false ) {
+		// For each operation that is created during transformation process, we keep a reference to the original operation
+		// which it comes from. The original operation works as a kind of "identifier". Every contextual information
+		// gathered during transformation that we want to save for given operation, is actually saved for the original operation.
+		// This way no matter if operation `a` is cloned, then transformed, even breaks, we still have access to the previously
+		// gathered data through original operation reference.
+		this.originalOperations = new Map();
+
 		// `model.History` instance which information about undone operations will be taken from.
 		// `model.History` instance which information about undone operations will be taken from.
 		this._history = document.history;
 		this._history = document.history;
 
 
@@ -396,13 +407,6 @@ class ContextFactory {
 
 
 		this._forceWeakRemove = !!forceWeakRemove;
 		this._forceWeakRemove = !!forceWeakRemove;
 
 
-		// For each operation that is created during transformation process, we keep a reference to the original operation
-		// which it comes from. The original operation works as a kind of "identifier". Every contextual information
-		// gathered during transformation that we want to save for given operation, is actually saved for the original operation.
-		// This way no matter if operation `a` is cloned, then transformed, even breaks, we still have access to the previously
-		// gathered data through original operation reference.
-		this._originalOperations = new Map();
-
 		// Relations is a double-map structure (maps in map) where for two operations we store how those operations were related
 		// Relations is a double-map structure (maps in map) where for two operations we store how those operations were related
 		// to each other. Those relations are evaluated during transformation process. For every transformated pair of operations
 		// to each other. Those relations are evaluated during transformation process. For every transformated pair of operations
 		// we keep relations between them.
 		// we keep relations between them.
@@ -428,10 +432,10 @@ class ContextFactory {
 	// @param {Array.<module:engine/model/operation/operation~Operation>} operations
 	// @param {Array.<module:engine/model/operation/operation~Operation>} operations
 	// @param {module:engine/model/operation/operation~Operation|null} [takeFrom=null]
 	// @param {module:engine/model/operation/operation~Operation|null} [takeFrom=null]
 	setOriginalOperations( operations, takeFrom = null ) {
 	setOriginalOperations( operations, takeFrom = null ) {
-		const originalOperation = takeFrom ? this._originalOperations.get( takeFrom ) : null;
+		const originalOperation = takeFrom ? this.originalOperations.get( takeFrom ) : null;
 
 
 		for ( const operation of operations ) {
 		for ( const operation of operations ) {
-			this._originalOperations.set( operation, originalOperation || operation );
+			this.originalOperations.set( operation, originalOperation || operation );
 		}
 		}
 	}
 	}
 
 
@@ -605,7 +609,7 @@ class ContextFactory {
 		// For `op`, get its original operation. After all, if `op` is a clone (or even transformed clone) of another
 		// For `op`, get its original operation. After all, if `op` is a clone (or even transformed clone) of another
 		// operation, literally `op` couldn't be undone. It was just generated. If anything, it was the operation it origins
 		// operation, literally `op` couldn't be undone. It was just generated. If anything, it was the operation it origins
 		// from which was undone. So get that original operation.
 		// from which was undone. So get that original operation.
-		const originalOp = this._originalOperations.get( op );
+		const originalOp = this.originalOperations.get( op );
 
 
 		// And check with the document if the original operation was undone.
 		// And check with the document if the original operation was undone.
 		return originalOp.wasUndone || this._history.isUndoneOperation( originalOp );
 		return originalOp.wasUndone || this._history.isUndoneOperation( originalOp );
@@ -637,7 +641,7 @@ class ContextFactory {
 	// @returns {String|null}
 	// @returns {String|null}
 	_getRelation( opA, opB ) {
 	_getRelation( opA, opB ) {
 		// Get the original operation. Similarly as in `wasUndone()` it is used as an universal identifier for stored data.
 		// Get the original operation. Similarly as in `wasUndone()` it is used as an universal identifier for stored data.
-		const origB = this._originalOperations.get( opB );
+		const origB = this.originalOperations.get( opB );
 		const undoneB = this._history.getUndoneOperation( origB );
 		const undoneB = this._history.getUndoneOperation( origB );
 
 
 		// If `opB` is not undoing any operation, there is no relation.
 		// If `opB` is not undoing any operation, there is no relation.
@@ -645,7 +649,7 @@ class ContextFactory {
 			return null;
 			return null;
 		}
 		}
 
 
-		const origA = this._originalOperations.get( opA );
+		const origA = this.originalOperations.get( opA );
 		const relationsA = this._relations.get( origA );
 		const relationsA = this._relations.get( origA );
 
 
 		// Get all relations for `opA`, and check if there is a relation with `opB`-undone-counterpart. If so, return it.
 		// Get all relations for `opA`, and check if there is a relation with `opB`-undone-counterpart. If so, return it.
@@ -664,8 +668,8 @@ class ContextFactory {
 	// @param {String} relation
 	// @param {String} relation
 	_setRelation( opA, opB, relation ) {
 	_setRelation( opA, opB, relation ) {
 		// As always, setting is for original operations, not the clones/transformed operations.
 		// As always, setting is for original operations, not the clones/transformed operations.
-		const origA = this._originalOperations.get( opA );
-		const origB = this._originalOperations.get( opB );
+		const origA = this.originalOperations.get( opA );
+		const origB = this.originalOperations.get( opB );
 
 
 		let relationsA = this._relations.get( origA );
 		let relationsA = this._relations.get( origA );
 
 

+ 44 - 3
packages/ckeditor5-engine/src/model/position.js

@@ -71,11 +71,11 @@ export default class Position {
 			/**
 			/**
 			 * Position path must be an array with at least one item.
 			 * Position path must be an array with at least one item.
 			 *
 			 *
-			 * @error model-position-path-incorrect
+			 * @error model-position-path-incorrect-format
 			 * @param path
 			 * @param path
 			 */
 			 */
 			throw new CKEditorError(
 			throw new CKEditorError(
-				'model-position-path-incorrect: Position path must be an array with at least one item.',
+				'model-position-path-incorrect-format: Position path must be an array with at least one item.',
 				root,
 				root,
 				{ path }
 				{ path }
 			);
 			);
@@ -161,13 +161,36 @@ export default class Position {
 	 * Also it is a good idea to cache `parent` property if it is used frequently in an algorithm (i.e. in a long loop).
 	 * Also it is a good idea to cache `parent` property if it is used frequently in an algorithm (i.e. in a long loop).
 	 *
 	 *
 	 * @readonly
 	 * @readonly
-	 * @type {module:engine/model/element~Element}
+	 * @type {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
 	 */
 	 */
 	get parent() {
 	get parent() {
 		let parent = this.root;
 		let parent = this.root;
 
 
 		for ( let i = 0; i < this.path.length - 1; i++ ) {
 		for ( let i = 0; i < this.path.length - 1; i++ ) {
 			parent = parent.getChild( parent.offsetToIndex( this.path[ i ] ) );
 			parent = parent.getChild( parent.offsetToIndex( this.path[ i ] ) );
+
+			if ( !parent ) {
+				throw new CKEditorError( 'model-position-path-incorrect: The position\'s path is incorrect.', this, { position: this } );
+			}
+		}
+
+		if ( parent.is( 'text' ) ) {
+			/**
+			 * The position's path is incorrect. This means that a position does not point to
+			 * a correct place in the tree and hence, some of its methods and getters cannot work correctly.
+			 *
+			 * **Note**: Unlike DOM and view positions, in the model, the
+			 * {@link module:engine/model/position~Position#parent position's parent} is always an element or a document fragment.
+			 * The last offset in the {@link module:engine/model/position~Position#path position's path} is the point in this element where
+			 * this position points.
+			 *
+			 * Read more about model positions and offsets in
+			 * the {@glink framework/guides/architecture/editing-engine#indexes-and-offsets Editing engine architecture guide}.
+			 *
+			 * @error position-incorrect-path
+			 * @param {module:engine/model/position~Position} position The incorrect position.
+			 */
+			throw new CKEditorError( 'model-position-path-incorrect: The position\'s path is incorrect.', this, { position: this } );
 		}
 		}
 
 
 		return parent;
 		return parent;
@@ -502,6 +525,24 @@ export default class Position {
 		}
 		}
 	}
 	}
 
 
+	/**
+	 * Checks whether this object is of the given.
+	 *
+	 *		position.is( 'position' ); // -> true
+	 *		position.is( 'model:position' ); // -> true
+	 *
+	 *		position.is( 'view:position' ); // -> false
+	 *		position.is( 'documentSelection' ); // -> false
+	 *
+	 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'position' || type == 'model:position';
+	}
+
 	/**
 	/**
 	 * Checks if two positions are in the same parent.
 	 * Checks if two positions are in the same parent.
 	 *
 	 *

+ 18 - 0
packages/ckeditor5-engine/src/model/range.js

@@ -143,6 +143,24 @@ export default class Range {
 		return this.containsPosition( pos ) || this.start.isEqual( pos );
 		return this.containsPosition( pos ) || this.start.isEqual( pos );
 	}
 	}
 
 
+	/**
+	 * Checks whether this object is of the given.
+	 *
+	 *		range.is( 'range' ); // -> true
+	 *		range.is( 'model:range' ); // -> true
+	 *
+	 *		range.is( 'view:range' ); // -> false
+	 *		range.is( 'documentSelection' ); // -> false
+	 *
+	 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'range' || type == 'model:range';
+	}
+
 	/**
 	/**
 	 * Two ranges are equal if their {@link #start} and {@link #end} positions are equal.
 	 * Two ranges are equal if their {@link #start} and {@link #end} positions are equal.
 	 *
 	 *

+ 28 - 3
packages/ckeditor5-engine/src/model/rootelement.js

@@ -55,13 +55,38 @@ export default class RootElement extends Element {
 	}
 	}
 
 
 	/**
 	/**
-	 * @inheritDoc
+	 * Checks whether this object is of the given.
+	 *
+	 *		rootElement.is( 'rootElement' ); // -> true
+	 *		rootElement.is( 'element' ); // -> true
+	 *		rootElement.is( 'node' ); // -> true
+	 *		rootElement.is( 'model:rootElement' ); // -> true
+	 *		rootElement.is( 'model:element' ); // -> true
+	 *		rootElement.is( 'model:node' ); // -> true
+	 *
+	 *		rootElement.is( 'view:element' ); // -> false
+	 *		rootElement.is( 'documentFragment' ); // -> false
+	 *
+	 * Assuming that the object being checked is an element, you can also check its
+	 * {@link module:engine/model/element~Element#name name}:
+	 *
+	 *		rootElement.is( '$root' ); // -> true if this is a $root element
+	 *		rootElement.is( 'rootElement', '$root' ); // -> same as above
+	 *		text.is( '$root' ); -> false
+	 *
+	 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
+	 *
+	 * @param {String} type Type to check when `name` parameter is present.
+	 * Otherwise, it acts like the `name` parameter.
+	 * @param {String} [name] Element name.
+	 * @returns {Boolean}
 	 */
 	 */
 	is( type, name ) {
 	is( type, name ) {
+		const cutType = type.replace( 'model:', '' );
 		if ( !name ) {
 		if ( !name ) {
-			return type == 'rootElement' || super.is( type );
+			return cutType == 'rootElement' || super.is( type );
 		} else {
 		} else {
-			return ( type == 'rootElement' && name == this.name ) || super.is( type, name );
+			return ( cutType == 'rootElement' && name == this.name ) || super.is( type, name );
 		}
 		}
 	}
 	}
 
 

+ 61 - 47
packages/ckeditor5-engine/src/model/selection.js

@@ -623,41 +623,59 @@ export default class Selection {
 	}
 	}
 
 
 	/**
 	/**
-	 * Checks whether object is of given type following the convention set by
-	 * {@link module:engine/model/node~Node#is `Node#is()`}.
+	 * Checks whether this object is of the given.
 	 *
 	 *
-	 *		const selection = new Selection( ... );
+	 *		selection.is( 'selection' ); // -> true
+	 *		selection.is( 'model:selection' ); // -> true
 	 *
 	 *
-	 *		selection.is( 'selection' ); // true
-	 *		selection.is( 'node' ); // false
-	 *		selection.is( 'element' ); // false
+	 *		selection.is( 'view:selection' ); // -> false
+	 *		selection.is( 'range' ); // -> false
+	 *
+	 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 	 *
 	 *
 	 * @param {String} type
 	 * @param {String} type
 	 * @returns {Boolean}
 	 * @returns {Boolean}
 	 */
 	 */
 	is( type ) {
 	is( type ) {
-		return type == 'selection';
+		return type == 'selection' || type == 'model:selection';
 	}
 	}
 
 
 	/**
 	/**
-	 * Gets elements of type "block" touched by the selection.
+	 * Gets elements of type {@link module:engine/model/schema~Schema#isBlock "block"} touched by the selection.
 	 *
 	 *
 	 * This method's result can be used for example to apply block styling to all blocks covered by this selection.
 	 * This method's result can be used for example to apply block styling to all blocks covered by this selection.
 	 *
 	 *
-	 * **Note:** `getSelectedBlocks()` always returns the deepest block.
+	 * **Note:** `getSelectedBlocks()` returns blocks that are nested in other non-block elements
+	 * but will not return blocks nested in other blocks.
 	 *
 	 *
-	 * In this case the function will return exactly all 3 paragraphs:
+	 * In this case the function will return exactly all 3 paragraphs (note: `<blockQuote>` is not a block itself):
 	 *
 	 *
 	 *		<paragraph>[a</paragraph>
 	 *		<paragraph>[a</paragraph>
-	 *		<quote>
+	 *		<blockQuote>
 	 *			<paragraph>b</paragraph>
 	 *			<paragraph>b</paragraph>
-	 *		</quote>
+	 *		</blockQuote>
 	 *		<paragraph>c]d</paragraph>
 	 *		<paragraph>c]d</paragraph>
 	 *
 	 *
 	 * In this case the paragraph will also be returned, despite the collapsed selection:
 	 * In this case the paragraph will also be returned, despite the collapsed selection:
 	 *
 	 *
 	 *		<paragraph>[]a</paragraph>
 	 *		<paragraph>[]a</paragraph>
 	 *
 	 *
+	 * In such a scenario, however, only blocks A, B & E will be returned as blocks C & D are nested in block B:
+	 *
+	 *		[<blockA></blockA>
+	 *		<blockB>
+	 *			<blockC></blockC>
+	 *			<blockD></blockD>
+	 *		</blockB>
+	 *		<blockE></blockE>]
+	 *
+	 * If the selection is inside a block all the inner blocks (A & B) are returned:
+	 *
+	 * 		<block>
+	 *			<blockA>[a</blockA>
+	 * 			<blockB>b]</blockB>
+	 * 		</block>
+	 *
 	 * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
 	 * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
 	 * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
 	 * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
 	 *
 	 *
@@ -671,56 +689,30 @@ export default class Selection {
 		const visited = new WeakSet();
 		const visited = new WeakSet();
 
 
 		for ( const range of this.getRanges() ) {
 		for ( const range of this.getRanges() ) {
+			// Get start block of range in case of a collapsed range.
 			const startBlock = getParentBlock( range.start, visited );
 			const startBlock = getParentBlock( range.start, visited );
 
 
-			if ( startBlock ) {
+			if ( startBlock && isTopBlockInRange( startBlock, range ) ) {
 				yield startBlock;
 				yield startBlock;
 			}
 			}
 
 
 			for ( const value of range.getWalker() ) {
 			for ( const value of range.getWalker() ) {
-				if ( value.type == 'elementEnd' && isUnvisitedBlockContainer( value.item, visited ) ) {
-					yield value.item;
+				const block = value.item;
+
+				if ( value.type == 'elementEnd' && isUnvisitedTopBlock( block, visited, range ) ) {
+					yield block;
 				}
 				}
 			}
 			}
 
 
 			const endBlock = getParentBlock( range.end, visited );
 			const endBlock = getParentBlock( range.end, visited );
 
 
 			// #984. Don't return the end block if the range ends right at its beginning.
 			// #984. Don't return the end block if the range ends right at its beginning.
-			if ( endBlock && !range.end.isTouching( Position._createAt( endBlock, 0 ) ) ) {
+			if ( endBlock && !range.end.isTouching( Position._createAt( endBlock, 0 ) ) && isTopBlockInRange( endBlock, range ) ) {
 				yield endBlock;
 				yield endBlock;
 			}
 			}
 		}
 		}
 	}
 	}
 
 
-	/**
-	 * Returns blocks that aren't nested in other selected blocks.
-	 *
-	 * In this case the method will return blocks A, B and E because C & D are children of block B:
-	 *
-	 *		[<blockA></blockA>
-	 *		<blockB>
-	 *			<blockC></blockC>
-	 *			<blockD></blockD>
-	 *		</blockB>
-	 *		<blockE></blockE>]
-	 *
-	 * **Note:** To get all selected blocks use {@link #getSelectedBlocks `getSelectedBlocks()`}.
-	 *
-	 * @returns {Iterable.<module:engine/model/element~Element>}
-	 */
-	* getTopMostBlocks() {
-		const selected = Array.from( this.getSelectedBlocks() );
-
-		for ( const block of selected ) {
-			const parentBlock = findAncestorBlock( block );
-
-			// Filter out blocks that are nested in other selected blocks (like paragraphs in tables).
-			if ( !parentBlock || !selected.includes( parentBlock ) ) {
-				yield block;
-			}
-		}
-	}
-
 	/**
 	/**
 	 * Checks whether the selection contains the entire content of the given element. This means that selection must start
 	 * Checks whether the selection contains the entire content of the given element. This means that selection must start
 	 * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
 	 * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
@@ -830,7 +822,7 @@ mix( Selection, EmitterMixin );
 
 
 // Checks whether the given element extends $block in the schema and has a parent (is not a root).
 // Checks whether the given element extends $block in the schema and has a parent (is not a root).
 // Marks it as already visited.
 // Marks it as already visited.
-function isUnvisitedBlockContainer( element, visited ) {
+function isUnvisitedBlock( element, visited ) {
 	if ( visited.has( element ) ) {
 	if ( visited.has( element ) ) {
 		return false;
 		return false;
 	}
 	}
@@ -840,6 +832,11 @@ function isUnvisitedBlockContainer( element, visited ) {
 	return element.document.model.schema.isBlock( element ) && element.parent;
 	return element.document.model.schema.isBlock( element ) && element.parent;
 }
 }
 
 
+// Checks if the given element is a $block was not previously visited and is a top block in a range.
+function isUnvisitedTopBlock( element, visited, range ) {
+	return isUnvisitedBlock( element, visited ) && isTopBlockInRange( element, range );
+}
+
 // Finds the lowest element in position's ancestors which is a block.
 // Finds the lowest element in position's ancestors which is a block.
 // It will search until first ancestor that is a limit element.
 // It will search until first ancestor that is a limit element.
 // Marks all ancestors as already visited to not include any of them later on.
 // Marks all ancestors as already visited to not include any of them later on.
@@ -858,7 +855,7 @@ function getParentBlock( position, visited ) {
 
 
 		hasParentLimit = schema.isLimit( element );
 		hasParentLimit = schema.isLimit( element );
 
 
-		return !hasParentLimit && isUnvisitedBlockContainer( element, visited );
+		return !hasParentLimit && isUnvisitedBlock( element, visited );
 	} );
 	} );
 
 
 	// Mark all ancestors of this position's parent, because find() might've stopped early and
 	// Mark all ancestors of this position's parent, because find() might've stopped early and
@@ -868,6 +865,23 @@ function getParentBlock( position, visited ) {
 	return block;
 	return block;
 }
 }
 
 
+// Checks if the blocks is not nested in other block inside a range.
+//
+// @param {module:engine/model/elmenent~Element} block Block to check.
+// @param {module:engine/model/range~Range} range Range to check.
+function isTopBlockInRange( block, range ) {
+	const parentBlock = findAncestorBlock( block );
+
+	if ( !parentBlock ) {
+		return true;
+	}
+
+	// Add loose flag to check as parentRange can be equal to range.
+	const isParentInRange = range.containsRange( Range._createOn( parentBlock ), true );
+
+	return !isParentInRange;
+}
+
 // Returns first ancestor block of a node.
 // Returns first ancestor block of a node.
 //
 //
 // @param {module:engine/model/node~Node} node
 // @param {module:engine/model/node~Node} node

+ 17 - 3
packages/ckeditor5-engine/src/model/text.js

@@ -20,7 +20,7 @@ import Node from './node';
  * this behavior, keeping references to `Text` is not recommended. Instead, consider creating
  * this behavior, keeping references to `Text` is not recommended. Instead, consider creating
  * {@link module:engine/model/liveposition~LivePosition live position} placed before the text node.
  * {@link module:engine/model/liveposition~LivePosition live position} placed before the text node.
  *
  *
- * @extends {module:engine/model/node~Node}
+ * @extends module:engine/model/node~Node
  */
  */
 export default class Text extends Node {
 export default class Text extends Node {
 	/**
 	/**
@@ -63,10 +63,24 @@ export default class Text extends Node {
 	}
 	}
 
 
 	/**
 	/**
-	 * @inheritDoc
+	 * Checks whether this object is of the given.
+	 *
+	 *		text.is( 'text' ); // -> true
+	 *		text.is( 'node' ); // -> true
+	 *		text.is( 'model:text' ); // -> true
+	 *		text.is( 'model:node' ); // -> true
+	 *
+	 *		text.is( 'view:text' ); // -> false
+	 *		text.is( 'documentSelection' ); // -> false
+	 *
+	 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
+	 *
+	 * @param {String} type Type to check when `name` parameter is present.
+	 * Otherwise, it acts like the `name` parameter.
+	 * @returns {Boolean}
 	 */
 	 */
 	is( type ) {
 	is( type ) {
-		return type == 'text' || super.is( type );
+		return type == 'text' || type == 'model:text' || super.is( type );
 	}
 	}
 
 
 	/**
 	/**

+ 9 - 3
packages/ckeditor5-engine/src/model/textproxy.js

@@ -173,15 +173,21 @@ export default class TextProxy {
 	}
 	}
 
 
 	/**
 	/**
-	 * Checks whether given model tree object is of given type.
+	 * Checks whether this object is of the given.
 	 *
 	 *
-	 * Read more in {@link module:engine/model/node~Node#is}.
+	 *		textProxy.is( 'textProxy' ); // -> true
+	 *		textProxy.is( 'model:textProxy' ); // -> true
+	 *
+	 *		textProxy.is( 'view:textProxy' ); // -> false
+	 *		textProxy.is( 'range' ); // -> false
+	 *
+	 * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
 	 *
 	 *
 	 * @param {String} type
 	 * @param {String} type
 	 * @returns {Boolean}
 	 * @returns {Boolean}
 	 */
 	 */
 	is( type ) {
 	is( type ) {
-		return type == 'textProxy';
+		return type == 'textProxy' || type == 'model:textProxy';
 	}
 	}
 
 
 	/**
 	/**

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

@@ -1010,7 +1010,7 @@ export default class Writer {
 	 *		updateMarker( markerName, { affectsData: false } );
 	 *		updateMarker( markerName, { affectsData: false } );
 	 *
 	 *
 	 * @see module:engine/model/markercollection~Marker
 	 * @see module:engine/model/markercollection~Marker
-	 * @param {String} markerOrName Name of a marker to update, or a marker instance.
+	 * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of a marker to update, or a marker instance.
 	 * @param {Object} [options] If options object is not defined then marker will be refreshed by triggering
 	 * @param {Object} [options] If options object is not defined then marker will be refreshed by triggering
 	 * downcast conversion for this marker with the same data.
 	 * downcast conversion for this marker with the same data.
 	 * @param {module:engine/model/range~Range} [options.range] Marker range to update.
 	 * @param {module:engine/model/range~Range} [options.range] Marker range to update.

+ 17 - 11
packages/ckeditor5-engine/src/utils/bindtwostepcarettoattribute.js

@@ -11,12 +11,15 @@ import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
 import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
 
 
 /**
 /**
- * This helper enabled the two-step caret (phantom) movement behavior for the given {@link module:engine/model/model~Model}
+ * This helper enables the two-step caret (phantom) movement behavior for the given {@link module:engine/model/model~Model}
  * attribute on arrow right (<kbd>→</kbd>) and left (<kbd>←</kbd>) key press.
  * attribute on arrow right (<kbd>→</kbd>) and left (<kbd>←</kbd>) key press.
  *
  *
  * Thanks to this (phantom) caret movement the user is able to type before/after as well as at the
  * Thanks to this (phantom) caret movement the user is able to type before/after as well as at the
  * beginning/end of an attribute.
  * beginning/end of an attribute.
  *
  *
+ * **Note:** This helper support right–to–left (Arabic, Hebrew, etc.) content by mirroring its behavior
+ * but for the sake of simplicity examples showcase only left–to–right use–cases.
+ *
  * # Forward movement
  * # Forward movement
  *
  *
  * ## "Entering" an attribute:
  * ## "Entering" an attribute:
@@ -78,13 +81,15 @@ import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
  *
  *
  *   		<$text a="true">ba{}r</$text>b{}az
  *   		<$text a="true">ba{}r</$text>b{}az
  *
  *
- * @param {module:engine/view/view~View} view View controller instance.
- * @param {module:engine/model/model~Model} model Data model instance.
- * @param {module:utils/dom/emittermixin~Emitter} emitter The emitter to which this behavior should be added
+ * @param {Object} options Helper options.
+ * @param {module:engine/view/view~View} options.view View controller instance.
+ * @param {module:engine/model/model~Model} options.model Data model instance.
+ * @param {module:utils/dom/emittermixin~Emitter} options.emitter The emitter to which this behavior should be added
  * (e.g. a plugin instance).
  * (e.g. a plugin instance).
- * @param {String} attribute Attribute for which this behavior will be added.
+ * @param {String} options.attribute Attribute for which this behavior will be added.
+ * @param {module:utils/locale~Locale} options.locale The {@link module:core/editor/editor~Editor#locale} instance.
  */
  */
-export default function bindTwoStepCaretToAttribute( view, model, emitter, attribute ) {
+export default function bindTwoStepCaretToAttribute( { view, model, emitter, attribute, locale } ) {
 	const twoStepCaretHandler = new TwoStepCaretHandler( model, emitter, attribute );
 	const twoStepCaretHandler = new TwoStepCaretHandler( model, emitter, attribute );
 	const modelSelection = model.document.selection;
 	const modelSelection = model.document.selection;
 
 
@@ -120,15 +125,16 @@ export default function bindTwoStepCaretToAttribute( view, model, emitter, attri
 		}
 		}
 
 
 		const position = modelSelection.getFirstPosition();
 		const position = modelSelection.getFirstPosition();
+		const contentDirection = locale.contentLanguageDirection;
 		let isMovementHandled;
 		let isMovementHandled;
 
 
-		if ( arrowRightPressed ) {
+		if ( ( contentDirection === 'ltr' && arrowRightPressed ) || ( contentDirection === 'rtl' && arrowLeftPressed ) ) {
 			isMovementHandled = twoStepCaretHandler.handleForwardMovement( position, data );
 			isMovementHandled = twoStepCaretHandler.handleForwardMovement( position, data );
 		} else {
 		} else {
 			isMovementHandled = twoStepCaretHandler.handleBackwardMovement( position, data );
 			isMovementHandled = twoStepCaretHandler.handleBackwardMovement( position, data );
 		}
 		}
 
 
-		// Stop the keydown event if the two-step arent movement handled it. Avoid collisions
+		// Stop the keydown event if the two-step caret movement handled it. Avoid collisions
 		// with other features which may also take over the caret movement (e.g. Widget).
 		// with other features which may also take over the caret movement (e.g. Widget).
 		if ( isMovementHandled ) {
 		if ( isMovementHandled ) {
 			evt.stop();
 			evt.stop();
@@ -137,13 +143,13 @@ export default function bindTwoStepCaretToAttribute( view, model, emitter, attri
 }
 }
 
 
 /**
 /**
- * This is a private helper–class for {@link module:engine/utils/bindtwostepcarettoattribute}.
+ * This is a protected helper–class for {@link module:engine/utils/bindtwostepcarettoattribute}.
  * It handles the state of the 2-step caret movement for a single {@link module:engine/model/model~Model}
  * It handles the state of the 2-step caret movement for a single {@link module:engine/model/model~Model}
  * attribute upon the `keypress` in the {@link module:engine/view/view~View}.
  * attribute upon the `keypress` in the {@link module:engine/view/view~View}.
  *
  *
- * @private
+ * @protected
  */
  */
-class TwoStepCaretHandler {
+export class TwoStepCaretHandler {
 	/*
 	/*
 	 * Creates two step handler instance.
 	 * Creates two step handler instance.
 	 *
 	 *

+ 29 - 3
packages/ckeditor5-engine/src/view/attributeelement.js

@@ -125,13 +125,39 @@ export default class AttributeElement extends Element {
 	}
 	}
 
 
 	/**
 	/**
-	 * @inheritDoc
+	 * Checks whether this object is of the given.
+	 *
+	 *		attributeElement.is( 'attributeElement' ); // -> true
+	 *		attributeElement.is( 'element' ); // -> true
+	 *		attributeElement.is( 'node' ); // -> true
+	 *		attributeElement.is( 'view:attributeElement' ); // -> true
+	 *		attributeElement.is( 'view:element' ); // -> true
+	 *		attributeElement.is( 'view:node' ); // -> true
+	 *
+	 *		attributeElement.is( 'model:element' ); // -> false
+	 *		attributeElement.is( 'documentFragment' ); // -> false
+	 *
+	 * Assuming that the object being checked is an attribute element, you can also check its
+	 * {@link module:engine/view/attributeelement~AttributeElement#name name}:
+	 *
+	 *		attributeElement.is( 'b' ); // -> true if this is a bold element
+	 *		attributeElement.is( 'attributeElement', 'b' ); // -> same as above
+	 *		text.is( 'b' ); -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
+	 *
+	 * @param {String} type Type to check when `name` parameter is present.
+	 * Otherwise, it acts like the `name` parameter.
+	 * @param {String} [name] Element name.
+	 * @returns {Boolean}
 	 */
 	 */
 	is( type, name = null ) {
 	is( type, name = null ) {
+		const cutType = type && type.replace( /^view:/, '' );
+
 		if ( !name ) {
 		if ( !name ) {
-			return type == 'attributeElement' || super.is( type );
+			return cutType == 'attributeElement' || super.is( type );
 		} else {
 		} else {
-			return ( type == 'attributeElement' && name == this.name ) || super.is( type, name );
+			return ( cutType == 'attributeElement' && name == this.name ) || super.is( type, name );
 		}
 		}
 	}
 	}
 
 

+ 28 - 3
packages/ckeditor5-engine/src/view/containerelement.js

@@ -51,13 +51,38 @@ export default class ContainerElement extends Element {
 	}
 	}
 
 
 	/**
 	/**
-	 * @inheritDoc
+	 * Checks whether this object is of the given.
+	 *
+	 *		containerElement.is( 'containerElement' ); // -> true
+	 *		containerElement.is( 'element' ); // -> true
+	 *		containerElement.is( 'node' ); // -> true
+	 *		containerElement.is( 'view:containerElement' ); // -> true
+	 *		containerElement.is( 'view:element' ); // -> true
+	 *		containerElement.is( 'view:node' ); // -> true
+	 *
+	 *		containerElement.is( 'model:element' ); // -> false
+	 *		containerElement.is( 'documentFragment' ); // -> false
+	 *
+	 * Assuming that the object being checked is a container element, you can also check its
+	 * {@link module:engine/view/containerelement~ContainerElement#name name}:
+	 *
+	 *		containerElement.is( 'div' ); // -> true if this is a div container element
+	 *		containerElement.is( 'contaienrElement', 'div' ); // -> same as above
+	 *		text.is( 'div' ); -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
+	 *
+	 * @param {String} type Type to check when `name` parameter is present.
+	 * Otherwise, it acts like the `name` parameter.
+	 * @param {String} [name] Element name.
+	 * @returns {Boolean}
 	 */
 	 */
 	is( type, name = null ) {
 	is( type, name = null ) {
+		const cutType = type && type.replace( /^view:/, '' );
 		if ( !name ) {
 		if ( !name ) {
-			return type == 'containerElement' || super.is( type );
+			return cutType == 'containerElement' || super.is( type );
 		} else {
 		} else {
-			return ( type == 'containerElement' && name == this.name ) || super.is( type, name );
+			return ( cutType == 'containerElement' && name == this.name ) || super.is( type, name );
 		}
 		}
 	}
 	}
 }
 }

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

@@ -94,15 +94,22 @@ export default class DocumentFragment {
 	}
 	}
 
 
 	/**
 	/**
-	 * Checks whether given view tree object is of given type.
+	 * Checks whether this object is of the given type.
 	 *
 	 *
-	 * Read more in {@link module:engine/view/node~Node#is}.
+	 *		docFrag.is( 'documentFragment' ); // -> true
+	 *		docFrag.is( 'view:documentFragment' ); // -> true
+	 *
+	 *		docFrag.is( 'model:documentFragment' ); // -> false
+	 *		docFrag.is( 'element' ); // -> false
+	 *		docFrag.is( 'node' ); // -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 	 *
 	 *
 	 * @param {String} type
 	 * @param {String} type
 	 * @returns {Boolean}
 	 * @returns {Boolean}
 	 */
 	 */
 	is( type ) {
 	is( type ) {
-		return type == 'documentFragment';
+		return type == 'documentFragment' || type == 'view:documentFragment';
 	}
 	}
 
 
 	/**
 	/**

+ 14 - 8
packages/ckeditor5-engine/src/view/documentselection.js

@@ -275,21 +275,27 @@ export default class DocumentSelection {
 	}
 	}
 
 
 	/**
 	/**
-	 * Checks whether object is of given type following the convention set by
-	 * {@link module:engine/view/node~Node#is `Node#is()`}.
+	 * Checks whether this object is of the given type.
 	 *
 	 *
-	 *		const selection = new DocumentSelection( ... );
+	 *		docSelection.is( 'selection' ); // -> true
+	 *		docSelection.is( 'documentSelection' ); // -> true
+	 *		docSelection.is( 'view:selection' ); // -> true
+	 *		docSelection.is( 'view:documentSelection' ); // -> true
 	 *
 	 *
-	 *		selection.is( 'selection' ); // true
-	 *		selection.is( 'documentSelection' ); // true
-	 *		selection.is( 'node' ); // false
-	 *		selection.is( 'element' ); // false
+	 *		docSelection.is( 'model:documentSelection' ); // -> false
+	 *		docSelection.is( 'element' ); // -> false
+	 *		docSelection.is( 'node' ); // -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 	 *
 	 *
 	 * @param {String} type
 	 * @param {String} type
 	 * @returns {Boolean}
 	 * @returns {Boolean}
 	 */
 	 */
 	is( type ) {
 	is( type ) {
-		return type == 'selection' || type == 'documentSelection';
+		return type == 'selection' ||
+			type == 'documentSelection' ||
+			type == 'view:selection' ||
+			type == 'view:documentSelection';
 	}
 	}
 
 
 	/**
 	/**

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

@@ -69,7 +69,7 @@ export default class DowncastWriter {
 	 *		writer.setSelection( range );
 	 *		writer.setSelection( range );
 	 *
 	 *
 	 *		// Sets selection to given ranges.
 	 *		// Sets selection to given ranges.
-	 * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
+	 * 		const ranges = [ writer.createRange( start1, end2 ), writer.createRange( start2, end2 ) ];
 	 *		writer.setSelection( range );
 	 *		writer.setSelection( range );
 	 *
 	 *
 	 *		// Sets selection to the other selection.
 	 *		// Sets selection to the other selection.
@@ -788,26 +788,26 @@ export default class DowncastWriter {
 	}
 	}
 
 
 	/**
 	/**
-     * Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
-     * If a collapsed range is provided, it will be wrapped only if it is equal to view selection.
-     *
-     * If a collapsed range was passed and is same as selection, the selection
-     * will be moved to the inside of the wrapped attribute element.
-     *
-     * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container`
-     * when {@link module:engine/view/range~Range#start}
-     * and {@link module:engine/view/range~Range#end} positions are not placed inside same parent container.
-     *
-     * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
-     * an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
-     *
-     * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-nonselection-collapsed-range` when passed range
-     * is collapsed and different than view selection.
-     *
-     * @param {module:engine/view/range~Range} range Range to wrap.
-     * @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
-     * @returns {module:engine/view/range~Range} range Range after wrapping, spanning over wrapping attribute element.
-    */
+	 * Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
+	 * If a collapsed range is provided, it will be wrapped only if it is equal to view selection.
+	 *
+	 * If a collapsed range was passed and is same as selection, the selection
+	 * will be moved to the inside of the wrapped attribute element.
+	 *
+	 * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container`
+	 * when {@link module:engine/view/range~Range#start}
+	 * and {@link module:engine/view/range~Range#end} positions are not placed inside same parent container.
+	 *
+	 * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
+	 * an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
+	 *
+	 * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-nonselection-collapsed-range` when passed range
+	 * is collapsed and different than view selection.
+	 *
+	 * @param {module:engine/view/range~Range} range Range to wrap.
+	 * @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
+	 * @returns {module:engine/view/range~Range} range Range after wrapping, spanning over wrapping attribute element.
+	*/
 	wrap( range, attribute ) {
 	wrap( range, attribute ) {
 		if ( !( attribute instanceof AttributeElement ) ) {
 		if ( !( attribute instanceof AttributeElement ) ) {
 			throw new CKEditorError( 'view-writer-wrap-invalid-attribute', this.document );
 			throw new CKEditorError( 'view-writer-wrap-invalid-attribute', this.document );

+ 28 - 3
packages/ckeditor5-engine/src/view/editableelement.js

@@ -67,13 +67,38 @@ export default class EditableElement extends ContainerElement {
 	}
 	}
 
 
 	/**
 	/**
-	 * @inheritDoc
+	 * Checks whether this object is of the given.
+	 *
+	 *		editableElement.is( 'editableElement' ); // -> true
+	 *		editableElement.is( 'element' ); // -> true
+	 *		editableElement.is( 'node' ); // -> true
+	 *		editableElement.is( 'view:editableElement' ); // -> true
+	 *		editableElement.is( 'view:element' ); // -> true
+	 *		editableElement.is( 'view:node' ); // -> true
+	 *
+	 *		editableElement.is( 'model:element' ); // -> false
+	 *		editableElement.is( 'documentFragment' ); // -> false
+	 *
+	 * Assuming that the object being checked is an editbale element, you can also check its
+	 * {@link module:engine/view/editableelement~EditableElement#name name}:
+	 *
+	 *		editableElement.is( 'div' ); // -> true if this is a div element
+	 *		editableElement.is( 'editableElement', 'div' ); // -> same as above
+	 *		text.is( 'div' ); -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
+	 *
+	 * @param {String} type Type to check when `name` parameter is present.
+	 * Otherwise, it acts like the `name` parameter.
+	 * @param {String} [name] Element name.
+	 * @returns {Boolean}
 	 */
 	 */
 	is( type, name = null ) {
 	is( type, name = null ) {
+		const cutType = type && type.replace( /^view:/, '' );
 		if ( !name ) {
 		if ( !name ) {
-			return type == 'editableElement' || super.is( type );
+			return cutType == 'editableElement' || super.is( type );
 		} else {
 		} else {
-			return ( type == 'editableElement' && name == this.name ) || super.is( type, name );
+			return ( cutType == 'editableElement' && name == this.name ) || super.is( type, name );
 		}
 		}
 	}
 	}
 
 

+ 21 - 10
packages/ckeditor5-engine/src/view/element.js

@@ -147,25 +147,36 @@ export default class Element extends Node {
 	}
 	}
 
 
 	/**
 	/**
-	 * Checks whether this view object is of the given type.
+	 * Checks whether this object is of the given.
 	 *
 	 *
-	 *		obj.is( 'element' ); // true
-	 *		obj.is( 'li' ); // true
-	 *		obj.is( 'element', 'li' ); // true
-	 *		obj.is( 'text' ); // false
-	 *		obj.is( 'element', 'img' ); // false
+	 *		element.is( 'element' ); // -> true
+	 *		element.is( 'node' ); // -> true
+	 *		element.is( 'view:element' ); // -> true
+	 *		element.is( 'view:node' ); // -> true
 	 *
 	 *
-	 * Read more in {@link module:engine/view/node~Node#is `Node#is()`}.
+	 *		element.is( 'model:element' ); // -> false
+	 *		element.is( 'documentSelection' ); // -> false
 	 *
 	 *
-	 * @param {String} type
+	 * Assuming that the object being checked is an element, you can also check its
+	 * {@link module:engine/view/element~Element#name name}:
+	 *
+	 *		element.is( 'img' ); // -> true if this is an <img> element
+	 *		element.is( 'element', 'img' ); // -> same as above
+	 *		text.is( 'img' ); -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
+	 *
+	 * @param {String} type Type to check when `name` parameter is present.
+	 * Otherwise, it acts like the `name` parameter.
 	 * @param {String} [name] Element name.
 	 * @param {String} [name] Element name.
 	 * @returns {Boolean}
 	 * @returns {Boolean}
 	 */
 	 */
 	is( type, name = null ) {
 	is( type, name = null ) {
+		const cutType = type.replace( /^view:/, '' );
 		if ( !name ) {
 		if ( !name ) {
-			return type == 'element' || type == this.name || super.is( type );
+			return cutType == 'element' || cutType == this.name || super.is( type );
 		} else {
 		} else {
-			return type == 'element' && name == this.name;
+			return cutType == 'element' && name == this.name;
 		}
 		}
 	}
 	}
 
 

+ 28 - 3
packages/ckeditor5-engine/src/view/emptyelement.js

@@ -44,13 +44,38 @@ export default class EmptyElement extends Element {
 	}
 	}
 
 
 	/**
 	/**
-	 * @inheritDoc
+	 * Checks whether this object is of the given.
+	 *
+	 *		emptyElement.is( 'emptyElement' ); // -> true
+	 *		emptyElement.is( 'element' ); // -> true
+	 *		emptyElement.is( 'node' ); // -> true
+	 *		emptyElement.is( 'view:emptyElement' ); // -> true
+	 *		emptyElement.is( 'view:element' ); // -> true
+	 *		emptyElement.is( 'view:node' ); // -> true
+	 *
+	 *		emptyElement.is( 'model:element' ); // -> false
+	 *		emptyElement.is( 'documentFragment' ); // -> false
+	 *
+	 * Assuming that the object being checked is an empty element, you can also check its
+	 * {@link module:engine/view/emptyelement~EmptyElement#name name}:
+	 *
+	 *		emptyElement.is( 'img' ); // -> true if this is a img element
+	 *		emptyElement.is( 'emptyElement', 'img' ); // -> same as above
+	 *		text.is( 'img' ); -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
+	 *
+	 * @param {String} type Type to check when `name` parameter is present.
+	 * Otherwise, it acts like the `name` parameter.
+	 * @param {String} [name] Element name.
+	 * @returns {Boolean}
 	 */
 	 */
 	is( type, name = null ) {
 	is( type, name = null ) {
+		const cutType = type.replace( /^view:/, '' );
 		if ( !name ) {
 		if ( !name ) {
-			return type == 'emptyElement' || super.is( type );
+			return cutType == 'emptyElement' || super.is( type );
 		} else {
 		} else {
-			return ( type == 'emptyElement' && name == this.name ) || super.is( type, name );
+			return ( cutType == 'emptyElement' && name == this.name ) || super.is( type, name );
 		}
 		}
 	}
 	}
 
 

+ 40 - 13
packages/ckeditor5-engine/src/view/node.js

@@ -290,25 +290,52 @@ export default class Node {
 	}
 	}
 
 
 	/**
 	/**
-	 * Checks whether this view object is of the given type.
+	 * Checks whether this object is of the given type.
 	 *
 	 *
-	 * This method is useful when processing view tree objects that are of unknown type. For example, a function
-	 * may return {@link module:engine/view/documentfragment~DocumentFragment} or {@link module:engine/view/node~Node}
-	 * that can be either text node or element. This method can be used to check what kind of object is returned.
+	 * This method is useful when processing view objects that are of unknown type. For example, a function
+	 * may return a {@link module:engine/view/documentfragment~DocumentFragment} or a {@link module:engine/view/node~Node}
+	 * that can be either a text node or an element. This method can be used to check what kind of object is returned.
 	 *
 	 *
-	 *		obj.is( 'node' ); // true for any node, false for document fragment and text fragment
-	 *		obj.is( 'documentFragment' ); // true for document fragment, false for any node
-	 *		obj.is( 'element' ); // true for any element, false for text node or document fragment
-	 *		obj.is( 'element', 'p' ); // true only for element which name is 'p'
-	 *		obj.is( 'p' ); // shortcut for obj.is( 'element', 'p' )
-	 *		obj.is( 'text' ); // true for text node, false for element and document fragment
+	 *		someObject.is( 'element' ); // -> true if this is an element
+	 *		someObject.is( 'node' ); // -> true if this is a node (a text node or an element)
+	 *		someObject.is( 'documentFragment' ); // -> true if this is a document fragment
 	 *
 	 *
-	 * @param {'element'|'containerElement'|'attributeElement'|'emptyElement'|'uiElement'|
-	 * 'rootElement'|'documentFragment'|'text'|'textProxy'} type
+	 * Since this method is also available on a range of model objects, you can prefix the type of the object with
+	 * `model:` or `view:` to check, for example, if this is the model's or view's element:
+	 *
+	 *		viewElement.is( 'view:element' ); // -> true
+	 *		viewElement.is( 'model:element' ); // -> false
+	 *
+	 * By using this method it is also possible to check a name of an element:
+	 *
+	 *		imgElement.is( 'img' ); // -> true
+	 *		imgElement.is( 'element', 'img' ); // -> same as above
+	 *		imgElement.is( 'view:element', 'img' ); // -> same as above, but more precise
+	 *
+	 * The list of view objects which implement the `is()` method:
+	 *
+	 * * {@link module:engine/view/attributeelement~AttributeElement#is `AttributeElement#is()`}
+	 * * {@link module:engine/view/containerelement~ContainerElement#is `ContainerElement#is()`}
+	 * * {@link module:engine/view/documentfragment~DocumentFragment#is `DocumentFragment#is()`}
+	 * * {@link module:engine/view/documentselection~DocumentSelection#is `DocumentSelection#is()`}
+	 * * {@link module:engine/view/editableelement~EditableElement#is `EditableElement#is()`}
+	 * * {@link module:engine/view/element~Element#is `Element#is()`}
+	 * * {@link module:engine/view/emptyelement~EmptyElement#is `EmptyElement#is()`}
+	 * * {@link module:engine/view/node~Node#is `Node#is()`}
+	 * * {@link module:engine/view/position~Position#is `Position#is()`}
+	 * * {@link module:engine/view/range~Range#is `Range#is()`}
+	 * * {@link module:engine/view/rooteditableelement~RootEditableElement#is `RootEditableElement#is()`}
+	 * * {@link module:engine/view/selection~Selection#is `Selection#is()`}
+	 * * {@link module:engine/view/text~Text#is `Text#is()`}
+	 * * {@link module:engine/view/textproxy~TextProxy#is `TextProxy#is()`}
+	 * * {@link module:engine/view/uielement~UIElement#is `UIElement#is()`}
+	 *
+	 * @method #is
+	 * @param {String} type
 	 * @returns {Boolean}
 	 * @returns {Boolean}
 	 */
 	 */
 	is( type ) {
 	is( type ) {
-		return type == 'node';
+		return type == 'node' || type == 'view:node';
 	}
 	}
 
 
 	/**
 	/**

+ 1 - 1
packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js

@@ -19,7 +19,7 @@ import { debounce } from 'lodash-es';
  * Fires {@link module:engine/view/document~Document#event:selectionChange selectionChange event} simulating natural behaviour of
  * Fires {@link module:engine/view/document~Document#event:selectionChange selectionChange event} simulating natural behaviour of
  * {@link module:engine/view/observer/selectionobserver~SelectionObserver SelectionObserver}.
  * {@link module:engine/view/observer/selectionobserver~SelectionObserver SelectionObserver}.
  *
  *
- * @extends module:engine/view/observer/observer~Observer.Observer
+ * @extends module:engine/view/observer/observer~Observer
  */
  */
 export default class FakeSelectionObserver extends Observer {
 export default class FakeSelectionObserver extends Observer {
 	/**
 	/**

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

@@ -206,6 +206,25 @@ export default class Position {
 		return i === 0 ? null : ancestorsA[ i - 1 ];
 		return i === 0 ? null : ancestorsA[ i - 1 ];
 	}
 	}
 
 
+	/**
+	 * Checks whether this object is of the given type.
+	 *
+	 *		position.is( 'position' ); // -> true
+	 *		position.is( 'view:position' ); // -> true
+	 *
+	 *		position.is( 'model:position' ); // -> false
+	 *		position.is( 'element' ); // -> false
+	 *		position.is( 'range' ); // -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'position' || type == 'view:position';
+	}
+
 	/**
 	/**
 	 * Checks whether this position equals given position.
 	 * Checks whether this position equals given position.
 	 *
 	 *

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

@@ -394,6 +394,25 @@ export default class Range {
 		}
 		}
 	}
 	}
 
 
+	/**
+	 * Checks whether this object is of the given type.
+	 *
+	 *		range.is( 'range' ); // -> true
+	 *		range.is( 'view:range' ); // -> true
+	 *
+	 *		range.is( 'model:range' ); // -> false
+	 *		range.is( 'element' ); // -> false
+	 *		range.is( 'selection' ); // -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'range' || type == 'view:range';
+	}
+
 	/**
 	/**
 	 * Checks and returns whether this range intersects with the given range.
 	 * Checks and returns whether this range intersects with the given range.
 	 *
 	 *

+ 30 - 3
packages/ckeditor5-engine/src/view/rooteditableelement.js

@@ -38,13 +38,40 @@ export default class RootEditableElement extends EditableElement {
 	}
 	}
 
 
 	/**
 	/**
-	 * @inheritDoc
+	 * Checks whether this object is of the given.
+	 *
+	 *		rootEditableElement.is( 'rootEditableElement' ); // -> true
+	 *		rootEditableElement.is( 'editableElement' ); // -> true
+	 *		rootEditableElement.is( 'element' ); // -> true
+	 *		rootEditableElement.is( 'node' ); // -> true
+	 *		rootEditableElement.is( 'view:rootEditableElement' ); // -> true
+	 *		rootEditableElement.is( 'view:editableElement' ); // -> true
+	 *		rootEditableElement.is( 'view:element' ); // -> true
+	 *		rootEditableElement.is( 'view:node' ); // -> true
+	 *
+	 *		rootEditableElement.is( 'model:element' ); // -> false
+	 *		rootEditableElement.is( 'documentFragment' ); // -> false
+	 *
+	 * Assuming that the object being checked is a root editbale element, you can also check its
+	 * {@link module:engine/view/rooteditableelement~RootEditableElement#name name}:
+	 *
+	 *		rootEditableElement.is( 'div' ); // -> true if this is a div root editable element
+	 *		rootEditableElement.is( 'rootEditableElement', 'div' ); // -> same as above
+	 *		text.is( 'div' ); -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
+	 *
+	 * @param {String} type Type to check when `name` parameter is present.
+	 * Otherwise, it acts like the `name` parameter.
+	 * @param {String} [name] Element name.
+	 * @returns {Boolean}
 	 */
 	 */
 	is( type, name = null ) {
 	is( type, name = null ) {
+		const cutType = type.replace( /^view:/, '' );
 		if ( !name ) {
 		if ( !name ) {
-			return type == 'rootElement' || super.is( type );
+			return cutType == 'rootElement' || super.is( type );
 		} else {
 		} else {
-			return ( type == 'rootElement' && name == this.name ) || super.is( type, name );
+			return ( cutType == 'rootElement' && name == this.name ) || super.is( type, name );
 		}
 		}
 	}
 	}
 
 

+ 9 - 7
packages/ckeditor5-engine/src/view/selection.js

@@ -598,20 +598,22 @@ export default class Selection {
 	}
 	}
 
 
 	/**
 	/**
-	 * Checks whether object is of given type following the convention set by
-	 * {@link module:engine/view/node~Node#is `Node#is()`}.
+	 * Checks whether this object is of the given type.
 	 *
 	 *
-	 *		const selection = new Selection( ... );
+	 *		selection.is( 'selection' ); // -> true
+	 *		selection.is( 'view:selection' ); // -> true
 	 *
 	 *
-	 *		selection.is( 'selection' ); // true
-	 *		selection.is( 'node' ); // false
-	 *		selection.is( 'element' ); // false
+	 *		selection.is( 'model:selection' ); // -> false
+	 *		selection.is( 'element' ); // -> false
+	 *		selection.is( 'range' ); // -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 	 *
 	 *
 	 * @param {String} type
 	 * @param {String} type
 	 * @returns {Boolean}
 	 * @returns {Boolean}
 	 */
 	 */
 	is( type ) {
 	is( type ) {
-		return type == 'selection';
+		return type == 'selection' || type == 'view:selection';
 	}
 	}
 
 
 	/**
 	/**

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

@@ -42,10 +42,24 @@ export default class Text extends Node {
 	}
 	}
 
 
 	/**
 	/**
-	 * @inheritDoc
+	 * Checks whether this object is of the given type.
+	 *
+	 *		text.is( 'text' ); // -> true
+	 *		text.is( 'node' ); // -> true
+	 *		text.is( 'view:text' ); // -> true
+	 *		text.is( 'view:node' ); // -> true
+	 *
+	 *		text.is( 'model:text' ); // -> false
+	 *		text.is( 'element' ); // -> false
+	 *		text.is( 'range' ); // -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
 	 */
 	 */
 	is( type ) {
 	is( type ) {
-		return type == 'text' || super.is( type );
+		return type == 'text' || type == 'view:text' || super.is( type );
 	}
 	}
 
 
 	/**
 	/**

+ 10 - 3
packages/ckeditor5-engine/src/view/textproxy.js

@@ -141,15 +141,22 @@ export default class TextProxy {
 	}
 	}
 
 
 	/**
 	/**
-	 * Checks whether given view tree object is of given type.
+	 * Checks whether this object is of the given type.
 	 *
 	 *
-	 * Read more in {@link module:engine/view/node~Node#is}.
+	 *		textProxy.is( 'textProxy' ); // -> true
+	 *		textProxy.is( 'view:textProxy' ); // -> true
+	 *
+	 *		textProxy.is( 'model:textProxy' ); // -> false
+	 *		textProxy.is( 'element' ); // -> false
+	 *		textProxy.is( 'range' ); // -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
 	 *
 	 *
 	 * @param {String} type
 	 * @param {String} type
 	 * @returns {Boolean}
 	 * @returns {Boolean}
 	 */
 	 */
 	is( type ) {
 	is( type ) {
-		return type == 'textProxy';
+		return type == 'textProxy' || type == 'view:textProxy';
 	}
 	}
 
 
 	/**
 	/**

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

@@ -57,13 +57,38 @@ export default class UIElement extends Element {
 	}
 	}
 
 
 	/**
 	/**
-	 * @inheritDoc
+	 * Checks whether this object is of the given.
+	 *
+	 *		uiElement.is( 'uiElement' ); // -> true
+	 *		uiElement.is( 'element' ); // -> true
+	 *		uiElement.is( 'node' ); // -> true
+	 *		uiElement.is( 'view:uiElement' ); // -> true
+	 *		uiElement.is( 'view:element' ); // -> true
+	 *		uiElement.is( 'view:node' ); // -> true
+	 *
+	 *		uiElement.is( 'model:element' ); // -> false
+	 *		uiElement.is( 'documentFragment' ); // -> false
+	 *
+	 * Assuming that the object being checked is an ui element, you can also check its
+	 * {@link module:engine/view/uielement~UIElement#name name}:
+	 *
+	 *		uiElement.is( 'span' ); // -> true if this is a span ui element
+	 *		uiElement.is( 'uiElement', 'span' ); // -> same as above
+	 *		text.is( 'span' ); -> false
+	 *
+	 * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
+	 *
+	 * @param {String} type Type to check when `name` parameter is present.
+	 * Otherwise, it acts like the `name` parameter.
+	 * @param {String} [name] Element name.
+	 * @returns {Boolean}
 	 */
 	 */
 	is( type, name = null ) {
 	is( type, name = null ) {
+		const cutType = type.replace( /^view:/, '' );
 		if ( !name ) {
 		if ( !name ) {
-			return type == 'uiElement' || super.is( type );
+			return cutType == 'uiElement' || super.is( type );
 		} else {
 		} else {
-			return ( type == 'uiElement' && name == this.name ) || super.is( type, name );
+			return ( cutType == 'uiElement' && name == this.name ) || super.is( type, name );
 		}
 		}
 	}
 	}
 
 

+ 18 - 1
packages/ckeditor5-engine/src/view/upcastwriter.js

@@ -27,7 +27,7 @@ import Selection from './selection';
  * section of the {@glink framework/guides/architecture/editing-engine Editing engine architecture} guide.
  * 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,
  * Unlike `DowncastWriter`, which is available in the {@link module:engine/view/view~View#change `View#change()`} block,
- * `UpcastWriter` can wherever you need it:
+ * `UpcastWriter` can be created wherever you need it:
  *
  *
  *		const writer = new UpcastWriter();
  *		const writer = new UpcastWriter();
  *		const text = writer.createText( 'foo!' );
  *		const text = writer.createText( 'foo!' );
@@ -172,6 +172,23 @@ export default class UpcastWriter {
 		return false;
 		return false;
 	}
 	}
 
 
+	/**
+	 * Removes given element from view structure and places its children in its position.
+	 * It does nothing if element has no parent.
+	 *
+	 * @param {module:engine/view/element~Element} element Element to unwrap.
+	 */
+	unwrapElement( element ) {
+		const parent = element.parent;
+
+		if ( parent ) {
+			const index = parent.getChildIndex( element );
+
+			this.remove( element );
+			this.insertChild( index, element.getChildren(), parent );
+		}
+	}
+
 	/**
 	/**
 	 * Renames element by creating a copy of a given element but with its name changed and then moving contents of the
 	 * Renames element by creating a copy of a given element but with its name changed and then moving contents of the
 	 * old element to the new one.
 	 * old element to the new one.

+ 25 - 9
packages/ckeditor5-engine/tests/conversion/conversion.js

@@ -301,6 +301,14 @@ describe( 'Conversion', () => {
 			} );
 			} );
 
 
 			it( 'config.view is an object with upcastAlso defined', () => {
 			it( 'config.view is an object with upcastAlso defined', () => {
+				schema.extend( '$text', {
+					allowAttributes: [ 'bold', 'xBold' ]
+				} );
+				conversion.attributeToElement( {
+					model: 'xBold',
+					view: 'x-bold'
+				} );
+
 				conversion.attributeToElement( {
 				conversion.attributeToElement( {
 					model: 'bold',
 					model: 'bold',
 					view: 'strong',
 					view: 'strong',
@@ -310,22 +318,18 @@ describe( 'Conversion', () => {
 							name: 'span',
 							name: 'span',
 							classes: 'bold'
 							classes: 'bold'
 						},
 						},
-						{
-							name: 'span',
-							styles: {
-								'font-weight': 'bold'
-							}
-						},
 						viewElement => {
 						viewElement => {
 							const fontWeight = viewElement.getStyle( 'font-weight' );
 							const fontWeight = viewElement.getStyle( 'font-weight' );
 
 
-							if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test( fontWeight ) && Number( fontWeight ) > 500 ) {
+							if ( fontWeight == 'bold' || Number( fontWeight ) > 500 ) {
 								return {
 								return {
-									name: true,
 									styles: [ 'font-weight' ]
 									styles: [ 'font-weight' ]
 								};
 								};
 							}
 							}
-						}
+						},
+						// Duplicates the `x-bold` from above to test if only one attribute would be converted.
+						// It should not convert to both bold & x-bold.
+						viewElement => viewElement.is( 'x-bold' ) ? { name: 'x-bold' } : null
 					]
 					]
 				} );
 				} );
 
 
@@ -363,6 +367,18 @@ describe( 'Conversion', () => {
 					'<paragraph><$text bold="true">Foo</$text></paragraph>',
 					'<paragraph><$text bold="true">Foo</$text></paragraph>',
 					'<p><strong>Foo</strong></p>'
 					'<p><strong>Foo</strong></p>'
 				);
 				);
+
+				test(
+					'<p style="font-weight: 600;">Foo</p>',
+					'<paragraph><$text bold="true">Foo</$text></paragraph>',
+					'<p><strong>Foo</strong></p>'
+				);
+
+				test(
+					'<p><x-bold style="font-wieght:bold">Foo</x-bold></p>',
+					'<paragraph><$text xBold="true">Foo</$text></paragraph>',
+					'<p><x-bold>Foo</x-bold></p>'
+				);
 			} );
 			} );
 
 
 			it( 'model attribute value is enumerable', () => {
 			it( 'model attribute value is enumerable', () => {

+ 34 - 8
packages/ckeditor5-engine/tests/conversion/downcastdispatcher.js

@@ -4,6 +4,8 @@
  */
  */
 
 
 import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
 import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
+import Mapper from '../../src/conversion/mapper';
+
 import Model from '../../src/model/model';
 import Model from '../../src/model/model';
 import ModelText from '../../src/model/text';
 import ModelText from '../../src/model/text';
 import ModelElement from '../../src/model/element';
 import ModelElement from '../../src/model/element';
@@ -13,13 +15,14 @@ import View from '../../src/view/view';
 import ViewContainerElement from '../../src/view/containerelement';
 import ViewContainerElement from '../../src/view/containerelement';
 
 
 describe( 'DowncastDispatcher', () => {
 describe( 'DowncastDispatcher', () => {
-	let dispatcher, doc, root, differStub, model, view;
+	let dispatcher, doc, root, differStub, model, view, mapper;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		model = new Model();
 		model = new Model();
 		view = new View();
 		view = new View();
 		doc = model.document;
 		doc = model.document;
-		dispatcher = new DowncastDispatcher();
+		mapper = new Mapper();
+		dispatcher = new DowncastDispatcher( { mapper } );
 		root = doc.createRoot();
 		root = doc.createRoot();
 
 
 		differStub = {
 		differStub = {
@@ -48,7 +51,7 @@ describe( 'DowncastDispatcher', () => {
 			differStub.getChanges = () => [ { type: 'insert', position, length: 1 } ];
 			differStub.getChanges = () => [ { type: 'insert', position, length: 1 } ];
 
 
 			view.change( writer => {
 			view.change( writer => {
-				dispatcher.convertChanges( differStub, writer );
+				dispatcher.convertChanges( differStub, model.markers, writer );
 			} );
 			} );
 
 
 			expect( dispatcher.convertInsert.calledOnce ).to.be.true;
 			expect( dispatcher.convertInsert.calledOnce ).to.be.true;
@@ -63,7 +66,7 @@ describe( 'DowncastDispatcher', () => {
 			differStub.getChanges = () => [ { type: 'remove', position, length: 2, name: '$text' } ];
 			differStub.getChanges = () => [ { type: 'remove', position, length: 2, name: '$text' } ];
 
 
 			view.change( writer => {
 			view.change( writer => {
-				dispatcher.convertChanges( differStub, writer );
+				dispatcher.convertChanges( differStub, model.markers, writer );
 			} );
 			} );
 
 
 			expect( dispatcher.convertRemove.calledWith( position, 2, '$text' ) ).to.be.true;
 			expect( dispatcher.convertRemove.calledWith( position, 2, '$text' ) ).to.be.true;
@@ -80,7 +83,7 @@ describe( 'DowncastDispatcher', () => {
 			];
 			];
 
 
 			view.change( writer => {
 			view.change( writer => {
-				dispatcher.convertChanges( differStub, writer );
+				dispatcher.convertChanges( differStub, model.markers, writer );
 			} );
 			} );
 
 
 			expect( dispatcher.convertAttribute.calledWith( range, 'key', null, 'foo' ) ).to.be.true;
 			expect( dispatcher.convertAttribute.calledWith( range, 'key', null, 'foo' ) ).to.be.true;
@@ -102,7 +105,7 @@ describe( 'DowncastDispatcher', () => {
 			];
 			];
 
 
 			view.change( writer => {
 			view.change( writer => {
-				dispatcher.convertChanges( differStub, writer );
+				dispatcher.convertChanges( differStub, model.markers, writer );
 			} );
 			} );
 
 
 			expect( dispatcher.convertInsert.calledTwice ).to.be.true;
 			expect( dispatcher.convertInsert.calledTwice ).to.be.true;
@@ -122,7 +125,7 @@ describe( 'DowncastDispatcher', () => {
 			];
 			];
 
 
 			view.change( writer => {
 			view.change( writer => {
-				dispatcher.convertChanges( differStub, writer );
+				dispatcher.convertChanges( differStub, model.markers, writer );
 			} );
 			} );
 
 
 			expect( dispatcher.convertMarkerAdd.calledWith( 'foo', fooRange ) );
 			expect( dispatcher.convertMarkerAdd.calledWith( 'foo', fooRange ) );
@@ -141,11 +144,34 @@ describe( 'DowncastDispatcher', () => {
 			];
 			];
 
 
 			view.change( writer => {
 			view.change( writer => {
-				dispatcher.convertChanges( differStub, writer );
+				dispatcher.convertChanges( differStub, model.markers, writer );
+			} );
+
+			expect( dispatcher.convertMarkerRemove.calledWith( 'foo', fooRange ) );
+			expect( dispatcher.convertMarkerRemove.calledWith( 'bar', barRange ) );
+		} );
+
+		it( 'should re-render markers which view elements got unbound during conversion', () => {
+			sinon.stub( dispatcher, 'convertMarkerRemove' );
+			sinon.stub( dispatcher, 'convertMarkerAdd' );
+
+			const fooRange = model.createRange( model.createPositionAt( root, 0 ), model.createPositionAt( root, 1 ) );
+			const barRange = model.createRange( model.createPositionAt( root, 3 ), model.createPositionAt( root, 6 ) );
+
+			model.markers._set( 'foo', fooRange );
+			model.markers._set( 'bar', barRange );
+
+			// Stub `Mapper#flushUnboundMarkerNames`.
+			dispatcher.conversionApi.mapper.flushUnboundMarkerNames = () => [ 'foo', 'bar' ];
+
+			view.change( writer => {
+				dispatcher.convertChanges( differStub, model.markers, writer );
 			} );
 			} );
 
 
 			expect( dispatcher.convertMarkerRemove.calledWith( 'foo', fooRange ) );
 			expect( dispatcher.convertMarkerRemove.calledWith( 'foo', fooRange ) );
 			expect( dispatcher.convertMarkerRemove.calledWith( 'bar', barRange ) );
 			expect( dispatcher.convertMarkerRemove.calledWith( 'bar', barRange ) );
+			expect( dispatcher.convertMarkerAdd.calledWith( 'foo', fooRange ) );
+			expect( dispatcher.convertMarkerAdd.calledWith( 'bar', barRange ) );
 		} );
 		} );
 	} );
 	} );
 
 

+ 39 - 0
packages/ckeditor5-engine/tests/conversion/mapper.js

@@ -23,15 +23,23 @@ describe( 'Mapper', () => {
 			const viewA = new ViewElement( 'a' );
 			const viewA = new ViewElement( 'a' );
 			const viewB = new ViewElement( 'b' );
 			const viewB = new ViewElement( 'b' );
 			const viewC = new ViewElement( 'c' );
 			const viewC = new ViewElement( 'c' );
+			const viewD = new ViewElement( 'd' );
 
 
 			const modelA = new ModelElement( 'a' );
 			const modelA = new ModelElement( 'a' );
 			const modelB = new ModelElement( 'b' );
 			const modelB = new ModelElement( 'b' );
 			const modelC = new ModelElement( 'c' );
 			const modelC = new ModelElement( 'c' );
+			const modelD = new ModelElement( 'd' );
 
 
 			const mapper = new Mapper();
 			const mapper = new Mapper();
+
 			mapper.bindElements( modelA, viewA );
 			mapper.bindElements( modelA, viewA );
 			mapper.bindElements( modelB, viewB );
 			mapper.bindElements( modelB, viewB );
 			mapper.bindElements( modelC, viewC );
 			mapper.bindElements( modelC, viewC );
+			mapper.bindElements( modelD, viewD );
+
+			mapper.bindElementToMarker( viewA, 'foo' );
+			mapper.bindElementToMarker( viewD, 'foo' );
+			mapper.bindElementToMarker( viewD, 'bar' );
 
 
 			expect( mapper.toModelElement( viewA ) ).to.equal( modelA );
 			expect( mapper.toModelElement( viewA ) ).to.equal( modelA );
 			expect( mapper.toModelElement( viewB ) ).to.equal( modelB );
 			expect( mapper.toModelElement( viewB ) ).to.equal( modelB );
@@ -41,6 +49,9 @@ describe( 'Mapper', () => {
 			expect( mapper.toViewElement( modelB ) ).to.equal( viewB );
 			expect( mapper.toViewElement( modelB ) ).to.equal( viewB );
 			expect( mapper.toViewElement( modelC ) ).to.equal( viewC );
 			expect( mapper.toViewElement( modelC ) ).to.equal( viewC );
 
 
+			expect( Array.from( mapper.markerNameToElements( 'foo' ) ) ).to.deep.equal( [ viewA, viewD ] );
+
+			mapper.unbindViewElement( viewD );
 			mapper.clearBindings();
 			mapper.clearBindings();
 
 
 			expect( mapper.toModelElement( viewA ) ).to.be.undefined;
 			expect( mapper.toModelElement( viewA ) ).to.be.undefined;
@@ -50,6 +61,9 @@ describe( 'Mapper', () => {
 			expect( mapper.toViewElement( modelA ) ).to.be.undefined;
 			expect( mapper.toViewElement( modelA ) ).to.be.undefined;
 			expect( mapper.toViewElement( modelB ) ).to.be.undefined;
 			expect( mapper.toViewElement( modelB ) ).to.be.undefined;
 			expect( mapper.toViewElement( modelC ) ).to.be.undefined;
 			expect( mapper.toViewElement( modelC ) ).to.be.undefined;
+
+			expect( mapper.markerNameToElements( 'foo' ) ).to.be.null;
+			expect( mapper.flushUnboundMarkerNames() ).to.deep.equal( [] );
 		} );
 		} );
 	} );
 	} );
 
 
@@ -648,11 +662,13 @@ describe( 'Mapper', () => {
 			const viewB = new ViewElement( 'b' );
 			const viewB = new ViewElement( 'b' );
 
 
 			mapper.bindElementToMarker( viewA, 'marker' );
 			mapper.bindElementToMarker( viewA, 'marker' );
+			mapper.bindElementToMarker( viewA, 'markerB' );
 			mapper.bindElementToMarker( viewB, 'marker' );
 			mapper.bindElementToMarker( viewB, 'marker' );
 
 
 			mapper.unbindElementFromMarkerName( viewA, 'marker' );
 			mapper.unbindElementFromMarkerName( viewA, 'marker' );
 
 
 			expect( Array.from( mapper.markerNameToElements( 'marker' ) ) ).to.deep.equal( [ viewB ] );
 			expect( Array.from( mapper.markerNameToElements( 'marker' ) ) ).to.deep.equal( [ viewB ] );
+			expect( Array.from( mapper.markerNameToElements( 'markerB' ) ) ).to.deep.equal( [ viewA ] );
 
 
 			mapper.unbindElementFromMarkerName( viewB, 'marker' );
 			mapper.unbindElementFromMarkerName( viewB, 'marker' );
 
 
@@ -751,4 +767,27 @@ describe( 'Mapper', () => {
 			expect( viewMappedAncestor ).to.equal( viewP );
 			expect( viewMappedAncestor ).to.equal( viewP );
 		} );
 		} );
 	} );
 	} );
+
+	describe( 'flushUnboundMarkerNames()', () => {
+		it( 'should return marker names of markers which elements has been unbound and clear that list', () => {
+			const viewA = new ViewElement( 'a' );
+			const viewB = new ViewElement( 'b' );
+
+			const mapper = new Mapper();
+
+			mapper.bindElementToMarker( viewA, 'foo' );
+			mapper.bindElementToMarker( viewA, 'bar' );
+			mapper.bindElementToMarker( viewB, 'bar' );
+
+			mapper.unbindViewElement( viewA );
+
+			expect( mapper.flushUnboundMarkerNames() ).to.deep.equal( [ 'foo', 'bar' ] );
+			expect( mapper.flushUnboundMarkerNames() ).to.deep.equal( [] );
+
+			mapper.unbindViewElement( viewB );
+
+			expect( mapper.flushUnboundMarkerNames() ).to.deep.equal( [ 'bar' ] );
+			expect( mapper.flushUnboundMarkerNames() ).to.deep.equal( [] );
+		} );
+	} );
 } );
 } );

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

@@ -21,7 +21,13 @@ ClassicEditor
 	.then( editor => {
 	.then( editor => {
 		const bold = editor.plugins.get( Bold );
 		const bold = editor.plugins.get( Bold );
 
 
-		bindTwoStepCaretToAttribute( editor.editing.view, editor.model, bold, 'bold' );
+		bindTwoStepCaretToAttribute( {
+			view: editor.editing.view,
+			model: editor.model,
+			emitter: bold,
+			attribute: 'bold',
+			locale: editor.locale
+		} );
 	} )
 	} )
 	.catch( err => {
 	.catch( err => {
 		console.error( err.stack );
 		console.error( err.stack );

+ 21 - 1
packages/ckeditor5-engine/tests/manual/two-step-caret.html

@@ -1,5 +1,25 @@
-<div id="editor">
+<h2>Left-to–right content</h2>
+
+<div id="editor-ltr">
 	<p>Foo <u>bar</u> biz</p>
 	<p>Foo <u>bar</u> biz</p>
 	<p>Foo <u>bar</u><i>biz</i> buz?</p>
 	<p>Foo <u>bar</u><i>biz</i> buz?</p>
 	<p>Foo <b>bar</b> biz</p>
 	<p>Foo <b>bar</b> biz</p>
 </div>
 </div>
+
+<h2>Right–to–left content</h2>
+
+<div id="editor-rtl">
+	<p>&nbsp;היא <u>תכונה</u> של</p>
+	<p>&nbsp;זהה <i>לזה</i><u>שיוצג</u> בתוצאה</p>
+	<p>וכדומה. <strong>תכונה</strong> זו מאפיינת</p>
+</div>
+
+<style>
+	u {
+		background: rgba(255,0,0,.3);
+	}
+
+	i {
+		background: rgba(0,255,0,.3);
+	}
+</style>

+ 46 - 3
packages/ckeditor5-engine/tests/manual/two-step-caret.js

@@ -15,7 +15,7 @@ import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 import bindTwoStepCaretToAttribute from '../../src/utils/bindtwostepcarettoattribute';
 import bindTwoStepCaretToAttribute from '../../src/utils/bindtwostepcarettoattribute';
 
 
 ClassicEditor
 ClassicEditor
-	.create( document.querySelector( '#editor' ), {
+	.create( document.querySelector( '#editor-ltr' ), {
 		plugins: [ Essentials, Paragraph, Underline, Bold, Italic ],
 		plugins: [ Essentials, Paragraph, Underline, Bold, Italic ],
 		toolbar: [ 'undo', 'redo', '|', 'bold', 'underline', 'italic' ]
 		toolbar: [ 'undo', 'redo', '|', 'bold', 'underline', 'italic' ]
 	} )
 	} )
@@ -23,8 +23,51 @@ ClassicEditor
 		const bold = editor.plugins.get( Italic );
 		const bold = editor.plugins.get( Italic );
 		const underline = editor.plugins.get( Underline );
 		const underline = editor.plugins.get( Underline );
 
 
-		bindTwoStepCaretToAttribute( editor.editing.view, editor.model, bold, 'italic' );
-		bindTwoStepCaretToAttribute( editor.editing.view, editor.model, underline, 'underline' );
+		bindTwoStepCaretToAttribute( {
+			view: editor.editing.view,
+			model: editor.model,
+			emitter: bold,
+			attribute: 'italic',
+			locale: editor.locale
+		} );
+		bindTwoStepCaretToAttribute( {
+			view: editor.editing.view,
+			model: editor.model,
+			emitter: underline,
+			attribute: 'underline',
+			locale: editor.locale
+		} );
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+
+ClassicEditor
+	.create( document.querySelector( '#editor-rtl' ), {
+		language: {
+			content: 'he'
+		},
+		plugins: [ Essentials, Paragraph, Underline, Bold, Italic ],
+		toolbar: [ 'undo', 'redo', '|', 'bold', 'underline', 'italic' ]
+	} )
+	.then( editor => {
+		const bold = editor.plugins.get( Italic );
+		const underline = editor.plugins.get( Underline );
+
+		bindTwoStepCaretToAttribute( {
+			view: editor.editing.view,
+			model: editor.model,
+			emitter: bold,
+			attribute: 'italic',
+			locale: editor.locale
+		} );
+		bindTwoStepCaretToAttribute( {
+			view: editor.editing.view,
+			model: editor.model,
+			emitter: underline,
+			attribute: 'underline',
+			locale: editor.locale
+		} );
 	} )
 	} )
 	.catch( err => {
 	.catch( err => {
 		console.error( err.stack );
 		console.error( err.stack );

+ 7 - 0
packages/ckeditor5-engine/tests/manual/two-step-caret.md

@@ -47,3 +47,10 @@
 
 
 ### Not bounded attribute
 ### Not bounded attribute
 Just make sure that two-steps caret movement is disabled for bold text from the third paragraph.
 Just make sure that two-steps caret movement is disabled for bold text from the third paragraph.
+
+### Right–to–left content
+
+**Tip**: Change the system keyboard to Hebrew before testing.
+
+Two-steps caret movement should also work when the content is right–to–left. Repeat all previous steps keeping in mind that the flow of the text is "reversed".
+

+ 30 - 0
packages/ckeditor5-engine/tests/model/differ.js

@@ -1243,6 +1243,36 @@ describe( 'Differ', () => {
 			} );
 			} );
 		} );
 		} );
 
 
+		it( 'on an element that got some nodes inserted', () => {
+			const p = root.getChild( 0 );
+
+			model.change( () => {
+				insert( new Text( 'x' ), Position._createAt( p, 3 ) );
+				insert( new Text( 'x' ), Position._createAt( p, 4 ) );
+				insert( new Text( 'x' ), Position._createAt( p, 5 ) );
+
+				attribute( new Range( Position._createAt( root, 0 ), Position._createAt( root, 1 ) ), 'a', null, true );
+
+				insert( new Text( 'y' ), Position._createAt( p, 6 ) );
+
+				expectChanges( [
+					{
+						type: 'attribute',
+						range: new Range( Position._createAt( root, 0 ), Position._createAt( p, 0 ) ),
+						attributeKey: 'a',
+						attributeOldValue: null,
+						attributeNewValue: true
+					},
+					{
+						type: 'insert',
+						position: Position._createAt( p, 3 ),
+						length: 4,
+						name: '$text'
+					}
+				] );
+			} );
+		} );
+
 		it( 'over all changed nodes and some not changed nodes', () => {
 		it( 'over all changed nodes and some not changed nodes', () => {
 			const p = root.getChild( 0 );
 			const p = root.getChild( 0 );
 
 

+ 4 - 2
packages/ckeditor5-engine/tests/model/documentfragment.js

@@ -61,7 +61,7 @@ describe( 'DocumentFragment', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		let frag;
 		let frag;
 
 
 		before( () => {
 		before( () => {
@@ -70,14 +70,16 @@ describe( 'DocumentFragment', () => {
 
 
 		it( 'should return true for documentFragment', () => {
 		it( 'should return true for documentFragment', () => {
 			expect( frag.is( 'documentFragment' ) ).to.be.true;
 			expect( frag.is( 'documentFragment' ) ).to.be.true;
+			expect( frag.is( 'model:documentFragment' ) ).to.be.true;
 		} );
 		} );
 
 
-		it( 'should return false for other accept values', () => {
+		it( 'should return false for other values', () => {
 			expect( frag.is( 'node' ) ).to.be.false;
 			expect( frag.is( 'node' ) ).to.be.false;
 			expect( frag.is( 'text' ) ).to.be.false;
 			expect( frag.is( 'text' ) ).to.be.false;
 			expect( frag.is( 'textProxy' ) ).to.be.false;
 			expect( frag.is( 'textProxy' ) ).to.be.false;
 			expect( frag.is( 'element' ) ).to.be.false;
 			expect( frag.is( 'element' ) ).to.be.false;
 			expect( frag.is( 'rootElement' ) ).to.be.false;
 			expect( frag.is( 'rootElement' ) ).to.be.false;
+			expect( frag.is( 'view:documentFragment' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 

+ 7 - 1
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -485,21 +485,27 @@ describe( 'DocumentSelection', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		it( 'should return true for selection', () => {
 		it( 'should return true for selection', () => {
 			expect( selection.is( 'selection' ) ).to.be.true;
 			expect( selection.is( 'selection' ) ).to.be.true;
+			expect( selection.is( 'model:selection' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return true for documentSelection', () => {
 		it( 'should return true for documentSelection', () => {
 			expect( selection.is( 'documentSelection' ) ).to.be.true;
 			expect( selection.is( 'documentSelection' ) ).to.be.true;
+			expect( selection.is( 'model:documentSelection' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other values', () => {
 		it( 'should return false for other values', () => {
 			expect( selection.is( 'node' ) ).to.be.false;
 			expect( selection.is( 'node' ) ).to.be.false;
+			expect( selection.is( 'model:node' ) ).to.be.false;
 			expect( selection.is( 'text' ) ).to.be.false;
 			expect( selection.is( 'text' ) ).to.be.false;
 			expect( selection.is( 'textProxy' ) ).to.be.false;
 			expect( selection.is( 'textProxy' ) ).to.be.false;
 			expect( selection.is( 'element' ) ).to.be.false;
 			expect( selection.is( 'element' ) ).to.be.false;
+			expect( selection.is( 'element', 'paragraph' ) ).to.be.false;
 			expect( selection.is( 'rootElement' ) ).to.be.false;
 			expect( selection.is( 'rootElement' ) ).to.be.false;
+			expect( selection.is( 'view:selection' ) ).to.be.false;
+			expect( selection.is( 'view:documentSelection' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 

+ 12 - 1
packages/ckeditor5-engine/tests/model/element.js

@@ -38,7 +38,7 @@ describe( 'Element', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		let element;
 		let element;
 
 
 		before( () => {
 		before( () => {
@@ -47,18 +47,29 @@ describe( 'Element', () => {
 
 
 		it( 'should return true for node, element, element with same name and element name', () => {
 		it( 'should return true for node, element, element with same name and element name', () => {
 			expect( element.is( 'node' ) ).to.be.true;
 			expect( element.is( 'node' ) ).to.be.true;
+			expect( element.is( 'model:node' ) ).to.be.true;
 			expect( element.is( 'element' ) ).to.be.true;
 			expect( element.is( 'element' ) ).to.be.true;
+			expect( element.is( 'model:element' ) ).to.be.true;
 			expect( element.is( 'element', 'paragraph' ) ).to.be.true;
 			expect( element.is( 'element', 'paragraph' ) ).to.be.true;
+			expect( element.is( 'model:element', 'paragraph' ) ).to.be.true;
 			expect( element.is( 'paragraph' ) ).to.be.true;
 			expect( element.is( 'paragraph' ) ).to.be.true;
+			expect( element.is( 'model:paragraph' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other accept values', () => {
 		it( 'should return false for other accept values', () => {
 			expect( element.is( 'element', 'image' ) ).to.be.false;
 			expect( element.is( 'element', 'image' ) ).to.be.false;
+			expect( element.is( 'model:element', 'image' ) ).to.be.false;
 			expect( element.is( 'image' ) ).to.be.false;
 			expect( element.is( 'image' ) ).to.be.false;
+			expect( element.is( 'model:image' ) ).to.be.false;
 			expect( element.is( 'text' ) ).to.be.false;
 			expect( element.is( 'text' ) ).to.be.false;
+			expect( element.is( 'model:text' ) ).to.be.false;
 			expect( element.is( 'textProxy' ) ).to.be.false;
 			expect( element.is( 'textProxy' ) ).to.be.false;
 			expect( element.is( 'documentFragment' ) ).to.be.false;
 			expect( element.is( 'documentFragment' ) ).to.be.false;
 			expect( element.is( 'rootElement' ) ).to.be.false;
 			expect( element.is( 'rootElement' ) ).to.be.false;
+			expect( element.is( 'model:rootElement' ) ).to.be.false;
+			expect( element.is( 'view:node' ) ).to.be.false;
+			expect( element.is( 'view:element' ) ).to.be.false;
+			expect( element.is( 'view:element' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 

+ 23 - 0
packages/ckeditor5-engine/tests/model/liveposition.js

@@ -41,6 +41,29 @@ describe( 'LivePosition', () =>
 		expect( live ).to.be.instanceof( Position );
 		expect( live ).to.be.instanceof( Position );
 	} );
 	} );
 
 
+	describe( 'is()', () => {
+		let live;
+
+		beforeEach( () => {
+			live = new LivePosition( root, [ 0 ] );
+			live.detach();
+		} );
+
+		it( 'should return true for "livePosition" and "position"', () => {
+			expect( live.is( 'livePosition' ) ).to.be.true;
+			expect( live.is( 'model:livePosition' ) ).to.be.true;
+			expect( live.is( 'position' ) ).to.be.true;
+			expect( live.is( 'model:position' ) ).to.be.true;
+		} );
+
+		it( 'should return false for incorrect values', () => {
+			expect( live.is( 'model' ) ).to.be.false;
+			expect( live.is( 'model:node' ) ).to.be.false;
+			expect( live.is( 'text' ) ).to.be.false;
+			expect( live.is( 'element', 'paragraph' ) ).to.be.false;
+		} );
+	} );
+
 	it( 'should throw if given root is not a RootElement', () => {
 	it( 'should throw if given root is not a RootElement', () => {
 		const docFrag = new DocumentFragment();
 		const docFrag = new DocumentFragment();
 
 

+ 23 - 0
packages/ckeditor5-engine/tests/model/liverange.js

@@ -183,6 +183,29 @@ describe( 'LiveRange', () => {
 		expect( spy.args[ 1 ][ 2 ].deletionPosition.isEqual( new Position( root, [ 0 ] ) ) ).to.be.true;
 		expect( spy.args[ 1 ][ 2 ].deletionPosition.isEqual( new Position( root, [ 0 ] ) ) ).to.be.true;
 	} );
 	} );
 
 
+	describe( 'is()', () => {
+		let live;
+
+		beforeEach( () => {
+			live = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
+			live.detach();
+		} );
+
+		it( 'should return true for "liveRange" and "range"', () => {
+			expect( live.is( 'liveRange' ) ).to.be.true;
+			expect( live.is( 'model:liveRange' ) ).to.be.true;
+			expect( live.is( 'range' ) ).to.be.true;
+			expect( live.is( 'model:range' ) ).to.be.true;
+		} );
+
+		it( 'should return false for incorrect values', () => {
+			expect( live.is( 'model' ) ).to.be.false;
+			expect( live.is( 'model:node' ) ).to.be.false;
+			expect( live.is( 'text' ) ).to.be.false;
+			expect( live.is( 'element', 'paragraph' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'should get transformed and fire change:range if', () => {
 	describe( 'should get transformed and fire change:range if', () => {
 		let live, spy;
 		let live, spy;
 
 

+ 21 - 0
packages/ckeditor5-engine/tests/model/markercollection.js

@@ -410,4 +410,25 @@ describe( 'Marker', () => {
 
 
 		expect( marker.affectsData ).to.be.false;
 		expect( marker.affectsData ).to.be.false;
 	} );
 	} );
+
+	describe( 'is()', () => {
+		let marker;
+
+		beforeEach( () => {
+			const range = new Range( Position._createAt( root, 1 ), Position._createAt( root, 2 ) );
+			marker = model.markers._set( 'name', range );
+		} );
+
+		it( 'should return true for "marker"', () => {
+			expect( marker.is( 'marker' ) ).to.be.true;
+			expect( marker.is( 'model:marker' ) ).to.be.true;
+		} );
+
+		it( 'should return false for incorrect values', () => {
+			expect( marker.is( 'model' ) ).to.be.false;
+			expect( marker.is( 'model:node' ) ).to.be.false;
+			expect( marker.is( 'text' ) ).to.be.false;
+			expect( marker.is( 'element', 'paragraph' ) ).to.be.false;
+		} );
+	} );
 } );
 } );

+ 8 - 0
packages/ckeditor5-engine/tests/model/node.js

@@ -165,6 +165,14 @@ describe( 'Node', () => {
 	describe( 'is()', () => {
 	describe( 'is()', () => {
 		it( 'should return true for node', () => {
 		it( 'should return true for node', () => {
 			expect( node.is( 'node' ) ).to.be.true;
 			expect( node.is( 'node' ) ).to.be.true;
+			expect( node.is( 'model:node' ) ).to.be.true;
+		} );
+
+		it( 'should return false for incorrect values', () => {
+			expect( node.is( 'model' ) ).to.be.false;
+			expect( node.is( 'model:text' ) ).to.be.false;
+			expect( node.is( 'text' ) ).to.be.false;
+			expect( node.is( 'element', 'paragraph' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 

+ 19 - 2
packages/ckeditor5-engine/tests/model/operation/mergeoperation.js

@@ -119,7 +119,7 @@ describe( 'MergeOperation', () => {
 				doc.version
 				doc.version
 			);
 			);
 
 
-			expectToThrowCKEditorError( () => operation._validate(), /merge-operation-target-position-invalid/, model );
+			expectToThrowCKEditorError( () => operation._validate(), /model-position-path-incorrect/, model );
 		} );
 		} );
 
 
 		it( 'should throw an error if source position is in root', () => {
 		it( 'should throw an error if source position is in root', () => {
@@ -139,6 +139,23 @@ describe( 'MergeOperation', () => {
 			expectToThrowCKEditorError( () => operation._validate(), /merge-operation-source-position-invalid/, model );
 			expectToThrowCKEditorError( () => operation._validate(), /merge-operation-source-position-invalid/, model );
 		} );
 		} );
 
 
+		it( 'should throw an error if target position is in root', () => {
+			const p1 = new Element( 'p1', null, new Text( 'Foo' ) );
+			const p2 = new Element( 'p2', null, new Text( 'bar' ) );
+
+			root._insertChild( 0, [ p1, p2 ] );
+
+			const operation = new MergeOperation(
+				new Position( root, [ 0, 3 ] ),
+				3,
+				new Position( root, [ 0 ] ),
+				gyPos,
+				doc.version
+			);
+
+			expectToThrowCKEditorError( () => operation._validate(), /merge-operation-target-position-invalid/, model );
+		} );
+
 		it( 'should throw an error if target position is invalid', () => {
 		it( 'should throw an error if target position is invalid', () => {
 			const p1 = new Element( 'p1', null, new Text( 'Foo' ) );
 			const p1 = new Element( 'p1', null, new Text( 'Foo' ) );
 			const p2 = new Element( 'p2', null, new Text( 'bar' ) );
 			const p2 = new Element( 'p2', null, new Text( 'bar' ) );
@@ -153,7 +170,7 @@ describe( 'MergeOperation', () => {
 				doc.version
 				doc.version
 			);
 			);
 
 
-			expectToThrowCKEditorError( () => operation._validate(), /merge-operation-source-position-invalid/, model );
+			expectToThrowCKEditorError( () => operation._validate(), /model-position-path-incorrect/, model );
 		} );
 		} );
 
 
 		it( 'should throw an error if number of nodes to move is invalid', () => {
 		it( 'should throw an error if number of nodes to move is invalid', () => {

+ 1 - 1
packages/ckeditor5-engine/tests/model/operation/moveoperation.js

@@ -171,7 +171,7 @@ describe( 'MoveOperation', () => {
 
 
 			root._removeChildren( 1 );
 			root._removeChildren( 1 );
 
 
-			expectToThrowCKEditorError( () => operation._validate(), /move-operation-position-invalid/, model );
+			expectToThrowCKEditorError( () => operation._validate(), /model-position-path-incorrect/, model );
 		} );
 		} );
 
 
 		it( 'should throw an error if operation tries to move a range between the beginning and the end of that range', () => {
 		it( 'should throw an error if operation tries to move a range between the beginning and the end of that range', () => {

+ 67 - 1
packages/ckeditor5-engine/tests/model/operation/transform.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
  */
 
 
-import { transform } from '../../../src/model/operation/transform';
+import { transform, transformSets } from '../../../src/model/operation/transform';
 
 
 import Model from '../../../src/model/model';
 import Model from '../../../src/model/model';
 import RootElement from '../../../src/model/rootelement';
 import RootElement from '../../../src/model/rootelement';
@@ -2607,3 +2607,69 @@ describe( 'transform', () => {
 		} );
 		} );
 	} );
 	} );
 } );
 } );
+
+describe( 'transformSets', () => {
+	let model, doc, root, node;
+
+	beforeEach( () => {
+		model = new Model();
+		doc = model.document;
+		root = doc.createRoot();
+
+		node = new Node();
+	} );
+
+	it( 'originalOperations should correctly link transformed operations with original operations #1', () => {
+		const position = new Position( root, [ 0 ] );
+
+		const a = new InsertOperation( position, [ node ], 0 );
+
+		const { operationsA, originalOperations } = transformSets( [ a ], [], {
+			document: doc,
+			useRelations: false,
+			padWithNoOps: false
+		} );
+
+		expect( originalOperations.get( operationsA[ 0 ] ) ).to.equal( a );
+	} );
+
+	it( 'originalOperations should correctly link transformed operations with original operations #2', () => {
+		const position = new Position( root, [ 0 ] );
+
+		const b = new InsertOperation( position, [ node ], 0 );
+
+		const { operationsB, originalOperations } = transformSets( [], [ b ], {
+			document: doc,
+			useRelations: false,
+			padWithNoOps: false
+		} );
+
+		expect( originalOperations.get( operationsB[ 0 ] ) ).to.equal( b );
+	} );
+
+	it( 'originalOperations should correctly link transformed operations with original operations #3', () => {
+		const position = new Position( root, [ 4 ] );
+
+		const a = new InsertOperation( position, [ node ], 0 );
+		const b = new AttributeOperation(
+			new Range(
+				new Position( root, [ 2 ] ),
+				new Position( root, [ 11 ] )
+			),
+			'foo',
+			'bar',
+			'xyz',
+			0
+		);
+
+		const { operationsA, operationsB, originalOperations } = transformSets( [ a ], [ b ], {
+			document: doc,
+			useRelations: false,
+			padWithNoOps: false
+		} );
+
+		expect( originalOperations.get( operationsA[ 0 ] ) ).to.equal( a );
+		expect( originalOperations.get( operationsB[ 0 ] ) ).to.equal( b );
+		expect( originalOperations.get( operationsB[ 1 ] ) ).to.equal( b );
+	} );
+} );

+ 192 - 85
packages/ckeditor5-engine/tests/model/position.js

@@ -38,7 +38,7 @@ describe( 'Position', () => {
 	//        |- b   Before: [ 1, 1, 0 ] After: [ 1, 1, 1 ]
 	//        |- b   Before: [ 1, 1, 0 ] After: [ 1, 1, 1 ]
 	//        |- a   Before: [ 1, 1, 1 ] After: [ 1, 1, 2 ]
 	//        |- a   Before: [ 1, 1, 1 ] After: [ 1, 1, 2 ]
 	//        |- r   Before: [ 1, 1, 2 ] After: [ 1, 1, 3 ]
 	//        |- r   Before: [ 1, 1, 2 ] After: [ 1, 1, 3 ]
-	before( () => {
+	beforeEach( () => {
 		model = new Model();
 		model = new Model();
 
 
 		doc = model.document;
 		doc = model.document;
@@ -110,11 +110,11 @@ describe( 'Position', () => {
 		it( 'should throw error if given path is incorrect', () => {
 		it( 'should throw error if given path is incorrect', () => {
 			expectToThrowCKEditorError( () => {
 			expectToThrowCKEditorError( () => {
 				new Position( root, {} ); // eslint-disable-line no-new
 				new Position( root, {} ); // eslint-disable-line no-new
-			}, /model-position-path-incorrect/, model );
+			}, /model-position-path-incorrect-format/, model );
 
 
 			expectToThrowCKEditorError( () => {
 			expectToThrowCKEditorError( () => {
 				new Position( root, [] ); // eslint-disable-line no-new
 				new Position( root, [] ); // eslint-disable-line no-new
-			}, /model-position-path-incorrect/, model );
+			}, /model-position-path-incorrect-format/, model );
 		} );
 		} );
 
 
 		it( 'should throw error if given root is invalid', () => {
 		it( 'should throw error if given root is invalid', () => {
@@ -128,6 +128,26 @@ describe( 'Position', () => {
 		} );
 		} );
 	} );
 	} );
 
 
+	describe( 'is()', () => {
+		let position;
+
+		beforeEach( () => {
+			position = new Position( root, [ 0 ] );
+		} );
+
+		it( 'should return true for "position"', () => {
+			expect( position.is( 'position' ) ).to.be.true;
+			expect( position.is( 'model:position' ) ).to.be.true;
+		} );
+
+		it( 'should return false for incorrect values', () => {
+			expect( position.is( 'model' ) ).to.be.false;
+			expect( position.is( 'model:node' ) ).to.be.false;
+			expect( position.is( 'text' ) ).to.be.false;
+			expect( position.is( 'element', 'paragraph' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'static creators', () => {
 	describe( 'static creators', () => {
 		describe( '_createAt()', () => {
 		describe( '_createAt()', () => {
 			it( 'should throw if no offset is passed', () => {
 			it( 'should throw if no offset is passed', () => {
@@ -263,119 +283,206 @@ describe( 'Position', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	it( 'should have parent', () => {
-		expect( new Position( root, [ 0 ] ) ).to.have.property( 'parent' ).that.equals( root );
-		expect( new Position( root, [ 1 ] ) ).to.have.property( 'parent' ).that.equals( root );
-		expect( new Position( root, [ 2 ] ) ).to.have.property( 'parent' ).that.equals( root );
+	describe( '#parent', () => {
+		it( 'should have parent', () => {
+			expect( new Position( root, [ 0 ] ) ).to.have.property( 'parent' ).that.equals( root );
+			expect( new Position( root, [ 1 ] ) ).to.have.property( 'parent' ).that.equals( root );
+			expect( new Position( root, [ 2 ] ) ).to.have.property( 'parent' ).that.equals( root );
 
 
-		expect( new Position( root, [ 0, 0 ] ) ).to.have.property( 'parent' ).that.equals( p );
+			expect( new Position( root, [ 0, 0 ] ) ).to.have.property( 'parent' ).that.equals( p );
 
 
-		expect( new Position( root, [ 1, 0 ] ) ).to.have.property( 'parent' ).that.equals( ul );
-		expect( new Position( root, [ 1, 1 ] ) ).to.have.property( 'parent' ).that.equals( ul );
-		expect( new Position( root, [ 1, 2 ] ) ).to.have.property( 'parent' ).that.equals( ul );
+			expect( new Position( root, [ 1, 0 ] ) ).to.have.property( 'parent' ).that.equals( ul );
+			expect( new Position( root, [ 1, 1 ] ) ).to.have.property( 'parent' ).that.equals( ul );
+			expect( new Position( root, [ 1, 2 ] ) ).to.have.property( 'parent' ).that.equals( ul );
 
 
-		expect( new Position( root, [ 1, 0, 0 ] ) ).to.have.property( 'parent' ).that.equals( li1 );
-		expect( new Position( root, [ 1, 0, 1 ] ) ).to.have.property( 'parent' ).that.equals( li1 );
-		expect( new Position( root, [ 1, 0, 2 ] ) ).to.have.property( 'parent' ).that.equals( li1 );
-		expect( new Position( root, [ 1, 0, 3 ] ) ).to.have.property( 'parent' ).that.equals( li1 );
-	} );
+			expect( new Position( root, [ 1, 0, 0 ] ) ).to.have.property( 'parent' ).that.equals( li1 );
+			expect( new Position( root, [ 1, 0, 1 ] ) ).to.have.property( 'parent' ).that.equals( li1 );
+			expect( new Position( root, [ 1, 0, 2 ] ) ).to.have.property( 'parent' ).that.equals( li1 );
+			expect( new Position( root, [ 1, 0, 3 ] ) ).to.have.property( 'parent' ).that.equals( li1 );
+		} );
+
+		it( 'should work with positions rooted in document fragment', () => {
+			const docFrag = new DocumentFragment();
+
+			expect( new Position( docFrag, [ 0 ] ) ).to.have.property( 'parent' ).that.equals( docFrag );
+		} );
 
 
-	it( 'should have offset', () => {
-		expect( new Position( root, [ 0 ] ) ).to.have.property( 'offset' ).that.equals( 0 );
-		expect( new Position( root, [ 1 ] ) ).to.have.property( 'offset' ).that.equals( 1 );
-		expect( new Position( root, [ 2 ] ) ).to.have.property( 'offset' ).that.equals( 2 );
+		it( 'should throw when path out of bounds', () => {
+			const position = new Position( root, [ 0, 0 ] );
 
 
-		expect( new Position( root, [ 0, 0 ] ) ).to.have.property( 'offset' ).that.equals( 0 );
+			expect( position ).to.have.property( 'parent' ).that.equals( p );
 
 
-		expect( new Position( root, [ 1, 0 ] ) ).to.have.property( 'offset' ).that.equals( 0 );
-		expect( new Position( root, [ 1, 1 ] ) ).to.have.property( 'offset' ).that.equals( 1 );
-		expect( new Position( root, [ 1, 2 ] ) ).to.have.property( 'offset' ).that.equals( 2 );
+			root._removeChildren( 0, 2 );
 
 
-		expect( new Position( root, [ 1, 0, 0 ] ) ).to.have.property( 'offset' ).that.equals( 0 );
-		expect( new Position( root, [ 1, 0, 1 ] ) ).to.have.property( 'offset' ).that.equals( 1 );
-		expect( new Position( root, [ 1, 0, 2 ] ) ).to.have.property( 'offset' ).that.equals( 2 );
-		expect( new Position( root, [ 1, 0, 3 ] ) ).to.have.property( 'offset' ).that.equals( 3 );
+			expectToThrowCKEditorError( () => {
+				position.parent;
+			}, /^model-position-path-incorrect:/, position, { position } );
+		} );
+
+		it( 'should throw when based on a path, the parent would be a text node', () => {
+			// 1,0,0 points at: <p></p><ul><li>^foz</li>...
+			const position = new Position( root, [ 1, 0, 0, 0 ] );
+
+			expectToThrowCKEditorError( () => {
+				position.parent;
+			}, /^model-position-path-incorrect:/, position, { position } );
+		} );
 	} );
 	} );
 
 
-	it( 'should have index', () => {
-		expect( new Position( root, [ 0 ] ) ).to.have.property( 'index' ).that.equals( 0 );
-		expect( new Position( root, [ 1 ] ) ).to.have.property( 'index' ).that.equals( 1 );
-		expect( new Position( root, [ 2 ] ) ).to.have.property( 'index' ).that.equals( 2 );
+	describe( '#offset', () => {
+		it( 'should have offset', () => {
+			expect( new Position( root, [ 0 ] ) ).to.have.property( 'offset' ).that.equals( 0 );
+			expect( new Position( root, [ 1 ] ) ).to.have.property( 'offset' ).that.equals( 1 );
+			expect( new Position( root, [ 2 ] ) ).to.have.property( 'offset' ).that.equals( 2 );
 
 
-		expect( new Position( root, [ 0, 0 ] ) ).to.have.property( 'index' ).that.equals( 0 );
+			expect( new Position( root, [ 0, 0 ] ) ).to.have.property( 'offset' ).that.equals( 0 );
 
 
-		expect( new Position( root, [ 1, 0 ] ) ).to.have.property( 'index' ).that.equals( 0 );
-		expect( new Position( root, [ 1, 1 ] ) ).to.have.property( 'index' ).that.equals( 1 );
-		expect( new Position( root, [ 1, 2 ] ) ).to.have.property( 'index' ).that.equals( 2 );
+			expect( new Position( root, [ 1, 0 ] ) ).to.have.property( 'offset' ).that.equals( 0 );
+			expect( new Position( root, [ 1, 1 ] ) ).to.have.property( 'offset' ).that.equals( 1 );
+			expect( new Position( root, [ 1, 2 ] ) ).to.have.property( 'offset' ).that.equals( 2 );
 
 
-		expect( new Position( root, [ 1, 0, 0 ] ) ).to.have.property( 'index' ).that.equals( 0 );
-		expect( new Position( root, [ 1, 0, 1 ] ) ).to.have.property( 'index' ).that.equals( 0 );
-		expect( new Position( root, [ 1, 0, 2 ] ) ).to.have.property( 'index' ).that.equals( 0 );
-		expect( new Position( root, [ 1, 0, 3 ] ) ).to.have.property( 'index' ).that.equals( 1 );
+			expect( new Position( root, [ 1, 0, 0 ] ) ).to.have.property( 'offset' ).that.equals( 0 );
+			expect( new Position( root, [ 1, 0, 1 ] ) ).to.have.property( 'offset' ).that.equals( 1 );
+			expect( new Position( root, [ 1, 0, 2 ] ) ).to.have.property( 'offset' ).that.equals( 2 );
+			expect( new Position( root, [ 1, 0, 3 ] ) ).to.have.property( 'offset' ).that.equals( 3 );
+		} );
 	} );
 	} );
 
 
-	it( 'should be able to set offset', () => {
-		const position = new Position( root, [ 1, 0, 2 ] );
-		position.offset = 4;
+	describe( '#index', () => {
+		it( 'should have index', () => {
+			expect( new Position( root, [ 0 ] ) ).to.have.property( 'index' ).that.equals( 0 );
+			expect( new Position( root, [ 1 ] ) ).to.have.property( 'index' ).that.equals( 1 );
+			expect( new Position( root, [ 2 ] ) ).to.have.property( 'index' ).that.equals( 2 );
+
+			expect( new Position( root, [ 0, 0 ] ) ).to.have.property( 'index' ).that.equals( 0 );
+
+			expect( new Position( root, [ 1, 0 ] ) ).to.have.property( 'index' ).that.equals( 0 );
+			expect( new Position( root, [ 1, 1 ] ) ).to.have.property( 'index' ).that.equals( 1 );
+			expect( new Position( root, [ 1, 2 ] ) ).to.have.property( 'index' ).that.equals( 2 );
+
+			expect( new Position( root, [ 1, 0, 0 ] ) ).to.have.property( 'index' ).that.equals( 0 );
+			expect( new Position( root, [ 1, 0, 1 ] ) ).to.have.property( 'index' ).that.equals( 0 );
+			expect( new Position( root, [ 1, 0, 2 ] ) ).to.have.property( 'index' ).that.equals( 0 );
+			expect( new Position( root, [ 1, 0, 3 ] ) ).to.have.property( 'index' ).that.equals( 1 );
+		} );
+
+		it( 'should be able to set offset', () => {
+			const position = new Position( root, [ 1, 0, 2 ] );
+			position.offset = 4;
+
+			expect( position.offset ).to.equal( 4 );
+			expect( position.path ).to.deep.equal( [ 1, 0, 4 ] );
+		} );
 
 
-		expect( position.offset ).to.equal( 4 );
-		expect( position.path ).to.deep.equal( [ 1, 0, 4 ] );
+		it( 'should throw when path out of bounds', () => {
+			const position = new Position( root, [ 0, 0 ] );
+
+			expect( position ).to.have.property( 'index' ).that.equals( 0 );
+
+			root._removeChildren( 0, 2 );
+
+			expectToThrowCKEditorError( () => {
+				position.index;
+			}, /^model-position-path-incorrect:/, position, { position } );
+		} );
 	} );
 	} );
 
 
-	it( 'should have nodeBefore if it is not inside a text node', () => {
-		expect( new Position( root, [ 0 ] ).nodeBefore ).to.be.null;
-		expect( new Position( root, [ 1 ] ).nodeBefore ).to.equal( p );
-		expect( new Position( root, [ 2 ] ).nodeBefore ).to.equal( ul );
+	describe( '#nodeBefore', () => {
+		it( 'should have nodeBefore if it is not inside a text node', () => {
+			expect( new Position( root, [ 0 ] ).nodeBefore ).to.be.null;
+			expect( new Position( root, [ 1 ] ).nodeBefore ).to.equal( p );
+			expect( new Position( root, [ 2 ] ).nodeBefore ).to.equal( ul );
+
+			expect( new Position( root, [ 0, 0 ] ).nodeBefore ).to.null;
+
+			expect( new Position( root, [ 1, 0 ] ).nodeBefore ).to.be.null;
+			expect( new Position( root, [ 1, 1 ] ).nodeBefore ).to.equal( li1 );
+			expect( new Position( root, [ 1, 2 ] ).nodeBefore ).to.equal( li2 );
 
 
-		expect( new Position( root, [ 0, 0 ] ).nodeBefore ).to.null;
+			expect( new Position( root, [ 1, 0, 0 ] ).nodeBefore ).to.be.null;
+			expect( new Position( root, [ 1, 0, 1 ] ).nodeBefore ).to.be.null;
+			expect( new Position( root, [ 1, 0, 2 ] ).nodeBefore ).to.be.null;
+			expect( new Position( root, [ 1, 0, 3 ] ).nodeBefore.data ).to.equal( 'foz' );
+		} );
+
+		it( 'should throw when path out of bounds', () => {
+			const position = new Position( root, [ 1, 1 ] );
+
+			expect( position ).to.have.property( 'nodeBefore' ).that.equals( li1 );
 
 
-		expect( new Position( root, [ 1, 0 ] ).nodeBefore ).to.be.null;
-		expect( new Position( root, [ 1, 1 ] ).nodeBefore ).to.equal( li1 );
-		expect( new Position( root, [ 1, 2 ] ).nodeBefore ).to.equal( li2 );
+			root._removeChildren( 0, 2 );
 
 
-		expect( new Position( root, [ 1, 0, 0 ] ).nodeBefore ).to.be.null;
-		expect( new Position( root, [ 1, 0, 1 ] ).nodeBefore ).to.be.null;
-		expect( new Position( root, [ 1, 0, 2 ] ).nodeBefore ).to.be.null;
-		expect( new Position( root, [ 1, 0, 3 ] ).nodeBefore.data ).to.equal( 'foz' );
+			expectToThrowCKEditorError( () => {
+				position.nodeBefore;
+			}, /^model-nodelist-offset-out-of-bounds:/, position );
+		} );
 	} );
 	} );
 
 
-	it( 'should have nodeAfter if it is not inside a text node', () => {
-		expect( new Position( root, [ 0 ] ).nodeAfter ).to.equal( p );
-		expect( new Position( root, [ 1 ] ).nodeAfter ).to.equal( ul );
-		expect( new Position( root, [ 2 ] ).nodeAfter ).to.be.null;
+	describe( '#nodeAfter', () => {
+		it( 'should have nodeAfter if it is not inside a text node', () => {
+			expect( new Position( root, [ 0 ] ).nodeAfter ).to.equal( p );
+			expect( new Position( root, [ 1 ] ).nodeAfter ).to.equal( ul );
+			expect( new Position( root, [ 2 ] ).nodeAfter ).to.be.null;
+
+			expect( new Position( root, [ 0, 0 ] ).nodeAfter ).to.be.null;
 
 
-		expect( new Position( root, [ 0, 0 ] ).nodeAfter ).to.be.null;
+			expect( new Position( root, [ 1, 0 ] ).nodeAfter ).to.equal( li1 );
+			expect( new Position( root, [ 1, 1 ] ).nodeAfter ).to.equal( li2 );
+			expect( new Position( root, [ 1, 2 ] ).nodeAfter ).to.be.null;
 
 
-		expect( new Position( root, [ 1, 0 ] ).nodeAfter ).to.equal( li1 );
-		expect( new Position( root, [ 1, 1 ] ).nodeAfter ).to.equal( li2 );
-		expect( new Position( root, [ 1, 2 ] ).nodeAfter ).to.be.null;
+			expect( new Position( root, [ 1, 0, 0 ] ).nodeAfter.data ).to.equal( 'foz' );
+			expect( new Position( root, [ 1, 0, 1 ] ).nodeAfter ).to.be.null;
+			expect( new Position( root, [ 1, 0, 2 ] ).nodeAfter ).to.be.null;
+			expect( new Position( root, [ 1, 0, 3 ] ).nodeAfter ).to.be.null;
+		} );
+
+		it( 'should throw when path out of bounds', () => {
+			const position = new Position( root, [ 1, 1 ] );
+
+			expect( position ).to.have.property( 'nodeAfter' ).that.equals( li2 );
 
 
-		expect( new Position( root, [ 1, 0, 0 ] ).nodeAfter.data ).to.equal( 'foz' );
-		expect( new Position( root, [ 1, 0, 1 ] ).nodeAfter ).to.be.null;
-		expect( new Position( root, [ 1, 0, 2 ] ).nodeAfter ).to.be.null;
-		expect( new Position( root, [ 1, 0, 3 ] ).nodeAfter ).to.be.null;
+			root._removeChildren( 0, 2 );
+
+			expectToThrowCKEditorError( () => {
+				position.nodeAfter;
+			}, /^model-nodelist-offset-out-of-bounds:/, position );
+		} );
 	} );
 	} );
 
 
-	it( 'should have a text node property if it is in text node', () => {
-		expect( new Position( root, [ 0 ] ).textNode ).to.be.null;
-		expect( new Position( root, [ 1 ] ).textNode ).to.be.null;
-		expect( new Position( root, [ 2 ] ).textNode ).to.be.null;
+	describe( '#textNode', () => {
+		it( 'should have a text node property if it is in text node', () => {
+			expect( new Position( root, [ 0 ] ).textNode ).to.be.null;
+			expect( new Position( root, [ 1 ] ).textNode ).to.be.null;
+			expect( new Position( root, [ 2 ] ).textNode ).to.be.null;
 
 
-		expect( new Position( root, [ 0, 0 ] ).textNode ).to.be.null;
+			expect( new Position( root, [ 0, 0 ] ).textNode ).to.be.null;
 
 
-		expect( new Position( root, [ 1, 0 ] ).textNode ).to.be.null;
-		expect( new Position( root, [ 1, 1 ] ).textNode ).to.be.null;
-		expect( new Position( root, [ 1, 2 ] ).textNode ).to.be.null;
+			expect( new Position( root, [ 1, 0 ] ).textNode ).to.be.null;
+			expect( new Position( root, [ 1, 1 ] ).textNode ).to.be.null;
+			expect( new Position( root, [ 1, 2 ] ).textNode ).to.be.null;
 
 
-		expect( new Position( root, [ 1, 0, 0 ] ).textNode ).to.be.null;
-		expect( new Position( root, [ 1, 0, 1 ] ).textNode ).to.equal( foz );
-		expect( new Position( root, [ 1, 0, 2 ] ).textNode ).to.equal( foz );
-		expect( new Position( root, [ 1, 0, 3 ] ).textNode ).to.be.null;
+			expect( new Position( root, [ 1, 0, 0 ] ).textNode ).to.be.null;
+			expect( new Position( root, [ 1, 0, 1 ] ).textNode ).to.equal( foz );
+			expect( new Position( root, [ 1, 0, 2 ] ).textNode ).to.equal( foz );
+			expect( new Position( root, [ 1, 0, 3 ] ).textNode ).to.be.null;
 
 
-		expect( new Position( root, [ 1, 1, 0 ] ).textNode ).to.be.null;
-		expect( new Position( root, [ 1, 1, 1 ] ).textNode ).to.equal( bar );
-		expect( new Position( root, [ 1, 1, 2 ] ).textNode ).to.equal( bar );
-		expect( new Position( root, [ 1, 1, 3 ] ).textNode ).to.be.null;
+			expect( new Position( root, [ 1, 1, 0 ] ).textNode ).to.be.null;
+			expect( new Position( root, [ 1, 1, 1 ] ).textNode ).to.equal( bar );
+			expect( new Position( root, [ 1, 1, 2 ] ).textNode ).to.equal( bar );
+			expect( new Position( root, [ 1, 1, 3 ] ).textNode ).to.be.null;
+		} );
+
+		it( 'should throw when path out of bounds', () => {
+			const position = new Position( root, [ 1, 0, 1 ] );
+
+			expect( position ).to.have.property( 'textNode' ).that.equals( foz );
+
+			root._removeChildren( 0, 2 );
+
+			expectToThrowCKEditorError( () => {
+				position.textNode;
+			}, /^model-nodelist-offset-out-of-bounds:/, position );
+		} );
 	} );
 	} );
 
 
 	it( 'should have proper parent path', () => {
 	it( 'should have proper parent path', () => {

+ 14 - 0
packages/ckeditor5-engine/tests/model/range.js

@@ -50,6 +50,20 @@ describe( 'Range', () => {
 		} );
 		} );
 	} );
 	} );
 
 
+	describe( 'is()', () => {
+		it( 'should return true for "range"', () => {
+			expect( range.is( 'range' ) ).to.be.true;
+			expect( range.is( 'model:range' ) ).to.be.true;
+		} );
+
+		it( 'should return false for incorrect values', () => {
+			expect( range.is( 'model' ) ).to.be.false;
+			expect( range.is( 'model:node' ) ).to.be.false;
+			expect( range.is( 'text' ) ).to.be.false;
+			expect( range.is( 'element', 'paragraph' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'root', () => {
 	describe( 'root', () => {
 		it( 'should be equal to start position root', () => {
 		it( 'should be equal to start position root', () => {
 			expect( range.root ).to.equal( start.root );
 			expect( range.root ).to.equal( start.root );

+ 11 - 1
packages/ckeditor5-engine/tests/model/rootelement.js

@@ -22,7 +22,7 @@ describe( 'RootElement', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		let root;
 		let root;
 
 
 		before( () => {
 		before( () => {
@@ -34,19 +34,29 @@ describe( 'RootElement', () => {
 
 
 		it( 'should return true for rootElement, element, element with same name and element name', () => {
 		it( 'should return true for rootElement, element, element with same name and element name', () => {
 			expect( root.is( 'element', '$root' ) ).to.be.true;
 			expect( root.is( 'element', '$root' ) ).to.be.true;
+			expect( root.is( 'model:element', '$root' ) ).to.be.true;
 			expect( root.is( 'element' ) ).to.be.true;
 			expect( root.is( 'element' ) ).to.be.true;
+			expect( root.is( 'model:element' ) ).to.be.true;
 			expect( root.is( '$root' ) ).to.be.true;
 			expect( root.is( '$root' ) ).to.be.true;
+			expect( root.is( 'model:$root' ) ).to.be.true;
 			expect( root.is( 'rootElement', '$root' ) ).to.be.true;
 			expect( root.is( 'rootElement', '$root' ) ).to.be.true;
+			expect( root.is( 'model:rootElement', '$root' ) ).to.be.true;
 			expect( root.is( 'rootElement' ) ).to.be.true;
 			expect( root.is( 'rootElement' ) ).to.be.true;
+			expect( root.is( 'model:rootElement' ) ).to.be.true;
+			expect( root.is( 'node' ) ).to.be.true;
+			expect( root.is( 'model:node' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other accept values', () => {
 		it( 'should return false for other accept values', () => {
 			expect( root.is( 'element', '$graveyard' ) ).to.be.false;
 			expect( root.is( 'element', '$graveyard' ) ).to.be.false;
+			expect( root.is( 'model:element', '$graveyard' ) ).to.be.false;
 			expect( root.is( 'rootElement', '$graveyard' ) ).to.be.false;
 			expect( root.is( 'rootElement', '$graveyard' ) ).to.be.false;
+			expect( root.is( 'model:rootElement', '$graveyard' ) ).to.be.false;
 			expect( root.is( '$graveyard' ) ).to.be.false;
 			expect( root.is( '$graveyard' ) ).to.be.false;
 			expect( root.is( 'text' ) ).to.be.false;
 			expect( root.is( 'text' ) ).to.be.false;
 			expect( root.is( 'textProxy' ) ).to.be.false;
 			expect( root.is( 'textProxy' ) ).to.be.false;
 			expect( root.is( 'documentFragment' ) ).to.be.false;
 			expect( root.is( 'documentFragment' ) ).to.be.false;
+			expect( root.is( 'view:element' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 } );
 } );

+ 110 - 114
packages/ckeditor5-engine/tests/model/selection.js

@@ -127,6 +127,26 @@ describe( 'Selection', () => {
 		} );
 		} );
 	} );
 	} );
 
 
+	describe( 'is()', () => {
+		let selection;
+
+		beforeEach( () => {
+			selection = new Selection();
+		} );
+
+		it( 'should return true for "selection"', () => {
+			expect( selection.is( 'selection' ) ).to.be.true;
+			expect( selection.is( 'model:selection' ) ).to.be.true;
+		} );
+
+		it( 'should return false for incorrect values', () => {
+			expect( selection.is( 'model' ) ).to.be.false;
+			expect( selection.is( 'model:node' ) ).to.be.false;
+			expect( selection.is( 'text' ) ).to.be.false;
+			expect( selection.is( 'element', 'paragraph' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'isCollapsed', () => {
 	describe( 'isCollapsed', () => {
 		it( 'should return false for empty selection', () => {
 		it( 'should return false for empty selection', () => {
 			expect( selection.isCollapsed ).to.be.false;
 			expect( selection.isCollapsed ).to.be.false;
@@ -806,7 +826,7 @@ describe( 'Selection', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		it( 'should return true for selection', () => {
 		it( 'should return true for selection', () => {
 			expect( selection.is( 'selection' ) ).to.be.true;
 			expect( selection.is( 'selection' ) ).to.be.true;
 		} );
 		} );
@@ -950,6 +970,12 @@ describe( 'Selection', () => {
 			// Special block which can contain another blocks.
 			// Special block which can contain another blocks.
 			model.schema.register( 'nestedBlock', { inheritAllFrom: '$block' } );
 			model.schema.register( 'nestedBlock', { inheritAllFrom: '$block' } );
 			model.schema.extend( 'nestedBlock', { allowIn: '$block' } );
 			model.schema.extend( 'nestedBlock', { allowIn: '$block' } );
+
+			model.schema.register( 'table', { isBlock: true, isLimit: true, isObject: true, allowIn: '$root' } );
+			model.schema.register( 'tableRow', { allowIn: 'table', isLimit: true } );
+			model.schema.register( 'tableCell', { allowIn: 'tableRow', isObject: true } );
+
+			model.schema.extend( 'p', { allowIn: 'tableCell' } );
 		} );
 		} );
 
 
 		it( 'returns an iterator', () => {
 		it( 'returns an iterator', () => {
@@ -961,7 +987,7 @@ describe( 'Selection', () => {
 		it( 'returns block for a collapsed selection', () => {
 		it( 'returns block for a collapsed selection', () => {
 			setData( model, '<p>a</p><p>[]b</p><p>c</p>' );
 			setData( model, '<p>a</p><p>[]b</p><p>c</p>' );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#b' ] );
 		} );
 		} );
 
 
 		it( 'returns block for a collapsed selection (empty block)', () => {
 		it( 'returns block for a collapsed selection (empty block)', () => {
@@ -976,86 +1002,102 @@ describe( 'Selection', () => {
 		it( 'returns block for a non collapsed selection', () => {
 		it( 'returns block for a non collapsed selection', () => {
 			setData( model, '<p>a</p><p>[b]</p><p>c</p>' );
 			setData( model, '<p>a</p><p>[b]</p><p>c</p>' );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#b' ] );
 		} );
 		} );
 
 
 		it( 'returns two blocks for a non collapsed selection', () => {
 		it( 'returns two blocks for a non collapsed selection', () => {
 			setData( model, '<p>a</p><h>[b</h><p>c]</p><p>d</p>' );
 			setData( model, '<p>a</p><h>[b</h><p>c]</p><p>d</p>' );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b', 'c' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'h#b', 'p#c' ] );
 		} );
 		} );
 
 
 		it( 'returns two blocks for a non collapsed selection (starts at block end)', () => {
 		it( 'returns two blocks for a non collapsed selection (starts at block end)', () => {
 			setData( model, '<p>a</p><h>b[</h><p>c]</p><p>d</p>' );
 			setData( model, '<p>a</p><h>b[</h><p>c]</p><p>d</p>' );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b', 'c' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'h#b', 'p#c' ] );
 		} );
 		} );
 
 
 		it( 'returns proper block for a multi-range selection', () => {
 		it( 'returns proper block for a multi-range selection', () => {
 			setData( model, '<p>a</p><h>[b</h><p>c]</p><p>d</p><p>[e]</p>' );
 			setData( model, '<p>a</p><h>[b</h><p>c]</p><p>d</p><p>[e]</p>' );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b', 'c', 'e' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'h#b', 'p#c', 'p#e' ] );
 		} );
 		} );
 
 
 		it( 'does not return a block twice if two ranges are anchored in it', () => {
 		it( 'does not return a block twice if two ranges are anchored in it', () => {
 			setData( model, '<p>[a]b[c]</p>' );
 			setData( model, '<p>[a]b[c]</p>' );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'abc' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#abc' ] );
 		} );
 		} );
 
 
 		it( 'returns only blocks', () => {
 		it( 'returns only blocks', () => {
 			setData( model, '<p>[a</p><image>b</image><p>c]</p>' );
 			setData( model, '<p>[a</p><image>b</image><p>c]</p>' );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'c' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a', 'p#c' ] );
 		} );
 		} );
 
 
 		it( 'gets deeper into the tree', () => {
 		it( 'gets deeper into the tree', () => {
 			setData( model, '<p>[a</p><blockquote><p>b</p><p>c</p></blockquote><p>d]</p>' );
 			setData( model, '<p>[a</p><blockquote><p>b</p><p>c</p></blockquote><p>d]</p>' );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b', 'c', 'd' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) )
+				.to.deep.equal( [ 'p#a', 'p#b', 'p#c', 'p#d' ] );
 		} );
 		} );
 
 
 		it( 'gets deeper into the tree (end deeper)', () => {
 		it( 'gets deeper into the tree (end deeper)', () => {
 			setData( model, '<p>[a</p><blockquote><p>b]</p><p>c</p></blockquote><p>d</p>' );
 			setData( model, '<p>[a</p><blockquote><p>b]</p><p>c</p></blockquote><p>d</p>' );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) )
+				.to.deep.equal( [ 'p#a', 'p#b' ] );
 		} );
 		} );
 
 
 		it( 'gets deeper into the tree (start deeper)', () => {
 		it( 'gets deeper into the tree (start deeper)', () => {
 			setData( model, '<p>a</p><blockquote><p>b</p><p>[c</p></blockquote><p>d]</p>' );
 			setData( model, '<p>a</p><blockquote><p>b</p><p>[c</p></blockquote><p>d]</p>' );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'c', 'd' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) )
+				.to.deep.equal( [ 'p#c', 'p#d' ] );
 		} );
 		} );
 
 
 		it( 'returns an empty array if none of the selected elements is a block', () => {
 		it( 'returns an empty array if none of the selected elements is a block', () => {
 			setData( model, '<p>a</p><image>[a</image><image>b]</image><p>b</p>' );
 			setData( model, '<p>a</p><image>[a</image><image>b]</image><p>b</p>' );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.be.empty;
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.be.empty;
 		} );
 		} );
 
 
 		it( 'returns an empty array if the selected element is not a block', () => {
 		it( 'returns an empty array if the selected element is not a block', () => {
 			setData( model, '<p>a</p><image>[]a</image><p>b</p>' );
 			setData( model, '<p>a</p><image>[]a</image><p>b</p>' );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.be.empty;
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.be.empty;
 		} );
 		} );
 
 
 		// Super edge case – should not happen (blocks should never be nested),
 		// Super edge case – should not happen (blocks should never be nested),
 		// but since the code handles it already it's worth testing.
 		// but since the code handles it already it's worth testing.
-		it( 'returns only the lowest block if blocks are nested', () => {
+		it( 'returns only the lowest block if blocks are nested (case #1)', () => {
 			setData( model, '<nestedBlock>a<nestedBlock>[]b</nestedBlock></nestedBlock>' );
 			setData( model, '<nestedBlock>a<nestedBlock>[]b</nestedBlock></nestedBlock>' );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'nestedBlock#b' ] );
 		} );
 		} );
 
 
-		// Like above but trickier.
-		it( 'returns only the lowest block if blocks are nested', () => {
+		// Like above but - with multiple ranges.
+		it( 'returns only the lowest block if blocks are nested (case #2)', () => {
 			setData(
 			setData(
 				model,
 				model,
 				'<nestedBlock>a<nestedBlock>[b</nestedBlock></nestedBlock>' +
 				'<nestedBlock>a<nestedBlock>[b</nestedBlock></nestedBlock>' +
 				'<nestedBlock>c<nestedBlock>d]</nestedBlock></nestedBlock>'
 				'<nestedBlock>c<nestedBlock>d]</nestedBlock></nestedBlock>'
 			);
 			);
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b', 'd' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) )
+				.to.deep.equal( [ 'nestedBlock#b', 'nestedBlock#d' ] );
+		} );
+
+		// Like above but - with multiple collapsed ranges.
+		it( 'returns only the lowest block if blocks are nested (case #3)', () => {
+			setData(
+				model,
+				'<nestedBlock>a<nestedBlock>[]b</nestedBlock></nestedBlock>' +
+				'<nestedBlock>c<nestedBlock>d[]</nestedBlock></nestedBlock>'
+			);
+
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) )
+				.to.deep.equal( [ 'nestedBlock#b', 'nestedBlock#d' ] );
 		} );
 		} );
 
 
 		it( 'returns nothing if directly in a root', () => {
 		it( 'returns nothing if directly in a root', () => {
@@ -1063,26 +1105,60 @@ describe( 'Selection', () => {
 
 
 			setData( model, 'a[b]c', { rootName: 'inlineOnlyRoot' } );
 			setData( model, 'a[b]c', { rootName: 'inlineOnlyRoot' } );
 
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.be.empty;
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.be.empty;
+		} );
+
+		it( 'does not go cross limit elements', () => {
+			model.schema.register( 'blk', { allowIn: [ '$root', 'tableCell' ], isObject: true, isBlock: true } );
+
+			setData( model, '<table><tableRow><tableCell><p>foo</p>[<blk></blk><p>bar]</p></tableCell></tableRow></table>' );
+
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'blk', 'p#bar' ] );
+		} );
+
+		it( 'returns only top most blocks (multiple selected)', () => {
+			setData( model, '<p>[foo</p><table><tableRow><tableCell><p>bar</p></tableCell></tableRow></table><p>baz]</p>' );
+
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#foo', 'table', 'p#baz' ] );
+		} );
+
+		it( 'returns only top most block (one selected)', () => {
+			setData( model, '[<table><tableRow><tableCell><p>bar</p></tableCell></tableRow></table>]' );
+
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'table' ] );
+		} );
+
+		it( 'returns only selected blocks even if nested in other blocks', () => {
+			setData( model, '<p>foo</p><table><tableRow><tableCell><p>[b]ar</p></tableCell></tableRow></table><p>baz</p>' );
+
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#bar' ] );
+		} );
+
+		it( 'returns only selected blocks even if nested in other blocks (selection on the block)', () => {
+			model.schema.register( 'blk', { allowIn: [ '$root', 'tableCell' ], isObject: true, isBlock: true } );
+
+			setData( model, '<table><tableRow><tableCell><p>foo</p>[<blk></blk><p>bar]</p></tableCell></tableRow></table>' );
+
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'blk', 'p#bar' ] );
 		} );
 		} );
 
 
 		describe( '#984', () => {
 		describe( '#984', () => {
 			it( 'does not return the last block if none of its content is selected', () => {
 			it( 'does not return the last block if none of its content is selected', () => {
 				setData( model, '<p>[a</p><p>b</p><p>]c</p>' );
 				setData( model, '<p>[a</p><p>b</p><p>]c</p>' );
 
 
-				expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b' ] );
+				expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a', 'p#b' ] );
 			} );
 			} );
 
 
 			it( 'returns only the first block for a non collapsed selection if only end of selection is touching a block', () => {
 			it( 'returns only the first block for a non collapsed selection if only end of selection is touching a block', () => {
 				setData( model, '<p>a</p><h>b[</h><p>]c</p><p>d</p>' );
 				setData( model, '<p>a</p><h>b[</h><p>]c</p><p>d</p>' );
 
 
-				expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b' ] );
+				expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'h#b' ] );
 			} );
 			} );
 
 
 			it( 'does not return the last block if none of its content is selected (nested case)', () => {
 			it( 'does not return the last block if none of its content is selected (nested case)', () => {
 				setData( model, '<p>[a</p><nestedBlock><nestedBlock>]b</nestedBlock></nestedBlock>' );
 				setData( model, '<p>[a</p><nestedBlock><nestedBlock>]b</nestedBlock></nestedBlock>' );
 
 
-				expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a' ] );
+				expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a' ] );
 			} );
 			} );
 
 
 			// Like a super edge case, we can live with this behavior as I don't even know what we could expect here
 			// Like a super edge case, we can live with this behavior as I don't even know what we could expect here
@@ -1090,20 +1166,20 @@ describe( 'Selection', () => {
 			it( 'does not return the last block if none of its content is selected (nested case, wrapper with a content)', () => {
 			it( 'does not return the last block if none of its content is selected (nested case, wrapper with a content)', () => {
 				setData( model, '<p>[a</p><nestedBlock>b<nestedBlock>]c</nestedBlock></nestedBlock>' );
 				setData( model, '<p>[a</p><nestedBlock>b<nestedBlock>]c</nestedBlock></nestedBlock>' );
 
 
-				expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a' ] );
+				expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a' ] );
 			} );
 			} );
 
 
 			it( 'returns the last block if at least one of its child nodes is selected', () => {
 			it( 'returns the last block if at least one of its child nodes is selected', () => {
 				setData( model, '<p>[a</p><p>b</p><p><image></image>]c</p>' );
 				setData( model, '<p>[a</p><p>b</p><p><image></image>]c</p>' );
 
 
-				expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+				expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a', 'p#b', 'p#c' ] );
 			} );
 			} );
 
 
 			// I needed these last 2 cases to justify the use of isTouching() instead of simple `offset == 0` check.
 			// I needed these last 2 cases to justify the use of isTouching() instead of simple `offset == 0` check.
 			it( 'returns the last block if at least one of its child nodes is selected (end in an inline element)', () => {
 			it( 'returns the last block if at least one of its child nodes is selected (end in an inline element)', () => {
 				setData( model, '<p>[a</p><p>b</p><p><image>x]</image>c</p>' );
 				setData( model, '<p>[a</p><p>b</p><p><image>x]</image>c</p>' );
 
 
-				expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+				expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a', 'p#b', 'p#c' ] );
 			} );
 			} );
 
 
 			it(
 			it(
@@ -1112,95 +1188,10 @@ describe( 'Selection', () => {
 				() => {
 				() => {
 					setData( model, '<p>[a</p><p>b</p><p><image>]x</image>c</p>' );
 					setData( model, '<p>[a</p><p>b</p><p><image>]x</image>c</p>' );
 
 
-					expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b' ] );
+					expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a', 'p#b' ] );
 				}
 				}
 			);
 			);
 		} );
 		} );
-
-		it( 'does not go beyond limit elements', () => {
-			model.schema.register( 'table', { isBlock: true, isLimit: true, isObject: true, allowIn: '$root' } );
-			model.schema.register( 'tableRow', { allowIn: 'table', isLimit: true } );
-			model.schema.register( 'tableCell', { allowIn: 'tableRow', isObject: true } );
-
-			model.schema.register( 'blk', { allowIn: [ '$root', 'tableCell' ], isObject: true, isBlock: true } );
-
-			model.schema.extend( 'p', { allowIn: 'tableCell' } );
-
-			setData( model, '<table><tableRow><tableCell><p>foo</p>[<blk></blk><p>bar]</p></tableCell></tableRow></table>' );
-
-			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'blk', 'p#bar' ] );
-		} );
-
-		// Map all elements to data of its first child text node.
-		function toText( elements ) {
-			return Array.from( elements ).map( el => {
-				return Array.from( el.getChildren() ).find( child => child.data ).data;
-			} );
-		}
-	} );
-
-	describe( 'getTopMostBlocks()', () => {
-		beforeEach( () => {
-			model.schema.register( 'p', { inheritAllFrom: '$block' } );
-			model.schema.register( 'table', { isBlock: true, isLimit: true, isObject: true, allowIn: '$root' } );
-			model.schema.register( 'tableRow', { allowIn: 'table', isLimit: true } );
-			model.schema.register( 'tableCell', { allowIn: 'tableRow', isObject: true } );
-
-			model.schema.extend( 'p', { allowIn: 'tableCell' } );
-		} );
-
-		it( 'returns an iterator', () => {
-			setData( model, '<p>a</p><p>[]b</p><p>c</p>' );
-
-			expect( doc.selection.getTopMostBlocks().next ).to.be.a( 'function' );
-		} );
-
-		it( 'returns block for a collapsed selection', () => {
-			setData( model, '<p>a</p><p>[]b</p><p>c</p>' );
-
-			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#b' ] );
-		} );
-
-		it( 'returns block for a collapsed selection (empty block)', () => {
-			setData( model, '<p>a</p><p>[]</p><p>c</p>' );
-
-			const blocks = Array.from( doc.selection.getTopMostBlocks() );
-
-			expect( blocks ).to.have.length( 1 );
-			expect( blocks[ 0 ].childCount ).to.equal( 0 );
-		} );
-
-		it( 'returns block for a non collapsed selection', () => {
-			setData( model, '<p>a</p><p>[b]</p><p>c</p>' );
-
-			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#b' ] );
-		} );
-
-		it( 'returns two blocks for a non collapsed selection', () => {
-			setData( model, '<p>a</p><p>[b</p><p>c]</p><p>d</p>' );
-
-			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#b', 'p#c' ] );
-		} );
-
-		it( 'returns only top most blocks', () => {
-			setData( model, '[<p>foo</p><table><tableRow><tableCell><p>bar</p></tableCell></tableRow></table><p>baz</p>]' );
-
-			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#foo', 'table', 'p#baz' ] );
-		} );
-
-		it( 'returns only selected blocks even if nested in other blocks', () => {
-			setData( model, '<p>foo</p><table><tableRow><tableCell><p>[b]ar</p></tableCell></tableRow></table><p>baz</p>' );
-
-			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#bar' ] );
-		} );
-
-		it( 'returns only selected blocks even if nested in other blocks (selection on the block)', () => {
-			model.schema.register( 'blk', { allowIn: [ '$root', 'tableCell' ], isObject: true, isBlock: true } );
-
-			setData( model, '<table><tableRow><tableCell><p>foo</p>[<blk></blk><p>bar]</p></tableCell></tableRow></table>' );
-
-			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'blk', 'p#bar' ] );
-		} );
 	} );
 	} );
 
 
 	describe( 'attributes interface', () => {
 	describe( 'attributes interface', () => {
@@ -1382,10 +1373,15 @@ describe( 'Selection', () => {
 		return Array.from( elements ).map( el => {
 		return Array.from( elements ).map( el => {
 			const name = el.name;
 			const name = el.name;
 
 
-			const firstChild = el.getChild( 0 );
-			const hasText = firstChild && firstChild.data;
+			let innerText = '';
+
+			for ( const child of el.getChildren() ) {
+				if ( child.is( 'text' ) ) {
+					innerText += child.data;
+				}
+			}
 
 
-			return hasText ? `${ name }#${ firstChild.data }` : name;
+			return innerText.length ? `${ name }#${ innerText }` : name;
 		} );
 		} );
 	}
 	}
 } );
 } );

+ 4 - 1
packages/ckeditor5-engine/tests/model/text.js

@@ -32,7 +32,7 @@ describe( 'Text', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		let text;
 		let text;
 
 
 		before( () => {
 		before( () => {
@@ -41,12 +41,15 @@ describe( 'Text', () => {
 
 
 		it( 'should return true for node, text', () => {
 		it( 'should return true for node, text', () => {
 			expect( text.is( 'node' ) ).to.be.true;
 			expect( text.is( 'node' ) ).to.be.true;
+			expect( text.is( 'model:node' ) ).to.be.true;
 			expect( text.is( 'text' ) ).to.be.true;
 			expect( text.is( 'text' ) ).to.be.true;
+			expect( text.is( 'model:text' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other accept values', () => {
 		it( 'should return false for other accept values', () => {
 			expect( text.is( 'textProxy' ) ).to.be.false;
 			expect( text.is( 'textProxy' ) ).to.be.false;
 			expect( text.is( 'element' ) ).to.be.false;
 			expect( text.is( 'element' ) ).to.be.false;
+			expect( text.is( 'model:element' ) ).to.be.false;
 			expect( text.is( 'rootElement' ) ).to.be.false;
 			expect( text.is( 'rootElement' ) ).to.be.false;
 			expect( text.is( 'documentFragment' ) ).to.be.false;
 			expect( text.is( 'documentFragment' ) ).to.be.false;
 		} );
 		} );

+ 4 - 1
packages/ckeditor5-engine/tests/model/textproxy.js

@@ -102,15 +102,18 @@ describe( 'TextProxy', () => {
 		}, /model-textproxy-wrong-length/, model );
 		}, /model-textproxy-wrong-length/, model );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		it( 'should return true for textProxy', () => {
 		it( 'should return true for textProxy', () => {
 			expect( textProxy.is( 'textProxy' ) ).to.be.true;
 			expect( textProxy.is( 'textProxy' ) ).to.be.true;
+			expect( textProxy.is( 'model:textProxy' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other accept values', () => {
 		it( 'should return false for other accept values', () => {
 			expect( textProxy.is( 'node' ) ).to.be.false;
 			expect( textProxy.is( 'node' ) ).to.be.false;
+			expect( textProxy.is( 'model:node' ) ).to.be.false;
 			expect( textProxy.is( 'text' ) ).to.be.false;
 			expect( textProxy.is( 'text' ) ).to.be.false;
 			expect( textProxy.is( 'element' ) ).to.be.false;
 			expect( textProxy.is( 'element' ) ).to.be.false;
+			expect( textProxy.is( 'model:element', 'image' ) ).to.be.false;
 			expect( textProxy.is( 'documentFragment' ) ).to.be.false;
 			expect( textProxy.is( 'documentFragment' ) ).to.be.false;
 			expect( textProxy.is( 'rootElement' ) ).to.be.false;
 			expect( textProxy.is( 'rootElement' ) ).to.be.false;
 		} );
 		} );

+ 107 - 4
packages/ckeditor5-engine/tests/utils/bindtwostepcarettoattribute.js

@@ -9,15 +9,18 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest
 import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
 import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
 import DomEventData from '../../src/view/observer/domeventdata';
 import DomEventData from '../../src/view/observer/domeventdata';
 import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
 import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
-import bindTwoStepCaretToAttribute from '../../src/utils/bindtwostepcarettoattribute';
+import bindTwoStepCaretToAttribute, { TwoStepCaretHandler } from '../../src/utils/bindtwostepcarettoattribute';
 import Position from '../../src/model/position';
 import Position from '../../src/model/position';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { setData } from '../../src/dev-utils/model';
 import { setData } from '../../src/dev-utils/model';
 
 
 describe( 'bindTwoStepCaretToAttribute()', () => {
 describe( 'bindTwoStepCaretToAttribute()', () => {
-	let editor, model, emitter, selection, view;
+	let editor, model, emitter, selection, view, locale;
 	let preventDefaultSpy, evtStopSpy;
 	let preventDefaultSpy, evtStopSpy;
 
 
+	testUtils.createSinonSandbox();
+
 	beforeEach( () => {
 	beforeEach( () => {
 		emitter = Object.create( DomEmitterMixin );
 		emitter = Object.create( DomEmitterMixin );
 
 
@@ -26,6 +29,7 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 			model = editor.model;
 			model = editor.model;
 			selection = model.document.selection;
 			selection = model.document.selection;
 			view = editor.editing.view;
 			view = editor.editing.view;
+			locale = editor.locale;
 
 
 			preventDefaultSpy = sinon.spy();
 			preventDefaultSpy = sinon.spy();
 			evtStopSpy = sinon.spy();
 			evtStopSpy = sinon.spy();
@@ -41,7 +45,13 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 			editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
 			editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
 			editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 			editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 
 
-			bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'a' );
+			bindTwoStepCaretToAttribute( {
+				view: editor.editing.view,
+				model: editor.model,
+				emitter,
+				attribute: 'a',
+				locale
+			} );
 		} );
 		} );
 	} );
 	} );
 
 
@@ -550,7 +560,13 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 
 
 	describe( 'multiple attributes', () => {
 	describe( 'multiple attributes', () => {
 		beforeEach( () => {
 		beforeEach( () => {
-			bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'c' );
+			bindTwoStepCaretToAttribute( {
+				view: editor.editing.view,
+				model: editor.model,
+				emitter,
+				attribute: 'c',
+				locale
+			} );
 		} );
 		} );
 
 
 		it( 'should work with the two-step caret movement (moving right)', () => {
 		it( 'should work with the two-step caret movement (moving right)', () => {
@@ -743,6 +759,93 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 		expect( getSelectionAttributesArray( selection ) ).to.have.members( [] );
 		expect( getSelectionAttributesArray( selection ) ).to.have.members( [] );
 	} );
 	} );
 
 
+	describe( 'left–to–right and right–to–left content', () => {
+		it( 'should call methods associated with the keys (LTR content direction)', () => {
+			const forwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleForwardMovement' );
+			const backwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleBackwardMovement' );
+
+			setData( model, '<$text>foo[]</$text><$text a="true">bar</$text>' );
+
+			fireKeyDownEvent( {
+				keyCode: keyCodes.arrowright
+			} );
+
+			sinon.assert.calledOnce( forwardStub );
+			sinon.assert.notCalled( backwardStub );
+
+			setData( model, '<$text>foo</$text><$text a="true">[]bar</$text>' );
+
+			fireKeyDownEvent( {
+				keyCode: keyCodes.arrowleft
+			} );
+
+			sinon.assert.calledOnce( backwardStub );
+			sinon.assert.calledOnce( forwardStub );
+		} );
+
+		it( 'should use the opposite helper methods (RTL content direction)', () => {
+			const forwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleForwardMovement' );
+			const backwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleBackwardMovement' );
+			const emitter = Object.create( DomEmitterMixin );
+
+			let model;
+
+			return VirtualTestEditor
+				.create( {
+					language: {
+						content: 'ar'
+					}
+				} )
+				.then( newEditor => {
+					model = newEditor.model;
+					selection = model.document.selection;
+					view = newEditor.editing.view;
+
+					newEditor.model.schema.extend( '$text', {
+						allowAttributes: [ 'a', 'b', 'c' ],
+						allowIn: '$root'
+					} );
+
+					model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+					newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'a', model: 'a' } );
+					newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'b', model: 'b' } );
+					newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
+					newEditor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+
+					bindTwoStepCaretToAttribute( {
+						view: newEditor.editing.view,
+						model: newEditor.model,
+						emitter,
+						attribute: 'a',
+						locale: newEditor.locale
+					} );
+
+					return newEditor;
+				} )
+				.then( newEditor => {
+					setData( model, '<$text>foo[]</$text><$text a="true">bar</$text>' );
+
+					fireKeyDownEvent( {
+						keyCode: keyCodes.arrowleft
+					} );
+
+					sinon.assert.calledOnce( forwardStub );
+					sinon.assert.notCalled( backwardStub );
+
+					setData( model, '<$text>foo</$text><$text a="true">[]bar</$text>' );
+
+					fireKeyDownEvent( {
+						keyCode: keyCodes.arrowright
+					} );
+
+					sinon.assert.calledOnce( backwardStub );
+					sinon.assert.calledOnce( forwardStub );
+
+					return newEditor.destroy();
+				} );
+		} );
+	} );
+
 	const keyMap = {
 	const keyMap = {
 		'→': 'arrowright',
 		'→': 'arrowright',
 		'←': 'arrowleft'
 		'←': 'arrowleft'

+ 9 - 1
packages/ckeditor5-engine/tests/view/attributeelement.js

@@ -21,7 +21,7 @@ describe( 'AttributeElement', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		let el;
 		let el;
 
 
 		before( () => {
 		before( () => {
@@ -30,16 +30,24 @@ describe( 'AttributeElement', () => {
 
 
 		it( 'should return true for attributeElement/element, also with correct name and element name', () => {
 		it( 'should return true for attributeElement/element, also with correct name and element name', () => {
 			expect( el.is( 'attributeElement' ) ).to.be.true;
 			expect( el.is( 'attributeElement' ) ).to.be.true;
+			expect( el.is( 'view:attributeElement' ) ).to.be.true;
 			expect( el.is( 'attributeElement', 'span' ) ).to.be.true;
 			expect( el.is( 'attributeElement', 'span' ) ).to.be.true;
+			expect( el.is( 'view:attributeElement', 'span' ) ).to.be.true;
 			expect( el.is( 'element' ) ).to.be.true;
 			expect( el.is( 'element' ) ).to.be.true;
+			expect( el.is( 'view:element' ) ).to.be.true;
 			expect( el.is( 'element', 'span' ) ).to.be.true;
 			expect( el.is( 'element', 'span' ) ).to.be.true;
+			expect( el.is( 'view:element', 'span' ) ).to.be.true;
 			expect( el.is( 'span' ) ).to.be.true;
 			expect( el.is( 'span' ) ).to.be.true;
+			expect( el.is( 'view:span' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other accept values', () => {
 		it( 'should return false for other accept values', () => {
 			expect( el.is( 'attributeElement', 'p' ) ).to.be.false;
 			expect( el.is( 'attributeElement', 'p' ) ).to.be.false;
+			expect( el.is( 'view:attributeElement', 'p' ) ).to.be.false;
 			expect( el.is( 'element', 'p' ) ).to.be.false;
 			expect( el.is( 'element', 'p' ) ).to.be.false;
+			expect( el.is( 'view:element', 'p' ) ).to.be.false;
 			expect( el.is( 'p' ) ).to.be.false;
 			expect( el.is( 'p' ) ).to.be.false;
+			expect( el.is( 'view:p' ) ).to.be.false;
 			expect( el.is( 'text' ) ).to.be.false;
 			expect( el.is( 'text' ) ).to.be.false;
 			expect( el.is( 'textProxy' ) ).to.be.false;
 			expect( el.is( 'textProxy' ) ).to.be.false;
 			expect( el.is( 'containerElement' ) ).to.be.false;
 			expect( el.is( 'containerElement' ) ).to.be.false;

+ 9 - 1
packages/ckeditor5-engine/tests/view/containerelement.js

@@ -18,7 +18,7 @@ describe( 'ContainerElement', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		let el;
 		let el;
 
 
 		before( () => {
 		before( () => {
@@ -27,16 +27,24 @@ describe( 'ContainerElement', () => {
 
 
 		it( 'should return true for containerElement/element, also with correct name and element name', () => {
 		it( 'should return true for containerElement/element, also with correct name and element name', () => {
 			expect( el.is( 'containerElement' ) ).to.be.true;
 			expect( el.is( 'containerElement' ) ).to.be.true;
+			expect( el.is( 'view:containerElement' ) ).to.be.true;
 			expect( el.is( 'containerElement', 'p' ) ).to.be.true;
 			expect( el.is( 'containerElement', 'p' ) ).to.be.true;
+			expect( el.is( 'view:containerElement', 'p' ) ).to.be.true;
 			expect( el.is( 'element' ) ).to.be.true;
 			expect( el.is( 'element' ) ).to.be.true;
+			expect( el.is( 'view:element' ) ).to.be.true;
 			expect( el.is( 'element', 'p' ) ).to.be.true;
 			expect( el.is( 'element', 'p' ) ).to.be.true;
+			expect( el.is( 'view:element', 'p' ) ).to.be.true;
 			expect( el.is( 'p' ) ).to.be.true;
 			expect( el.is( 'p' ) ).to.be.true;
+			expect( el.is( 'view:p' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other accept values', () => {
 		it( 'should return false for other accept values', () => {
 			expect( el.is( 'containerElement', 'span' ) ).to.be.false;
 			expect( el.is( 'containerElement', 'span' ) ).to.be.false;
+			expect( el.is( 'view:containerElement', 'span' ) ).to.be.false;
 			expect( el.is( 'element', 'span' ) ).to.be.false;
 			expect( el.is( 'element', 'span' ) ).to.be.false;
+			expect( el.is( 'view:element', 'span' ) ).to.be.false;
 			expect( el.is( 'span' ) ).to.be.false;
 			expect( el.is( 'span' ) ).to.be.false;
+			expect( el.is( 'view:span' ) ).to.be.false;
 			expect( el.is( 'text' ) ).to.be.false;
 			expect( el.is( 'text' ) ).to.be.false;
 			expect( el.is( 'textProxy' ) ).to.be.false;
 			expect( el.is( 'textProxy' ) ).to.be.false;
 			expect( el.is( 'attributeElement' ) ).to.be.false;
 			expect( el.is( 'attributeElement' ) ).to.be.false;

+ 4 - 1
packages/ckeditor5-engine/tests/view/documentfragment.js

@@ -71,7 +71,7 @@ describe( 'DocumentFragment', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		let frag;
 		let frag;
 
 
 		before( () => {
 		before( () => {
@@ -80,13 +80,16 @@ describe( 'DocumentFragment', () => {
 
 
 		it( 'should return true for documentFragment', () => {
 		it( 'should return true for documentFragment', () => {
 			expect( frag.is( 'documentFragment' ) ).to.be.true;
 			expect( frag.is( 'documentFragment' ) ).to.be.true;
+			expect( frag.is( 'view:documentFragment' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other accept values', () => {
 		it( 'should return false for other accept values', () => {
 			expect( frag.is( 'node' ) ).to.be.false;
 			expect( frag.is( 'node' ) ).to.be.false;
+			expect( frag.is( 'view:node' ) ).to.be.false;
 			expect( frag.is( 'text' ) ).to.be.false;
 			expect( frag.is( 'text' ) ).to.be.false;
 			expect( frag.is( 'textProxy' ) ).to.be.false;
 			expect( frag.is( 'textProxy' ) ).to.be.false;
 			expect( frag.is( 'element' ) ).to.be.false;
 			expect( frag.is( 'element' ) ).to.be.false;
+			expect( frag.is( 'view:element' ) ).to.be.false;
 			expect( frag.is( 'containerElement' ) ).to.be.false;
 			expect( frag.is( 'containerElement' ) ).to.be.false;
 			expect( frag.is( 'attributeElement' ) ).to.be.false;
 			expect( frag.is( 'attributeElement' ) ).to.be.false;
 			expect( frag.is( 'uiElement' ) ).to.be.false;
 			expect( frag.is( 'uiElement' ) ).to.be.false;

+ 7 - 1
packages/ckeditor5-engine/tests/view/documentselection.js

@@ -725,21 +725,27 @@ describe( 'DocumentSelection', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		it( 'should return true for selection', () => {
 		it( 'should return true for selection', () => {
 			expect( documentSelection.is( 'selection' ) ).to.be.true;
 			expect( documentSelection.is( 'selection' ) ).to.be.true;
+			expect( documentSelection.is( 'view:selection' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return true for documentSelection', () => {
 		it( 'should return true for documentSelection', () => {
 			expect( documentSelection.is( 'documentSelection' ) ).to.be.true;
 			expect( documentSelection.is( 'documentSelection' ) ).to.be.true;
+			expect( documentSelection.is( 'view:documentSelection' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other values', () => {
 		it( 'should return false for other values', () => {
 			expect( documentSelection.is( 'node' ) ).to.be.false;
 			expect( documentSelection.is( 'node' ) ).to.be.false;
+			expect( documentSelection.is( 'view:node' ) ).to.be.false;
 			expect( documentSelection.is( 'text' ) ).to.be.false;
 			expect( documentSelection.is( 'text' ) ).to.be.false;
+			expect( documentSelection.is( 'view:text' ) ).to.be.false;
 			expect( documentSelection.is( 'textProxy' ) ).to.be.false;
 			expect( documentSelection.is( 'textProxy' ) ).to.be.false;
 			expect( documentSelection.is( 'element' ) ).to.be.false;
 			expect( documentSelection.is( 'element' ) ).to.be.false;
 			expect( documentSelection.is( 'rootElement' ) ).to.be.false;
 			expect( documentSelection.is( 'rootElement' ) ).to.be.false;
+			expect( documentSelection.is( 'model:selection' ) ).to.be.false;
+			expect( documentSelection.is( 'model:documentSelection' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 

+ 50 - 8
packages/ckeditor5-engine/tests/view/editableelement.js

@@ -5,16 +5,58 @@
 
 
 import createDocumentMock from '../../tests/view/_utils/createdocumentmock';
 import createDocumentMock from '../../tests/view/_utils/createdocumentmock';
 
 
-import RootEditableElement from '../../src/view/rooteditableelement';
+import EditableElement from '../../src/view/editableelement';
 import Range from '../../src/view/range';
 import Range from '../../src/view/range';
 import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 
 describe( 'EditableElement', () => {
 describe( 'EditableElement', () => {
+	describe( 'is', () => {
+		let el;
+
+		before( () => {
+			el = new EditableElement( 'div' );
+		} );
+
+		it( 'should return true for containerElement/editable/element, also with correct name and element name', () => {
+			expect( el.is( 'containerElement' ) ).to.be.true;
+			expect( el.is( 'view:containerElement' ) ).to.be.true;
+			expect( el.is( 'containerElement', 'div' ) ).to.be.true;
+			expect( el.is( 'view:containerElement', 'div' ) ).to.be.true;
+			expect( el.is( 'editableElement' ) ).to.be.true;
+			expect( el.is( 'view:editableElement' ) ).to.be.true;
+			expect( el.is( 'editableElement', 'div' ) ).to.be.true;
+			expect( el.is( 'view:editableElement', 'div' ) ).to.be.true;
+			expect( el.is( 'element' ) ).to.be.true;
+			expect( el.is( 'view:element' ) ).to.be.true;
+			expect( el.is( 'element', 'div' ) ).to.be.true;
+			expect( el.is( 'view:element', 'div' ) ).to.be.true;
+			expect( el.is( 'div' ) ).to.be.true;
+			expect( el.is( 'view:div' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( el.is( 'rootElement', 'p' ) ).to.be.false;
+			expect( el.is( 'view:rootElement', 'p' ) ).to.be.false;
+			expect( el.is( 'containerElement', 'p' ) ).to.be.false;
+			expect( el.is( 'view:containerElement', 'p' ) ).to.be.false;
+			expect( el.is( 'element', 'p' ) ).to.be.false;
+			expect( el.is( 'view:element', 'p' ) ).to.be.false;
+			expect( el.is( 'p' ) ).to.be.false;
+			expect( el.is( 'view:p' ) ).to.be.false;
+			expect( el.is( 'text' ) ).to.be.false;
+			expect( el.is( 'textProxy' ) ).to.be.false;
+			expect( el.is( 'attributeElement' ) ).to.be.false;
+			expect( el.is( 'uiElement' ) ).to.be.false;
+			expect( el.is( 'emptyElement' ) ).to.be.false;
+			expect( el.is( 'documentFragment' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'document', () => {
 	describe( 'document', () => {
 		let element, docMock;
 		let element, docMock;
 
 
 		beforeEach( () => {
 		beforeEach( () => {
-			element = new RootEditableElement( 'div' );
+			element = new EditableElement( 'div' );
 			docMock = createDocumentMock();
 			docMock = createDocumentMock();
 		} );
 		} );
 
 
@@ -51,16 +93,16 @@ describe( 'EditableElement', () => {
 		beforeEach( () => {
 		beforeEach( () => {
 			docMock = createDocumentMock();
 			docMock = createDocumentMock();
 
 
-			viewMain = new RootEditableElement( 'div' );
+			viewMain = new EditableElement( 'div' );
 			viewMain._document = docMock;
 			viewMain._document = docMock;
 
 
-			viewHeader = new RootEditableElement( 'h1' );
+			viewHeader = new EditableElement( 'h1' );
 			viewHeader._document = docMock;
 			viewHeader._document = docMock;
 			viewHeader.rootName = 'header';
 			viewHeader.rootName = 'header';
 		} );
 		} );
 
 
 		it( 'should be observable', () => {
 		it( 'should be observable', () => {
-			const root = new RootEditableElement( 'div' );
+			const root = new EditableElement( 'div' );
 			root._document = createDocumentMock();
 			root._document = createDocumentMock();
 
 
 			expect( root.isFocused ).to.be.false;
 			expect( root.isFocused ).to.be.false;
@@ -114,7 +156,7 @@ describe( 'EditableElement', () => {
 
 
 	describe( 'isReadOnly', () => {
 	describe( 'isReadOnly', () => {
 		it( 'should be observable', () => {
 		it( 'should be observable', () => {
-			const root = new RootEditableElement( 'div' );
+			const root = new EditableElement( 'div' );
 			root._document = createDocumentMock();
 			root._document = createDocumentMock();
 
 
 			expect( root.isReadOnly ).to.be.false;
 			expect( root.isReadOnly ).to.be.false;
@@ -131,7 +173,7 @@ describe( 'EditableElement', () => {
 		} );
 		} );
 
 
 		it( 'should be bound to the document#isReadOnly', () => {
 		it( 'should be bound to the document#isReadOnly', () => {
-			const root = new RootEditableElement( 'div' );
+			const root = new EditableElement( 'div' );
 			root._document = createDocumentMock();
 			root._document = createDocumentMock();
 
 
 			root.document.isReadOnly = false;
 			root.document.isReadOnly = false;
@@ -147,7 +189,7 @@ describe( 'EditableElement', () => {
 	describe( 'getDocument', () => {
 	describe( 'getDocument', () => {
 		it( 'should return document', () => {
 		it( 'should return document', () => {
 			const docMock = createDocumentMock();
 			const docMock = createDocumentMock();
-			const root = new RootEditableElement( 'div' );
+			const root = new EditableElement( 'div' );
 			root._document = docMock;
 			root._document = docMock;
 
 
 			expect( root.document ).to.equal( docMock );
 			expect( root.document ).to.equal( docMock );

+ 10 - 1
packages/ckeditor5-engine/tests/view/element.js

@@ -82,7 +82,7 @@ describe( 'Element', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		let el;
 		let el;
 
 
 		before( () => {
 		before( () => {
@@ -91,21 +91,30 @@ describe( 'Element', () => {
 
 
 		it( 'should return true for node, element, element with correct name and element name', () => {
 		it( 'should return true for node, element, element with correct name and element name', () => {
 			expect( el.is( 'node' ) ).to.be.true;
 			expect( el.is( 'node' ) ).to.be.true;
+			expect( el.is( 'view:node' ) ).to.be.true;
 			expect( el.is( 'element' ) ).to.be.true;
 			expect( el.is( 'element' ) ).to.be.true;
+			expect( el.is( 'view:element' ) ).to.be.true;
 			expect( el.is( 'element', 'p' ) ).to.be.true;
 			expect( el.is( 'element', 'p' ) ).to.be.true;
+			expect( el.is( 'view:element', 'p' ) ).to.be.true;
 			expect( el.is( 'p' ) ).to.be.true;
 			expect( el.is( 'p' ) ).to.be.true;
+			expect( el.is( 'view:p' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other accept values', () => {
 		it( 'should return false for other accept values', () => {
 			expect( el.is( 'element', 'span' ) ).to.be.false;
 			expect( el.is( 'element', 'span' ) ).to.be.false;
+			expect( el.is( 'view:element', 'span' ) ).to.be.false;
 			expect( el.is( 'span' ) ).to.be.false;
 			expect( el.is( 'span' ) ).to.be.false;
+			expect( el.is( 'view:span' ) ).to.be.false;
 			expect( el.is( 'text' ) ).to.be.false;
 			expect( el.is( 'text' ) ).to.be.false;
+			expect( el.is( 'view:text' ) ).to.be.false;
 			expect( el.is( 'textProxy' ) ).to.be.false;
 			expect( el.is( 'textProxy' ) ).to.be.false;
 			expect( el.is( 'containerElement' ) ).to.be.false;
 			expect( el.is( 'containerElement' ) ).to.be.false;
 			expect( el.is( 'attributeElement' ) ).to.be.false;
 			expect( el.is( 'attributeElement' ) ).to.be.false;
 			expect( el.is( 'uiElement' ) ).to.be.false;
 			expect( el.is( 'uiElement' ) ).to.be.false;
 			expect( el.is( 'emptyElement' ) ).to.be.false;
 			expect( el.is( 'emptyElement' ) ).to.be.false;
+			expect( el.is( 'view:emptyElement' ) ).to.be.false;
 			expect( el.is( 'rootElement' ) ).to.be.false;
 			expect( el.is( 'rootElement' ) ).to.be.false;
+			expect( el.is( 'view:ootElement' ) ).to.be.false;
 			expect( el.is( 'documentFragment' ) ).to.be.false;
 			expect( el.is( 'documentFragment' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );

+ 10 - 1
packages/ckeditor5-engine/tests/view/emptyelement.js

@@ -20,7 +20,7 @@ describe( 'EmptyElement', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		let el;
 		let el;
 
 
 		before( () => {
 		before( () => {
@@ -29,17 +29,26 @@ describe( 'EmptyElement', () => {
 
 
 		it( 'should return true for emptyElement/element, also with correct name and element name', () => {
 		it( 'should return true for emptyElement/element, also with correct name and element name', () => {
 			expect( el.is( 'emptyElement' ) ).to.be.true;
 			expect( el.is( 'emptyElement' ) ).to.be.true;
+			expect( el.is( 'view:emptyElement' ) ).to.be.true;
 			expect( el.is( 'emptyElement', 'p' ) ).to.be.true;
 			expect( el.is( 'emptyElement', 'p' ) ).to.be.true;
+			expect( el.is( 'view:emptyElement', 'p' ) ).to.be.true;
 			expect( el.is( 'element' ) ).to.be.true;
 			expect( el.is( 'element' ) ).to.be.true;
+			expect( el.is( 'view:element' ) ).to.be.true;
 			expect( el.is( 'element', 'p' ) ).to.be.true;
 			expect( el.is( 'element', 'p' ) ).to.be.true;
+			expect( el.is( 'view:element', 'p' ) ).to.be.true;
 			expect( el.is( 'p' ) ).to.be.true;
 			expect( el.is( 'p' ) ).to.be.true;
+			expect( el.is( 'view:p' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other accept values', () => {
 		it( 'should return false for other accept values', () => {
 			expect( el.is( 'emptyElement', 'span' ) ).to.be.false;
 			expect( el.is( 'emptyElement', 'span' ) ).to.be.false;
+			expect( el.is( 'view:emptyElement', 'span' ) ).to.be.false;
 			expect( el.is( 'element', 'span' ) ).to.be.false;
 			expect( el.is( 'element', 'span' ) ).to.be.false;
+			expect( el.is( 'view:element', 'span' ) ).to.be.false;
 			expect( el.is( 'span' ) ).to.be.false;
 			expect( el.is( 'span' ) ).to.be.false;
+			expect( el.is( 'view:span' ) ).to.be.false;
 			expect( el.is( 'text' ) ).to.be.false;
 			expect( el.is( 'text' ) ).to.be.false;
+			expect( el.is( 'view:text' ) ).to.be.false;
 			expect( el.is( 'textProxy' ) ).to.be.false;
 			expect( el.is( 'textProxy' ) ).to.be.false;
 			expect( el.is( 'containerElement' ) ).to.be.false;
 			expect( el.is( 'containerElement' ) ).to.be.false;
 			expect( el.is( 'attributeElement' ) ).to.be.false;
 			expect( el.is( 'attributeElement' ) ).to.be.false;

+ 20 - 2
packages/ckeditor5-engine/tests/view/node.js

@@ -31,10 +31,28 @@ describe( 'Node', () => {
 	} );
 	} );
 
 
 	describe( 'is()', () => {
 	describe( 'is()', () => {
-		it( 'should return true for node', () => {
-			const node = new Node();
+		let node;
 
 
+		beforeEach( () => {
+			node = new Node();
+		} );
+
+		it( 'should return true for node', () => {
 			expect( node.is( 'node' ) ).to.be.true;
 			expect( node.is( 'node' ) ).to.be.true;
+			expect( node.is( 'view:node' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( node.is( 'rootElement' ) ).to.be.false;
+			expect( node.is( 'containerElement' ) ).to.be.false;
+			expect( node.is( 'element' ) ).to.be.false;
+			expect( node.is( 'p' ) ).to.be.false;
+			expect( node.is( 'text' ) ).to.be.false;
+			expect( node.is( 'textProxy' ) ).to.be.false;
+			expect( node.is( 'attributeElement' ) ).to.be.false;
+			expect( node.is( 'uiElement' ) ).to.be.false;
+			expect( node.is( 'emptyElement' ) ).to.be.false;
+			expect( node.is( 'documentFragment' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 

+ 30 - 3
packages/ckeditor5-engine/tests/view/position.js

@@ -24,10 +24,37 @@ describe( 'Position', () => {
 
 
 	describe( 'constructor()', () => {
 	describe( 'constructor()', () => {
 		it( 'should create element without attributes', () => {
 		it( 'should create element without attributes', () => {
-			const elem = new Position( parentMock, 5 );
+			const position = new Position( parentMock, 5 );
 
 
-			expect( elem ).to.have.property( 'parent' ).that.equals( parentMock );
-			expect( elem ).to.have.property( 'offset' ).that.equals( 5 );
+			expect( position ).to.have.property( 'parent' ).that.equals( parentMock );
+			expect( position ).to.have.property( 'offset' ).that.equals( 5 );
+		} );
+	} );
+
+	describe( 'is()', () => {
+		let position;
+
+		beforeEach( () => {
+			position = new Position( parentMock, 5 );
+		} );
+
+		it( 'should return true for "position"', () => {
+			expect( position.is( 'position' ) ).to.be.true;
+			expect( position.is( 'view:position' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( position.is( 'rootElement' ) ).to.be.false;
+			expect( position.is( 'containerElement' ) ).to.be.false;
+			expect( position.is( 'element' ) ).to.be.false;
+			expect( position.is( 'p' ) ).to.be.false;
+			expect( position.is( 'text' ) ).to.be.false;
+			expect( position.is( 'textProxy' ) ).to.be.false;
+			expect( position.is( 'attributeElement' ) ).to.be.false;
+			expect( position.is( 'uiElement' ) ).to.be.false;
+			expect( position.is( 'emptyElement' ) ).to.be.false;
+			expect( position.is( 'documentFragment' ) ).to.be.false;
+			expect( position.is( 'model:position' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 

+ 28 - 0
packages/ckeditor5-engine/tests/view/range.js

@@ -43,6 +43,34 @@ describe( 'Range', () => {
 		} );
 		} );
 	} );
 	} );
 
 
+	describe( 'is()', () => {
+		let range;
+
+		before( () => {
+			const start = new Position( {}, 1 );
+			range = new Range( start );
+		} );
+
+		it( 'should return true for "range"', () => {
+			expect( range.is( 'range' ) ).to.be.true;
+			expect( range.is( 'view:range' ) ).to.be.true;
+		} );
+
+		it( 'should return false for other accept values', () => {
+			expect( range.is( 'rootElement' ) ).to.be.false;
+			expect( range.is( 'containerElement' ) ).to.be.false;
+			expect( range.is( 'element' ) ).to.be.false;
+			expect( range.is( 'p' ) ).to.be.false;
+			expect( range.is( 'text' ) ).to.be.false;
+			expect( range.is( 'textProxy' ) ).to.be.false;
+			expect( range.is( 'attributeElement' ) ).to.be.false;
+			expect( range.is( 'uiElement' ) ).to.be.false;
+			expect( range.is( 'emptyElement' ) ).to.be.false;
+			expect( range.is( 'documentFragment' ) ).to.be.false;
+			expect( range.is( 'model:range' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'iterator', () => {
 	describe( 'iterator', () => {
 		it( 'should iterate over the range returning tree walker values', () => {
 		it( 'should iterate over the range returning tree walker values', () => {
 			const range = getRange( '<p>fo{o</p><p>bar</p><p>xy}z</p>' );
 			const range = getRange( '<p>fo{o</p><p>bar</p><p>xy}z</p>' );

+ 16 - 1
packages/ckeditor5-engine/tests/view/rooteditableelement.js

@@ -38,7 +38,7 @@ describe( 'RootEditableElement', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		let el;
 		let el;
 
 
 		before( () => {
 		before( () => {
@@ -47,27 +47,42 @@ describe( 'RootEditableElement', () => {
 
 
 		it( 'should return true for rootElement/containerElement/editable/element, also with correct name and element name', () => {
 		it( 'should return true for rootElement/containerElement/editable/element, also with correct name and element name', () => {
 			expect( el.is( 'rootElement' ) ).to.be.true;
 			expect( el.is( 'rootElement' ) ).to.be.true;
+			expect( el.is( 'view:rootElement' ) ).to.be.true;
 			expect( el.is( 'rootElement', 'div' ) ).to.be.true;
 			expect( el.is( 'rootElement', 'div' ) ).to.be.true;
+			expect( el.is( 'view:rootElement', 'div' ) ).to.be.true;
 			expect( el.is( 'containerElement' ) ).to.be.true;
 			expect( el.is( 'containerElement' ) ).to.be.true;
+			expect( el.is( 'view:containerElement' ) ).to.be.true;
 			expect( el.is( 'containerElement', 'div' ) ).to.be.true;
 			expect( el.is( 'containerElement', 'div' ) ).to.be.true;
+			expect( el.is( 'view:containerElement', 'div' ) ).to.be.true;
 			expect( el.is( 'editableElement' ) ).to.be.true;
 			expect( el.is( 'editableElement' ) ).to.be.true;
+			expect( el.is( 'view:editableElement' ) ).to.be.true;
 			expect( el.is( 'editableElement', 'div' ) ).to.be.true;
 			expect( el.is( 'editableElement', 'div' ) ).to.be.true;
+			expect( el.is( 'view:editableElement', 'div' ) ).to.be.true;
 			expect( el.is( 'element' ) ).to.be.true;
 			expect( el.is( 'element' ) ).to.be.true;
+			expect( el.is( 'view:element' ) ).to.be.true;
 			expect( el.is( 'element', 'div' ) ).to.be.true;
 			expect( el.is( 'element', 'div' ) ).to.be.true;
+			expect( el.is( 'view:element', 'div' ) ).to.be.true;
 			expect( el.is( 'div' ) ).to.be.true;
 			expect( el.is( 'div' ) ).to.be.true;
+			expect( el.is( 'view:div' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other accept values', () => {
 		it( 'should return false for other accept values', () => {
 			expect( el.is( 'rootElement', 'p' ) ).to.be.false;
 			expect( el.is( 'rootElement', 'p' ) ).to.be.false;
+			expect( el.is( 'view:rootElement', 'p' ) ).to.be.false;
 			expect( el.is( 'containerElement', 'p' ) ).to.be.false;
 			expect( el.is( 'containerElement', 'p' ) ).to.be.false;
+			expect( el.is( 'view:containerElement', 'p' ) ).to.be.false;
 			expect( el.is( 'element', 'p' ) ).to.be.false;
 			expect( el.is( 'element', 'p' ) ).to.be.false;
+			expect( el.is( 'view:element', 'p' ) ).to.be.false;
 			expect( el.is( 'p' ) ).to.be.false;
 			expect( el.is( 'p' ) ).to.be.false;
+			expect( el.is( 'view:p' ) ).to.be.false;
 			expect( el.is( 'text' ) ).to.be.false;
 			expect( el.is( 'text' ) ).to.be.false;
+			expect( el.is( 'view:text' ) ).to.be.false;
 			expect( el.is( 'textProxy' ) ).to.be.false;
 			expect( el.is( 'textProxy' ) ).to.be.false;
 			expect( el.is( 'attributeElement' ) ).to.be.false;
 			expect( el.is( 'attributeElement' ) ).to.be.false;
 			expect( el.is( 'uiElement' ) ).to.be.false;
 			expect( el.is( 'uiElement' ) ).to.be.false;
 			expect( el.is( 'emptyElement' ) ).to.be.false;
 			expect( el.is( 'emptyElement' ) ).to.be.false;
 			expect( el.is( 'documentFragment' ) ).to.be.false;
 			expect( el.is( 'documentFragment' ) ).to.be.false;
+			expect( el.is( 'model:rootElement' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 

+ 4 - 1
packages/ckeditor5-engine/tests/view/selection.js

@@ -600,18 +600,21 @@ describe( 'Selection', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		it( 'should return true for selection', () => {
 		it( 'should return true for selection', () => {
 			expect( selection.is( 'selection' ) ).to.be.true;
 			expect( selection.is( 'selection' ) ).to.be.true;
+			expect( selection.is( 'view:selection' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other values', () => {
 		it( 'should return false for other values', () => {
 			expect( selection.is( 'documentSelection' ) ).to.be.false;
 			expect( selection.is( 'documentSelection' ) ).to.be.false;
+			expect( selection.is( 'view:documentSelection' ) ).to.be.false;
 			expect( selection.is( 'node' ) ).to.be.false;
 			expect( selection.is( 'node' ) ).to.be.false;
 			expect( selection.is( 'text' ) ).to.be.false;
 			expect( selection.is( 'text' ) ).to.be.false;
 			expect( selection.is( 'textProxy' ) ).to.be.false;
 			expect( selection.is( 'textProxy' ) ).to.be.false;
 			expect( selection.is( 'element' ) ).to.be.false;
 			expect( selection.is( 'element' ) ).to.be.false;
 			expect( selection.is( 'rootElement' ) ).to.be.false;
 			expect( selection.is( 'rootElement' ) ).to.be.false;
+			expect( selection.is( 'model:selection' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 

+ 7 - 1
packages/ckeditor5-engine/tests/view/text.js

@@ -17,7 +17,7 @@ describe( 'Text', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		let text;
 		let text;
 
 
 		before( () => {
 		before( () => {
@@ -26,18 +26,24 @@ describe( 'Text', () => {
 
 
 		it( 'should return true for node, text', () => {
 		it( 'should return true for node, text', () => {
 			expect( text.is( 'node' ) ).to.be.true;
 			expect( text.is( 'node' ) ).to.be.true;
+			expect( text.is( 'view:node' ) ).to.be.true;
 			expect( text.is( 'text' ) ).to.be.true;
 			expect( text.is( 'text' ) ).to.be.true;
+			expect( text.is( 'view:text' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other accept values', () => {
 		it( 'should return false for other accept values', () => {
 			expect( text.is( 'textProxy' ) ).to.be.false;
 			expect( text.is( 'textProxy' ) ).to.be.false;
+			expect( text.is( 'view:textProxy' ) ).to.be.false;
 			expect( text.is( 'element' ) ).to.be.false;
 			expect( text.is( 'element' ) ).to.be.false;
+			expect( text.is( 'view:element' ) ).to.be.false;
 			expect( text.is( 'containerElement' ) ).to.be.false;
 			expect( text.is( 'containerElement' ) ).to.be.false;
 			expect( text.is( 'attributeElement' ) ).to.be.false;
 			expect( text.is( 'attributeElement' ) ).to.be.false;
 			expect( text.is( 'uiElement' ) ).to.be.false;
 			expect( text.is( 'uiElement' ) ).to.be.false;
 			expect( text.is( 'emptyElement' ) ).to.be.false;
 			expect( text.is( 'emptyElement' ) ).to.be.false;
 			expect( text.is( 'rootElement' ) ).to.be.false;
 			expect( text.is( 'rootElement' ) ).to.be.false;
 			expect( text.is( 'documentFragment' ) ).to.be.false;
 			expect( text.is( 'documentFragment' ) ).to.be.false;
+			expect( text.is( 'model:text' ) ).to.be.false;
+			expect( text.is( 'model:node' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 

+ 5 - 1
packages/ckeditor5-engine/tests/view/textproxy.js

@@ -61,14 +61,17 @@ describe( 'TextProxy', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'is', () => {
+	describe( 'is()', () => {
 		it( 'should return true for textProxy', () => {
 		it( 'should return true for textProxy', () => {
 			expect( textProxy.is( 'textProxy' ) ).to.be.true;
 			expect( textProxy.is( 'textProxy' ) ).to.be.true;
+			expect( textProxy.is( 'view:textProxy' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other accept values', () => {
 		it( 'should return false for other accept values', () => {
 			expect( textProxy.is( 'node' ) ).to.be.false;
 			expect( textProxy.is( 'node' ) ).to.be.false;
+			expect( textProxy.is( 'view:node' ) ).to.be.false;
 			expect( textProxy.is( 'text' ) ).to.be.false;
 			expect( textProxy.is( 'text' ) ).to.be.false;
+			expect( textProxy.is( 'view:text' ) ).to.be.false;
 			expect( textProxy.is( 'element' ) ).to.be.false;
 			expect( textProxy.is( 'element' ) ).to.be.false;
 			expect( textProxy.is( 'containerElement' ) ).to.be.false;
 			expect( textProxy.is( 'containerElement' ) ).to.be.false;
 			expect( textProxy.is( 'attributeElement' ) ).to.be.false;
 			expect( textProxy.is( 'attributeElement' ) ).to.be.false;
@@ -76,6 +79,7 @@ describe( 'TextProxy', () => {
 			expect( textProxy.is( 'emptyElement' ) ).to.be.false;
 			expect( textProxy.is( 'emptyElement' ) ).to.be.false;
 			expect( textProxy.is( 'rootElement' ) ).to.be.false;
 			expect( textProxy.is( 'rootElement' ) ).to.be.false;
 			expect( textProxy.is( 'documentFragment' ) ).to.be.false;
 			expect( textProxy.is( 'documentFragment' ) ).to.be.false;
+			expect( textProxy.is( 'model:textProxy' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 

+ 13 - 0
packages/ckeditor5-engine/tests/view/uielement.js

@@ -46,16 +46,26 @@ describe( 'UIElement', () => {
 
 
 		it( 'should return true for uiElement/element, also with correct name and element name', () => {
 		it( 'should return true for uiElement/element, also with correct name and element name', () => {
 			expect( el.is( 'uiElement' ) ).to.be.true;
 			expect( el.is( 'uiElement' ) ).to.be.true;
+			expect( el.is( 'view:uiElement' ) ).to.be.true;
 			expect( el.is( 'uiElement', 'span' ) ).to.be.true;
 			expect( el.is( 'uiElement', 'span' ) ).to.be.true;
+			expect( el.is( 'view:uiElement', 'span' ) ).to.be.true;
 			expect( el.is( 'element' ) ).to.be.true;
 			expect( el.is( 'element' ) ).to.be.true;
+			expect( el.is( 'view:element' ) ).to.be.true;
+			expect( el.is( 'node' ) ).to.be.true;
+			expect( el.is( 'view:node' ) ).to.be.true;
 			expect( el.is( 'element', 'span' ) ).to.be.true;
 			expect( el.is( 'element', 'span' ) ).to.be.true;
+			expect( el.is( 'view:element', 'span' ) ).to.be.true;
 			expect( el.is( 'span' ) ).to.be.true;
 			expect( el.is( 'span' ) ).to.be.true;
+			expect( el.is( 'view:span' ) ).to.be.true;
 		} );
 		} );
 
 
 		it( 'should return false for other accept values', () => {
 		it( 'should return false for other accept values', () => {
 			expect( el.is( 'uiElement', 'p' ) ).to.be.false;
 			expect( el.is( 'uiElement', 'p' ) ).to.be.false;
+			expect( el.is( 'view:uiElement', 'p' ) ).to.be.false;
 			expect( el.is( 'element', 'p' ) ).to.be.false;
 			expect( el.is( 'element', 'p' ) ).to.be.false;
+			expect( el.is( 'view:element', 'p' ) ).to.be.false;
 			expect( el.is( 'p' ) ).to.be.false;
 			expect( el.is( 'p' ) ).to.be.false;
+			expect( el.is( 'view:p' ) ).to.be.false;
 			expect( el.is( 'text' ) ).to.be.false;
 			expect( el.is( 'text' ) ).to.be.false;
 			expect( el.is( 'textProxy' ) ).to.be.false;
 			expect( el.is( 'textProxy' ) ).to.be.false;
 			expect( el.is( 'containerElement' ) ).to.be.false;
 			expect( el.is( 'containerElement' ) ).to.be.false;
@@ -63,6 +73,9 @@ describe( 'UIElement', () => {
 			expect( el.is( 'emptyElement' ) ).to.be.false;
 			expect( el.is( 'emptyElement' ) ).to.be.false;
 			expect( el.is( 'rootElement' ) ).to.be.false;
 			expect( el.is( 'rootElement' ) ).to.be.false;
 			expect( el.is( 'documentFragment' ) ).to.be.false;
 			expect( el.is( 'documentFragment' ) ).to.be.false;
+			expect( el.is( 'model:element' ) ).to.be.false;
+			expect( el.is( 'model:span' ) ).to.be.false;
+			expect( el.is( 'model:node' ) ).to.be.false;
 		} );
 		} );
 	} );
 	} );
 
 

+ 30 - 0
packages/ckeditor5-engine/tests/view/upcastwriter.js

@@ -328,6 +328,36 @@ describe( 'UpcastWriter', () => {
 		} );
 		} );
 	} );
 	} );
 
 
+	describe( 'unwrapElement', () => {
+		it( 'should unwrap simple element', () => {
+			const documentFragment = dataprocessor.toView( '<ul><li><p>foo</p></li></ul>' );
+			const paragraph = documentFragment.getChild( 0 ).getChild( 0 ).getChild( 0 );
+
+			writer.unwrapElement( paragraph );
+
+			expect( dataprocessor.toData( documentFragment ) ).to.equal( '<ul><li>foo</li></ul>' );
+		} );
+
+		it( 'should unwrap element with children', () => {
+			const documentFragment = dataprocessor.toView(
+				'<p><span style="color:red"><strong>foo</strong><a href="example.com">example</a>bar</span></p>' );
+			const span = documentFragment.getChild( 0 ).getChild( 0 );
+
+			writer.unwrapElement( span );
+
+			expect( dataprocessor.toData( documentFragment ) ).to.equal(
+				'<p><strong>foo</strong><a href="example.com">example</a>bar</p>' );
+		} );
+
+		it( 'should do nothing for elements without parent', () => {
+			const element = new Element( 'p', null, 'foo' );
+
+			writer.unwrapElement( element );
+
+			expect( dataprocessor.toData( element ) ).to.equal( '<p>foo</p>' );
+		} );
+	} );
+
 	describe( 'rename', () => {
 	describe( 'rename', () => {
 		it( 'should rename simple element', () => {
 		it( 'should rename simple element', () => {
 			const el = view.getChild( 0 ).getChild( 1 );
 			const el = view.getChild( 0 ).getChild( 1 );

+ 9 - 1
packages/ckeditor5-engine/theme/placeholder.css

@@ -4,7 +4,8 @@
  */
  */
 
 
 /* See ckeditor/ckeditor5#936. */
 /* See ckeditor/ckeditor5#936. */
-.ck.ck-placeholder, .ck .ck-placeholder {
+.ck.ck-placeholder,
+.ck .ck-placeholder {
 	&::before {
 	&::before {
 		content: attr(data-placeholder);
 		content: attr(data-placeholder);
 
 
@@ -12,3 +13,10 @@
 		pointer-events: none;
 		pointer-events: none;
 	}
 	}
 }
 }
+
+/* See ckeditor/ckeditor5#1987. */
+.ck.ck-read-only .ck-placeholder {
+	&::before {
+		display: none;
+	}
+}