Переглянути джерело

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

# Conflicts:
#	src/view/view.js
Maciej Gołaszewski 6 роки тому
батько
коміт
d18db3569a
100 змінених файлів з 3422 додано та 771 видалено
  1. 4 6
      packages/ckeditor5-engine/.travis.yml
  2. 52 0
      packages/ckeditor5-engine/CHANGELOG.md
  3. 0 2
      packages/ckeditor5-engine/README.md
  4. 0 0
      packages/ckeditor5-engine/docs/_snippets/framework/build-extending-content-source.html
  5. 15 0
      packages/ckeditor5-engine/docs/_snippets/framework/build-extending-content-source.js
  6. 12 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-external-link-target.html
  7. 47 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-external-link-target.js
  8. 14 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-heading-class.html
  9. 33 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-heading-class.js
  10. 14 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-link-class.html
  11. 43 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-link-class.js
  12. 13 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-unsafe-link-class.html
  13. 43 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-unsafe-link-class.js
  14. 12 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-div-attributes.html
  15. 74 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-div-attributes.js
  16. 33 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-link-target.html
  17. 51 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-link-target.js
  18. 7 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-arbitrary-attribute-values.html
  19. 69 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-arbitrary-attribute-values.js
  20. 23 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.html
  21. 169 0
      packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.js
  22. 1 1
      packages/ckeditor5-engine/docs/api/engine.md
  23. 359 0
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-extending-output.md
  24. 128 0
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-introduction.md
  25. 454 0
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-preserving-custom-content.md
  26. 18 18
      packages/ckeditor5-engine/package.json
  27. 4 4
      packages/ckeditor5-engine/src/controller/datacontroller.js
  28. 1 1
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  29. 7 4
      packages/ckeditor5-engine/src/conversion/conversion.js
  30. 15 10
      packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
  31. 15 15
      packages/ckeditor5-engine/src/conversion/downcasthelpers.js
  32. 67 4
      packages/ckeditor5-engine/src/conversion/mapper.js
  33. 22 1
      packages/ckeditor5-engine/src/conversion/upcastdispatcher.js
  34. 18 15
      packages/ckeditor5-engine/src/conversion/upcasthelpers.js
  35. 1 1
      packages/ckeditor5-engine/src/conversion/viewconsumable.js
  36. 1 2
      packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js
  37. 1 2
      packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js
  38. 39 5
      packages/ckeditor5-engine/src/model/differ.js
  39. 2 0
      packages/ckeditor5-engine/src/model/document.js
  40. 10 3
      packages/ckeditor5-engine/src/model/documentfragment.js
  41. 39 40
      packages/ckeditor5-engine/src/model/documentselection.js
  42. 20 12
      packages/ckeditor5-engine/src/model/element.js
  43. 22 1
      packages/ckeditor5-engine/src/model/liveposition.js
  44. 20 0
      packages/ckeditor5-engine/src/model/liverange.js
  45. 24 6
      packages/ckeditor5-engine/src/model/markercollection.js
  46. 29 19
      packages/ckeditor5-engine/src/model/model.js
  47. 49 25
      packages/ckeditor5-engine/src/model/node.js
  48. 12 6
      packages/ckeditor5-engine/src/model/nodelist.js
  49. 3 1
      packages/ckeditor5-engine/src/model/operation/attributeoperation.js
  50. 1 1
      packages/ckeditor5-engine/src/model/operation/detachoperation.js
  51. 2 1
      packages/ckeditor5-engine/src/model/operation/insertoperation.js
  52. 7 7
      packages/ckeditor5-engine/src/model/operation/mergeoperation.js
  53. 4 13
      packages/ckeditor5-engine/src/model/operation/moveoperation.js
  54. 4 2
      packages/ckeditor5-engine/src/model/operation/renameoperation.js
  55. 4 0
      packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js
  56. 4 4
      packages/ckeditor5-engine/src/model/operation/splitoperation.js
  57. 32 29
      packages/ckeditor5-engine/src/model/operation/transform.js
  58. 10 4
      packages/ckeditor5-engine/src/model/operation/utils.js
  59. 70 8
      packages/ckeditor5-engine/src/model/position.js
  60. 22 1
      packages/ckeditor5-engine/src/model/range.js
  61. 28 3
      packages/ckeditor5-engine/src/model/rootelement.js
  62. 43 15
      packages/ckeditor5-engine/src/model/schema.js
  63. 73 51
      packages/ckeditor5-engine/src/model/selection.js
  64. 17 3
      packages/ckeditor5-engine/src/model/text.js
  65. 11 5
      packages/ckeditor5-engine/src/model/textproxy.js
  66. 5 1
      packages/ckeditor5-engine/src/model/treewalker.js
  67. 29 14
      packages/ckeditor5-engine/src/model/utils/insertcontent.js
  68. 45 23
      packages/ckeditor5-engine/src/model/writer.js
  69. 17 11
      packages/ckeditor5-engine/src/utils/bindtwostepcarettoattribute.js
  70. 31 4
      packages/ckeditor5-engine/src/view/attributeelement.js
  71. 28 3
      packages/ckeditor5-engine/src/view/containerelement.js
  72. 35 9
      packages/ckeditor5-engine/src/view/document.js
  73. 10 3
      packages/ckeditor5-engine/src/view/documentfragment.js
  74. 14 8
      packages/ckeditor5-engine/src/view/documentselection.js
  75. 116 45
      packages/ckeditor5-engine/src/view/domconverter.js
  76. 74 69
      packages/ckeditor5-engine/src/view/downcastwriter.js
  77. 29 4
      packages/ckeditor5-engine/src/view/editableelement.js
  78. 21 10
      packages/ckeditor5-engine/src/view/element.js
  79. 32 4
      packages/ckeditor5-engine/src/view/emptyelement.js
  80. 18 40
      packages/ckeditor5-engine/src/view/filler.js
  81. 41 14
      packages/ckeditor5-engine/src/view/node.js
  82. 1 1
      packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js
  83. 44 0
      packages/ckeditor5-engine/src/view/observer/inputobserver.js
  84. 1 1
      packages/ckeditor5-engine/src/view/observer/keyobserver.js
  85. 6 10
      packages/ckeditor5-engine/src/view/observer/selectionobserver.js
  86. 1 3
      packages/ckeditor5-engine/src/view/placeholder.js
  87. 24 3
      packages/ckeditor5-engine/src/view/position.js
  88. 19 0
      packages/ckeditor5-engine/src/view/range.js
  89. 76 26
      packages/ckeditor5-engine/src/view/renderer.js
  90. 30 3
      packages/ckeditor5-engine/src/view/rooteditableelement.js
  91. 17 11
      packages/ckeditor5-engine/src/view/selection.js
  92. 16 2
      packages/ckeditor5-engine/src/view/text.js
  93. 12 5
      packages/ckeditor5-engine/src/view/textproxy.js
  94. 5 1
      packages/ckeditor5-engine/src/view/treewalker.js
  95. 29 4
      packages/ckeditor5-engine/src/view/uielement.js
  96. 18 1
      packages/ckeditor5-engine/src/view/upcastwriter.js
  97. 38 30
      packages/ckeditor5-engine/src/view/view.js
  98. 31 31
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  99. 32 16
      packages/ckeditor5-engine/tests/conversion/conversion.js
  100. 36 10
      packages/ckeditor5-engine/tests/conversion/downcastdispatcher.js

+ 4 - 6
packages/ckeditor5-engine/.travis.yml

@@ -1,9 +1,11 @@
 sudo: required
-dist: trusty
+dist: xenial
 addons:
   chrome: stable
   firefox: latest
 language: node_js
+services:
+- xvfb
 node_js:
 - '8'
 cache:
@@ -13,8 +15,6 @@ branches:
   - stable
 before_install:
 - export START_TIME=$( date +%s )
-- export DISPLAY=:99.0
-- sh -e /etc/init.d/xvfb start
 - npm i -g yarn
 install:
 - yarn add @ckeditor/ckeditor5-dev-tests
@@ -28,6 +28,4 @@ after_script:
 - ckeditor5-dev-tests-notify-travis-status
 env:
   global:
-  - secure: CSFKXnTb2oQW8YIjh+SWXYSYzx1jcIg1wGcl6jWrbaOU06JHEO77YLKlhMxLfRniFtilBEksK4ugIcn+lihykhHleMxI27+kifILdLLZKvwWlZiDrtpJSzLk08SpRNDwwthSlHVKrxbHVFE1K9SYpX4s3qTpxullDMPgc13OP4c=
-  - secure: WGdwDEavTOIE/yw8pdpWmx3Cehm2/GN8gZMimuot8sWPbOZRuPh+2lnVWpASLe25qJUKKeLWYLRHccpMLKbyweiordSEBwzNrOGOFyUuaDPITNZZrp5ekh2vHBp1+mwhRpRekdyWdzzVsm01RkFrnJqQlaXxX9i/r9Tu4EsK54M=
-  - secure: BKoFnOWcfKn8zaTvilMn9UORF+gR1zBqetqai5FKfSKraJXqiBrz1AI3ecytCjJOKS1g6HoRtZHwVS4a+uG3JjK3bRERQF2NyHiyjbrsL+2HaKaRnVmox8PC/59m1PJlcRKAV9RTYJqL/oqa7N8OBegt4LpIVnEcil8WCErCF6Q=
+  - secure: GMesvAe0tAa8QfdhGApzmzjhf9mpfjL6jMx8aFqpRli5tuyX9JsjNjFELnU/MMlNDyySPwXAuQVrvcWkCFy9a3n705tGt/VdmStCrTdwV2oRNmNKqGgFovmiK3an+RAIts2PX2iguDjwiogqkAW0FstWAMdG0Yk4AVUxKC1niIo=

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

@@ -1,6 +1,58 @@
 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)
+
+Internal changes only (updated dependencies, documentation, etc.).
+
+
+## [13.2.0](https://github.com/ckeditor/ckeditor5-engine/compare/v13.1.1...v13.2.0) (2019-07-04)
+
+### Features
+
+* Added `view.Document#event:beforeinput`. ([c74c3d6](https://github.com/ckeditor/ckeditor5-engine/commit/c74c3d6))
+* Introduced the `type` parameter to the `model.createBatch()` method. ([389b72e](https://github.com/ckeditor/ckeditor5-engine/commit/389b72e))
+
+### Bug fixes
+
+* `model.Writer#insert()` will no longer crash when the data to set contains markers that are already in the editor content. Closes [#1721](https://github.com/ckeditor/ckeditor5-engine/issues/1721). ([4ff0656](https://github.com/ckeditor/ckeditor5-engine/commit/4ff0656))
+* Selection will not change during forbidden copy-paste operation inside table cell. Closes [ckeditor/ckeditor5#1380](https://github.com/ckeditor/ckeditor5/issues/1380). ([ab15b17](https://github.com/ckeditor/ckeditor5-engine/commit/ab15b17))
+
+### Other changes
+
+* Changed how ` `s are generated on the view->DOM rendering. Closes [#1747](https://github.com/ckeditor/ckeditor5-engine/issues/1747). ([da5670a](https://github.com/ckeditor/ckeditor5-engine/commit/da5670a))
+
+
 ## [13.1.1](https://github.com/ckeditor/ckeditor5-engine/compare/v13.1.0...v13.1.1) (2019-06-05)
 
 ### Bug fixes

+ 0 - 2
packages/ckeditor5-engine/README.md

@@ -1,10 +1,8 @@
 CKEditor 5 editing engine
 ========================================
 
-[![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-engine.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine)
 [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-engine.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-engine)
-[![BrowserStack Status](https://automate.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://automate.browserstack.com/public-build/d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)
 [![Coverage Status](https://coveralls.io/repos/github/ckeditor/ckeditor5-engine/badge.svg?branch=master)](https://coveralls.io/github/ckeditor/ckeditor5-engine?branch=master)
 <br>
 [![Dependency Status](https://david-dm.org/ckeditor/ckeditor5-engine/status.svg)](https://david-dm.org/ckeditor/ckeditor5-engine)

+ 0 - 0
packages/ckeditor5-engine/docs/_snippets/framework/build-extending-content-source.html


+ 15 - 0
packages/ckeditor5-engine/docs/_snippets/framework/build-extending-content-source.js

@@ -0,0 +1,15 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals window */
+
+import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
+import Code from '@ckeditor/ckeditor5-basic-styles/src/code';
+import Font from '@ckeditor/ckeditor5-font/src/font';
+
+ClassicEditor.builtinPlugins.push( Code );
+ClassicEditor.builtinPlugins.push( Font );
+
+window.ClassicEditor = ClassicEditor;

+ 12 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-external-link-target.html

@@ -0,0 +1,12 @@
+<style>
+	#snippet-link-external + .ck-editor .ck-content a[target="_blank"]::after {
+		content: '\29C9';
+	}
+</style>
+
+<div id="snippet-link-external">
+	<p>External links in this <a href="https://ckeditor.com">editor</a> have a <code>target="_blank"</code>
+	<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes">attribute</a> and will open in a new
+	<a href="https://developer.mozilla.org/en-US/docs/Web/API/Window">window</a>.</p>
+	<p>Edit the URL of the links including "ckeditor.com" and other domains to see them marked as "internal" or "external".</p>
+</div>

+ 47 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-external-link-target.js

@@ -0,0 +1,47 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function AddTargetToExternalLinks( editor ) {
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+			const viewSelection = viewWriter.document.selection;
+			const viewElement = viewWriter.createAttributeElement( 'a', {
+				target: '_blank'
+			}, {
+				priority: 5
+			} );
+
+			if ( data.attributeNewValue.match( /ckeditor\.com/ ) ) {
+				viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+			} else {
+				if ( data.item.is( 'selection' ) ) {
+					viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
+				} else {
+					viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+				}
+			}
+		}, { priority: 'low' } );
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-link-external' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ AddTargetToExternalLinks ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 14 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-heading-class.html

@@ -0,0 +1,14 @@
+<style>
+	.ck-content h2.my-heading {
+		font-family: Georgia, Times, Times New Roman, serif;
+		border-left: 6px solid #fd0000;
+		padding-left: .8em;
+		padding: .1em .8em;
+	}
+</style>
+
+<div id="snippet-heading-class">
+	<h2>Heading with <code>.my-heading</code> class</h2>
+	<h3>Regular heading</h3>
+	<p>Some content.</p>
+</div>

+ 33 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-heading-class.js

@@ -0,0 +1,33 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function AddClassToAllHeading1( editor ) {
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		dispatcher.on( 'insert:heading1', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+
+			viewWriter.addClass( 'my-heading', conversionApi.mapper.toViewElement( data.item ) );
+		}, { priority: 'low' } );
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-heading-class' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ AddClassToAllHeading1 ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 14 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-link-class.html

@@ -0,0 +1,14 @@
+<style>
+	.my-green-link {
+		color: #209a25;
+		border: 1px solid #209a25;
+		border-radius: 2px;
+		padding: 0 3px;
+		box-shadow: 1px 1px 0 0 #209a25;
+	}
+</style>
+
+<div id="snippet-link-classes">
+	<p>All links in this <a href="https://ckeditor.com">editor</a> have a custom <code>.my-green-link</code>
+	CSS <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class">class</a> attribute.</p>
+</div>

+ 43 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-link-class.js

@@ -0,0 +1,43 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function AddClassToAllLinks( editor ) {
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+			const viewSelection = viewWriter.document.selection;
+			const viewElement = viewWriter.createAttributeElement( 'a', {
+				class: 'my-green-link'
+			}, {
+				priority: 5
+			} );
+
+			if ( data.item.is( 'selection' ) ) {
+				viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
+			} else {
+				viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+			}
+		}, { priority: 'low' } );
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-link-classes' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ AddClassToAllLinks ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 13 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-unsafe-link-class.html

@@ -0,0 +1,13 @@
+<style>
+	.unsafe-link {
+		padding: 0 2px;
+		outline: 2px dashed red;
+		background: #ffff00;
+	}
+</style>
+
+<div id="snippet-link-unsafe-classes">
+	<p>All links in this <a href="https://ckeditor.com">editor</a> that do not use the <a href="http://developer.mozilla.org/en-US/docs/Glossary/https">HTTPS</a> protocol
+	have a custom <code>.unsafe-link</code> CSS <a href="http://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class">class</a> that marks them red.</p>
+	<p>Edit the URL of the links using "http://" or "https://" to see them marked as "safe" or "unsafe".</p>
+</div>

+ 43 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-add-unsafe-link-class.js

@@ -0,0 +1,43 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function AddClassToUnsafeLinks( editor ) {
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+			const viewSelection = viewWriter.document.selection;
+			const viewElement = viewWriter.createAttributeElement( 'a', { class: 'unsafe-link' }, { priority: 5 } );
+
+			if ( data.attributeNewValue.match( /http:\/\// ) ) {
+				if ( data.item.is( 'selection' ) ) {
+					viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
+				} else {
+					viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+				}
+			} else {
+				viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+			}
+		}, { priority: 'low' } );
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-link-unsafe-classes' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ AddClassToUnsafeLinks ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 12 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-div-attributes.html

@@ -0,0 +1,12 @@
+<div id="snippet-div-attributes">
+	<div id="special-section-a" style="background: #eafbd7;padding:.8em .8em 0;margin-bottom:.8em;border:1px solid #8bc34a;">
+		<p><strong>Special section A</strong>: It has set "style" and "id" attributes.</p>
+	</div>
+
+	<p>Regular content of the editor.</p>
+
+	<div id="special-section-b" style="background: #ffeed0;padding:.8em .8em 0;margin-bottom:.8em;border:1px solid #ff8f00;" spellcheck="false">
+		<p><strong>Special section B</strong>: It has set "style", "id" and spellcheck="false" attributes.</p>
+		<p>This section disables the native browser spellchecker.</p>
+	</div>
+</div>

+ 74 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-div-attributes.js

@@ -0,0 +1,74 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function ConvertDivAttributes( editor ) {
+	// Allow <div> elements in the model.
+	editor.model.schema.register( 'div', {
+		allowWhere: '$block',
+		allowContentOf: '$root'
+	} );
+
+	// Allow <div> elements in the model to have all attributes.
+	editor.model.schema.addAttributeCheck( context => {
+		if ( context.endsWith( 'div' ) ) {
+			return true;
+		}
+	} );
+
+	// View-to-model converter converting a view <div> with all its attributes to the model.
+	editor.conversion.for( 'upcast' ).elementToElement( {
+		view: 'div',
+		model: ( viewElement, modelWriter ) => {
+			return modelWriter.createElement( 'div', viewElement.getAttributes() );
+		}
+	} );
+
+	// Model-to-view converter for the <div> element (attrbiutes are converted separately).
+	editor.conversion.for( 'downcast' ).elementToElement( {
+		model: 'div',
+		view: 'div'
+	} );
+
+	// Model-to-view converter for <div> attributes.
+	// Note that a lower-level, event-based API is used here.
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		dispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
+			// Convert <div> attributes only.
+			if ( data.item.name != 'div' ) {
+				return;
+			}
+
+			const viewWriter = conversionApi.writer;
+			const viewDiv = conversionApi.mapper.toViewElement( data.item );
+
+			// In the model-to-view conversion we convert changes. An attribute can be added or removed or changed.
+			// The below code handles all 3 cases.
+			if ( data.attributeNewValue ) {
+				viewWriter.setAttribute( data.attributeKey, data.attributeNewValue, viewDiv );
+			} else {
+				viewWriter.removeAttribute( data.attributeKey, viewDiv );
+			}
+		} );
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-div-attributes' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ ConvertDivAttributes ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 33 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-link-target.html

@@ -0,0 +1,33 @@
+<style>
+	#snippet-link-target + .ck-editor .ck-content a[target]::after {
+		content: "target=\"" attr(target) "\"";
+		font-size: 0.6em;
+		position: relative;
+		left: 0em;
+		top: -1em;
+		background: #00ffa6;
+		color: #000;
+		padding: 1px 3px;
+		border-radius: 10px;
+	}
+
+	#snippet-link-target-content {
+		width: 100%;
+		height: 8em;
+		font-family: monospace;
+	}
+</style>
+
+<div id="snippet-link-target">
+	<p>This <a href="https://ckeditor.com" target="_blank">editor</a> allows <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a" target="_parent">links</a> with a <code>target</code> attribute to be loaded into the content.</p>
+</div>
+
+<p><b>Note</b>: You can play with the content to see that different link <code>target</code> values are also handled.</p>
+
+<textarea id="snippet-link-target-content">
+&lt;p&gt;A few links with different targets:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&#9;&lt;li&gt;&lt;a href=&quot;https://ckeditor.com&quot; target=&quot;_blank&quot;&gt;CKEditor&lt;/a&gt;&lt;/li&gt;&#10;&#9;&lt;li&gt;&lt;a href=&quot;https://ckeditor.com/ckfinder/&quot;&gt;CKFinder&lt;/a&gt;&lt;/li&gt;&#10;&#9;&lt;li&gt;&lt;a href=&quot;https://cksource.com&quot; target=&quot;_parent&quot;&gt;CKSource&lt;/a&gt;&lt;/li&gt;&#10;&#9;&lt;li&gt;&lt;a href=&quot;https://cksource.com/team/&quot; target=&quot;_top&quot;&gt;Team&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;
+</textarea>
+
+<p>
+	<button type="button" id="snippet-link-target-content-update">Set editor data</button>
+</p>

+ 51 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-allow-link-target.js

@@ -0,0 +1,51 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function AllowLinkTarget( editor ) {
+	editor.model.schema.extend( '$text', { allowAttributes: 'linkTarget' } );
+
+	editor.conversion.for( 'downcast' ).attributeToElement( {
+		model: 'linkTarget',
+		view: ( attributeValue, writer ) => {
+			const linkElement = writer.createAttributeElement( 'a', { target: attributeValue }, { priority: 5 } );
+			writer.setCustomProperty( 'link', true, linkElement );
+
+			return linkElement;
+		},
+		converterPriority: 'low'
+	} );
+
+	editor.conversion.for( 'upcast' ).attributeToAttribute( {
+		view: {
+			name: 'a',
+			key: 'target'
+		},
+		model: 'linkTarget',
+		converterPriority: 'low'
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-link-target' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ AllowLinkTarget ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+
+		document.querySelector( '#snippet-link-target-content-update' ).addEventListener( 'click', () => {
+			editor.setData( document.querySelector( '#snippet-link-target-content' ).value );
+		} );
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 7 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-arbitrary-attribute-values.html

@@ -0,0 +1,7 @@
+<div id="snippet-arbitrary-attribute-values">
+	<ul>
+		<li>test: <span style="font-size:13.5px">13.5px (gets rounded to 14px automatically)</span></li>
+		<li>test: <span style="font-size:32px">32px</span></li>
+		<li>test: <span style="font-size:8px">8px</span></li>
+	</ul>
+</div>

+ 69 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-arbitrary-attribute-values.js

@@ -0,0 +1,69 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+function HandleFontSizeValue( editor ) {
+	// Add a special catch-all converter for the font size feature.
+	editor.conversion.for( 'upcast' ).elementToAttribute( {
+		view: {
+			name: 'span',
+			styles: {
+				'font-size': /[\s\S]+/
+			}
+		},
+		model: {
+			key: 'fontSize',
+			value: viewElement => {
+				const value = parseFloat( viewElement.getStyle( 'font-size' ) ).toFixed( 0 );
+
+				// It might be needed to further convert the value to meet business requirements.
+				// In the sample the font size is configured to handle only the sizes:
+				// 12, 14, 'default', 18, 20, 22, 24, 26, 28, 30
+				// Other sizes will be converted to the model but the UI might not be aware of them.
+
+				// The font size feature expects numeric values to be Number, not String.
+				return parseInt( value );
+			}
+		},
+		converterPriority: 'high'
+	} );
+
+	// Add a special converter for the font size feature to convert all (even not configured)
+	// model attribute values.
+	editor.conversion.for( 'downcast' ).attributeToElement( {
+		model: {
+			key: 'fontSize'
+		},
+		view: ( modelValue, viewWriter ) => {
+			return viewWriter.createAttributeElement( 'span', {
+				style: `font-size:${ modelValue }px`
+			} );
+		},
+		converterPriority: 'high'
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-arbitrary-attribute-values' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ HandleFontSizeValue ],
+		toolbar: {
+			items: [ 'heading', '|', 'bold', 'italic', '|', 'fontSize' ],
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		},
+		fontSize: {
+			options: [ 10, 12, 14, 'default', 18, 20, 22 ]
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+

+ 23 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.html

@@ -0,0 +1,23 @@
+<head>
+	<style>
+		.ck-content figure.table-foo table,
+		.ck-content figure.img-foo {
+			border: 2px solid deeppink;
+		}
+	</style>
+</head>
+
+<div id="snippet-custom-figure-attributes">
+	<h2>Image:</h2>
+	<figure class="image">
+		<img alt="bar" class="img-foo" id="img-foo-1" src="%BASE_PATH%/assets/img/fields.jpg">
+		<figcaption>Caption</figcaption>
+	</figure>
+
+	<h2>Table:</h2>
+	<table class="table-foo" id="table-foo-1" >
+		<tr>
+			<td>foo</td>
+		</tr>
+	</table>
+</div>

+ 169 - 0
packages/ckeditor5-engine/docs/_snippets/framework/extending-content-custom-figure-attributes.js

@@ -0,0 +1,169 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals ClassicEditor, console, window, document */
+
+import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
+
+/**
+ * Plugin that converts custom attributes for elements that are wrapped in <figure> in the view.
+ */
+function CustomFigureAttributes( editor ) {
+	// Define on which elements the CSS classes should be preserved:
+	setupCustomClassConversion( 'img', 'image', editor );
+	setupCustomClassConversion( 'table', 'table', editor );
+
+	editor.conversion.for( 'upcast' ).add( upcastCustomClasses( 'figure' ), { priority: 'low' } );
+
+	// Define custom attributes that should be preserved.
+	setupCustomAttributeConversion( 'img', 'image', 'id', editor );
+	setupCustomAttributeConversion( 'table', 'table', 'id', editor );
+}
+
+/**
+ * Sets up a conversion that preservers classes on <img> and <table> elements.
+ */
+function setupCustomClassConversion( viewElementName, modelElementName, editor ) {
+	// The 'customClass' attribute will store custom classes from the data in the model so schema definitions allow this attribute.
+	editor.model.schema.extend( modelElementName, { allowAttributes: [ 'customClass' ] } );
+
+	// Define upcast converters for the <img> and <table> elements with a "low" priority so they are run after the default converters.
+	editor.conversion.for( 'upcast' ).add( upcastCustomClasses( viewElementName ), { priority: 'low' } );
+
+	// Define downcast converters for a model element with a "low" priority so they are run after the default converters.
+	editor.conversion.for( 'downcast' ).add( downcastCustomClasses( modelElementName ), { priority: 'low' } );
+}
+
+/**
+ * Sets up a conversion for a custom attribute on view elements contained inside a <figure>.
+ *
+ * This method:
+ * - Adds proper schema rules.
+ * - Adds an upcast converter.
+ * - Adds a downcast converter.
+ */
+function setupCustomAttributeConversion( viewElementName, modelElementName, viewAttribute, editor ) {
+	// Extend the schema to store an attribute in the model.
+	const modelAttribute = `custom${ viewAttribute }`;
+
+	editor.model.schema.extend( modelElementName, { allowAttributes: [ modelAttribute ] } );
+
+	editor.conversion.for( 'upcast' ).add( upcastAttribute( viewElementName, viewAttribute, modelAttribute ) );
+	editor.conversion.for( 'downcast' ).add( downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) );
+}
+
+/**
+ * Creates an upcast converter that will pass all classes from the view element to the model element.
+ */
+function upcastCustomClasses( elementName ) {
+	return dispatcher => dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
+		const viewItem = data.viewItem;
+		const modelRange = data.modelRange;
+
+		const modelElement = modelRange && modelRange.start.nodeAfter;
+
+		if ( !modelElement ) {
+			return;
+		}
+
+		// The upcast conversion picks up classes from the base element and from the <figure> element so it should be extensible.
+		const currentAttributeValue = modelElement.getAttribute( 'customClass' ) || [];
+
+		currentAttributeValue.push( ...viewItem.getClassNames() );
+
+		conversionApi.writer.setAttribute( 'customClass', currentAttributeValue, modelElement );
+	} );
+}
+
+/**
+ * Creates a downcast converter that adds classes defined in the `customClass` attribute to a given view element.
+ *
+ * This converter expects that the view element is nested in a <figure> element.
+ */
+function downcastCustomClasses( modelElementName ) {
+	return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
+		const modelElement = data.item;
+
+		const viewFigure = conversionApi.mapper.toViewElement( modelElement );
+
+		if ( !viewFigure ) {
+			return;
+		}
+
+		// The code below assumes that classes are set on the <figure> element...
+		conversionApi.writer.addClass( modelElement.getAttribute( 'customClass' ), viewFigure );
+
+		// ... but if you prefer the classes to be passed to the <img> element, find the view element inside the <figure>:
+		//
+		// const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
+		//
+		// conversionApi.writer.addClass( modelElement.getAttribute( 'customClass' ), viewElement );
+	} );
+}
+
+/**
+ * Helper method that searches for a given view element in all children of the model element.
+ *
+ * @param {module:engine/view/item~Item} viewElement
+ * @param {String} viewElementName
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
+ * @return {module:engine/view/item~Item}
+ */
+function findViewChild( viewElement, viewElementName, conversionApi ) {
+	const viewChildren = Array.from( conversionApi.writer.createRangeIn( viewElement ).getItems() );
+
+	return viewChildren.find( item => item.is( viewElementName ) );
+}
+
+/**
+ * Returns the custom attribute upcast converter.
+ */
+function upcastAttribute( viewElementName, viewAttribute, modelAttribute ) {
+	return dispatcher => dispatcher.on( `element:${ viewElementName }`, ( evt, data, conversionApi ) => {
+		const viewItem = data.viewItem;
+		const modelRange = data.modelRange;
+
+		const modelElement = modelRange && modelRange.start.nodeAfter;
+
+		if ( !modelElement ) {
+			return;
+		}
+
+		conversionApi.writer.setAttribute( modelAttribute, viewItem.getAttribute( viewAttribute ), modelElement );
+	} );
+}
+
+/**
+ * Returns the custom attribute downcast converter.
+ */
+function downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) {
+	return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
+		const modelElement = data.item;
+
+		const viewFigure = conversionApi.mapper.toViewElement( modelElement );
+		const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
+
+		if ( !viewElement ) {
+			return;
+		}
+
+		conversionApi.writer.setAttribute( viewAttribute, modelElement.getAttribute( modelAttribute ), viewElement );
+	} );
+}
+
+ClassicEditor
+	.create( document.querySelector( '#snippet-custom-figure-attributes' ), {
+		cloudServices: CS_CONFIG,
+		extraPlugins: [ CustomFigureAttributes ],
+		toolbar: {
+			viewportTopOffset: window.getViewportTopOffsetConfig()
+		}
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 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 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)

+ 359 - 0
packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-extending-output.md

@@ -0,0 +1,359 @@
+---
+category: framework-deep-dive-conversion
+menu-title: Extending editor output
+order: 20
+---
+
+{@snippet framework/build-extending-content-source}
+
+# Extending the editor output
+
+This guide focuses on customization of the one–way {@link framework/guides/architecture/editing-engine#editing-pipeline "downcast"} pipeline of CKEditor 5. This pipeline transforms the data from the model to the editing view and the output data. The following examples do not customize the model and do not process the (input) data &mdash; you can picture them as post–processors (filters) applied to the output only.
+
+If you want to learn how to load some extra content (element, attributes, classes) into the rich-text editor, check out the {@link framework/guides/deep-dive/conversion-preserving-custom-content next guide} of this section.
+
+## Before starting
+
+### Code architecture
+
+It is recommended that the code that customizes the editor data and editing pipelines is delivered as {@link framework/guides/architecture/core-editor-architecture#plugins plugins} and all examples in this guide follow this convention.
+
+Also for the sake of simplicity all examples use the same {@link module:editor-classic/classiceditor~ClassicEditor `ClassicEditor`}, but keep in mind that code snippets will work with other editors, too.
+
+Finally, none of the converters covered in this guide require to import any module from CKEditor 5 Framework, hence, you can write them without rebuilding the editor. In other words, such converters can easily be added to existing {@link builds/guides/overview CKEditor 5 builds}.
+
+### Granular converters
+
+You can create separate converters for the data and editing (downcast) pipelines. The former (`dataDowncast`) will customize the data in the editor output (e.g. when {@link builds/guides/integration/saving-data#manually-retrieving-the-data obtaining the editor data}). The latter (`editingDowncast`) will only work for the content of the editor when editing.
+
+If you do not want to complicate your conversion, you can just add a single (`downcast`) converter which will apply both to the data and the editing view. We did that in all examples to keep them simple but keep in mind you have options:
+
+```js
+// Adds a conversion dispatcher for the editing downcast pipeline only.
+editor.conversion.for( 'editingDowncast' ).add( dispatcher => {
+	// ...
+} );
+
+// Adds a conversion dispatcher for the data downcast pipeline only.
+editor.conversion.for( 'dataDowncast' ).add( dispatcher => {
+	// ...
+} );
+
+// Adds a conversion dispatcher for both the data and the editing downcast pipelines.
+editor.conversion.for( 'downcast' ).add( dispatcher => {
+	// ...
+} );
+```
+
+### CKEditor 5 inspector
+
+{@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} is an invaluable help when working with the model and view structures. It allows browsing their structure and checking selection positions like in typical browser developer tools. Make sure to enable the inspector when playing with CKEditor 5.
+
+## Adding a CSS class to inline elements
+
+In this example all links (`<a href="...">...</a>`) get the `.my-green-link` CSS class. This includes all links in the editor output (`editor.getData()`) and all links in the edited content (existing and future ones).
+
+<info-box>
+	Note that the same behavior can be obtained with {@link features/link#custom-link-attributes-decorators link decorators}:
+
+	```js
+	ClassicEditor
+		.create( ..., {
+			// ...
+			link: {
+				decorators: {
+					addGreenLink: {
+						mode: 'automatic',
+						attributes: {
+							class: 'my-green-link'
+						}
+					}
+				}
+			}
+		} )
+	```
+</info-box>
+
+{@snippet framework/extending-content-add-link-class}
+
+A custom CSS class is added to all links by a custom converter plugged into the downcast pipeline, following the default converters brought by the {@link features/link link} feature:
+
+```js
+// This plugin brings customization to the downcast pipeline of the editor.
+function AddClassToAllLinks( editor ) {
+	// Both the data and the editing pipelines are affected by this conversion.
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		// Links are represented in the model as a "linkHref" attribute.
+		// Use the "low" listener priority to apply the changes after the link feature.
+		dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+			const viewSelection = viewWriter.document.selection;
+
+			// Adding a new CSS class is done by wrapping all link ranges and selection
+			// in a new attribute element with a class.
+			const viewElement = viewWriter.createAttributeElement( 'a', {
+					class: 'my-green-link'
+				}, {
+					priority: 5
+				} );
+
+			if ( data.item.is( 'selection' ) ) {
+				viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
+			} else {
+				viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+			}
+		}, { priority: 'low' } );
+	} );
+}
+```
+
+Activate the plugin in the editor:
+
+```js
+ClassicEditor
+	.create( ..., {
+		extraPlugins: [ AddClassToAllLinks ],
+	} )
+	.then( editor => {
+		// ...
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```
+
+Add some CSS styles for `.my-green-link` to see the customization in action:
+
+```css
+.my-green-link {
+	color: #209a25;
+	border: 1px solid #209a25;
+	border-radius: 2px;
+	padding: 0 3px;
+	box-shadow: 1px 1px 0 0 #209a25;
+}
+```
+
+## Adding an HTML attribute to certain inline elements
+
+In this example all links (`<a href="...">...</a>`) that do not have "ckeditor.com" in their `href="..."` get the `target="_blank"` attribute. This includes all links in the editor output (`editor.getData()`) and all links in the edited content (existing and future ones).
+
+<info-box>
+	Note that similar behavior can be obtained with {@link module:link/link~LinkConfig#addTargetToExternalLinks link decorators}:
+
+	```js
+	ClassicEditor
+		.create( ..., {
+			// ...
+			link: {
+				addTargetToExternalLinks: true
+			}
+		} )
+	```
+</info-box>
+
+{@snippet framework/extending-content-add-external-link-target}
+
+The `target` attribute is added to all "external" links by a custom converter plugged into the downcast pipeline, following the default converters brought by the {@link features/link link} feature:
+
+```js
+// This plugin brings customization to the downcast pipeline of the editor.
+function AddTargetToExternalLinks( editor ) {
+	// Both the data and the editing pipelines are affected by this conversion.
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		// Links are represented in the model as a "linkHref" attribute.
+		// Use the "low" listener priority to apply the changes after the link feature.
+		dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+			const viewSelection = viewWriter.document.selection;
+
+			// Adding a new CSS class is done by wrapping all link ranges and selection
+			// in a new attribute element with the "target" attribute.
+			const viewElement = viewWriter.createAttributeElement( 'a', {
+					target: '_blank'
+				}, {
+					priority: 5
+				} );
+
+			if ( data.attributeNewValue.match( /ckeditor\.com/ ) ) {
+				viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+			} else {
+				if ( data.item.is( 'selection' ) ) {
+					viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
+				} else {
+					viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+				}
+			}
+		}, { priority: 'low' } );
+	} );
+}
+```
+
+Activate the plugin in the editor:
+
+```js
+ClassicEditor
+	.create( ..., {
+		extraPlugins: [ AddTargetToExternalLinks ],
+	} )
+	.then( editor => {
+		// ...
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```
+
+Add some CSS styles for links with `target="_blank"` to mark them with with the "&#10697;" symbol:
+
+```css
+a[target="_blank"]::after {
+	content: '\29C9';
+}
+```
+
+## Adding a CSS class to certain inline elements
+
+In this example all links (`<a href="...">...</a>`) that do not have `https://` in their `href="..."` attribute get the `.unsafe-link` CSS class. This includes all links in the editor output (`editor.getData()`) and all links in the edited content (existing and future ones).
+
+<info-box>
+	Note that the same behavior can be obtained with {@link features/link#custom-link-attributes-decorators link decorators}:
+
+	```js
+	ClassicEditor
+		.create( ..., {
+			// ...
+			link: {
+				decorators: {
+					markUnsafeLink: {
+						mode: 'automatic',
+						callback: url => /^(http:)?\/\//.test( url ),
+						attributes: {
+							class: 'unsafe-link'
+						}
+					}
+				}
+			}
+		} )
+	```
+</info-box>
+
+{@snippet framework/extending-content-add-unsafe-link-class}
+
+The `.unsafe-link` CSS class is added to all "unsafe" links by a custom converter plugged into the downcast pipeline, following the default converters brought by the {@link features/link link} feature:
+
+```js
+// This plugin brings customization to the downcast pipeline of the editor.
+function AddClassToUnsafeLinks( editor ) {
+	// Both the data and the editing pipelines are affected by this conversion.
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		// Links are represented in the model as a "linkHref" attribute.
+		// Use the "low" listener priority to apply the changes after the link feature.
+		dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+			const viewSelection = viewWriter.document.selection;
+
+			// Adding a new CSS class is done by wrapping all link ranges and selection
+			// in a new attribute element with the "target" attribute.
+			const viewElement = viewWriter.createAttributeElement( 'a', {
+					class: 'unsafe-link'
+				}, {
+					priority: 5
+				} );
+
+			if ( data.attributeNewValue.match( /http:\/\// ) ) {
+				if ( data.item.is( 'selection' ) ) {
+					viewWriter.wrap( viewSelection.getFirstRange(), viewElement );
+				} else {
+					viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+				}
+			} else {
+				viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement );
+			}
+		}, { priority: 'low' } );
+	} );
+}
+```
+
+Activate the plugin in the editor:
+
+```js
+ClassicEditor
+	.create( ..., {
+		extraPlugins: [ AddClassToUnsafeLinks ],
+	} )
+	.then( editor => {
+		// ...
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```
+
+Add some CSS styles for "unsafe" links to make them visible:
+
+```css
+.unsafe-link {
+	padding: 0 2px;
+	outline: 2px dashed red;
+	background: #ffff00;
+}
+```
+
+## Adding a CSS class to block elements
+
+In this example all second–level headings (`<h2>...</h2>`) get the `.my-heading` CSS class. This includes all heading elements in the editor output (`editor.getData()`) and in the edited content (existing and future ones).
+
+{@snippet framework/extending-content-add-heading-class}
+
+A custom CSS class is added to all `<h2>...</h2>` elements by a custom converter plugged into the downcast pipeline, following the default converters brought by the {@link features/headings headings} feature:
+
+<info-box>
+	The `heading1` element in the model corresponds to `<h2>...</h2>` in the output HTML because in the default {@link features/headings#configuring-heading-levels headings feature configuration} `<h1>...</h1>` is reserved for the top–most heading of the webpage.
+</info-box>
+
+```js
+// This plugin brings customization to the downcast pipeline of the editor.
+function AddClassToAllHeading1( editor ) {
+	// Both the data and the editing pipelines are affected by this conversion.
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		// Headings are represented in the model as a "heading1" element.
+		// Use the "low" listener priority to apply the changes after the headings feature.
+		dispatcher.on( 'insert:heading1', ( evt, data, conversionApi ) => {
+			const viewWriter = conversionApi.writer;
+
+			viewWriter.addClass( 'my-heading', conversionApi.mapper.toViewElement( data.item ) );
+		}, { priority: 'low' } );
+	} );
+}
+```
+
+Activate the plugin in the editor:
+
+```js
+ClassicEditor
+	.create( ..., {
+		extraPlugins: [ AddClassToAllHeading1 ],
+	} )
+	.then( editor => {
+		// ...
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```
+
+Add some CSS styles for `.my-heading` to see the customization in action:
+
+```css
+.my-heading {
+	font-family: Georgia, Times, Times New Roman, serif;
+	border-left: 6px solid #fd0000;
+	padding-left: .8em;
+	padding: .1em .8em;
+}
+```
+
+## What's next?
+
+If you would like to read more about how to make CKEditor 5 accept more content, refer to the {@link framework/guides/deep-dive/conversion-preserving-custom-content Preserving custom content} guide.

+ 128 - 0
packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-introduction.md

@@ -0,0 +1,128 @@
+---
+category: framework-deep-dive-conversion
+menu-title: Advanced concepts
+order: 10
+
+# IMPORTANT:
+# This guide is meant to become "Introduction to conversion" later on, hence the file name.
+# For now, due to lack of content, it is called "advanced concepts".
+---
+
+# Advanced conversion concepts
+
+This guide extends the {@link framework/guides/architecture/editing-engine introduction to CKEditor 5 editing engine architecture}. Therefore, we highly recommend reading the former guide first.
+
+In this guide we will dive deeper into some of the conversion concepts.
+
+## Inline and block content
+
+Generally speaking, there are two main types of the content in the editor view and data output: inline and block.
+
+The inline content means elements like `<strong>`, `<a>` or `<span>`. Unlike `<p>`, `<blockquote>` or `<div>`, inline elements do not structure the data. Instead, they mark some text in a specific (visual and semantical) way. These elements are a characteristic of a text. For instance, you could say that some part of the text is bold, or is linked, etc.. This concept has its reflection in the model of the rich-text editor where `<a>` or `<strong>` are not represented as elements. Instead, they are attributes of the text.
+
+For example &mdash; in the model, you might have a `<paragraph>` element with the "Foo bar" text, where "bar" has the `bold` attribute set to `true`. A pseudo–code of this *model* data structure could look as follows:
+
+```html
+<paragraph>
+	"Foo "			// no attributes
+	"bar"			// bold=true
+</paragraph>
+```
+
+<info-box>
+	Throughout the rest of this guide the following, shorter convention will be used to represent model text attributes:
+
+	```html
+	<paragraph>Foo <$text bold="true">bar</$text></paragraph>
+	```
+</info-box>
+
+Note that there is no `<strong>` or any other additional element there, it is just some text with an attribute.
+
+So, when does this text become wrapped with a `<strong>` element? This happens during the conversion to the view. It is also important to know which type of a view element needs to be used. In case of elements that represent inline formatting, this should be an {@link module:engine/view/attributeelement~AttributeElement}.
+
+## Conversion of multiple text attributes
+
+A model text node may have multiple attributes (e.g. be bolded and linked) and all of them are converted to their respective view elements by independent converters.
+
+Keep in mind that in the model, attributes do not have any specific order. This is contrary to the editor view or HTML output, where inline elements are nested in one another. Fortunately, the nesting happens automatically during the conversion from the model to the view. This makes working in the model simpler, as features do not need to take care of breaking or rearranging elements in the model.
+
+For instance, consider the following model structure:
+
+```html
+<paragraph>
+	<$text bold="true" linkHref="url">Foo </$text>
+	<$text linkHref="url">bar</$text>
+	<$text bold="true"> baz</$text>
+</paragraph>
+```
+
+During the conversion, it will be converted to the following view structure:
+
+```html
+<p>
+	<a href="url"><strong>Foo </strong>bar</a><strong> baz</strong>
+</p>
+```
+
+Note that the `<a>` element is converted in such way that it always becomes the "topmost" element. This is intentional so that no element ever breaks a link, which would otherwise look as follows:
+
+```html
+<p>
+	<strong><a href="url">Foo </a></strong><a href="url">bar</a><strong> baz</strong>
+</p>
+```
+
+There are two links with the same `href` attribute next to each other in the generated view (editor output), which is semantically wrong. To make sure that it never happens, the view element that represents a link must have a *priority* defined. Most elements, like for instance `<strong>`, do not care about it and stick to the default priority (`10`). The {@link features/link link feature} ensures that all view `<a>` elements have the priority set to `5` so they are kept outside other elements.
+
+## Merging attribute elements during conversion
+
+Most of the simple view inline elements like `<strong>` or `<em>` do not have any attributes. Some of them have just one, for instance `<a>` has its `href`.
+
+But it is easy to come up with features that style a part of a text in a more complex way. An example would be the {@link features/font font family feature}. When used, it adds the `fontFamily` attribute to the text in the model, which is later converted to a `<span>` element with a corresponding `style` attribute.
+
+So what would happen if several attributes were set on the same part of the text? Take this model example where `fontSize` is used next to `fontFamily`:
+
+```html
+<paragraph>
+	<$text fontFamily="Tahoma" fontSize="big">foo</$text>
+</paragraph>
+```
+
+CKEditor 5 features are implemented in a granular way, which means that e.g. the font size converter is completely independent from the font family converter. This means that the above example is converted as follows:
+
+* `fontFamily="value"` converts to `<span style="font-family: value;">`,
+* `fontSize="value"` converts to `<span class="text-value">`.
+
+And, in theory, you could expect the following HTML as a result:
+
+```html
+<p>
+	<span style="font-family: Tahoma;">
+		<span class="text-big">foo</span>
+	</span>
+</p>
+```
+
+But this is not the most optimal output you can get from the rich-text editor. Why not have just one `<span>` element instead?
+
+```html
+<p>
+	<span style="font-family: Tahoma;" class="text-big">foo</span>
+</p>
+```
+
+Obviously a single `<span>` makes more sense. And thanks to the merging mechanism built into the conversion process, this would be the actual result of the conversion.
+
+Why is it so? In the above scenario, two model attributes are converted to `<span>` elements. When the first attribute (say, `fontFamily`) is converted, there is no `<span>` in the view yet. So the first `<span>` is added with the `style` attribute. But then, when `fontSize` is converted, the `<span>` is already in the view. The {@link module:engine/view/downcastwriter~DowncastWriter downcast writer} recognizes it and checks whether the elements can be merged, following these 3 rules:
+
+1. Both elements must have the same {@link module:engine/view/element~Element#name name}.
+2. Both elements must have the same {@link module:engine/view/attributeelement~AttributeElement#priority priority}.
+3. Neither can have an {@link module:engine/view/attributeelement~AttributeElement#id ID}.
+
+## Examples
+
+Once you understand more about the conversion of model attributes, you can check some examples of:
+
+* {@link framework/guides/deep-dive/conversion-extending-output Extending the editor output} &mdash; How to extend the output of existing CKEditor 5 features.
+* {@link framework/guides/deep-dive/conversion-preserving-custom-content Preserving custom content} &mdash; How to make CKEditor 5 accept more content.

+ 454 - 0
packages/ckeditor5-engine/docs/framework/guides/deep-dive/conversion-preserving-custom-content.md

@@ -0,0 +1,454 @@
+---
+category: framework-deep-dive-conversion
+menu-title: Preserving custom content
+order: 30
+---
+
+{@snippet framework/build-extending-content-source}
+
+# Preserving custom content
+
+The {@link framework/guides/deep-dive/conversion-extending-output previous guide} focused on post–processing of the CKEditor 5 data output. In this one, you will also extend the editor model so custom data can be loaded into it ({@link framework/guides/architecture/editing-engine#data-pipeline "upcasted"}). This will allow you not only to "correct" the editor output but, for instance, losslessly load data unsupported by the CKEditor 5 features.
+
+Eventually, this knowledge will allow you to create your custom features on top of the core features of CKEditor 5.
+
+## Before starting
+
+### Code architecture
+
+It is recommended that the code that customizes the editor data and editing pipelines is delivered as {@link framework/guides/architecture/core-editor-architecture#plugins plugins} and all examples in this guide follow this convention.
+
+Also for the sake of simplicity all examples use the same {@link module:editor-classic/classiceditor~ClassicEditor `ClassicEditor`}, but keep in mind that code snippets will work with other editors, too.
+
+Finally, none of the converters covered in this guide require to import any module from CKEditor 5 Framework, hence, you can write them without rebuilding the editor. In other words, such converters can easily be added to existing {@link builds/guides/overview CKEditor 5 builds}.
+
+### CKEditor 5 inspector
+
+{@link framework/guides/development-tools#ckeditor-5-inspector CKEditor 5 inspector} is an invaluable help when working with the model and view structures. It allows browsing their structure and checking selection positions like in typical browser developer tools. Make sure to enable the inspector when playing with CKEditor 5.
+
+## Loading content with a custom attribute
+
+In this example links (`<a href="...">...</a>`) loaded into the editor content will preserve their `target` attribute, which is not supported by the {@link features/link Link} feature. The DOM `target` attribute will be stored in the editor model as a `linkTarget` attribute.
+
+Unlike the {@link framework/guides/deep-dive/conversion-extending-output#adding-an-html-attribute-to-certain-inline-elements downcast–only solution}, this approach does not change the content loaded into the editor. Links without the `target` attribute will not get one and links with the attribute will preserve its value.
+
+<info-box>
+	Note that the same behavior can be obtained with {@link features/link#custom-link-attributes-decorators link decorators}:
+
+	```js
+	ClassicEditor
+		.create( ..., {
+			// ...
+			link: {
+				decorators: {
+					addGreenLink: {
+						mode: 'automatic',
+						attributes: {
+							class: 'my-green-link'
+						}
+					}
+				}
+			}
+		} )
+	```
+</info-box>
+
+{@snippet framework/extending-content-allow-link-target}
+
+The `target` attribute in the editor is allowed thanks to two custom converters plugged into the "downcast" and "upcast" pipelines, following the default converters brought by the {@link features/link Link} feature:
+
+```js
+function AllowLinkTarget( editor ) {
+	// Allow the "linkTarget" attribute in the editor model.
+	editor.model.schema.extend( '$text', { allowAttributes: 'linkTarget' } );
+
+	// Tell the editor that the model "linkTarget" attribute converts into <a target="..."></a>
+	editor.conversion.for( 'downcast' ).attributeToElement( {
+		model: 'linkTarget',
+		view: ( attributeValue, writer ) => {
+			const linkElement = writer.createAttributeElement( 'a', { target: attributeValue }, { priority: 5 } );
+			writer.setCustomProperty( 'link', true, linkElement );
+
+			return linkElement;
+		},
+		converterPriority: 'low'
+	} );
+
+	// Tell the editor that <a target="..."></a> converts into the "linkTarget" attribute in the model.
+	editor.conversion.for( 'upcast' ).attributeToAttribute( {
+		view: {
+			name: 'a',
+			key: 'target'
+		},
+		model: 'linkTarget',
+		converterPriority: 'low'
+	} );
+}
+```
+
+Activate the plugin in the editor:
+
+```js
+ClassicEditor
+	.create( ..., {
+		extraPlugins: [ AllowLinkTarget ],
+	} )
+	.then( editor => {
+		// ...
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```
+
+Add some CSS styles to easily see different link targets:
+
+```css
+a[target]::after {
+	content: "target=\"" attr(target) "\"";
+	font-size: 0.6em;
+	position: relative;
+	left: 0em;
+	top: -1em;
+	background: #00ffa6;
+	color: #000;
+	padding: 1px 3px;
+	border-radius: 10px;
+}
+```
+
+## Loading content with all attributes
+
+In this example `<div>` elements (`<div>...</div>`) loaded into the editor content will preserve their attributes. All the DOM attributes will be stored in the editor model as corresponding attributes.
+
+{@snippet framework/extending-content-allow-div-attributes}
+
+All attributes are allowed on `<div>` elements thanks to custom "upcast" and "downcast" converters that copy each attribute one by one.
+
+Allowing every possible attribute on a `<div>` element in the model is done by adding an {@link module:engine/model/schema~Schema#addAttributeCheck addAttributeCheck()} callback.
+
+<info-box>
+	Allowing every attribute on `<div>` elements might introduce security issues &mdash; including XSS attacks. The production code should use only application-related attributes and/or properly encode data.
+</info-box>
+
+Adding "upcast" and "downcast" converters for the `<div>` element is enough for cases where its attributes do not change. If the attributes in the model are modified, these `elementToElement()` converters will not be called as the `<div>` is already converted. To overcome this, a lower-level API is used.
+
+Instead of using predefined converters, the {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event-attribute `attribute`} event listener is registered for the "downcast" dispatcher.
+
+```js
+function ConvertDivAttributes( editor ) {
+	// Allow <div> elements in the model.
+	editor.model.schema.register( 'div', {
+		allowWhere: '$block',
+		allowContentOf: '$root'
+	} );
+
+	// Allow <div> elements in the model to have all attributes.
+	editor.model.schema.addAttributeCheck( context => {
+		if ( context.endsWith( 'div' ) ) {
+			return true;
+		}
+	} );
+
+	// View-to-model converter converting a view <div> with all its attributes to the model.
+	editor.conversion.for( 'upcast' ).elementToElement( {
+		view: 'div',
+		model: ( viewElement, modelWriter ) => {
+			return modelWriter.createElement( 'div', viewElement.getAttributes() );
+		}
+	} );
+
+	// Model-to-view converter for the <div> element (attrbiutes are converted separately).
+	editor.conversion.for( 'downcast' ).elementToElement( {
+		model: 'div',
+		view: 'div'
+	} );
+
+	// Model-to-view converter for <div> attributes.
+	// Note that a lower-level, event-based API is used here.
+	editor.conversion.for( 'downcast' ).add( dispatcher => {
+		dispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
+			// Convert <div> attributes only.
+			if ( data.item.name != 'div' ) {
+				return;
+			}
+
+			const viewWriter = conversionApi.writer;
+			const viewDiv = conversionApi.mapper.toViewElement( data.item );
+
+			// In the model-to-view conversion we convert changes.
+			// An attribute can be added or removed or changed.
+			// The below code handles all 3 cases.
+			if ( data.attributeNewValue ) {
+				viewWriter.setAttribute( data.attributeKey, data.attributeNewValue, viewDiv );
+			} else {
+				viewWriter.removeAttribute( data.attributeKey, viewDiv );
+			}
+		} );
+	} );
+}
+```
+
+Activate the plugin in the editor:
+
+```js
+ClassicEditor
+	.create( ..., {
+		extraPlugins: [ ConvertDivAttributes ],
+	} )
+	.then( editor => {
+		// ...
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```
+
+## Parsing attribute values
+
+Some features, like {@link features/font Font}, allow only specific values for inline attributes. In this example you will add a converter that will parse any `font-size` value into one of the defined values.
+
+{@snippet framework/extending-content-arbitrary-attribute-values}
+
+Parsing any font value to the model requires adding a custom "upcast" converter that will override the default converter from `FontSize`. Unlike the default one, this converter parses values set in CSS nad sets them into the model.
+
+As the default "downcast" converter only operates on pre-defined values, you will also add a model-to-view converter that simply outputs any model value to font size using `px` units.
+
+```js
+function HandleFontSizeValue( editor ) {
+	// Add a special catch-all converter for the font size feature.
+	editor.conversion.for( 'upcast' ).elementToAttribute( {
+		view: {
+			name: 'span',
+			styles: {
+				'font-size': /[\s\S]+/
+			}
+		},
+		model: {
+			key: 'fontSize',
+			value: viewElement => {
+				const value = parseFloat( viewElement.getStyle( 'font-size' ) ).toFixed( 0 );
+
+				// It might be needed to further convert the value to meet business requirements.
+				// In the sample the font size is configured to handle only the sizes:
+				// 12, 14, 'default', 18, 20, 22, 24, 26, 28, 30
+				// Other sizes will be converted to the model but the UI might not be aware of them.
+
+				// The font size feature expects numeric values to be Number, not String.
+				return parseInt( value );
+			}
+		},
+		converterPriority: 'high'
+	} );
+
+	// Add a special converter for the font size feature to convert all (even not configured)
+	// model attribute values.
+	editor.conversion.for( 'downcast' ).attributeToElement( {
+		model: {
+			key: 'fontSize'
+		},
+		view: ( modelValue, viewWriter ) => {
+			return viewWriter.createAttributeElement( 'span', {
+				style: `font-size:${ modelValue }px`
+			} );
+		},
+		converterPriority: 'high'
+	} );
+}
+```
+
+Activate the plugin in the editor:
+
+```js
+ClassicEditor
+	.create( ..., {
+		items: [ 'heading', '|', 'bold', 'italic', '|', 'fontSize' ],
+		fontSize: {
+			options: [ 10, 12, 14, 'default', 18, 20, 22 ]
+		},
+		extraPlugins: [ HandleFontSizeValue ],
+	} )
+	.then( editor => {
+		// ...
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```
+
+## Adding extra attributes to elements contained in a figure
+
+The {@link features/image Image} and {@link features/table Table} features wrap view elements (`<img>` for Image nad `<table>` for Table) in `<figure>`. During the downcast conversion, the model element is mapped to `<figure>` and not the inner element. In such cases the default `conversion.attributeToAttribute()` conversion helpers could lose information about the element that the attribute should be set on.
+
+To overcome this limitation it is sufficient to write a custom converter that adds custom attributes to elements already converted by base features. The key point is to add these converters with a lower priority than the base converters so they will be called after the base ones.
+
+{@snippet framework/extending-content-custom-figure-attributes}
+
+The sample below is extensible. To add your own attributes to preserve, just add another `setupCustomAttributeConversion()` call with desired names.
+
+```js
+/**
+ * Plugin that converts custom attributes for elements that are wrapped in <figure> in the view.
+ */
+function CustomFigureAttributes( editor ) {
+	// Define on which elements the CSS classes should be preserved:
+	setupCustomClassConversion( 'img', 'image', editor );
+	setupCustomClassConversion( 'table', 'table', editor );
+
+	editor.conversion.for( 'upcast' ).add( upcastCustomClasses( 'figure' ), { priority: 'low' } );
+
+	// Define custom attributes that should be preserved.
+	setupCustomAttributeConversion( 'img', 'image', 'id', editor );
+	setupCustomAttributeConversion( 'table', 'table', 'id', editor );
+}
+
+/**
+ * Sets up a conversion that preservers classes on <img> and <table> elements.
+ */
+function setupCustomClassConversion( viewElementName, modelElementName, editor ) {
+	// The 'customClass' attribute will store custom classes from the data in the model so schema definitions allow this attribute.
+	editor.model.schema.extend( modelElementName, { allowAttributes: [ 'customClass' ] } );
+
+	// Define upcast converters for the <img> and <table> elements with a "low" priority so they are run after the default converters.
+	editor.conversion.for( 'upcast' ).add( upcastCustomClasses( viewElementName ), { priority: 'low' } );
+
+	// Define downcast converters for a model element with a "low" priority so they are run after the default converters.
+	editor.conversion.for( 'downcast' ).add( downcastCustomClasses( modelElementName ), { priority: 'low' } );
+}
+
+/**
+ * Sets up a conversion for a custom attribute on view elements contained inside a <figure>.
+ *
+ * This method:
+ * - Adds proper schema rules.
+ * - Adds an upcast converter.
+ * - Adds a downcast converter.
+ */
+function setupCustomAttributeConversion( viewElementName, modelElementName, viewAttribute, editor ) {
+	// Extend the schema to store an attribute in the model.
+	const modelAttribute = `custom${ viewAttribute }`;
+
+	editor.model.schema.extend( modelElementName, { allowAttributes: [ modelAttribute ] } );
+
+	editor.conversion.for( 'upcast' ).add( upcastAttribute( viewElementName, viewAttribute, modelAttribute ) );
+	editor.conversion.for( 'downcast' ).add( downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) );
+}
+
+/**
+ * Creates an upcast converter that will pass all classes from the view element to the model element.
+ */
+function upcastCustomClasses( elementName ) {
+	return dispatcher => dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => {
+		const viewItem = data.viewItem;
+		const modelRange = data.modelRange;
+
+		const modelElement = modelRange && modelRange.start.nodeAfter;
+
+		if ( !modelElement ) {
+			return;
+		}
+
+		// The upcast conversion picks up classes from the base element and from the <figure> element so it should be extensible.
+		const currentAttributeValue = modelElement.getAttribute( 'customClass' ) || [];
+
+		currentAttributeValue.push( ...viewItem.getClassNames() );
+
+		conversionApi.writer.setAttribute( 'customClass', currentAttributeValue, modelElement );
+	} );
+}
+
+/**
+ * Creates a downcast converter that adds classes defined in the `customClass` attribute to a given view element.
+ *
+ * This converter expects that the view element is nested in a <figure> element.
+ */
+function downcastCustomClasses( modelElementName ) {
+	return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
+		const modelElement = data.item;
+
+		const viewFigure = conversionApi.mapper.toViewElement( modelElement );
+
+		if ( !viewFigure ) {
+			return;
+		}
+
+		// The code below assumes that classes are set on the <figure> element...
+		conversionApi.writer.addClass( modelElement.getAttribute( 'customClass' ), viewFigure );
+
+		// ... but if you prefer the classes to be passed to the <img> element, find the view element inside the <figure>:
+		//
+		// const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
+		//
+		// conversionApi.writer.addClass( modelElement.getAttribute( 'customClass' ), viewElement );
+	} );
+}
+
+/**
+ * Helper method that searches for a given view element in all children of the model element.
+ *
+ * @param {module:engine/view/item~Item} viewElement
+ * @param {String} viewElementName
+ * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi
+ * @return {module:engine/view/item~Item}
+ */
+function findViewChild( viewElement, viewElementName, conversionApi ) {
+	const viewChildren = Array.from( conversionApi.writer.createRangeIn( viewElement ).getItems() );
+
+	return viewChildren.find( item => item.is( viewElementName ) );
+}
+
+/**
+ * Returns the custom attribute upcast converter.
+ */
+function upcastAttribute( viewElementName, viewAttribute, modelAttribute ) {
+	return dispatcher => dispatcher.on( `element:${ viewElementName }`, ( evt, data, conversionApi ) => {
+		const viewItem = data.viewItem;
+		const modelRange = data.modelRange;
+
+		const modelElement = modelRange && modelRange.start.nodeAfter;
+
+		if ( !modelElement ) {
+			return;
+		}
+
+		conversionApi.writer.setAttribute( modelAttribute, viewItem.getAttribute( viewAttribute ), modelElement );
+	} );
+}
+
+/**
+ * Returns the custom attribute downcast converter.
+ */
+function downcastAttribute( modelElementName, viewElementName, viewAttribute, modelAttribute ) {
+	return dispatcher => dispatcher.on( `insert:${ modelElementName }`, ( evt, data, conversionApi ) => {
+		const modelElement = data.item;
+
+		const viewFigure = conversionApi.mapper.toViewElement( modelElement );
+		const viewElement = findViewChild( viewFigure, viewElementName, conversionApi );
+
+		if ( !viewElement ) {
+			return;
+		}
+
+		conversionApi.writer.setAttribute( viewAttribute, modelElement.getAttribute( modelAttribute ), viewElement );
+	} );
+}
+```
+
+Activate the plugin in the editor:
+
+```js
+ClassicEditor
+	.create( ..., {
+		extraPlugins: [ CustomFigureAttributes ],
+	} )
+	.then( editor => {
+		// ...
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );
+```
+
+## What's next?
+
+If you would like to read more about how to extend the output of existing CKEditor 5 features, refer to the {@link framework/guides/deep-dive/conversion-extending-output Extending the editor output} guide.

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

@@ -1,6 +1,6 @@
 {
   "name": "@ckeditor/ckeditor5-engine",
-  "version": "13.1.1",
+  "version": "14.0.0",
   "description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
   "keywords": [
     "wysiwyg",
@@ -21,26 +21,26 @@
     "ckeditor 5"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-utils": "^12.1.1",
+    "@ckeditor/ckeditor5-utils": "^14.0.0",
     "lodash-es": "^4.17.10"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^11.1.1",
-    "@ckeditor/ckeditor5-block-quote": "^11.1.0",
-    "@ckeditor/ckeditor5-core": "^12.1.1",
-    "@ckeditor/ckeditor5-editor-classic": "^12.1.1",
-    "@ckeditor/ckeditor5-enter": "^11.0.2",
-    "@ckeditor/ckeditor5-essentials": "^11.0.2",
-    "@ckeditor/ckeditor5-heading": "^11.0.2",
-    "@ckeditor/ckeditor5-link": "^11.0.2",
-    "@ckeditor/ckeditor5-list": "^12.0.2",
-    "@ckeditor/ckeditor5-paragraph": "^11.0.2",
-    "@ckeditor/ckeditor5-theme-lark": "^14.0.0",
-    "@ckeditor/ckeditor5-typing": "^12.0.2",
-    "@ckeditor/ckeditor5-undo": "^11.0.2",
-    "@ckeditor/ckeditor5-widget": "^11.0.2",
+    "@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-config-ckeditor5": "^1.0.11",
+    "eslint-config-ckeditor5": "^2.0.0",
     "husky": "^1.3.1",
     "lint-staged": "^7.0.0"
   },
@@ -51,7 +51,7 @@
   "author": "CKSource (http://cksource.com/)",
   "license": "GPL-2.0-or-later",
   "homepage": "https://ckeditor.com/ckeditor-5",
-  "bugs": "https://github.com/ckeditor/ckeditor5-engine/issues",
+  "bugs": "https://github.com/ckeditor/ckeditor5/issues",
   "repository": {
     "type": "git",
     "url": "https://github.com/ckeditor/ckeditor5-engine.git"

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

@@ -142,7 +142,7 @@ export default class DataController {
 			 *
 			 * @error datacontroller-get-non-existent-root
 			 */
-			throw new CKEditorError( 'datacontroller-get-non-existent-root: Attempting to get data from a non-existing root.' );
+			throw new CKEditorError( 'datacontroller-get-non-existent-root: Attempting to get data from a non-existing root.', this );
 		}
 
 		const root = this.model.document.getRoot( rootName );
@@ -240,7 +240,7 @@ export default class DataController {
 			 *
 			 * @error datacontroller-init-document-not-empty
 			 */
-			throw new CKEditorError( 'datacontroller-init-document-not-empty: Trying to set initial data to not empty document.' );
+			throw new CKEditorError( 'datacontroller-init-document-not-empty: Trying to set initial data to not empty document.', this );
 		}
 
 		let initialData = {};
@@ -262,7 +262,7 @@ export default class DataController {
 			 *
 			 * @error datacontroller-init-non-existent-root
 			 */
-			throw new CKEditorError( 'datacontroller-init-non-existent-root: Attempting to init data on a non-existing root.' );
+			throw new CKEditorError( 'datacontroller-init-non-existent-root: Attempting to init data on a non-existing root.', this );
 		}
 
 		this.model.enqueueChange( 'transparent', writer => {
@@ -316,7 +316,7 @@ export default class DataController {
 			 *
 			 * @error datacontroller-set-non-existent-root
 			 */
-			throw new CKEditorError( 'datacontroller-set-non-existent-root: Attempting to set data on a non-existing root.' );
+			throw new CKEditorError( 'datacontroller-set-non-existent-root: Attempting to set data on a non-existing root.', this );
 		}
 
 		this.model.enqueueChange( 'transparent', writer => {

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

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

+ 7 - 4
packages/ckeditor5-engine/src/conversion/conversion.js

@@ -105,8 +105,11 @@ export default class Conversion {
 			 *
 			 * @error conversion-add-alias-dispatcher-not-registered
 			 */
-			throw new CKEditorError( 'conversion-add-alias-dispatcher-not-registered: ' +
-				'Trying to register and alias for a dispatcher that nas not been registered.' );
+			throw new CKEditorError(
+				'conversion-add-alias-dispatcher-not-registered: ' +
+				'Trying to register and alias for a dispatcher that nas not been registered.',
+				this
+			);
 		}
 
 		this._createConversionHelpers( { name: alias, dispatchers: [ dispatcher ], isDowncast } );
@@ -180,7 +183,7 @@ export default class Conversion {
 			 *
 			 * @error conversion-for-unknown-group
 			 */
-			throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.' );
+			throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.', this );
 		}
 
 		return this._helpers.get( groupName );
@@ -585,7 +588,7 @@ export default class Conversion {
 			 *
 			 * @error conversion-group-exists
 			 */
-			throw new CKEditorError( 'conversion-group-exists: Trying to register a group name that has already been registered.' );
+			throw new CKEditorError( 'conversion-group-exists: Trying to register a group name that has already been registered.', this );
 		}
 
 		const helpers = isDowncast ? new DowncastHelpers( dispatchers ) : new UpcastHelpers( dispatchers );

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

@@ -106,10 +106,10 @@ export default class DowncastDispatcher {
 	 * Creates a `DowncastDispatcher` instance.
 	 *
 	 * @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`.
 	 */
-	constructor( conversionApi = {} ) {
+	constructor( conversionApi ) {
 		/**
 		 * 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.
 	 *
 	 * @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.
 	 */
-	convertChanges( differ, writer ) {
+	convertChanges( differ, markers, writer ) {
 		// Before the view is updated, remove markers which have changed.
 		for ( const change of differ.getMarkersToRemove() ) {
 			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.
 		for ( const change of differ.getMarkersToAdd() ) {
 			this.convertMarkerAdd( change.name, change.range, writer );
@@ -252,7 +260,7 @@ export default class DowncastDispatcher {
 	 * @fires addMarker
 	 * @fires attribute
 	 * @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.
 	 */
 	convertSelection( selection, markers, writer ) {
@@ -532,7 +540,6 @@ export default class DowncastDispatcher {
 	 * @param {String} data.attributeKey Attribute key.
 	 * @param {*} data.attributeOldValue Attribute value before the change. This is `null` when selection attribute is converted.
 	 * @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
 	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
@@ -542,7 +549,6 @@ export default class DowncastDispatcher {
 	 *
 	 * @event selection
 	 * @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
 	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
@@ -558,17 +564,17 @@ export default class DowncastDispatcher {
 	 * If the marker range is not collapsed:
 	 *
 	 * * 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:
 	 *
 	 * * 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:
 	 *
 	 * * there is only one event,
-	 * * consumables object includes selection instance with event name.
+	 * * `conversionApi.consumable` includes selection instance with event name.
 	 *
 	 * @event addMarker
 	 * @param {Object} data Additional information about the change.
@@ -578,7 +584,6 @@ export default class DowncastDispatcher {
 	 * the marker range was not collapsed.
 	 * @param {module:engine/model/range~Range} data.markerRange Marker range.
 	 * @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
 	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */

+ 15 - 15
packages/ckeditor5-engine/src/conversion/downcasthelpers.js

@@ -3,6 +3,12 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
+/**
+ * Contains downcast (model-to-view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
+ *
+ * @module engine/conversion/downcasthelpers
+ */
+
 import ModelRange from '../model/range';
 import ModelSelection from '../model/selection';
 import ModelElement from '../model/element';
@@ -11,14 +17,8 @@ import ViewAttributeElement from '../view/attributeelement';
 import DocumentSelection from '../model/documentselection';
 import ConversionHelpers from './conversionhelpers';
 
-import log from '@ckeditor/ckeditor5-utils/src/log';
 import { cloneDeep } from 'lodash-es';
-
-/**
- * Contains downcast (model-to-view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
- *
- * @module engine/conversion/downcasthelpers
- */
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * Downcast conversion helper functions.
@@ -730,9 +730,8 @@ function removeUIElement() {
 			return;
 		}
 
-		conversionApi.mapper.unbindElementsFromMarkerName( data.markerName );
-
 		for ( const element of elements ) {
+			conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
 			conversionApi.writer.clear( conversionApi.writer.createRangeOn( element ), element );
 		}
 
@@ -824,10 +823,11 @@ function changeAttribute( attributeCreator ) {
 			 *
 			 * @error conversion-attribute-to-attribute-on-text
 			 */
-			log.warn( 'conversion-attribute-to-attribute-on-text: ' +
-				'Trying to convert text node\'s attribute with attribute-to-attribute converter.' );
-
-			return;
+			throw new CKEditorError(
+				'conversion-attribute-to-attribute-on-text: ' +
+				'Trying to convert text node\'s attribute with attribute-to-attribute converter.',
+				[ data, conversionApi ]
+			);
 		}
 
 		// First remove the old attribute if there was one.
@@ -1030,9 +1030,9 @@ function removeHighlight( highlightDescriptor ) {
 			return;
 		}
 
-		conversionApi.mapper.unbindElementsFromMarkerName( data.markerName );
-
 		for ( const element of elements ) {
+			conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
+
 			if ( element.is( 'attributeElement' ) ) {
 				conversionApi.writer.unwrap( conversionApi.writer.createRangeOn( element ), viewHighlightElement );
 			} else {

+ 67 - 4
packages/ckeditor5-engine/src/conversion/mapper.js

@@ -77,6 +77,25 @@ export default class Mapper {
 		 */
 		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.
 		this.on( 'modelToViewPosition', ( evt, data ) => {
 			if ( data.viewPosition ) {
@@ -132,6 +151,12 @@ export default class Mapper {
 
 		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 ) {
 			this._modelToViewMapping.delete( modelElement );
 		}
@@ -167,19 +192,55 @@ export default class Mapper {
 	 */
 	bindElementToMarker( element, name ) {
 		const elements = this._markerNameToElements.get( name ) || new Set();
-
 		elements.add( element );
 
+		const names = this._elementToMarkerNames.get( element ) || new Set();
+		names.add( name );
+
 		this._markerNameToElements.set( name, elements );
+		this._elementToMarkerNames.set( element, names );
 	}
 
 	/**
-	 * Unbinds all elements from given marker name.
+	 * Unbinds an element from given marker name.
 	 *
+	 * @param {module:engine/view/element~Element} element Element to unbind.
 	 * @param {String} name Marker name.
 	 */
-	unbindElementsFromMarkerName( name ) {
-		this._markerNameToElements.delete( name );
+	unbindElementFromMarkerName( element, name ) {
+		const nameToElements = this._markerNameToElements.get( name );
+
+		if ( nameToElements ) {
+			nameToElements.delete( element );
+
+			if ( nameToElements.size == 0 ) {
+				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;
 	}
 
 	/**
@@ -189,6 +250,8 @@ export default class Mapper {
 		this._modelToViewMapping = new WeakMap();
 		this._viewToModelMapping = new WeakMap();
 		this._markerNameToElements = new Map();
+		this._elementToMarkerNames = new Map();
+		this._unboundMarkerNames = new Set();
 	}
 
 	/**

+ 22 - 1
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).
  *  	editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
  *  	 	// When element is already consumed by higher priority converters then do nothing.
@@ -231,7 +252,7 @@ export default class UpcastDispatcher {
 			 *
 			 * @error view-conversion-dispatcher-incorrect-result
 			 */
-			throw new CKEditorError( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.' );
+			throw new CKEditorError( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.', this );
 		}
 
 		return { modelRange: data.modelRange, modelCursor: data.modelCursor };

+ 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
 	 * 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`.
-	 * @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}
 	 */
 	elementToAttribute( config ) {
@@ -405,7 +405,7 @@ function upcastElementToElement( config ) {
 
 	const converter = prepareToElementConverter( config );
 
-	const elementName = getViewElementNameFromConfig( config );
+	const elementName = getViewElementNameFromConfig( config.view );
 	const eventName = elementName ? 'element:' + elementName : 'element';
 
 	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
 // 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`.
-// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
+// @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
 // @returns {Function} Conversion helper.
 function upcastElementToAttribute( config ) {
 	config = cloneDeep( config );
@@ -431,7 +431,7 @@ function upcastElementToAttribute( config ) {
 
 	const converter = prepareToAttributeConverter( config, false );
 
-	const elementName = getViewElementNameFromConfig( config );
+	const elementName = getViewElementNameFromConfig( config.view );
 	const eventName = elementName ? 'element:' + elementName : 'element';
 
 	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
 // 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.
-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;
@@ -684,7 +684,7 @@ function prepareToAttributeConverter( config, shallow ) {
 			return;
 		}
 
-		if ( onlyViewNameIsDefined( config ) ) {
+		if ( onlyViewNameIsDefined( config.view, data.viewItem ) ) {
 			match.match.name = true;
 		} else {
 			// 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.
 //
-// @param {Object} config Conversion config.
+// @param {Object} config Conversion view config.
 // @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 !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}

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

@@ -503,7 +503,7 @@ class ViewElementConsumables {
 				 *
 				 * @error viewconsumable-invalid-attribute
 				 */
-				throw new CKEditorError( 'viewconsumable-invalid-attribute: Classes and styles should be handled separately.' );
+				throw new CKEditorError( 'viewconsumable-invalid-attribute: Classes and styles should be handled separately.', this );
 			}
 
 			consumables.set( name, true );

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

@@ -11,7 +11,6 @@
 
 import BasicHtmlWriter from './basichtmlwriter';
 import DomConverter from '../view/domconverter';
-import { NBSP_FILLER } from '../view/filler';
 
 /**
  * The HTML data processor class.
@@ -38,7 +37,7 @@ export default class HtmlDataProcessor {
 		 * @private
 		 * @member {module:engine/view/domconverter~DomConverter}
 		 */
-		this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } );
+		this._domConverter = new DomConverter( { blockFillerMode: 'nbsp' } );
 
 		/**
 		 * A basic HTML writer instance used to convert DOM elements to an HTML string.

+ 1 - 2
packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js

@@ -11,7 +11,6 @@
 
 import BasicHtmlWriter from './basichtmlwriter';
 import DomConverter from '../view/domconverter';
-import { NBSP_FILLER } from '../view/filler';
 
 /**
  * The XML data processor class.
@@ -54,7 +53,7 @@ export default class XmlDataProcessor {
 		 * @private
 		 * @member {module:engine/view/domconverter~DomConverter}
 		 */
-		this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } );
+		this._domConverter = new DomConverter( { blockFillerMode: 'nbsp' } );
 
 		/**
 		 * A basic HTML writer instance used to convert DOM elements to an XML string.

+ 39 - 5
packages/ckeditor5-engine/src/model/differ.js

@@ -110,6 +110,32 @@ export default class Differ {
 		return this._changesInElement.size == 0 && this._changedMarkers.size == 0;
 	}
 
+	/**
+	 * Marks given `item` in differ to be "refreshed". It means that the item will be marked as removed and inserted in the differ changes
+	 * set, so it will be effectively re-converted when differ changes will be handled by a dispatcher.
+	 *
+	 * @param {module:engine/model/item~Item} item Item to refresh.
+	 */
+	refreshItem( item ) {
+		if ( this._isInInsertedElement( item.parent ) ) {
+			return;
+		}
+
+		this._markRemove( item.parent, item.startOffset, item.offsetSize );
+		this._markInsert( item.parent, item.startOffset, item.offsetSize );
+
+		const range = Range._createOn( item );
+
+		for ( const marker of this._markerCollection.getMarkersIntersectingRange( range ) ) {
+			const markerRange = marker.getRange();
+
+			this.bufferMarkerChange( marker.name, markerRange, markerRange, marker.affectsData );
+		}
+
+		// Clear cache after each buffered operation as it is no longer valid.
+		this._cachedChanges = null;
+	}
+
 	/**
 	 * Buffers the given operation. An operation has to be buffered before it is executed.
 	 *
@@ -136,7 +162,7 @@ export default class Differ {
 			case 'addAttribute':
 			case 'removeAttribute':
 			case 'changeAttribute': {
-				for ( const item of operation.range.getItems() ) {
+				for ( const item of operation.range.getItems( { shallow: true } ) ) {
 					if ( this._isInInsertedElement( item.parent ) ) {
 						continue;
 					}
@@ -1086,19 +1112,25 @@ function _generateActionsFromChanges( oldChildrenLength, changes ) {
 	for ( const change of changes ) {
 		// First, fill "holes" between changes with "equal" actions.
 		if ( change.offset > offset ) {
-			actions.push( ...'e'.repeat( change.offset - offset ).split( '' ) );
+			for ( let i = 0; i < change.offset - offset; i++ ) {
+				actions.push( 'e' );
+			}
 
 			oldChildrenHandled += change.offset - offset;
 		}
 
 		// Then, fill up actions accordingly to change type.
 		if ( change.type == 'insert' ) {
-			actions.push( ...'i'.repeat( change.howMany ).split( '' ) );
+			for ( let i = 0; i < change.howMany; i++ ) {
+				actions.push( 'i' );
+			}
 
 			// The last handled offset is after inserted range.
 			offset = change.offset + change.howMany;
 		} else if ( change.type == 'remove' ) {
-			actions.push( ...'r'.repeat( change.howMany ).split( '' ) );
+			for ( let i = 0; i < change.howMany; i++ ) {
+				actions.push( 'r' );
+			}
 
 			// The last handled offset is at the position where the nodes were removed.
 			offset = change.offset;
@@ -1117,7 +1149,9 @@ function _generateActionsFromChanges( oldChildrenLength, changes ) {
 	// Fill "equal" actions at the end of actions set. Use `oldChildrenHandled` to see how many children
 	// has not been changed / removed at the end of their parent.
 	if ( oldChildrenHandled < oldChildrenLength ) {
-		actions.push( ...'e'.repeat( oldChildrenLength - oldChildrenHandled ).split( '' ) );
+		for ( let i = 0; i < oldChildrenLength - oldChildrenHandled - offset; i++ ) {
+			actions.push( 'e' );
+		}
 	}
 
 	return actions;

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

@@ -127,6 +127,7 @@ export default class Document {
 				 */
 				throw new CKEditorError(
 					'model-document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
+					this,
 					{ operation }
 				);
 			}
@@ -201,6 +202,7 @@ export default class Document {
 			 */
 			throw new CKEditorError(
 				'model-document-createRoot-name-exists: Root with specified name already exists.',
+				this,
 				{ name: rootName }
 			);
 		}

+ 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
 	 * @returns {Boolean}
 	 */
 	is( type ) {
-		return type == 'documentFragment';
+		return type == 'documentFragment' || type == 'model:documentFragment';
 	}
 
 	/**

+ 39 - 40
packages/ckeditor5-engine/src/model/documentselection.js

@@ -17,7 +17,6 @@ import TextProxy from './textproxy';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import log from '@ckeditor/ckeditor5-utils/src/log';
 import uid from '@ckeditor/ckeditor5-utils/src/uid';
 
 const storePrefix = 'selection:';
@@ -234,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.
 	 *
-	 * **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>
-	 *		<quote>
+	 *		<blockQuote>
 	 *			<paragraph>b</paragraph>
-	 *		</quote>
+	 *		</blockQuote>
 	 *		<paragraph>c]d</paragraph>
 	 *
 	 * In this case the paragraph will also be returned, despite the collapsed selection:
 	 *
 	 *		<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>
 	 *		<blockB>
@@ -277,12 +261,24 @@ export default class DocumentSelection {
 	 *		</blockB>
 	 *		<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>}
 	 */
-	getTopMostBlocks() {
-		return this._selection.getTopMostBlocks();
+	getSelectedBlocks() {
+		return this._selection.getSelectedBlocks();
 	}
 
 	/**
@@ -368,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
 	 * @returns {Boolean}
 	 */
 	is( type ) {
-		return type == 'selection' || type == 'documentSelection';
+		return type == 'selection' ||
+			type == 'model:selection' ||
+			type == 'documentSelection' ||
+			type == 'model:documentSelection';
 	}
 
 	/**
@@ -639,6 +641,7 @@ class LiveSelection extends Selection {
 					 */
 					throw new CKEditorError(
 						'document-selection-wrong-position: Range from document selection starts or ends at incorrect position.',
+						this,
 						{ range }
 					);
 				}
@@ -767,6 +770,7 @@ class LiveSelection extends Selection {
 			 */
 			throw new CKEditorError(
 				'document-selection-gravity-wrong-restore: Attempting to restore the selection gravity for an unknown UID.',
+				this,
 				{ uid }
 			);
 		}
@@ -802,12 +806,7 @@ class LiveSelection extends Selection {
 		this._checkRange( range );
 
 		if ( range.root == this._document.graveyard ) {
-			/**
-			 * Trying to add a Range that is in the graveyard root. Range rejected.
-			 *
-			 * @warning model-selection-range-in-graveyard
-			 */
-			log.warn( 'model-selection-range-in-graveyard: Trying to add a Range that is in the graveyard root. Range rejected.' );
+			// @if CK_DEBUG // console.warn( 'Trying to add a Range that is in the graveyard root. Range rejected.' );
 
 			return;
 		}

+ 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.
  *
- * @extends {module:engine/model/node~Node}
+ * @extends module:engine/model/node~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.
 	 * Otherwise, it acts like the `name` parameter.
@@ -108,10 +114,12 @@ export default class Element extends Node {
 	 * @returns {Boolean}
 	 */
 	is( type, name = null ) {
+		const cutType = type.replace( /^model:/, '' );
+
 		if ( !name ) {
-			return type == 'element' || type == this.name || super.is( type );
+			return cutType == 'element' || cutType == this.name || super.is( type );
 		} else {
-			return type == 'element' && name == this.name;
+			return cutType == 'element' && name == this.name;
 		}
 	}
 

+ 22 - 1
packages/ckeditor5-engine/src/model/liveposition.js

@@ -46,7 +46,8 @@ export default class LivePosition extends Position {
 			 * @error liveposition-root-not-rootelement
 			 */
 			throw new CKEditorError(
-				'model-liveposition-root-not-rootelement: LivePosition\'s root has to be an instance of RootElement.'
+				'model-liveposition-root-not-rootelement: LivePosition\'s root has to be an instance of RootElement.',
+				root
 			);
 		}
 
@@ -62,6 +63,26 @@ export default class LivePosition extends Position {
 		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.
 	 *

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

@@ -40,6 +40,26 @@ export default class LiveRange extends Range {
 		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.
 	 *

+ 24 - 6
packages/ckeditor5-engine/src/model/markercollection.js

@@ -167,7 +167,7 @@ export default class MarkerCollection {
 		const marker = this._markers.get( markerName );
 
 		if ( !marker ) {
-			throw new CKEditorError( 'markercollection-refresh-marker-not-exists: Marker with provided name does not exists.' );
+			throw new CKEditorError( 'markercollection-refresh-marker-not-exists: Marker with provided name does not exists.', this );
 		}
 
 		const range = marker.getRange();
@@ -383,7 +383,7 @@ class Marker {
 	 */
 	get managedUsingOperations() {
 		if ( !this._liveRange ) {
-			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
+			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
 		}
 
 		return this._managedUsingOperations;
@@ -396,7 +396,7 @@ class Marker {
 	 */
 	get affectsData() {
 		if ( !this._liveRange ) {
-			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
+			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
 		}
 
 		return this._affectsData;
@@ -409,7 +409,7 @@ class Marker {
 	 */
 	getStart() {
 		if ( !this._liveRange ) {
-			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
+			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
 		}
 
 		return this._liveRange.start.clone();
@@ -422,7 +422,7 @@ class Marker {
 	 */
 	getEnd() {
 		if ( !this._liveRange ) {
-			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
+			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
 		}
 
 		return this._liveRange.end.clone();
@@ -442,12 +442,30 @@ class Marker {
 	 */
 	getRange() {
 		if ( !this._liveRange ) {
-			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
+			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
 		}
 
 		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.
 	 *

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

@@ -24,6 +24,7 @@ import deleteContent from './utils/deletecontent';
 import modifySelection from './utils/modifyselection';
 import getSelectedContent from './utils/getselectedcontent';
 import { injectSelectionPostFixer } from './utils/selection-post-fixer';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * Editor's data model. Read about the model in the
@@ -153,14 +154,18 @@ export default class Model {
 	 * @returns {*} Value returned by the callback.
 	 */
 	change( callback ) {
-		if ( this._pendingChanges.length === 0 ) {
-			// If this is the outermost block, create a new batch and start `_runPendingChanges` execution flow.
-			this._pendingChanges.push( { batch: new Batch(), callback } );
-
-			return this._runPendingChanges()[ 0 ];
-		} else {
-			// If this is not the outermost block, just execute the callback.
-			return callback( this._currentWriter );
+		try {
+			if ( this._pendingChanges.length === 0 ) {
+				// If this is the outermost block, create a new batch and start `_runPendingChanges` execution flow.
+				this._pendingChanges.push( { batch: new Batch(), callback } );
+
+				return this._runPendingChanges()[ 0 ];
+			} else {
+				// If this is not the outermost block, just execute the callback.
+				return callback( this._currentWriter );
+			}
+		} catch ( err ) {
+			CKEditorError.rethrowUnexpectedError( err, this );
 		}
 	}
 
@@ -198,17 +203,21 @@ export default class Model {
 	 * @param {Function} callback Callback function which may modify the model.
 	 */
 	enqueueChange( batchOrType, callback ) {
-		if ( typeof batchOrType === 'string' ) {
-			batchOrType = new Batch( batchOrType );
-		} else if ( typeof batchOrType == 'function' ) {
-			callback = batchOrType;
-			batchOrType = new Batch();
-		}
+		try {
+			if ( typeof batchOrType === 'string' ) {
+				batchOrType = new Batch( batchOrType );
+			} else if ( typeof batchOrType == 'function' ) {
+				callback = batchOrType;
+				batchOrType = new Batch();
+			}
 
-		this._pendingChanges.push( { batch: batchOrType, callback } );
+			this._pendingChanges.push( { batch: batchOrType, callback } );
 
-		if ( this._pendingChanges.length == 1 ) {
-			this._runPendingChanges();
+			if ( this._pendingChanges.length == 1 ) {
+				this._runPendingChanges();
+			}
+		} catch ( err ) {
+			CKEditorError.rethrowUnexpectedError( err, this );
 		}
 	}
 
@@ -702,10 +711,11 @@ export default class Model {
 	 * * {@link #change `change()`},
 	 * * {@link #enqueueChange `enqueueChange()`}.
 	 *
+	 * @param {'transparent'|'default'} [type='default'] The type of the batch.
 	 * @returns {module:engine/model/batch~Batch}
 	 */
-	createBatch() {
-		return new Batch();
+	createBatch( type ) {
+		return new Batch( type );
 	}
 
 	/**

+ 49 - 25
packages/ckeditor5-engine/src/model/node.js

@@ -88,7 +88,7 @@ export default class Node {
 		}
 
 		if ( ( pos = this.parent.getChildIndex( this ) ) === null ) {
-			throw new CKEditorError( 'model-node-not-found-in-parent: The node\'s parent does not contain this node.' );
+			throw new CKEditorError( 'model-node-not-found-in-parent: The node\'s parent does not contain this node.', this );
 		}
 
 		return pos;
@@ -112,7 +112,7 @@ export default class Node {
 		}
 
 		if ( ( pos = this.parent.getChildStartOffset( this ) ) === null ) {
-			throw new CKEditorError( 'model-node-not-found-in-parent: The node\'s parent does not contain this node.' );
+			throw new CKEditorError( 'model-node-not-found-in-parent: The node\'s parent does not contain this node.', this );
 		}
 
 		return pos;
@@ -396,6 +396,53 @@ export default class Node {
 		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.
 	 *
@@ -460,29 +507,6 @@ export default class Node {
 	_clearAttributes() {
 		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';
-	}
 }
 
 /**

+ 12 - 6
packages/ckeditor5-engine/src/model/nodelist.js

@@ -124,7 +124,7 @@ export default class NodeList {
 			 *
 			 * @error nodelist-index-out-of-bounds
 			 */
-			throw new CKEditorError( 'model-nodelist-index-out-of-bounds: Given index cannot be found in the node list.' );
+			throw new CKEditorError( 'model-nodelist-index-out-of-bounds: Given index cannot be found in the node list.', this );
 		}
 
 		return this.getNodeStartOffset( node );
@@ -158,10 +158,13 @@ export default class NodeList {
 			 * @param {Number} offset
 			 * @param {module:engine/model/nodelist~NodeList} nodeList Stringified node list.
 			 */
-			throw new CKEditorError( 'model-nodelist-offset-out-of-bounds: Given offset cannot be found in the node list.', {
-				offset,
-				nodeList: this
-			} );
+			throw new CKEditorError( 'model-nodelist-offset-out-of-bounds: Given offset cannot be found in the node list.',
+				this,
+				{
+					offset,
+					nodeList: this
+				}
+			);
 		}
 
 		return this.length;
@@ -183,7 +186,10 @@ export default class NodeList {
 				 *
 				 * @error nodelist-insertNodes-not-node
 				 */
-				throw new CKEditorError( 'model-nodelist-insertNodes-not-node: Trying to insert an object which is not a Node instance.' );
+				throw new CKEditorError(
+					'model-nodelist-insertNodes-not-node: Trying to insert an object which is not a Node instance.',
+					this
+				);
 			}
 		}
 

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

@@ -128,7 +128,7 @@ export default class AttributeOperation extends Operation {
 			 *
 			 * @error attribute-operation-range-not-flat
 			 */
-			throw new CKEditorError( 'attribute-operation-range-not-flat: The range to change is not flat.' );
+			throw new CKEditorError( 'attribute-operation-range-not-flat: The range to change is not flat.', this );
 		}
 
 		for ( const item of this.range.getItems( { shallow: true } ) ) {
@@ -144,6 +144,7 @@ export default class AttributeOperation extends Operation {
 				throw new CKEditorError(
 					'attribute-operation-wrong-old-value: Changed node has different attribute value than operation\'s ' +
 					'old attribute value.',
+					this,
 					{ item, key: this.key, value: this.oldValue }
 				);
 			}
@@ -158,6 +159,7 @@ export default class AttributeOperation extends Operation {
 				 */
 				throw new CKEditorError(
 					'attribute-operation-attribute-exists: The attribute with given key already exists.',
+					this,
 					{ node: item, key: this.key }
 				);
 			}

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

@@ -73,7 +73,7 @@ export default class DetachOperation extends Operation {
 			 *
 			 * @error detach-operation-on-document-node
 			 */
-			throw new CKEditorError( 'detach-operation-on-document-node: Cannot detach document node.' );
+			throw new CKEditorError( 'detach-operation-on-document-node: Cannot detach document node.', this );
 		}
 	}
 

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

@@ -115,7 +115,8 @@ export default class InsertOperation extends Operation {
 			 * @error insert-operation-position-invalid
 			 */
 			throw new CKEditorError(
-				'insert-operation-position-invalid: Insertion position is invalid.'
+				'insert-operation-position-invalid: Insertion position is invalid.',
+				this
 			);
 		}
 	}

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

@@ -141,27 +141,27 @@ export default class MergeOperation extends Operation {
 		const targetElement = this.targetPosition.parent;
 
 		// 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
 			 */
-			throw new CKEditorError( 'merge-operation-source-position-invalid: Merge source position is invalid.' );
-		} else if ( !targetElement || !targetElement.is( 'element' ) || !targetElement.parent ) {
+			throw new CKEditorError( 'merge-operation-source-position-invalid: Merge source position is invalid.', this );
+		} 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
 			 */
-			throw new CKEditorError( 'merge-operation-target-position-invalid: Merge target position is invalid.' );
+			throw new CKEditorError( 'merge-operation-target-position-invalid: Merge target position is invalid.', this );
 		} else if ( this.howMany != sourceElement.maxOffset ) {
 			/**
 			 * Merge operation specifies wrong number of nodes to move.
 			 *
 			 * @error merge-operation-how-many-invalid
 			 */
-			throw new CKEditorError( 'merge-operation-how-many-invalid: Merge operation specifies wrong number of nodes to move.' );
+			throw new CKEditorError( 'merge-operation-how-many-invalid: Merge operation specifies wrong number of nodes to move.', this );
 		}
 	}
 

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

@@ -123,23 +123,14 @@ export default class MoveOperation extends Operation {
 		// 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.
 		// 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.'
-			);
-		} else if ( sourceOffset + this.howMany > sourceElement.maxOffset ) {
+		if ( sourceOffset + this.howMany > sourceElement.maxOffset ) {
 			/**
 			 * The nodes which should be moved do not exist.
 			 *
 			 * @error move-operation-nodes-do-not-exist
 			 */
 			throw new CKEditorError(
-				'move-operation-nodes-do-not-exist: The nodes which should be moved do not exist.'
+				'move-operation-nodes-do-not-exist: The nodes which should be moved do not exist.', this
 			);
 		} else if ( sourceElement === targetElement && sourceOffset < targetOffset && targetOffset < sourceOffset + this.howMany ) {
 			/**
@@ -148,7 +139,7 @@ export default class MoveOperation extends Operation {
 			 * @error move-operation-range-into-itself
 			 */
 			throw new CKEditorError(
-				'move-operation-range-into-itself: Trying to move a range of nodes to the inside of that range.'
+				'move-operation-range-into-itself: Trying to move a range of nodes to the inside of that range.', this
 			);
 		} else if ( this.sourcePosition.root == this.targetPosition.root ) {
 			if ( compareArrays( this.sourcePosition.getParentPath(), this.targetPosition.getParentPath() ) == 'prefix' ) {
@@ -161,7 +152,7 @@ export default class MoveOperation extends Operation {
 					 * @error move-operation-node-into-itself
 					 */
 					throw new CKEditorError(
-						'move-operation-node-into-itself: Trying to move a range of nodes into one of nodes from that range.'
+						'move-operation-node-into-itself: Trying to move a range of nodes into one of nodes from that range.', this
 					);
 				}
 			}

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

@@ -94,7 +94,8 @@ export default class RenameOperation extends Operation {
 			 * @error rename-operation-wrong-position
 			 */
 			throw new CKEditorError(
-				'rename-operation-wrong-position: Given position is invalid or node after it is not an instance of Element.'
+				'rename-operation-wrong-position: Given position is invalid or node after it is not an instance of Element.',
+				this
 			);
 		} else if ( element.name !== this.oldName ) {
 			/**
@@ -103,7 +104,8 @@ export default class RenameOperation extends Operation {
 			 * @error rename-operation-wrong-name
 			 */
 			throw new CKEditorError(
-				'rename-operation-wrong-name: Element to change has different name than operation\'s old name.'
+				'rename-operation-wrong-name: Element to change has different name than operation\'s old name.',
+				this
 			);
 		}
 	}

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

@@ -116,6 +116,7 @@ export default class RootAttributeOperation extends Operation {
 			 */
 			throw new CKEditorError(
 				'rootattribute-operation-not-a-root: The element to change is not a root element.',
+				this,
 				{ root: this.root, key: this.key }
 			);
 		}
@@ -132,6 +133,7 @@ export default class RootAttributeOperation extends Operation {
 			throw new CKEditorError(
 				'rootattribute-operation-wrong-old-value: Changed node has different attribute value than operation\'s ' +
 				'old attribute value.',
+				this,
 				{ root: this.root, key: this.key }
 			);
 		}
@@ -146,6 +148,7 @@ export default class RootAttributeOperation extends Operation {
 			 */
 			throw new CKEditorError(
 				'rootattribute-operation-attribute-exists: The attribute with given key already exists.',
+				this,
 				{ root: this.root, key: this.key }
 			);
 		}
@@ -197,6 +200,7 @@ export default class RootAttributeOperation extends Operation {
 			 */
 			throw new CKEditorError(
 				'rootattribute-operation-fromjson-no-root: Cannot create RootAttributeOperation. Root with specified name does not exist.',
+				this,
 				{ rootName: json.root }
 			);
 		}

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

@@ -149,28 +149,28 @@ export default class SplitOperation extends Operation {
 			 *
 			 * @error split-operation-position-invalid
 			 */
-			throw new CKEditorError( 'split-operation-position-invalid: Split position is invalid.' );
+			throw new CKEditorError( 'split-operation-position-invalid: Split position is invalid.', this );
 		} else if ( !element.parent ) {
 			/**
 			 * Cannot split root element.
 			 *
 			 * @error split-operation-split-in-root
 			 */
-			throw new CKEditorError( 'split-operation-split-in-root: Cannot split root element.' );
+			throw new CKEditorError( 'split-operation-split-in-root: Cannot split root element.', this );
 		} else if ( this.howMany != element.maxOffset - this.splitPosition.offset ) {
 			/**
 			 * Split operation specifies wrong number of nodes to move.
 			 *
 			 * @error split-operation-how-many-invalid
 			 */
-			throw new CKEditorError( 'split-operation-how-many-invalid: Split operation specifies wrong number of nodes to move.' );
+			throw new CKEditorError( 'split-operation-how-many-invalid: Split operation specifies wrong number of nodes to move.', this );
 		} else if ( this.graveyardPosition && !this.graveyardPosition.nodeAfter ) {
 			/**
 			 * Graveyard position invalid.
 			 *
 			 * @error split-operation-graveyard-position-invalid
 			 */
-			throw new CKEditorError( 'split-operation-graveyard-position-invalid: Graveyard position invalid.' );
+			throw new CKEditorError( 'split-operation-graveyard-position-invalid: Graveyard position invalid.', this );
 		}
 	}
 

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

@@ -16,7 +16,6 @@ import Range from '../range';
 import Position from '../position';
 
 import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
-import log from '@ckeditor/ckeditor5-utils/src/log';
 
 const transformations = new Map();
 
@@ -102,14 +101,14 @@ export function transform( a, b, context = {} ) {
 
 		return transformationFunction( a, b, context );
 	} catch ( e ) {
-		log.error( 'Error during operation transformation!', e.message );
-		log.error( 'Transformed operation', a );
-		log.error( 'Operation transformed by', b );
-		log.error( 'context.aIsStrong', context.aIsStrong );
-		log.error( 'context.aWasUndone', context.aWasUndone );
-		log.error( 'context.bWasUndone', context.bWasUndone );
-		log.error( 'context.abRelation', context.abRelation );
-		log.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;
 	}
@@ -149,6 +148,8 @@ export function transform( a, b, context = {} ) {
  * @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.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 ) {
 	// Create new arrays so the originally passed arguments are not changed.
@@ -156,9 +157,15 @@ export function transformSets( operationsA, operationsB, options ) {
 	operationsA = operationsA.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 ( operationsA.length == 0 || operationsB.length == 0 ) {
-		return { operationsA, operationsB };
+		return { operationsA, operationsB, originalOperations };
 	}
 	//
 	// Following is a description of transformation process:
@@ -306,10 +313,6 @@ export function transformSets( operationsA, operationsB, options ) {
 		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`.
 	let i = 0;
 
@@ -375,7 +378,7 @@ export function transformSets( operationsA, operationsB, options ) {
 	updateBaseVersions( operationsA, data.nextBaseVersionB );
 	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
@@ -389,6 +392,13 @@ class ContextFactory {
 	// @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.
 	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.
 		this._history = document.history;
 
@@ -397,13 +407,6 @@ class ContextFactory {
 
 		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
 		// to each other. Those relations are evaluated during transformation process. For every transformated pair of operations
 		// we keep relations between them.
@@ -429,10 +432,10 @@ class ContextFactory {
 	// @param {Array.<module:engine/model/operation/operation~Operation>} operations
 	// @param {module:engine/model/operation/operation~Operation|null} [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 ) {
-			this._originalOperations.set( operation, originalOperation || operation );
+			this.originalOperations.set( operation, originalOperation || operation );
 		}
 	}
 
@@ -606,7 +609,7 @@ class ContextFactory {
 		// 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
 		// 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.
 		return originalOp.wasUndone || this._history.isUndoneOperation( originalOp );
@@ -638,7 +641,7 @@ class ContextFactory {
 	// @returns {String|null}
 	_getRelation( opA, opB ) {
 		// 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 );
 
 		// If `opB` is not undoing any operation, there is no relation.
@@ -646,7 +649,7 @@ class ContextFactory {
 			return null;
 		}
 
-		const origA = this._originalOperations.get( opA );
+		const origA = this.originalOperations.get( opA );
 		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.
@@ -665,8 +668,8 @@ class ContextFactory {
 	// @param {String} relation
 	_setRelation( opA, opB, relation ) {
 		// 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 );
 

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

@@ -69,8 +69,11 @@ export function _remove( range ) {
 		 *
 		 * @error operation-utils-remove-range-not-flat
 		 */
-		throw new CKEditorError( 'operation-utils-remove-range-not-flat: ' +
-			'Trying to remove a range which starts and ends in different element.' );
+		throw new CKEditorError(
+			'operation-utils-remove-range-not-flat: ' +
+			'Trying to remove a range which starts and ends in different element.',
+			this
+		);
 	}
 
 	const parent = range.start.parent;
@@ -105,8 +108,11 @@ export function _move( sourceRange, targetPosition ) {
 		 *
 		 * @error operation-utils-move-range-not-flat
 		 */
-		throw new CKEditorError( 'operation-utils-move-range-not-flat: ' +
-			'Trying to move a range which starts and ends in different element.' );
+		throw new CKEditorError(
+			'operation-utils-move-range-not-flat: ' +
+			'Trying to move a range which starts and ends in different element.',
+			this
+		);
 	}
 
 	const nodes = _remove( sourceRange );

+ 70 - 8
packages/ckeditor5-engine/src/model/position.js

@@ -61,17 +61,24 @@ export default class Position {
 			 *
 			 * @error model-position-root-invalid
 			 */
-			throw new CKEditorError( 'model-position-root-invalid: Position root invalid.' );
+			throw new CKEditorError(
+				'model-position-root-invalid: Position root invalid.',
+				root
+			);
 		}
 
 		if ( !( path instanceof Array ) || path.length === 0 ) {
 			/**
 			 * Position path must be an array with at least one item.
 			 *
-			 * @error model-position-path-incorrect
+			 * @error model-position-path-incorrect-format
 			 * @param path
 			 */
-			throw new CKEditorError( 'model-position-path-incorrect: Position path must be an array with at least one item.', { path } );
+			throw new CKEditorError(
+				'model-position-path-incorrect-format: Position path must be an array with at least one item.',
+				root,
+				{ path }
+			);
 		}
 
 		// Normalize the root and path (if element was passed).
@@ -154,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).
 	 *
 	 * @readonly
-	 * @type {module:engine/model/element~Element}
+	 * @type {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
 	 */
 	get parent() {
 		let parent = this.root;
 
 		for ( let i = 0; i < this.path.length - 1; 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;
@@ -495,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.
 	 *
@@ -858,7 +906,9 @@ export default class Position {
 				 */
 				throw new CKEditorError(
 					'model-createPositionAt-offset-required: ' +
-					'Model#createPositionAt() requires the offset when the first parameter is a model item.' );
+					'Model#createPositionAt() requires the offset when the first parameter is a model item.',
+					[ this, itemOrPosition ]
+				);
 			}
 
 			if ( !node.is( 'element' ) && !node.is( 'documentFragment' ) ) {
@@ -867,7 +917,10 @@ export default class Position {
 				 *
 				 * @error model-position-parent-incorrect
 				 */
-				throw new CKEditorError( 'model-position-parent-incorrect: Position parent have to be a element or document fragment.' );
+				throw new CKEditorError(
+					'model-position-parent-incorrect: Position parent have to be a element or document fragment.',
+					[ this, itemOrPosition ]
+				);
 			}
 
 			const path = node.getPath();
@@ -894,7 +947,11 @@ export default class Position {
 			 * @error model-position-after-root
 			 * @param {module:engine/model/item~Item} root
 			 */
-			throw new CKEditorError( 'model-position-after-root: You cannot make a position after root.', { root: item } );
+			throw new CKEditorError(
+				'model-position-after-root: You cannot make a position after root.',
+				[ this, item ],
+				{ root: item }
+			);
 		}
 
 		return this._createAt( item.parent, item.endOffset, stickiness );
@@ -916,7 +973,11 @@ export default class Position {
 			 * @error model-position-before-root
 			 * @param {module:engine/model/item~Item} root
 			 */
-			throw new CKEditorError( 'model-position-before-root: You cannot make a position before root.', { root: item } );
+			throw new CKEditorError(
+				'model-position-before-root: You cannot make a position before root.',
+				item,
+				{ root: item }
+			);
 		}
 
 		return this._createAt( item.parent, item.startOffset, stickiness );
@@ -946,6 +1007,7 @@ export default class Position {
 			 */
 			throw new CKEditorError(
 				'model-position-fromjson-no-root: Cannot create position for document. Root with specified name does not exist.',
+				doc,
 				{ rootName: json.root }
 			);
 		}

+ 22 - 1
packages/ckeditor5-engine/src/model/range.js

@@ -143,6 +143,24 @@ export default class Range {
 		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.
 	 *
@@ -877,7 +895,10 @@ export default class Range {
 			 *
 			 * @error range-create-from-ranges-empty-array
 			 */
-			throw new CKEditorError( 'range-create-from-ranges-empty-array: At least one range has to be passed.' );
+			throw new CKEditorError(
+				'range-create-from-ranges-empty-array: At least one range has to be passed.',
+				null
+			);
 		} else if ( ranges.length == 1 ) {
 			return ranges[ 0 ].clone();
 		}

+ 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 ) {
+		const cutType = type.replace( 'model:', '' );
 		if ( !name ) {
-			return type == 'rootElement' || super.is( type );
+			return cutType == 'rootElement' || super.is( type );
 		} else {
-			return ( type == 'rootElement' && name == this.name ) || super.is( type, name );
+			return ( cutType == 'rootElement' && name == this.name ) || super.is( type, name );
 		}
 	}
 

+ 43 - 15
packages/ckeditor5-engine/src/model/schema.js

@@ -91,9 +91,13 @@ export default class Schema {
 			 * @param itemName The name of the model element that is being registered twice.
 			 * @error schema-cannot-register-item-twice
 			 */
-			throw new CKEditorError( 'schema-cannot-register-item-twice: A single item cannot be registered twice in the schema.', {
-				itemName
-			} );
+			throw new CKEditorError(
+				'schema-cannot-register-item-twice: A single item cannot be registered twice in the schema.',
+				this,
+				{
+					itemName
+				}
+			);
 		}
 
 		this._sourceDefinitions[ itemName ] = [
@@ -136,9 +140,9 @@ export default class Schema {
 			 * {@link #register registered} yet.
 			 *
 			 * @param itemName The name of the model element which is being extended.
-			 * @error schema-cannot-register-item-twice
+			 * @error schema-cannot-extend-missing-item
 			 */
-			throw new CKEditorError( 'schema-cannot-extend-missing-item: Cannot extend an item which was not registered yet.', {
+			throw new CKEditorError( 'schema-cannot-extend-missing-item: Cannot extend an item which was not registered yet.', this, {
 				itemName
 			} );
 		}
@@ -352,7 +356,10 @@ export default class Schema {
 				 *
 				 * @error schema-check-merge-no-element-before
 				 */
-				throw new CKEditorError( 'schema-check-merge-no-element-before: The node before the merge position must be an element.' );
+				throw new CKEditorError(
+					'schema-check-merge-no-element-before: The node before the merge position must be an element.',
+					this
+				);
 			}
 
 			if ( !( nodeAfter instanceof Element ) ) {
@@ -361,7 +368,10 @@ export default class Schema {
 				 *
 				 * @error schema-check-merge-no-element-after
 				 */
-				throw new CKEditorError( 'schema-check-merge-no-element-after: The node after the merge position must be an element.' );
+				throw new CKEditorError(
+					'schema-check-merge-no-element-after: The node after the merge position must be an element.',
+					this
+				);
 			}
 
 			return this.checkMerge( nodeBefore, nodeAfter );
@@ -723,14 +733,23 @@ export default class Schema {
 	 */
 	removeDisallowedAttributes( nodes, writer ) {
 		for ( const node of nodes ) {
-			for ( const attribute of node.getAttributeKeys() ) {
-				if ( !this.checkAttribute( node, attribute ) ) {
-					writer.removeAttribute( attribute, node );
-				}
+			// When node is a `Text` it has no children, so just filter it out.
+			if ( node.is( 'text' ) ) {
+				removeDisallowedAttributeFromNode( this, node, writer );
 			}
-
-			if ( node.is( 'element' ) ) {
-				this.removeDisallowedAttributes( node.getChildren(), writer );
+			// In a case of `Element` iterates through positions between nodes inside this element
+			// and filter out node before the current position, or position parent when position
+			// is at start of an element. Using positions prevent from omitting merged nodes
+			// see https://github.com/ckeditor/ckeditor5-engine/issues/1789.
+			else {
+				const rangeInNode = Range._createIn( node );
+				const positionsInRange = rangeInNode.getPositions();
+
+				for ( const position of positionsInRange ) {
+					const item = position.nodeBefore || position.parent;
+
+					removeDisallowedAttributeFromNode( this, item, writer );
+				}
 			}
 		}
 	}
@@ -1018,7 +1037,7 @@ mix( Schema, ObservableMixin );
  * (paragraphs, lists items, headings, images) which, in turn, may contain text inside.
  *
  * By inheriting from the generic items you can define new items which will get extended by other editor features.
- * Read more about generic types in the {@linkTODO Defining schema} guide.
+ * Read more about generic types in the {@glink framework/guides/deep-dive/schema Defining schema} guide.
  *
  * # Example definitions
  *
@@ -1353,6 +1372,7 @@ export class SchemaContext {
  * @typedef {Object} module:engine/model/schema~AttributeProperties
  * @property {Boolean} [isFormatting] Indicates that the attribute should be considered as a visual formatting, like `bold`, `italic` or
  * `fontSize` rather than semantic attribute (such as `src`, `listType`, etc.). For example, it is used by the "Remove format" feature.
+ * @property {Boolean} [copyOnEnter] Indicates that given text attribute should be copied to the next block when enter is pressed.
  */
 
 function compileBaseItemRule( sourceItemRules, itemName ) {
@@ -1581,3 +1601,11 @@ function* convertToMinimalFlatRanges( ranges ) {
 		yield* range.getMinimalFlatRanges();
 	}
 }
+
+function removeDisallowedAttributeFromNode( schema, node, writer ) {
+	for ( const attribute of node.getAttributeKeys() ) {
+		if ( !schema.checkAttribute( node, attribute ) ) {
+			writer.removeAttribute( attribute, node );
+		}
+	}
+}

+ 73 - 51
packages/ckeditor5-engine/src/model/selection.js

@@ -395,7 +395,9 @@ export default class Selection {
 				 */
 				throw new CKEditorError(
 					'model-selection-setTo-required-second-parameter: ' +
-					'selection.setTo requires the second parameter when the first parameter is a node.' );
+					'selection.setTo requires the second parameter when the first parameter is a node.',
+					[ this, selectable ]
+				);
 			}
 
 			this._setRanges( [ range ], backward );
@@ -415,7 +417,10 @@ export default class Selection {
 			 *
 			 * @error model-selection-setTo-not-selectable
 			 */
-			throw new CKEditorError( 'model-selection-setTo-not-selectable: Cannot set the selection to the given place.' );
+			throw new CKEditorError(
+				'model-selection-setTo-not-selectable: Cannot set the selection to the given place.',
+				[ this, selectable ]
+			);
 		}
 	}
 
@@ -449,7 +454,8 @@ export default class Selection {
 				 */
 				throw new CKEditorError(
 					'model-selection-set-ranges-not-range: ' +
-					'Selection range set to an object that is not an instance of model.Range.'
+					'Selection range set to an object that is not an instance of model.Range.',
+					[ this, newRanges ]
 				);
 			}
 
@@ -493,7 +499,8 @@ export default class Selection {
 			 * @error model-selection-setFocus-no-ranges
 			 */
 			throw new CKEditorError(
-				'model-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.'
+				'model-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.',
+				[ this, itemOrPosition ]
 			);
 		}
 
@@ -616,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
 	 * @returns {Boolean}
 	 */
 	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.
 	 *
-	 * **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>
-	 *		<quote>
+	 *		<blockQuote>
 	 *			<paragraph>b</paragraph>
-	 *		</quote>
+	 *		</blockQuote>
 	 *		<paragraph>c]d</paragraph>
 	 *
 	 * In this case the paragraph will also be returned, despite the collapsed selection:
 	 *
 	 *		<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
 	 * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
 	 *
@@ -664,56 +689,30 @@ export default class Selection {
 		const visited = new WeakSet();
 
 		for ( const range of this.getRanges() ) {
+			// Get start block of range in case of a collapsed range.
 			const startBlock = getParentBlock( range.start, visited );
 
-			if ( startBlock ) {
+			if ( startBlock && isTopBlockInRange( startBlock, range ) ) {
 				yield startBlock;
 			}
 
 			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 );
 
 			// #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;
 			}
 		}
 	}
 
-	/**
-	 * 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
 	 * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
@@ -763,6 +762,7 @@ export default class Selection {
 				 */
 				throw new CKEditorError(
 					'model-selection-range-intersects: Trying to add a range that intersects with another range in the selection.',
+					[ this, range ],
 					{ addedRange: range, intersectingRange: this._ranges[ i ] }
 				);
 			}
@@ -822,7 +822,7 @@ mix( Selection, EmitterMixin );
 
 // Checks whether the given element extends $block in the schema and has a parent (is not a root).
 // Marks it as already visited.
-function isUnvisitedBlockContainer( element, visited ) {
+function isUnvisitedBlock( element, visited ) {
 	if ( visited.has( element ) ) {
 		return false;
 	}
@@ -832,6 +832,11 @@ function isUnvisitedBlockContainer( element, visited ) {
 	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.
 // 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.
@@ -850,7 +855,7 @@ function getParentBlock( position, visited ) {
 
 		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
@@ -860,6 +865,23 @@ function getParentBlock( position, visited ) {
 	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.
 //
 // @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
  * {@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 {
 	/**
@@ -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 ) {
-		return type == 'text' || super.is( type );
+		return type == 'text' || type == 'model:text' || super.is( type );
 	}
 
 	/**

+ 11 - 5
packages/ckeditor5-engine/src/model/textproxy.js

@@ -64,7 +64,7 @@ export default class TextProxy {
 			 *
 			 * @error model-textproxy-wrong-offsetintext
 			 */
-			throw new CKEditorError( 'model-textproxy-wrong-offsetintext: Given offsetInText value is incorrect.' );
+			throw new CKEditorError( 'model-textproxy-wrong-offsetintext: Given offsetInText value is incorrect.', this );
 		}
 
 		if ( length < 0 || offsetInText + length > textNode.offsetSize ) {
@@ -73,7 +73,7 @@ export default class TextProxy {
 			 *
 			 * @error model-textproxy-wrong-length
 			 */
-			throw new CKEditorError( 'model-textproxy-wrong-length: Given length value is incorrect.' );
+			throw new CKEditorError( 'model-textproxy-wrong-length: Given length value is incorrect.', this );
 		}
 
 		/**
@@ -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
 	 * @returns {Boolean}
 	 */
 	is( type ) {
-		return type == 'textProxy';
+		return type == 'textProxy' || type == 'model:textProxy';
 	}
 
 	/**

+ 5 - 1
packages/ckeditor5-engine/src/model/treewalker.js

@@ -42,7 +42,10 @@ export default class TreeWalker {
 			 *
 			 * @error model-tree-walker-no-start-position
 			 */
-			throw new CKEditorError( 'model-tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
+			throw new CKEditorError(
+				'model-tree-walker-no-start-position: Neither boundaries nor starting position have been defined.',
+				null
+			);
 		}
 
 		const direction = options.direction || 'forward';
@@ -50,6 +53,7 @@ export default class TreeWalker {
 		if ( direction != 'forward' && direction != 'backward' ) {
 			throw new CKEditorError(
 				'model-tree-walker-unknown-direction: Only `backward` and `forward` direction allowed.',
+				options,
 				{ direction }
 			);
 		}

+ 29 - 14
packages/ckeditor5-engine/src/model/utils/insertcontent.js

@@ -11,9 +11,9 @@ import Position from '../position';
 import LivePosition from '../liveposition';
 import Element from '../element';
 import Range from '../range';
-import log from '@ckeditor/ckeditor5-utils/src/log';
 import DocumentSelection from '../documentselection';
 import Selection from '../selection';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * Inserts content into the editor (specified selection) as one would expect the paste
@@ -85,13 +85,8 @@ export default function insertContent( model, content, selectable, placeOrOffset
 		} else {
 			// We are not testing else because it's a safe check for unpredictable edge cases:
 			// an insertion without proper range to select.
-
-			/**
-			 * Cannot determine a proper selection range after insertion.
-			 *
-			 * @warning insertcontent-no-range
-			 */
-			log.warn( 'insertcontent-no-range: Cannot determine a proper selection range after insertion.' );
+			//
+			// @if CK_DEBUG // console.warn( 'Cannot determine a proper selection range after insertion.' );
 		}
 
 		const affectedRange = insertion.getAffectedRange() || model.createRange( insertionPosition );
@@ -322,12 +317,19 @@ class Insertion {
 		if ( !this.schema.checkChild( this.position, node ) ) {
 			// Algorithm's correctness check. We should never end up here but it's good to know that we did.
 			// Note that it would often be a silent issue if we insert node in a place where it's not allowed.
-			log.error(
-				'insertcontent-wrong-position: The node cannot be inserted on the given position.',
+
+			/**
+			 * Given node cannot be inserted on the given position.
+			 *
+			 * @error insertcontent-wrong-position
+			 * @param {module:engine/model/node~Node} node Node to insert.
+			 * @param {module:engine/model/position~Position} position Position to insert the node at.
+			 */
+			throw new CKEditorError(
+				'insertcontent-wrong-position: Given node cannot be inserted on the given position.',
+				this,
 				{ node, position: this.position }
 			);
-
-			return;
 		}
 
 		const livePos = LivePosition.fromPosition( this.position, 'toNext' );
@@ -442,7 +444,13 @@ class Insertion {
 				// Algorithm's correctness check. We should never end up here but it's good to know that we did.
 				// At this point the insertion position should be after the node we'll merge. If it isn't,
 				// it should need to be secured as in the left merge case.
-				log.error( 'insertcontent-wrong-position-on-merge: The insertion position should equal the merge position' );
+				/**
+				 * An internal error occured during merging insertion content with siblings.
+				 * The insertion position should equal to the merge position.
+				 *
+				 * @error insertcontent-invalid-insertion-position
+				 */
+				throw new CKEditorError( 'insertcontent-invalid-insertion-position', this );
 			}
 
 			// Move the position to the previous node, so it isn't moved to the graveyard on merge.
@@ -561,8 +569,15 @@ class Insertion {
 				this.position = this.writer.createPositionBefore( parent );
 
 				// Special case – parent is empty (<p>^</p>).
+				//
+				// 1. parent.isEmpty
 				// We can remove the element after moving insertion position out of it.
-				if ( parent.isEmpty ) {
+				//
+				// 2. parent.parent === allowedIn
+				// However parent should remain in place when allowed element is above limit element in document tree.
+				// For example there shouldn't be allowed to remove empty paragraph from tableCell, when is pasted
+				// content allowed in $root.
+				if ( parent.isEmpty && parent.parent === allowedIn ) {
 					this.writer.remove( parent );
 				}
 			} else if ( this.position.isAtEnd ) {

+ 45 - 23
packages/ckeditor5-engine/src/model/writer.js

@@ -177,8 +177,18 @@ export default class Writer {
 			// If it isn't the same root.
 			else {
 				if ( item.root.document ) {
-					// It is forbidden to move a node that was already in a document outside of it.
-					throw new Error( 'model-writer-insert-forbidden-move: Cannot move a node from a document to a different tree.' );
+					/**
+					 * Cannot move a node from a document to a different tree.
+					 * It is forbidden to move a node that was already in a document outside of it.
+					 *
+					 * @error model-writer-insert-forbidden-move
+					 */
+					throw new CKEditorError(
+						'model-writer-insert-forbidden-move: ' +
+						'Cannot move a node from a document to a different tree. ' +
+						'It is forbidden to move a node that was already in a document outside of it.',
+						this
+					);
 				} else {
 					// Move between two different document fragments or from document fragment to a document is possible.
 					// In that case, remove the item from it's original parent.
@@ -208,7 +218,13 @@ export default class Writer {
 					markerRange.end._getCombined( rangeRootPosition, position )
 				);
 
-				this.addMarker( markerName, { range, usingOperation: true, affectsData: true } );
+				const options = { range, usingOperation: true, affectsData: true };
+
+				if ( this.model.markers.has( markerName ) ) {
+					this.updateMarker( markerName, options );
+				} else {
+					this.addMarker( markerName, options );
+				}
 			}
 		}
 	}
@@ -456,7 +472,7 @@ export default class Writer {
 			 *
 			 * @error writer-move-invalid-range
 			 */
-			throw new CKEditorError( 'writer-move-invalid-range: Invalid range to move.' );
+			throw new CKEditorError( 'writer-move-invalid-range: Invalid range to move.', this );
 		}
 
 		if ( !range.isFlat ) {
@@ -465,7 +481,7 @@ export default class Writer {
 			 *
 			 * @error writer-move-range-not-flat
 			 */
-			throw new CKEditorError( 'writer-move-range-not-flat: Range to move is not flat.' );
+			throw new CKEditorError( 'writer-move-range-not-flat: Range to move is not flat.', this );
 		}
 
 		const position = Position._createAt( itemOrPosition, offset );
@@ -485,7 +501,7 @@ export default class Writer {
 			 *
 			 * @error writer-move-different-document
 			 */
-			throw new CKEditorError( 'writer-move-different-document: Range is going to be moved between different documents.' );
+			throw new CKEditorError( 'writer-move-different-document: Range is going to be moved between different documents.', this );
 		}
 
 		const version = range.root.document ? range.root.document.version : null;
@@ -537,7 +553,7 @@ export default class Writer {
 			 *
 			 * @error writer-merge-no-element-before
 			 */
-			throw new CKEditorError( 'writer-merge-no-element-before: Node before merge position must be an element.' );
+			throw new CKEditorError( 'writer-merge-no-element-before: Node before merge position must be an element.', this );
 		}
 
 		if ( !( nodeAfter instanceof Element ) ) {
@@ -546,7 +562,7 @@ export default class Writer {
 			 *
 			 * @error writer-merge-no-element-after
 			 */
-			throw new CKEditorError( 'writer-merge-no-element-after: Node after merge position must be an element.' );
+			throw new CKEditorError( 'writer-merge-no-element-after: Node after merge position must be an element.', this );
 		}
 
 		if ( !position.root.document ) {
@@ -696,7 +712,8 @@ export default class Writer {
 			 * @error writer-rename-not-element-instance
 			 */
 			throw new CKEditorError(
-				'writer-rename-not-element-instance: Trying to rename an object which is not an instance of Element.'
+				'writer-rename-not-element-instance: Trying to rename an object which is not an instance of Element.',
+				this
 			);
 		}
 
@@ -732,7 +749,7 @@ export default class Writer {
 			 *
 			 * @error writer-split-element-no-parent
 			 */
-			throw new CKEditorError( 'writer-split-element-no-parent: Element with no parent can not be split.' );
+			throw new CKEditorError( 'writer-split-element-no-parent: Element with no parent can not be split.', this );
 		}
 
 		// When limit element is not defined lets set splitElement parent as limit.
@@ -741,7 +758,7 @@ export default class Writer {
 		}
 
 		if ( !position.parent.getAncestors( { includeSelf: true } ).includes( limitElement ) ) {
-			throw new CKEditorError( 'writer-split-invalid-limit-element: Limit element is not a position ancestor.' );
+			throw new CKEditorError( 'writer-split-invalid-limit-element: Limit element is not a position ancestor.', this );
 		}
 
 		// We need to cache elements that will be created as a result of the first split because
@@ -791,7 +808,7 @@ export default class Writer {
 			 *
 			 * @error writer-wrap-range-not-flat
 			 */
-			throw new CKEditorError( 'writer-wrap-range-not-flat: Range to wrap is not flat.' );
+			throw new CKEditorError( 'writer-wrap-range-not-flat: Range to wrap is not flat.', this );
 		}
 
 		const element = elementOrString instanceof Element ? elementOrString : new Element( elementOrString );
@@ -802,7 +819,7 @@ export default class Writer {
 			 *
 			 * @error writer-wrap-element-not-empty
 			 */
-			throw new CKEditorError( 'writer-wrap-element-not-empty: Element to wrap with is not empty.' );
+			throw new CKEditorError( 'writer-wrap-element-not-empty: Element to wrap with is not empty.', this );
 		}
 
 		if ( element.parent !== null ) {
@@ -811,7 +828,7 @@ export default class Writer {
 			 *
 			 * @error writer-wrap-element-attached
 			 */
-			throw new CKEditorError( 'writer-wrap-element-attached: Element to wrap with is already attached to tree model.' );
+			throw new CKEditorError( 'writer-wrap-element-attached: Element to wrap with is already attached to tree model.', this );
 		}
 
 		this.insert( element, range.start );
@@ -837,7 +854,7 @@ export default class Writer {
 			 *
 			 * @error writer-unwrap-element-no-parent
 			 */
-			throw new CKEditorError( 'writer-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
+			throw new CKEditorError( 'writer-unwrap-element-no-parent: Trying to unwrap an element which has no parent.', this );
 		}
 
 		this.move( Range._createIn( element ), this.createPositionAfter( element ) );
@@ -893,7 +910,8 @@ export default class Writer {
 			 * @error writer-addMarker-no-usingOperation
 			 */
 			throw new CKEditorError(
-				'writer-addMarker-no-usingOperation: The options.usingOperation parameter is required when adding a new marker.'
+				'writer-addMarker-no-usingOperation: The options.usingOperation parameter is required when adding a new marker.',
+				this
 			);
 		}
 
@@ -907,7 +925,7 @@ export default class Writer {
 			 *
 			 * @error writer-addMarker-marker-exists
 			 */
-			throw new CKEditorError( 'writer-addMarker-marker-exists: Marker with provided name already exists.' );
+			throw new CKEditorError( 'writer-addMarker-marker-exists: Marker with provided name already exists.', this );
 		}
 
 		if ( !range ) {
@@ -916,7 +934,10 @@ export default class Writer {
 			 *
 			 * @error writer-addMarker-no-range
 			 */
-			throw new CKEditorError( 'writer-addMarker-no-range: Range parameter is required when adding a new marker.' );
+			throw new CKEditorError(
+				'writer-addMarker-no-range: Range parameter is required when adding a new marker.',
+				this
+			);
 		}
 
 		if ( !usingOperation ) {
@@ -989,7 +1010,7 @@ export default class Writer {
 	 *		updateMarker( markerName, { affectsData: false } );
 	 *
 	 * @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
 	 * downcast conversion for this marker with the same data.
 	 * @param {module:engine/model/range~Range} [options.range] Marker range to update.
@@ -1009,7 +1030,7 @@ export default class Writer {
 			 *
 			 * @error writer-updateMarker-marker-not-exists
 			 */
-			throw new CKEditorError( 'writer-updateMarker-marker-not-exists: Marker with provided name does not exists.' );
+			throw new CKEditorError( 'writer-updateMarker-marker-not-exists: Marker with provided name does not exists.', this );
 		}
 
 		if ( !options ) {
@@ -1031,7 +1052,8 @@ export default class Writer {
 			 * @error writer-updateMarker-wrong-options
 			 */
 			throw new CKEditorError(
-				'writer-updateMarker-wrong-options: One of the options is required - provide range, usingOperations or affectsData.'
+				'writer-updateMarker-wrong-options: One of the options is required - provide range, usingOperations or affectsData.',
+				this
 			);
 		}
 
@@ -1082,7 +1104,7 @@ export default class Writer {
 			 *
 			 * @error writer-removeMarker-no-marker
 			 */
-			throw new CKEditorError( 'writer-removeMarker-no-marker: Trying to remove marker which does not exist.' );
+			throw new CKEditorError( 'writer-removeMarker-no-marker: Trying to remove marker which does not exist.', this );
 		}
 
 		const marker = this.model.markers.get( name );
@@ -1315,7 +1337,7 @@ export default class Writer {
 		 * @error writer-incorrect-use
 		 */
 		if ( this.model._currentWriter !== this ) {
-			throw new CKEditorError( 'writer-incorrect-use: Trying to use a writer outside the change() block.' );
+			throw new CKEditorError( 'writer-incorrect-use: Trying to use a writer outside the change() block.', this );
 		}
 	}
 

+ 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';
 
 /**
- * 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.
  *
  * Thanks to this (phantom) caret movement the user is able to type before/after as well as at the
  * 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
  *
  * ## "Entering" an attribute:
@@ -78,13 +81,15 @@ import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
  *
  *   		<$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).
- * @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 modelSelection = model.document.selection;
 
@@ -120,15 +125,16 @@ export default function bindTwoStepCaretToAttribute( view, model, emitter, attri
 		}
 
 		const position = modelSelection.getFirstPosition();
+		const contentDirection = locale.contentLanguageDirection;
 		let isMovementHandled;
 
-		if ( arrowRightPressed ) {
+		if ( ( contentDirection === 'ltr' && arrowRightPressed ) || ( contentDirection === 'rtl' && arrowLeftPressed ) ) {
 			isMovementHandled = twoStepCaretHandler.handleForwardMovement( position, data );
 		} else {
 			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).
 		if ( isMovementHandled ) {
 			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}
  * attribute upon the `keypress` in the {@link module:engine/view/view~View}.
  *
- * @private
+ * @protected
  */
-class TwoStepCaretHandler {
+export class TwoStepCaretHandler {
 	/*
 	 * Creates two step handler instance.
 	 *

+ 31 - 4
packages/ckeditor5-engine/src/view/attributeelement.js

@@ -116,7 +116,8 @@ export default class AttributeElement extends Element {
 			 */
 			throw new CKEditorError(
 				'attribute-element-get-elements-with-same-id-no-id: ' +
-				'Cannot get elements with the same id for an attribute element without id.'
+				'Cannot get elements with the same id for an attribute element without id.',
+				this
 			);
 		}
 
@@ -124,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 ) {
+		const cutType = type && type.replace( /^view:/, '' );
+
 		if ( !name ) {
-			return type == 'attributeElement' || super.is( type );
+			return cutType == 'attributeElement' || super.is( type );
 		} 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 ) {
+		const cutType = type && type.replace( /^view:/, '' );
 		if ( !name ) {
-			return type == 'containerElement' || super.is( type );
+			return cutType == 'containerElement' || super.is( type );
 		} else {
-			return ( type == 'containerElement' && name == this.name ) || super.is( type, name );
+			return ( cutType == 'containerElement' && name == this.name ) || super.is( type, name );
 		}
 	}
 }

+ 35 - 9
packages/ckeditor5-engine/src/view/document.js

@@ -100,23 +100,49 @@ export default class Document {
 	}
 
 	/**
-	 * Used to register a post-fixer callback. A post-fixers mechanism allows to update view tree just before rendering
+	 * Allows registering post-fixer callbacks. A post-fixers mechanism allows to update the view tree just before it is rendered
 	 * to the DOM.
 	 *
-	 * Post-fixers are fired just after all changes from the outermost change block were applied but
+	 * Post-fixers are executed right after all changes from the outermost change block were applied but
 	 * before the {@link module:engine/view/view~View#event:render render event} is fired. If a post-fixer callback made
 	 * a change, it should return `true`. When this happens, all post-fixers are fired again to check if something else should
 	 * not be fixed in the new document tree state.
 	 *
-	 * View post-fixers are useful when you wants to update view structure whenever it changes, for instance add some classes
-	 * to elements based on the view structure or selection. However, is you need DOM elements to be already updated, use
-	 * {@link module:engine/view/view~View#event:render render event}.
+	 * View post-fixers are useful when you want to apply some fixes whenever the view structure changes. Keep in mind that
+	 * changes executed in a view post-fixer should not break model-view mapping.
 	 *
-	 * As a parameter, a post-fixer callback receives a {@link module:engine/view/downcastwriter~DowncastWriter downcast writer}
-	 * instance connected with the executed changes block.
+	 * The types of changes which should be safe:
 	 *
-	 * Note that registering a post-fixer won't re-render the editor's view. If the view should change after registering the post-fixer then
-	 * it should be done manually calling `view.forceRender();`.
+	 * * adding or removing attribute from elements,
+	 * * changes inside of {@link module:engine/view/uielement~UIElement UI elements},
+	 * * {@link module:engine/model/differ~Differ#refreshItem marking some of the model elements to be re-converted}.
+	 *
+	 * Try to avoid changes which touch view structure:
+	 *
+	 * * you should not add or remove nor wrap or unwrap any view elements,
+	 * * you should not change the editor data model in a view post-fixer.
+	 *
+	 * As a parameter, a post-fixer callback receives a {@link module:engine/view/downcastwriter~DowncastWriter downcast writer}.
+	 *
+	 * Typically, a post-fixer will look like this:
+	 *
+	 *		editor.editing.view.document.registerPostFixer( writer => {
+	 *			if ( checkSomeCondition() ) {
+	 *				writer.doSomething();
+	 *
+	 *				// Let other post-fixers know that something changed.
+	 *				return true;
+	 *			}
+	 *		} );
+	 *
+	 * Note that nothing happens right after you register a post-fixer (e.g. execute such a code in the console).
+	 * That is because adding a post-fixer does not execute it.
+	 * The post-fixer will be executed as soon as any change in the document needs to cause its rendering.
+	 * If you want to re-render the editor's view after registering the post-fixer then you should do it manually by calling
+	 * {@link module:engine/view/view~View#forceRender `view.forceRender()`}.
+	 *
+	 * If you need to register a callback which is executed when DOM elements are already updated,
+	 * use {@link module:engine/view/view~View#event:render render event}.
 	 *
 	 * @param {Function} postFixer
 	 */

+ 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
 	 * @returns {Boolean}
 	 */
 	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
 	 * @returns {Boolean}
 	 */
 	is( type ) {
-		return type == 'selection' || type == 'documentSelection';
+		return type == 'selection' ||
+			type == 'documentSelection' ||
+			type == 'view:selection' ||
+			type == 'view:documentSelection';
 	}
 
 	/**

+ 116 - 45
packages/ckeditor5-engine/src/view/domconverter.js

@@ -16,7 +16,7 @@ import ViewRange from './range';
 import ViewSelection from './selection';
 import ViewDocumentFragment from './documentfragment';
 import ViewTreeWalker from './treewalker';
-import { BR_FILLER, INLINE_FILLER_LENGTH, isBlockFiller, isInlineFiller, startsWithFiller, getDataWithoutFiller } from './filler';
+import { BR_FILLER, getDataWithoutFiller, INLINE_FILLER_LENGTH, isInlineFiller, NBSP_FILLER, startsWithFiller } from './filler';
 
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import indexOf from '@ckeditor/ckeditor5-utils/src/dom/indexof';
@@ -25,6 +25,9 @@ import getCommonAncestor from '@ckeditor/ckeditor5-utils/src/dom/getcommonancest
 import isText from '@ckeditor/ckeditor5-utils/src/dom/istext';
 import { isElement } from 'lodash-es';
 
+// eslint-disable-next-line new-cap
+const BR_FILLER_REF = BR_FILLER( document );
+
 /**
  * DomConverter is a set of tools to do transformations between DOM nodes and view nodes. It also handles
  * {@link module:engine/view/domconverter~DomConverter#bindElements binding} these nodes.
@@ -42,30 +45,19 @@ export default class DomConverter {
 	 * Creates DOM converter.
 	 *
 	 * @param {Object} options Object with configuration options.
-	 * @param {Function} [options.blockFiller=module:engine/view/filler~BR_FILLER] Block filler creator.
+	 * @param {module:engine/view/filler~BlockFillerMode} [options.blockFillerMode='br'] The type of the block filler to use.
 	 */
 	constructor( options = {} ) {
-		// Using WeakMap prevent memory leaks: when the converter will be destroyed all referenced between View and DOM
-		// will be removed. Also because it is a *Weak*Map when both view and DOM elements will be removed referenced
-		// will be also removed, isn't it brilliant?
-		//
-		// Yes, PJ. It is.
-		//
-		// You guys so smart.
-		//
-		// I've been here. Seen stuff. Afraid of code now.
-
 		/**
-		 * Block {@link module:engine/view/filler filler} creator, which is used to create all block fillers during the
-		 * view to DOM conversion and to recognize block fillers during the DOM to view conversion.
+		 * The mode of a block filler used by DOM converter.
 		 *
 		 * @readonly
-		 * @member {Function} module:engine/view/domconverter~DomConverter#blockFiller
+		 * @member {'br'|'nbsp'} module:engine/view/domconverter~DomConverter#blockFillerMode
 		 */
-		this.blockFiller = options.blockFiller || BR_FILLER;
+		this.blockFillerMode = options.blockFillerMode || 'br';
 
 		/**
-		 * Tag names of DOM `Element`s which are considered pre-formatted elements.
+		 * Elements which are considered pre-formatted elements.
 		 *
 		 * @readonly
 		 * @member {Array.<String>} module:engine/view/domconverter~DomConverter#preElements
@@ -73,12 +65,27 @@ export default class DomConverter {
 		this.preElements = [ 'pre' ];
 
 		/**
-		 * Tag names of DOM `Element`s which are considered block elements.
+		 * Elements which are considered block elements (and hence should be filled with a
+		 * {@link #isBlockFiller block filler}).
+		 *
+		 * Whether an element is considered a block element also affects handling of trailing whitespaces.
+		 *
+		 * You can extend this array if you introduce support for block elements which are not yet recognized here.
 		 *
 		 * @readonly
 		 * @member {Array.<String>} module:engine/view/domconverter~DomConverter#blockElements
 		 */
-		this.blockElements = [ 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ];
+		this.blockElements = [ 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'dd', 'dt', 'figcaption' ];
+
+		/**
+		 * Block {@link module:engine/view/filler filler} creator, which is used to create all block fillers during the
+		 * view to DOM conversion and to recognize block fillers during the DOM to view conversion.
+		 *
+		 * @readonly
+		 * @private
+		 * @member {Function} module:engine/view/domconverter~DomConverter#_blockFiller
+		 */
+		this._blockFiller = this.blockFillerMode == 'br' ? BR_FILLER : NBSP_FILLER;
 
 		/**
 		 * DOM to View mapping.
@@ -106,8 +113,9 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Binds given DOM element that represents fake selection to {@link module:engine/view/documentselection~DocumentSelection
-	 * document selection}. Document selection copy is stored and can be retrieved by
+	 * Binds given DOM element that represents fake selection to a **position** of a
+	 * {@link module:engine/view/documentselection~DocumentSelection document selection}.
+	 * Document selection copy is stored and can be retrieved by
 	 * {@link module:engine/view/domconverter~DomConverter#fakeSelectionToView} method.
 	 *
 	 * @param {HTMLElement} domElement
@@ -216,7 +224,11 @@ export default class DomConverter {
 				return domElement;
 			} else {
 				// Create DOM element.
-				domElement = domDocument.createElement( viewNode.name );
+				if ( viewNode.hasAttribute( 'xmlns' ) ) {
+					domElement = domDocument.createElementNS( viewNode.getAttribute( 'xmlns' ), viewNode.name );
+				} else {
+					domElement = domDocument.createElement( viewNode.name );
+				}
 
 				if ( options.bind ) {
 					this.bindElements( domElement, viewNode );
@@ -254,7 +266,7 @@ export default class DomConverter {
 
 		for ( const childView of viewElement.getChildren() ) {
 			if ( fillerPositionOffset === offset ) {
-				yield this.blockFiller( domDocument );
+				yield this._blockFiller( domDocument );
 			}
 
 			yield this.viewToDom( childView, domDocument, options );
@@ -263,7 +275,7 @@ export default class DomConverter {
 		}
 
 		if ( fillerPositionOffset === offset ) {
-			yield this.blockFiller( domDocument );
+			yield this._blockFiller( domDocument );
 		}
 	}
 
@@ -370,7 +382,7 @@ export default class DomConverter {
 	 * or `null` if DOM node is a {@link module:engine/view/filler filler} or the given node is an empty text node.
 	 */
 	domToView( domNode, options = {} ) {
-		if ( isBlockFiller( domNode, this.blockFiller ) ) {
+		if ( this.isBlockFiller( domNode, this.blockFillerMode ) ) {
 			return null;
 		}
 
@@ -528,7 +540,7 @@ export default class DomConverter {
 	 * @returns {module:engine/view/position~Position} viewPosition View position.
 	 */
 	domPositionToView( domParent, domOffset ) {
-		if ( isBlockFiller( domParent, this.blockFiller ) ) {
+		if ( this.isBlockFiller( domParent, this.blockFillerMode ) ) {
 			return this.domPositionToView( domParent.parentNode, indexOf( domParent ) );
 		}
 
@@ -785,6 +797,23 @@ export default class DomConverter {
 		return node && node.nodeType == Node.COMMENT_NODE;
 	}
 
+	/**
+	 * Checks if the node is an instance of the block filler for this DOM converter.
+	 *
+	 *		const converter = new DomConverter( { blockFillerMode: 'br' } );
+	 *
+	 *		converter.isBlockFiller( BR_FILLER( document ) ); // true
+	 *		converter.isBlockFiller( NBSP_FILLER( document ) ); // false
+	 *
+	 * **Note:**: For the `'nbsp'` mode the method also checks context of a node so it cannot be a detached node.
+	 *
+	 * @param {Node} domNode DOM node to check.
+	 * @returns {Boolean} True if a node is considered a block filler for given mode.
+	 */
+	isBlockFiller( domNode ) {
+		return this.blockFillerMode == 'br' ? domNode.isEqualNode( BR_FILLER_REF ) : isNbspBlockFiller( domNode, this.blockElements );
+	}
+
 	/**
 	 * Returns `true` if given selection is a backward selection, that is, if it's `focus` is before `anchor`.
 	 *
@@ -889,7 +918,8 @@ export default class DomConverter {
 	 *
 	 * * a space at the beginning is changed to `&nbsp;` if this is the first text node in its container
 	 * element or if a previous text node ends with a space character,
-	 * * space at the end of the text node is changed to `&nbsp;` if this is the last text node in its container,
+	 * * space at the end of the text node is changed to `&nbsp;` if there are two spaces at the end of a node or if next node
+	 * starts with a space or if it is the last text node in its container,
 	 * * remaining spaces are replaced to a chain of spaces and `&nbsp;` (e.g. `'x   x'` becomes `'x &nbsp; x'`).
 	 *
 	 * Content of {@link #preElements} is not processed.
@@ -918,15 +948,24 @@ export default class DomConverter {
 			}
 		}
 
-		// 2. Replace the last space with a nbsp if this is the last text node (container element boundary).
+		// 2. Replace the last space with nbsp if there are two spaces at the end or if the next node starts with space or there is no
+		// next node (container element boundary).
+		//
+		// Keep in mind that Firefox prefers $nbsp; before tag, not inside it:
+		//
+		// Foo <span>&nbsp;bar</span>  <-- bad.
+		// Foo&nbsp;<span> bar</span>  <-- good.
+		//
+		// More here: https://github.com/ckeditor/ckeditor5-engine/issues/1747.
 		if ( data.charAt( data.length - 1 ) == ' ' ) {
 			const nextNode = this._getTouchingViewTextNode( node, true );
 
-			if ( !nextNode ) {
+			if ( data.charAt( data.length - 2 ) == ' ' || !nextNode || nextNode.data.charAt( 0 ) == ' ' ) {
 				data = data.substr( 0, data.length - 1 ) + '\u00A0';
 			}
 		}
 
+		// 3. Create space+nbsp pairs.
 		return data.replace( / {2}/g, ' \u00A0' );
 	}
 
@@ -955,7 +994,9 @@ export default class DomConverter {
 	 * * multiple whitespaces are replaced to a single space,
 	 * * space at the beginning of a text node is removed if it is the first text node in its container
 	 * element or if the previous text node ends with a space character,
-	 * * space at the end of the text node is removed, if it is the last text node in its container.
+	 * * space at the end of the text node is removed if there are two spaces at the end of a node or if next node
+	 * starts with a space or if it is the last text node in its container
+	 * * nbsps are converted to spaces.
 	 *
 	 * @param {Node} node DOM text node to process.
 	 * @returns {String} Processed data.
@@ -998,28 +1039,25 @@ export default class DomConverter {
 		data = getDataWithoutFiller( new Text( data ) );
 
 		// At this point we should have removed all whitespaces from DOM text data.
-
-		// Now we have to change &nbsp; chars, that were in DOM text data because of rendering reasons, to spaces.
-		// First, change all ` \u00A0` pairs (space + &nbsp;) to two spaces. DOM converter changes two spaces from model/view as
-		// ` \u00A0` to ensure proper rendering. Since here we convert back, we recognize those pairs and change them
-		// to `  ` which is what we expect to have in model/view.
+		//
+		// Now, We will reverse the process that happens in `_processDataFromViewText`.
+		//
+		// We have to change &nbsp; chars, that were in DOM text data because of rendering reasons, to spaces.
+		// First, change all ` \u00A0` pairs (space + &nbsp;) to two spaces. DOM converter changes two spaces from model/view to
+		// ` \u00A0` to ensure proper rendering. Since here we convert back, we recognize those pairs and change them back to `  `.
 		data = data.replace( / \u00A0/g, '  ' );
 
+		// Then, let's change the last nbsp to a space.
+		if ( /( |\u00A0)\u00A0$/.test( data ) || !nextNode || ( nextNode.data && nextNode.data.charAt( 0 ) == ' ' ) ) {
+			data = data.replace( /\u00A0$/, ' ' );
+		}
+
 		// Then, change &nbsp; character that is at the beginning of the text node to space character.
-		// As above, that &nbsp; was created for rendering reasons but it's real meaning is just a space character.
 		// We do that replacement only if this is the first node or the previous node ends on whitespace character.
 		if ( shouldLeftTrim ) {
 			data = data.replace( /^\u00A0/, ' ' );
 		}
 
-		// Since input text data could be: `x_ _`, we would not replace the first &nbsp; after `x` character.
-		// We have to fix it. Since we already change all ` &nbsp;`, we will have something like this at the end of text data:
-		// `x_ _ _` -> `x_    `. Find &nbsp; at the end of string (can be followed only by spaces).
-		// We do that replacement only if this is the last node or the next node starts with &nbsp; or is a <br>.
-		if ( isText( nextNode ) ? nextNode.data.charAt( 0 ) == '\u00A0' : true ) {
-			data = data.replace( /\u00A0( *)$/, ' $1' );
-		}
-
 		// At this point, all whitespaces should be removed and all &nbsp; created for rendering reasons should be
 		// changed to normal space. All left &nbsp; are &nbsp; inserted intentionally.
 		return data;
@@ -1048,7 +1086,7 @@ export default class DomConverter {
 	 * be trimmed from the right side.
 	 *
 	 * @param {Node} node
-	 * @param {Node} prevNode
+	 * @param {Node} nextNode
 	 */
 	_checkShouldRightTrimDomText( node, nextNode ) {
 		if ( nextNode ) {
@@ -1187,3 +1225,36 @@ function forEachDomNodeAncestor( node, callback ) {
 		node = node.parentNode;
 	}
 }
+
+// Checks if given node is a nbsp block filler.
+//
+// A &nbsp; is a block filler only if it is a single child of a block element.
+//
+// @param {Node} domNode DOM node.
+// @returns {Boolean}
+function isNbspBlockFiller( domNode, blockElements ) {
+	const isNBSP = isText( domNode ) && domNode.data == '\u00A0';
+
+	return isNBSP && hasBlockParent( domNode, blockElements ) && domNode.parentNode.childNodes.length === 1;
+}
+
+// Checks if domNode has block parent.
+//
+// @param {Node} domNode DOM node.
+// @returns {Boolean}
+function hasBlockParent( domNode, blockElements ) {
+	const parent = domNode.parentNode;
+
+	return parent && parent.tagName && blockElements.includes( parent.tagName.toLowerCase() );
+}
+
+/**
+ * Enum representing type of the block filler.
+ *
+ * Possible values:
+ *
+ * * `br` - for `<br>` block filler used in editing view,
+ * * `nbsp` - for `&nbsp;` block fillers used in the data.
+ *
+ * @typedef {String} module:engine/view/filler~BlockFillerMode
+ */

+ 74 - 69
packages/ckeditor5-engine/src/view/downcastwriter.js

@@ -69,7 +69,7 @@ export default class DowncastWriter {
 	 *		writer.setSelection( range );
 	 *
 	 *		// 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 );
 	 *
 	 *		// Sets selection to the other selection.
@@ -444,7 +444,8 @@ export default class DowncastWriter {
 			 * @error view-writer-break-non-container-element
 			 */
 			throw new CKEditorError(
-				'view-writer-break-non-container-element: Trying to break an element which is not a container element.'
+				'view-writer-break-non-container-element: Trying to break an element which is not a container element.',
+				this.document
 			);
 		}
 
@@ -454,7 +455,7 @@ export default class DowncastWriter {
 			 *
 			 * @error view-writer-break-root
 			 */
-			throw new CKEditorError( 'view-writer-break-root: Trying to break root element.' );
+			throw new CKEditorError( 'view-writer-break-root: Trying to break root element.', this.document );
 		}
 
 		if ( position.isAtStart ) {
@@ -577,7 +578,7 @@ export default class DowncastWriter {
 			 * @error view-writer-merge-containers-invalid-position
 			 */
 			throw new CKEditorError( 'view-writer-merge-containers-invalid-position: ' +
-				'Element before and after given position cannot be merged.' );
+				'Element before and after given position cannot be merged.', this.document );
 		}
 
 		const lastChild = prev.getChild( prev.childCount - 1 );
@@ -612,7 +613,7 @@ export default class DowncastWriter {
 		nodes = isIterable( nodes ) ? [ ...nodes ] : [ nodes ];
 
 		// Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text.
-		validateNodesToInsert( nodes );
+		validateNodesToInsert( nodes, this.document );
 
 		const container = getParentContainer( position );
 
@@ -622,7 +623,7 @@ export default class DowncastWriter {
 			 *
 			 * @error view-writer-invalid-position-container
 			 */
-			throw new CKEditorError( 'view-writer-invalid-position-container' );
+			throw new CKEditorError( 'view-writer-invalid-position-container', this.document );
 		}
 
 		const insertionPosition = this._breakAttributes( position, true );
@@ -665,7 +666,7 @@ export default class DowncastWriter {
 	remove( rangeOrItem ) {
 		const range = rangeOrItem instanceof Range ? rangeOrItem : Range._createOn( rangeOrItem );
 
-		validateRangeContainer( range );
+		validateRangeContainer( range, this.document );
 
 		// If range is collapsed - nothing to remove.
 		if ( range.isCollapsed ) {
@@ -705,7 +706,7 @@ export default class DowncastWriter {
 	 * @param {module:engine/view/element~Element} element Element to remove.
 	 */
 	clear( range, element ) {
-		validateRangeContainer( range );
+		validateRangeContainer( range, this.document );
 
 		// Create walker on given range.
 		// We walk backward because when we remove element during walk it modifies range end position.
@@ -787,32 +788,32 @@ 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 ) {
 		if ( !( attribute instanceof AttributeElement ) ) {
-			throw new CKEditorError( 'view-writer-wrap-invalid-attribute' );
+			throw new CKEditorError( 'view-writer-wrap-invalid-attribute', this.document );
 		}
 
-		validateRangeContainer( range );
+		validateRangeContainer( range, this.document );
 
 		if ( !range.isCollapsed ) {
 			// Non-collapsed range. Wrap it with the attribute element.
@@ -854,10 +855,10 @@ export default class DowncastWriter {
 			 *
 			 * @error view-writer-unwrap-invalid-attribute
 			 */
-			throw new CKEditorError( 'view-writer-unwrap-invalid-attribute' );
+			throw new CKEditorError( 'view-writer-unwrap-invalid-attribute', this.document );
 		}
 
-		validateRangeContainer( range );
+		validateRangeContainer( range, this.document );
 
 		// If range is collapsed - nothing to unwrap.
 		if ( range.isCollapsed ) {
@@ -1093,7 +1094,7 @@ export default class DowncastWriter {
 			// <p><span class="bar">abc</span></p>  -->  <p><span class="foo bar">abc</span></p>
 			//
 			if ( isAttribute && this._wrapAttributeElement( wrapElement, child ) ) {
-				wrapPositions.push(	new Position( parent, i ) );
+				wrapPositions.push( new Position( parent, i ) );
 			}
 			//
 			// Wrap the child if it is not an attribute element or if it is an attribute element that should be inside
@@ -1113,7 +1114,7 @@ export default class DowncastWriter {
 				parent._insertChild( i, newAttribute );
 				this._addToClonedElementsGroup( newAttribute );
 
-				wrapPositions.push(	new Position( parent, i ) );
+				wrapPositions.push( new Position( parent, i ) );
 			}
 			//
 			// If other nested attribute is found and it wasn't wrapped (see above), continue wrapping inside it.
@@ -1489,7 +1490,7 @@ export default class DowncastWriter {
 		const rangeStart = range.start;
 		const rangeEnd = range.end;
 
-		validateRangeContainer( range );
+		validateRangeContainer( range, this.document );
 
 		// Break at the collapsed position. Return new collapsed range.
 		if ( range.isCollapsed ) {
@@ -1534,7 +1535,7 @@ export default class DowncastWriter {
 			 *
 			 * @error view-writer-cannot-break-empty-element
 			 */
-			throw new CKEditorError( 'view-writer-cannot-break-empty-element' );
+			throw new CKEditorError( 'view-writer-cannot-break-empty-element', this.document );
 		}
 
 		// If position is placed inside UIElement - throw an exception as we cannot break inside.
@@ -1544,7 +1545,7 @@ export default class DowncastWriter {
 			 *
 			 * @error view-writer-cannot-break-ui-element
 			 */
-			throw new CKEditorError( 'view-writer-cannot-break-ui-element' );
+			throw new CKEditorError( 'view-writer-cannot-break-ui-element', this.document );
 		}
 
 		// There are no attributes to break and text nodes breaking is not forced.
@@ -1571,40 +1572,41 @@ export default class DowncastWriter {
 			const newPosition = new Position( positionParent.parent, positionParent.index + 1 );
 
 			return this._breakAttributes( newPosition, forceSplitText );
-		} else
-		// <p>foo<b><u>{}bar</u></b></p>
-		// <p>foo<b>[]<u>bar</u></b></p>
-		// <p>foo{}<b><u>bar</u></b></p>
-		if ( positionOffset === 0 ) {
-			const newPosition = new Position( positionParent.parent, positionParent.index );
+		} else {
+			// <p>foo<b><u>{}bar</u></b></p>
+			// <p>foo<b>[]<u>bar</u></b></p>
+			// <p>foo{}<b><u>bar</u></b></p>
+			if ( positionOffset === 0 ) {
+				const newPosition = new Position( positionParent.parent, positionParent.index );
 
-			return this._breakAttributes( newPosition, forceSplitText );
-		}
-		// <p>foo<b><u>b{}ar</u></b></p>
-		// <p>foo<b><u>b[]ar</u></b></p>
-		// <p>foo<b><u>b</u>[]<u>ar</u></b></p>
-		// <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
-		else {
-			const offsetAfter = positionParent.index + 1;
+				return this._breakAttributes( newPosition, forceSplitText );
+			}
+			// <p>foo<b><u>b{}ar</u></b></p>
+			// <p>foo<b><u>b[]ar</u></b></p>
+			// <p>foo<b><u>b</u>[]<u>ar</u></b></p>
+			// <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
+			else {
+				const offsetAfter = positionParent.index + 1;
 
-			// Break element.
-			const clonedNode = positionParent._clone();
+				// Break element.
+				const clonedNode = positionParent._clone();
 
-			// Insert cloned node to position's parent node.
-			positionParent.parent._insertChild( offsetAfter, clonedNode );
-			this._addToClonedElementsGroup( clonedNode );
+				// Insert cloned node to position's parent node.
+				positionParent.parent._insertChild( offsetAfter, clonedNode );
+				this._addToClonedElementsGroup( clonedNode );
 
-			// Get nodes to move.
-			const count = positionParent.childCount - positionOffset;
-			const nodesToMove = positionParent._removeChildren( positionOffset, count );
+				// Get nodes to move.
+				const count = positionParent.childCount - positionOffset;
+				const nodesToMove = positionParent._removeChildren( positionOffset, count );
 
-			// Move nodes to cloned node.
-			clonedNode._appendChild( nodesToMove );
+				// Move nodes to cloned node.
+				clonedNode._appendChild( nodesToMove );
 
-			// Create new position to work on.
-			const newPosition = new Position( positionParent.parent, offsetAfter );
+				// Create new position to work on.
+				const newPosition = new Position( positionParent.parent, offsetAfter );
 
-			return this._breakAttributes( newPosition, forceSplitText );
+				return this._breakAttributes( newPosition, forceSplitText );
+			}
 		}
 	}
 
@@ -1825,7 +1827,8 @@ function mergeTextNodes( t1, t2 ) {
 //
 // @param Iterable.<module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement
 // |module:engine/view/containerelement~ContainerElement> nodes
-function validateNodesToInsert( nodes ) {
+// @param {Object} errorContext
+function validateNodesToInsert( nodes, errorContext ) {
 	for ( const node of nodes ) {
 		if ( !validNodesToInsert.some( ( validNode => node instanceof validNode ) ) ) { // eslint-disable-line no-use-before-define
 			/**
@@ -1836,11 +1839,11 @@ function validateNodesToInsert( nodes ) {
 			 *
 			 * @error view-writer-insert-invalid-node
 			 */
-			throw new CKEditorError( 'view-writer-insert-invalid-node' );
+			throw new CKEditorError( 'view-writer-insert-invalid-node', errorContext );
 		}
 
 		if ( !node.is( 'text' ) ) {
-			validateNodesToInsert( node.getChildren() );
+			validateNodesToInsert( node.getChildren(), errorContext );
 		}
 	}
 }
@@ -1860,7 +1863,8 @@ function isContainerOrFragment( node ) {
 // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when validation fails.
 //
 // @param {module:engine/view/range~Range} range
-function validateRangeContainer( range ) {
+// @param {Object} errorContext
+function validateRangeContainer( range, errorContext ) {
 	const startContainer = getParentContainer( range.start );
 	const endContainer = getParentContainer( range.end );
 
@@ -1872,7 +1876,8 @@ function validateRangeContainer( range ) {
 		 *
 		 * @error view-writer-invalid-range-container
 		 */
-		throw new CKEditorError( 'view-writer-invalid-range-container' );
+
+		throw new CKEditorError( 'view-writer-invalid-range-container', errorContext );
 	}
 }
 

+ 29 - 4
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 ) {
+		const cutType = type && type.replace( /^view:/, '' );
 		if ( !name ) {
-			return type == 'editableElement' || super.is( type );
+			return cutType == 'editableElement' || super.is( type );
 		} else {
-			return ( type == 'editableElement' && name == this.name ) || super.is( type, name );
+			return ( cutType == 'editableElement' && name == this.name ) || super.is( type, name );
 		}
 	}
 
@@ -104,7 +129,7 @@ export default class EditableElement extends ContainerElement {
 			 *
 			 * @error view-editableelement-document-already-set
 			 */
-			throw new CKEditorError( 'view-editableelement-document-already-set: View document is already set.' );
+			throw new CKEditorError( 'view-editableelement-document-already-set: View document is already set.', this );
 		}
 
 		this._setCustomProperty( documentSymbol, document );

+ 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.
 	 * @returns {Boolean}
 	 */
 	is( type, name = null ) {
+		const cutType = type.replace( /^view:/, '' );
 		if ( !name ) {
-			return type == 'element' || type == this.name || super.is( type );
+			return cutType == 'element' || cutType == this.name || super.is( type );
 		} else {
-			return type == 'element' && name == this.name;
+			return cutType == 'element' && name == this.name;
 		}
 	}
 

+ 32 - 4
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 ) {
+		const cutType = type.replace( /^view:/, '' );
 		if ( !name ) {
-			return type == 'emptyElement' || super.is( type );
+			return cutType == 'emptyElement' || super.is( type );
 		} else {
-			return ( type == 'emptyElement' && name == this.name ) || super.is( type, name );
+			return ( cutType == 'emptyElement' && name == this.name ) || super.is( type, name );
 		}
 	}
 
@@ -68,7 +93,10 @@ export default class EmptyElement extends Element {
 			 *
 			 * @error view-emptyelement-cannot-add
 			 */
-			throw new CKEditorError( 'view-emptyelement-cannot-add: Cannot add child nodes to EmptyElement instance.' );
+			throw new CKEditorError(
+				'view-emptyelement-cannot-add: Cannot add child nodes to EmptyElement instance.',
+				[ this, nodes ]
+			);
 		}
 	}
 }

+ 18 - 40
packages/ckeditor5-engine/src/view/filler.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* globals window */
-
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import isText from '@ckeditor/ckeditor5-utils/src/dom/istext';
 
@@ -36,6 +34,15 @@ import isText from '@ckeditor/ckeditor5-utils/src/dom/istext';
  * @module engine/view/filler
  */
 
+/**
+ * Non-breaking space filler creator. This is a function which creates `&nbsp;` text node.
+ * It defines how the filler is created.
+ *
+ * @see module:engine/view/filler~BR_FILLER
+ * @function
+ */
+export const NBSP_FILLER = domDocument => domDocument.createTextNode( '\u00A0' );
+
 /**
  * `<br>` filler creator. This is a function which creates `<br data-cke-filler="true">` element.
  * It defines how the filler is created.
@@ -50,28 +57,23 @@ export const BR_FILLER = domDocument => {
 	return fillerBr;
 };
 
-/**
- * Non-breaking space filler creator. This is a function which creates `&nbsp;` text node.
- * It defines how the filler is created.
- *
- * @see module:engine/view/filler~BR_FILLER
- * @function
- */
-export const NBSP_FILLER = domDocument => domDocument.createTextNode( '\u00A0' );
-
 /**
  * Length of the {@link module:engine/view/filler~INLINE_FILLER INLINE_FILLER}.
  */
 export const INLINE_FILLER_LENGTH = 7;
 
 /**
- * Inline filler which is sequence of the zero width spaces.
+ * Inline filler which is a sequence of the zero width spaces.
  */
-export let INLINE_FILLER = '';
+export const INLINE_FILLER = ( () => {
+	let inlineFiller = '';
 
-for ( let i = 0; i < INLINE_FILLER_LENGTH; i++ ) {
-	INLINE_FILLER += '\u200b';
-}
+	for ( let i = 0; i < INLINE_FILLER_LENGTH; i++ ) {
+		inlineFiller += '\u200b';
+	}
+
+	return inlineFiller;
+} )(); // Usu IIF so the INLINE_FILLER appears as a constant in the docs.
 
 /**
  * Checks if the node is a text node which starts with the {@link module:engine/view/filler~INLINE_FILLER inline filler}.
@@ -119,30 +121,6 @@ export function getDataWithoutFiller( domText ) {
 	}
 }
 
-// Cache block fillers templates to improve performance.
-const templateBlockFillers = new WeakMap();
-
-/**
- * Checks if the node is an instance of the block filler of the given type.
- *
- *		const brFillerInstance = BR_FILLER( document );
- *		isBlockFiller( brFillerInstance, BR_FILLER ); // true
- *
- * @param {Node} domNode DOM node to check.
- * @param {Function} blockFiller Block filler creator.
- * @returns {Boolean} True if text node contains only {@link module:engine/view/filler~INLINE_FILLER inline filler}.
- */
-export function isBlockFiller( domNode, blockFiller ) {
-	let templateBlockFiller = templateBlockFillers.get( blockFiller );
-
-	if ( !templateBlockFiller ) {
-		templateBlockFiller = blockFiller( window.document );
-		templateBlockFillers.set( blockFiller, templateBlockFiller );
-	}
-
-	return domNode.isEqualNode( templateBlockFiller );
-}
-
 /**
  * Assign key observer which move cursor from the end of the inline filler to the beginning of it when
  * the left arrow is pressed, so the filler does not break navigation.

+ 41 - 14
packages/ckeditor5-engine/src/view/node.js

@@ -62,7 +62,7 @@ export default class Node {
 			 *
 			 * @error view-node-not-found-in-parent
 			 */
-			throw new CKEditorError( 'view-node-not-found-in-parent: The node\'s parent does not contain this node.' );
+			throw new CKEditorError( 'view-node-not-found-in-parent: The node\'s parent does not contain this node.', this );
 		}
 
 		return pos;
@@ -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}
 	 */
 	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
  * {@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 {
 	/**

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

@@ -0,0 +1,44 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/view/observer/inputobserver
+ */
+
+import DomEventObserver from './domeventobserver';
+
+/**
+ * Observer for events connected with data input.
+ *
+ * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
+ *
+ * @extends module:engine/view/observer/domeventobserver~DomEventObserver
+ */
+export default class InputObserver extends DomEventObserver {
+	constructor( view ) {
+		super( view );
+
+		this.domEventType = [ 'beforeinput' ];
+	}
+
+	onDomEvent( domEvent ) {
+		this.fire( domEvent.type, domEvent );
+	}
+}
+
+/**
+ * Fired before browser inputs (or deletes) some data.
+ *
+ * This event is available only on browsers which support DOM `beforeinput` event.
+ *
+ * Introduced by {@link module:engine/view/observer/inputobserver~InputObserver}.
+ *
+ * Note that because {@link module:engine/view/observer/inputobserver~InputObserver} is attached by the
+ * {@link module:engine/view/view~View} this event is available by default.
+ *
+ * @see module:engine/view/observer/inputobserver~InputObserver
+ * @event module:engine/view/document~Document#event:beforeinput
+ * @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
+ */

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

@@ -11,7 +11,7 @@ import DomEventObserver from './domeventobserver';
 import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
 /**
- * {@link module:engine/view/document~Document#event:keydown Key down} event observer.
+ * Observer for events connected with pressing keyboard keys.
  *
  * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
  *

+ 6 - 10
packages/ckeditor5-engine/src/view/observer/selectionobserver.js

@@ -11,7 +11,6 @@
 
 import Observer from './observer';
 import MutationObserver from './mutationobserver';
-import log from '@ckeditor/ckeditor5-utils/src/log';
 import { debounce } from 'lodash-es';
 
 /**
@@ -150,15 +149,12 @@ export default class SelectionObserver extends Observer {
 		// This counter is reset each second. 60 selection changes in 1 second is enough high number
 		// to be very difficult (impossible) to achieve using just keyboard keys (during normal editor use).
 		if ( ++this._loopbackCounter > 60 ) {
-			/**
-			 * Selection change observer detected an infinite rendering loop.
-			 * Most probably you try to put the selection in the position which is not allowed
-			 * by the browser and browser fixes it automatically what causes `selectionchange` event on
-			 * which a loopback through a model tries to re-render the wrong selection and again.
-			 *
-			 * @error selectionchange-infinite-loop
-			 */
-			log.warn( 'selectionchange-infinite-loop: Selection change observer detected an infinite rendering loop.' );
+			// Selection change observer detected an infinite rendering loop.
+			// Most probably you try to put the selection in the position which is not allowed
+			// by the browser and browser fixes it automatically what causes `selectionchange` event on
+			// which a loopback through a model tries to re-render the wrong selection and again.
+			//
+			// @if CK_DEBUG // console.warn( 'Selection change observer detected an infinite rendering loop.' );
 
 			return;
 		}

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

@@ -140,9 +140,7 @@ export function hidePlaceholder( writer, element ) {
  * {@link module:engine/view/placeholder~enablePlaceholder `enablePlaceholder()`} in that case or make
  * sure the correct element is passed to the helper.
  *
- * @param {module:engine/view/downcastwriter~DowncastWriter} writer
  * @param {module:engine/view/element~Element} element
- * @param {String} text
  * @returns {Boolean}
  */
 export function needsPlaceholder( element ) {
@@ -176,7 +174,7 @@ export function needsPlaceholder( element ) {
 // Updates all placeholders associated with a document in a post–fixer callback.
 //
 // @private
-// @param { module:engine/model/document~Document} doc
+// @param { module:engine/view/document~Document} doc
 // @param {module:engine/view/downcastwriter~DowncastWriter} writer
 // @returns {Boolean} True if any changes were made to the view document.
 function updateDocumentPlaceholders( doc, writer ) {

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

@@ -206,6 +206,25 @@ export default class Position {
 		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.
 	 *
@@ -341,7 +360,9 @@ export default class Position {
 				 */
 				throw new CKEditorError(
 					'view-createPositionAt-offset-required: ' +
-					'View#createPositionAt() requires the offset when the first parameter is a view item.' );
+					'View#createPositionAt() requires the offset when the first parameter is a view item.',
+					node
+				);
 			}
 
 			return new Position( node, offset );
@@ -368,7 +389,7 @@ export default class Position {
 			 * @error view-position-after-root
 			 * @param {module:engine/view/node~Node} root
 			 */
-			throw new CKEditorError( 'view-position-after-root: You can not make position after root.', { root: item } );
+			throw new CKEditorError( 'view-position-after-root: You can not make position after root.', item, { root: item } );
 		}
 
 		return new Position( item.parent, item.index + 1 );
@@ -394,7 +415,7 @@ export default class Position {
 			 * @error view-position-before-root
 			 * @param {module:engine/view/node~Node} root
 			 */
-			throw new CKEditorError( 'view-position-before-root: You can not make position before root.', { root: item } );
+			throw new CKEditorError( 'view-position-before-root: You can not make position before root.', item, { root: item } );
 		}
 
 		return new Position( item.parent, item.index );

+ 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.
 	 *

+ 76 - 26
packages/ckeditor5-engine/src/view/renderer.js

@@ -11,7 +11,7 @@
 
 import ViewText from './text';
 import ViewPosition from './position';
-import { INLINE_FILLER, INLINE_FILLER_LENGTH, startsWithFiller, isInlineFiller, isBlockFiller } from './filler';
+import { INLINE_FILLER, INLINE_FILLER_LENGTH, startsWithFiller, isInlineFiller } from './filler';
 
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import diff from '@ckeditor/ckeditor5-utils/src/diff';
@@ -101,6 +101,13 @@ export default class Renderer {
 		 */
 		this.isFocused = false;
 
+		/**
+		 * Indicates if the composition is in progress inside the view document view.
+		 *
+		 * @member {Boolean}
+		 */
+		this.isComposing = false;
+
 		/**
 		 * The text node in which the inline filler was rendered.
 		 *
@@ -154,7 +161,7 @@ export default class Renderer {
 				 *
 				 * @error renderer-unknown-type
 				 */
-				throw new CKEditorError( 'view-renderer-unknown-type: Unknown type passed to Renderer.markToSync.' );
+				throw new CKEditorError( 'view-renderer-unknown-type: Unknown type passed to Renderer.markToSync.', this );
 			}
 		}
 	}
@@ -392,7 +399,7 @@ export default class Renderer {
 			 *
 			 * @error view-renderer-filler-was-lost
 			 */
-			throw new CKEditorError( 'view-renderer-filler-was-lost: The inline filler node was lost.' );
+			throw new CKEditorError( 'view-renderer-filler-was-lost: The inline filler node was lost.', this );
 		}
 
 		if ( isInlineFiller( domFillerNode ) ) {
@@ -588,7 +595,7 @@ export default class Renderer {
 	_diffNodeLists( actualDomChildren, expectedDomChildren ) {
 		actualDomChildren = filterOutFakeSelectionContainer( actualDomChildren, this._fakeSelectionContainer );
 
-		return diff( actualDomChildren, expectedDomChildren, sameNodes.bind( null, this.domConverter.blockFiller ) );
+		return diff( actualDomChildren, expectedDomChildren, sameNodes.bind( null, this.domConverter ) );
 	}
 
 	/**
@@ -696,41 +703,32 @@ export default class Renderer {
 	 */
 	_updateFakeSelection( domRoot ) {
 		const domDocument = domRoot.ownerDocument;
-		let container = this._fakeSelectionContainer;
 
-		// Create fake selection container if one does not exist.
-		if ( !container ) {
-			this._fakeSelectionContainer = container = domDocument.createElement( 'div' );
+		if ( !this._fakeSelectionContainer ) {
+			this._fakeSelectionContainer = createFakeSelectionContainer( domDocument );
+		}
+
+		const container = this._fakeSelectionContainer;
 
-			Object.assign( container.style, {
-				position: 'fixed',
-				top: 0,
-				left: '-9999px',
-				// See https://github.com/ckeditor/ckeditor5/issues/752.
-				width: '42px'
-			} );
+		// Bind fake selection container with the current selection *position*.
+		this.domConverter.bindFakeSelection( container, this.selection );
 
-			// Fill it with a text node so we can update it later.
-			container.textContent = '\u00A0';
+		if ( !this._fakeSelectionNeedsUpdate( domRoot ) ) {
+			return;
 		}
 
 		if ( !container.parentElement || container.parentElement != domRoot ) {
 			domRoot.appendChild( container );
 		}
 
-		// Update contents.
 		container.textContent = this.selection.fakeSelectionLabel || '\u00A0';
 
-		// Update selection.
 		const domSelection = domDocument.getSelection();
 		const domRange = domDocument.createRange();
 
 		domSelection.removeAllRanges();
 		domRange.selectNodeContents( container );
 		domSelection.addRange( domRange );
-
-		// Bind fake selection container with current selection.
-		this.domConverter.bindFakeSelection( container, this.selection );
 	}
 
 	/**
@@ -776,6 +774,11 @@ export default class Renderer {
 	 * @returns {Boolean}
 	 */
 	_domSelectionNeedsUpdate( domSelection ) {
+		// Remain DOM selection untouched while composing. See #1782.
+		if ( this.isComposing ) {
+			return false;
+		}
+
 		if ( !this.domConverter.isDomSelectionCorrect( domSelection ) ) {
 			// Current DOM selection is in incorrect position. We need to update it.
 			return true;
@@ -797,6 +800,31 @@ export default class Renderer {
 		return true;
 	}
 
+	/**
+	 * Checks whether the fake selection needs to be updated.
+	 *
+	 * @private
+	 * @param {HTMLElement} domRoot A valid DOM root where a new fake selection container should be added.
+	 * @returns {Boolean}
+	 */
+	_fakeSelectionNeedsUpdate( domRoot ) {
+		const container = this._fakeSelectionContainer;
+		const domSelection = domRoot.ownerDocument.getSelection();
+
+		// Fake selection needs to be updated if there's no fake selection container, or the container currently sits
+		// in a different root.
+		if ( !container || container.parentElement !== domRoot ) {
+			return true;
+		}
+
+		// Make sure that the selection actually is within the fake selection.
+		if ( domSelection.anchorNode !== container && !container.contains( domSelection.anchorNode ) ) {
+			return true;
+		}
+
+		return container.textContent !== this.selection.fakeSelectionLabel;
+	}
+
 	/**
 	 * Removes the DOM selection.
 	 *
@@ -915,11 +943,11 @@ function areSimilar( node1, node2 ) {
 //		* Two block filler elements.
 //
 // @private
-// @param {Function} blockFiller Block filler creator function, see {@link module:engine/view/domconverter~DomConverter#blockFiller}.
+// @param {String} blockFillerMode Block filler mode, see {@link module:engine/view/domconverter~DomConverter#blockFillerMode}.
 // @param {Node} node1
 // @param {Node} node2
 // @returns {Boolean}
-function sameNodes( blockFiller, actualDomChild, expectedDomChild ) {
+function sameNodes( domConverter, actualDomChild, expectedDomChild ) {
 	// Elements.
 	if ( actualDomChild === expectedDomChild ) {
 		return true;
@@ -929,8 +957,8 @@ function sameNodes( blockFiller, actualDomChild, expectedDomChild ) {
 		return actualDomChild.data === expectedDomChild.data;
 	}
 	// Block fillers.
-	else if ( isBlockFiller( actualDomChild, blockFiller ) &&
-		isBlockFiller( expectedDomChild, blockFiller ) ) {
+	else if ( domConverter.isBlockFiller( actualDomChild ) &&
+		domConverter.isBlockFiller( expectedDomChild ) ) {
 		return true;
 	}
 
@@ -978,3 +1006,25 @@ function filterOutFakeSelectionContainer( domChildList, fakeSelectionContainer )
 
 	return childList;
 }
+
+// Creates a fake selection container for a given document.
+//
+// @private
+// @param {Document} domDocument
+// @returns {HTMLElement}
+function createFakeSelectionContainer( domDocument ) {
+	const container = domDocument.createElement( 'div' );
+
+	Object.assign( container.style, {
+		position: 'fixed',
+		top: 0,
+		left: '-9999px',
+		// See https://github.com/ckeditor/ckeditor5/issues/752.
+		width: '42px'
+	} );
+
+	// Fill it with a text node so we can update it later.
+	container.textContent = '\u00A0';
+
+	return container;
+}

+ 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 ) {
+		const cutType = type.replace( /^view:/, '' );
 		if ( !name ) {
-			return type == 'rootElement' || super.is( type );
+			return cutType == 'rootElement' || super.is( type );
 		} else {
-			return ( type == 'rootElement' && name == this.name ) || super.is( type, name );
+			return ( cutType == 'rootElement' && name == this.name ) || super.is( type, name );
 		}
 	}
 

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

@@ -524,7 +524,8 @@ export default class Selection {
 				 */
 				throw new CKEditorError(
 					'view-selection-setTo-required-second-parameter: ' +
-					'selection.setTo requires the second parameter when the first parameter is a node.'
+					'selection.setTo requires the second parameter when the first parameter is a node.',
+					this
 				);
 			} else if ( placeOrOffset == 'in' ) {
 				range = Range._createIn( selectable );
@@ -547,7 +548,7 @@ export default class Selection {
 			 *
 			 * @error view-selection-setTo-not-selectable
 			 */
-			throw new CKEditorError( 'view-selection-setTo-not-selectable: Cannot set selection to given place.' );
+			throw new CKEditorError( 'view-selection-setTo-not-selectable: Cannot set selection to given place.', this );
 		}
 
 		this.fire( 'change' );
@@ -572,7 +573,8 @@ export default class Selection {
 			 * @error view-selection-setFocus-no-ranges
 			 */
 			throw new CKEditorError(
-				'view-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.'
+				'view-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.',
+				this
 			);
 		}
 
@@ -596,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
 	 * @returns {Boolean}
 	 */
 	is( type ) {
-		return type == 'selection';
+		return type == 'selection' || type == 'view:selection';
 	}
 
 	/**
@@ -680,7 +684,8 @@ export default class Selection {
 			 */
 			throw new CKEditorError(
 				'view-selection-add-range-not-range: ' +
-				'Selection range set to an object that is not an instance of view.Range'
+				'Selection range set to an object that is not an instance of view.Range',
+				this
 			);
 		}
 
@@ -709,6 +714,7 @@ export default class Selection {
 				 */
 				throw new CKEditorError(
 					'view-selection-range-intersects: Trying to add a range that intersects with another range from selection.',
+					this,
 					{ addedRange: range, intersectingRange: storedRange }
 				);
 			}

+ 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 ) {
-		return type == 'text' || super.is( type );
+		return type == 'text' || type == 'view:text' || super.is( type );
 	}
 
 	/**

+ 12 - 5
packages/ckeditor5-engine/src/view/textproxy.js

@@ -55,7 +55,7 @@ export default class TextProxy {
 			 *
 			 * @error view-textproxy-wrong-offsetintext
 			 */
-			throw new CKEditorError( 'view-textproxy-wrong-offsetintext: Given offsetInText value is incorrect.' );
+			throw new CKEditorError( 'view-textproxy-wrong-offsetintext: Given offsetInText value is incorrect.', this );
 		}
 
 		if ( length < 0 || offsetInText + length > textNode.data.length ) {
@@ -64,7 +64,7 @@ export default class TextProxy {
 			 *
 			 * @error view-textproxy-wrong-length
 			 */
-			throw new CKEditorError( 'view-textproxy-wrong-length: Given length value is incorrect.' );
+			throw new CKEditorError( 'view-textproxy-wrong-length: Given length value is incorrect.', this );
 		}
 
 		/**
@@ -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
 	 * @returns {Boolean}
 	 */
 	is( type ) {
-		return type == 'textProxy';
+		return type == 'textProxy' || type == 'view:textProxy';
 	}
 
 	/**

+ 5 - 1
packages/ckeditor5-engine/src/view/treewalker.js

@@ -42,12 +42,16 @@ export default class TreeWalker {
 			 *
 			 * @error view-tree-walker-no-start-position
 			 */
-			throw new CKEditorError( 'view-tree-walker-no-start-position: Neither boundaries nor starting position have been defined.' );
+			throw new CKEditorError(
+				'view-tree-walker-no-start-position: Neither boundaries nor starting position have been defined.',
+				null
+			);
 		}
 
 		if ( options.direction && options.direction != 'forward' && options.direction != 'backward' ) {
 			throw new CKEditorError(
 				'view-tree-walker-unknown-direction: Only `backward` and `forward` direction allowed.',
+				options.startPosition,
 				{ direction: options.direction }
 			);
 		}

+ 29 - 4
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 ) {
+		const cutType = type.replace( /^view:/, '' );
 		if ( !name ) {
-			return type == 'uiElement' || super.is( type );
+			return cutType == 'uiElement' || super.is( type );
 		} else {
-			return ( type == 'uiElement' && name == this.name ) || super.is( type, name );
+			return ( cutType == 'uiElement' && name == this.name ) || super.is( type, name );
 		}
 	}
 
@@ -81,7 +106,7 @@ export default class UIElement extends Element {
 			 *
 			 * @error view-uielement-cannot-add
 			 */
-			throw new CKEditorError( 'view-uielement-cannot-add: Cannot add child nodes to UIElement instance.' );
+			throw new CKEditorError( 'view-uielement-cannot-add: Cannot add child nodes to UIElement instance.', this );
 		}
 	}
 

+ 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.
  *
  * 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 text = writer.createText( 'foo!' );
@@ -172,6 +172,23 @@ export default class UpcastWriter {
 		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
 	 * old element to the new one.

+ 38 - 30
packages/ckeditor5-engine/src/view/view.js

@@ -22,14 +22,15 @@ import SelectionObserver from './observer/selectionobserver';
 import FocusObserver from './observer/focusobserver';
 import CompositionObserver from './observer/compositionobserver';
 import SelectionRendererObserver from './observer/selectionrendererobserver';
+import InputObserver from './observer/inputobserver';
 
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
-import log from '@ckeditor/ckeditor5-utils/src/log';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll';
 import { injectUiElementHandling } from './uielement';
 import { injectQuirksHandling } from './filler';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import env from '@ckeditor/ckeditor5-utils/src/env';
 
 /**
  * Editor's view controller class. Its main responsibility is DOM - View management for editing purposes, to provide
@@ -105,6 +106,7 @@ export default class View {
 		 */
 		this._renderer = new Renderer( this.domConverter, this.document.selection );
 		this._renderer.bind( 'isFocused' ).to( this.document );
+		this._renderer.bind( 'isComposing' ).to( this.document );
 
 		/**
 		 * A DOM root attributes cache. It saves the initial values of DOM root attributes before the DOM element
@@ -175,6 +177,10 @@ export default class View {
 		this.addObserver( CompositionObserver );
 		this.addObserver( SelectionRendererObserver );
 
+		if ( env.isAndroid ) {
+			this.addObserver( InputObserver );
+		}
+
 		// Inject quirks handlers.
 		injectQuirksHandling( this );
 		injectUiElementHandling( this );
@@ -394,14 +400,11 @@ export default class View {
 				this.domConverter.focus( editable );
 				this.forceRender();
 			} else {
-				/**
-				 * Before focusing view document, selection should be placed inside one of the view's editables.
-				 * Normally its selection will be converted from model document (which have default selection), but
-				 * when using view document on its own, we need to manually place selection before focusing it.
-				 *
-				 * @error view-focus-no-selection
-				 */
-				log.warn( 'view-focus-no-selection: There is no selection in any editable to focus.' );
+				// Before focusing view document, selection should be placed inside one of the view's editables.
+				// Normally its selection will be converted from model document (which have default selection), but
+				// when using view document on its own, we need to manually place selection before focusing it.
+				//
+				// @if CK_DEBUG // console.warn( 'There is no selection in any editable to focus.' );
 			}
 		}
 	}
@@ -451,33 +454,38 @@ export default class View {
 			throw new CKEditorError(
 				'cannot-change-view-tree: ' +
 				'Attempting to make changes to the view when it is in an incorrect state: rendering or post-fixers are in progress. ' +
-				'This may cause some unexpected behavior and inconsistency between the DOM and the view.'
+				'This may cause some unexpected behavior and inconsistency between the DOM and the view.',
+				this
 			);
 		}
 
-		// Recursive call to view.change() method - execute listener immediately.
-		if ( this._ongoingChange ) {
-			return callback( this._writer );
-		}
-
-		// This lock will assure that all recursive calls to view.change() will end up in same block - one "render"
-		// event for all nested calls.
-		this._ongoingChange = true;
-		const callbackResult = callback( this._writer );
-		this._ongoingChange = false;
+		try {
+			// Recursive call to view.change() method - execute listener immediately.
+			if ( this._ongoingChange ) {
+				return callback( this._writer );
+			}
 
-		// This lock is used by editing controller to render changes from outer most model.change() once. As plugins might call
-		// view.change() inside model.change() block - this will ensures that postfixers and rendering are called once after all changes.
-		// Also, we don't need to render anything if there're no changes since last rendering.
-		if ( !this._renderingDisabled && this._hasChangedSinceTheLastRendering ) {
-			this._postFixersInProgress = true;
-			this.document._callPostFixers( this._writer );
-			this._postFixersInProgress = false;
+			// This lock will assure that all recursive calls to view.change() will end up in same block - one "render"
+			// event for all nested calls.
+			this._ongoingChange = true;
+			const callbackResult = callback( this._writer );
+			this._ongoingChange = false;
+
+			// This lock is used by editing controller to render changes from outer most model.change() once. As plugins might call
+			// view.change() inside model.change() block - this will ensures that postfixers and rendering are called once after all
+			// changes. Also, we don't need to render anything if there're no changes since last rendering.
+			if ( !this._renderingDisabled && this._hasChangedSinceTheLastRendering ) {
+				this._postFixersInProgress = true;
+				this.document._callPostFixers( this._writer );
+				this._postFixersInProgress = false;
+
+				this.fire( 'render' );
+			}
 
-			this.fire( 'render' );
+			return callbackResult;
+		} catch ( err ) {
+			CKEditorError.rethrowUnexpectedError( err, this );
 		}
-
-		return callbackResult;
 	}
 
 	/**

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

@@ -8,7 +8,6 @@ import ModelRange from '../../src/model/range';
 import ViewRange from '../../src/view/range';
 import DataController from '../../src/controller/datacontroller';
 import HtmlDataProcessor from '../../src/dataprocessor/htmldataprocessor';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import ModelDocumentFragment from '../../src/model/documentfragment';
 import ViewDocumentFragment from '../../src/view/documentfragment';
@@ -20,6 +19,7 @@ import count from '@ckeditor/ckeditor5-utils/src/count';
 
 import UpcastHelpers from '../../src/conversion/upcasthelpers';
 import DowncastHelpers from '../../src/conversion/downcasthelpers';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DataController', () => {
 	let model, modelDocument, htmlDataProcessor, data, schema, upcastHelpers, downcastHelpers;
@@ -173,12 +173,9 @@ describe( 'DataController', () => {
 		it( 'should throw an error when document data is already initialized', () => {
 			data.init( '<p>Foo</p>' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				data.init( '<p>Bar</p>' );
-			} ).to.throw(
-				CKEditorError,
-				'datacontroller-init-document-not-empty: Trying to set initial data to not empty document.'
-			);
+			}, /datacontroller-init-document-not-empty:/, model );
 		} );
 
 		it( 'should set data to default main root', () => {
@@ -230,23 +227,17 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should throw an error when non-existent root is used (single)', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				data.init( { nonexistent: '<p>Bar</p>' } );
-			} ).to.throw(
-				CKEditorError,
-				'datacontroller-init-non-existent-root: Attempting to init data on a non-existing root.'
-			);
+			}, /^datacontroller-init-non-existent-root:/ );
 		} );
 
 		it( 'should throw an error when non-existent root is used (one of many)', () => {
 			schema.extend( '$text', { allowIn: '$root' } );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				data.init( { main: 'bar', nonexistent: '<p>Bar</p>' } );
-			} ).to.throw(
-				CKEditorError,
-				'datacontroller-init-non-existent-root: Attempting to init data on a non-existing root.'
-			);
+			}, /^datacontroller-init-non-existent-root:/, model );
 
 			expect( getData( model, { withoutSelection: true } ) ).to.equal( '' );
 		} );
@@ -315,27 +306,39 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should throw an error when non-existent root is used (single)', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				data.set( { nonexistent: '<p>Bar</p>' } );
-			} ).to.throw(
-				CKEditorError,
-				'datacontroller-set-non-existent-root: Attempting to set data on a non-existing root.'
-			);
+			}, /datacontroller-set-non-existent-root:/, model );
 		} );
 
 		it( 'should throw an error when non-existent root is used (one of many) without touching any roots data', () => {
 			schema.extend( '$text', { allowIn: '$root' } );
 			data.set( 'foo' );
 
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				data.set( { main: 'bar', nonexistent: '<p>Bar</p>' } );
-			} ).to.throw(
-				CKEditorError,
-				'datacontroller-set-non-existent-root: Attempting to set data on a non-existing root.'
-			);
+			}, /datacontroller-set-non-existent-root:/, model );
 
 			expect( getData( model, { withoutSelection: true } ) ).to.equal( 'foo' );
 		} );
+
+		// https://github.com/ckeditor/ckeditor5-engine/issues/1721.
+		it( 'should not throw when setting the data with markers that already exist in the editor', () => {
+			schema.extend( '$text', { allowIn: '$root' } );
+
+			data.set( 'foo' );
+
+			downcastHelpers.markerToElement( { model: 'marker', view: 'marker' } );
+			upcastHelpers.elementToMarker( { view: 'marker', model: 'marker' } );
+
+			model.change( writer => {
+				writer.addMarker( 'marker', { range: writer.createRangeIn( modelDocument.getRoot() ), usingOperation: true } );
+			} );
+
+			expect( () => {
+				data.set( data.get() );
+			} ).not.to.throw();
+		} );
 	} );
 
 	describe( 'get()', () => {
@@ -423,12 +426,9 @@ describe( 'DataController', () => {
 		} );
 
 		it( 'should throw an error when non-existent root is used', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				data.get( { rootName: 'nonexistent' } );
-			} ).to.throw(
-				CKEditorError,
-				'datacontroller-get-non-existent-root: Attempting to get data from a non-existing root.'
-			);
+			}, /datacontroller-get-non-existent-root:/ );
 		} );
 	} );
 

+ 32 - 16
packages/ckeditor5-engine/tests/conversion/conversion.js

@@ -4,7 +4,6 @@
  */
 
 import Conversion from '../../src/conversion/conversion';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
 
@@ -18,6 +17,7 @@ import Model from '../../src/model/model';
 import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view';
 import { stringify as modelStringify } from '../../src/dev-utils/model';
 import ConversionHelpers from '../../src/conversion/conversionhelpers';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'Conversion', () => {
 	let conversion, downcastDispA, upcastDispaA, downcastDispB;
@@ -34,15 +34,15 @@ describe( 'Conversion', () => {
 
 	describe( 'addAlias()', () => {
 		it( 'should throw when trying to use same group name twice', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				conversion.addAlias( 'upcast', upcastDispaA );
-			} ).to.throw( CKEditorError, /conversion-group-exists/ );
+			}, /conversion-group-exists/ );
 		} );
 
 		it( 'should throw when trying to add not registered dispatcher', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				conversion.addAlias( 'foo', {} );
-			} ).to.throw( CKEditorError, /conversion-add-alias-dispatcher-not-registered/ );
+			}, /conversion-add-alias-dispatcher-not-registered/ );
 		} );
 	} );
 
@@ -53,9 +53,9 @@ describe( 'Conversion', () => {
 		} );
 
 		it( 'should throw if non-existing group name has been used', () => {
-			expect( () => {
+			expectToThrowCKEditorError( () => {
 				conversion.for( 'foo' );
-			} ).to.throw( CKEditorError, /conversion-for-unknown-group/ );
+			}, /conversion-for-unknown-group/ );
 		} );
 
 		it( 'should return proper helpers for group', () => {
@@ -301,6 +301,14 @@ describe( 'Conversion', () => {
 			} );
 
 			it( 'config.view is an object with upcastAlso defined', () => {
+				schema.extend( '$text', {
+					allowAttributes: [ 'bold', 'xBold' ]
+				} );
+				conversion.attributeToElement( {
+					model: 'xBold',
+					view: 'x-bold'
+				} );
+
 				conversion.attributeToElement( {
 					model: 'bold',
 					view: 'strong',
@@ -310,22 +318,18 @@ describe( 'Conversion', () => {
 							name: 'span',
 							classes: 'bold'
 						},
-						{
-							name: 'span',
-							styles: {
-								'font-weight': 'bold'
-							}
-						},
 						viewElement => {
 							const fontWeight = viewElement.getStyle( 'font-weight' );
 
-							if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test( fontWeight ) && Number( fontWeight ) > 500 ) {
+							if ( fontWeight == 'bold' || Number( fontWeight ) > 500 ) {
 								return {
-									name: true,
 									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>',
 					'<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', () => {

+ 36 - 10
packages/ckeditor5-engine/tests/conversion/downcastdispatcher.js

@@ -4,6 +4,8 @@
  */
 
 import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
+import Mapper from '../../src/conversion/mapper';
+
 import Model from '../../src/model/model';
 import ModelText from '../../src/model/text';
 import ModelElement from '../../src/model/element';
@@ -13,13 +15,14 @@ import View from '../../src/view/view';
 import ViewContainerElement from '../../src/view/containerelement';
 
 describe( 'DowncastDispatcher', () => {
-	let dispatcher, doc, root, differStub, model, view;
+	let dispatcher, doc, root, differStub, model, view, mapper;
 
 	beforeEach( () => {
 		model = new Model();
 		view = new View();
 		doc = model.document;
-		dispatcher = new DowncastDispatcher();
+		mapper = new Mapper();
+		dispatcher = new DowncastDispatcher( { mapper } );
 		root = doc.createRoot();
 
 		differStub = {
@@ -48,7 +51,7 @@ describe( 'DowncastDispatcher', () => {
 			differStub.getChanges = () => [ { type: 'insert', position, length: 1 } ];
 
 			view.change( writer => {
-				dispatcher.convertChanges( differStub, writer );
+				dispatcher.convertChanges( differStub, model.markers, writer );
 			} );
 
 			expect( dispatcher.convertInsert.calledOnce ).to.be.true;
@@ -63,7 +66,7 @@ describe( 'DowncastDispatcher', () => {
 			differStub.getChanges = () => [ { type: 'remove', position, length: 2, name: '$text' } ];
 
 			view.change( writer => {
-				dispatcher.convertChanges( differStub, writer );
+				dispatcher.convertChanges( differStub, model.markers, writer );
 			} );
 
 			expect( dispatcher.convertRemove.calledWith( position, 2, '$text' ) ).to.be.true;
@@ -80,7 +83,7 @@ describe( 'DowncastDispatcher', () => {
 			];
 
 			view.change( writer => {
-				dispatcher.convertChanges( differStub, writer );
+				dispatcher.convertChanges( differStub, model.markers, writer );
 			} );
 
 			expect( dispatcher.convertAttribute.calledWith( range, 'key', null, 'foo' ) ).to.be.true;
@@ -102,7 +105,7 @@ describe( 'DowncastDispatcher', () => {
 			];
 
 			view.change( writer => {
-				dispatcher.convertChanges( differStub, writer );
+				dispatcher.convertChanges( differStub, model.markers, writer );
 			} );
 
 			expect( dispatcher.convertInsert.calledTwice ).to.be.true;
@@ -122,7 +125,7 @@ describe( 'DowncastDispatcher', () => {
 			];
 
 			view.change( writer => {
-				dispatcher.convertChanges( differStub, writer );
+				dispatcher.convertChanges( differStub, model.markers, writer );
 			} );
 
 			expect( dispatcher.convertMarkerAdd.calledWith( 'foo', fooRange ) );
@@ -141,11 +144,34 @@ describe( 'DowncastDispatcher', () => {
 			];
 
 			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( 'bar', barRange ) );
+			expect( dispatcher.convertMarkerAdd.calledWith( 'foo', fooRange ) );
+			expect( dispatcher.convertMarkerAdd.calledWith( 'bar', barRange ) );
 		} );
 	} );
 
@@ -392,8 +418,8 @@ describe( 'DowncastDispatcher', () => {
 			const viewFigure = new ViewContainerElement( 'figure', null, viewCaption );
 
 			// Create custom highlight handler mock.
-			viewFigure._setCustomProperty( 'addHighlight', () => { } );
-			viewFigure._setCustomProperty( 'removeHighlight', () => { } );
+			viewFigure._setCustomProperty( 'addHighlight', () => {} );
+			viewFigure._setCustomProperty( 'removeHighlight', () => {} );
 
 			// Create mapper mock.
 			dispatcher.conversionApi.mapper = {

Деякі файли не було показано, через те що забагато файлів було змінено