Răsfoiți Sursa

Merge tag 'v1.0.0-beta.1' into stable

Release: v1.0.0-beta.1.
Piotrek Koszuliński 7 ani în urmă
părinte
comite
8618e439a8
100 a modificat fișierele cu 11371 adăugiri și 7339 ștergeri
  1. 1 1
      packages/ckeditor5-engine/.eslintrc.js
  2. 15 11
      packages/ckeditor5-engine/.travis.yml
  3. 92 0
      packages/ckeditor5-engine/CHANGELOG.md
  4. 1 1
      packages/ckeditor5-engine/LICENSE.md
  5. 3 1
      packages/ckeditor5-engine/README.md
  6. 1 1
      packages/ckeditor5-engine/docs/api/engine.md
  7. 16 16
      packages/ckeditor5-engine/package.json
  8. 135 198
      packages/ckeditor5-engine/src/controller/datacontroller.js
  9. 121 84
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  10. 0 147
      packages/ckeditor5-engine/src/controller/getselectedcontent.js
  11. 0 432
      packages/ckeditor5-engine/src/conversion/buildmodelconverter.js
  12. 0 539
      packages/ckeditor5-engine/src/conversion/buildviewconverter.js
  13. 567 0
      packages/ckeditor5-engine/src/conversion/conversion.js
  14. 1036 0
      packages/ckeditor5-engine/src/conversion/downcast-converters.js
  15. 129 0
      packages/ckeditor5-engine/src/conversion/downcast-selection-converters.js
  16. 605 0
      packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
  17. 54 28
      packages/ckeditor5-engine/src/conversion/mapper.js
  18. 0 276
      packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js
  19. 0 667
      packages/ckeditor5-engine/src/conversion/model-to-view-converters.js
  20. 34 15
      packages/ckeditor5-engine/src/conversion/modelconsumable.js
  21. 0 745
      packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js
  22. 573 0
      packages/ckeditor5-engine/src/conversion/upcast-converters.js
  23. 8 8
      packages/ckeditor5-engine/src/conversion/upcast-selection-converters.js
  24. 518 0
      packages/ckeditor5-engine/src/conversion/upcastdispatcher.js
  25. 0 61
      packages/ckeditor5-engine/src/conversion/view-to-model-converters.js
  26. 1 1
      packages/ckeditor5-engine/src/conversion/viewconsumable.js
  27. 0 365
      packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js
  28. 4 4
      packages/ckeditor5-engine/src/dataprocessor/basichtmlwriter.js
  29. 8 8
      packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc
  30. 15 15
      packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js
  31. 3 3
      packages/ckeditor5-engine/src/dataprocessor/htmlwriter.js
  32. 20 20
      packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js
  33. 15 16
      packages/ckeditor5-engine/src/dev-utils/deltareplayer.js
  34. 216 152
      packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js
  35. 159 136
      packages/ckeditor5-engine/src/dev-utils/model.js
  36. 233 209
      packages/ckeditor5-engine/src/dev-utils/view.js
  37. 34 95
      packages/ckeditor5-engine/src/model/batch.js
  38. 2 151
      packages/ckeditor5-engine/src/model/delta/attributedelta.js
  39. 1 2
      packages/ckeditor5-engine/src/model/delta/basic-deltas.js
  40. 3 3
      packages/ckeditor5-engine/src/model/delta/basic-transformations.js
  41. 1 1
      packages/ckeditor5-engine/src/model/delta/delta.js
  42. 1 1
      packages/ckeditor5-engine/src/model/delta/deltafactory.js
  43. 2 52
      packages/ckeditor5-engine/src/model/delta/insertdelta.js
  44. 3 88
      packages/ckeditor5-engine/src/model/delta/markerdelta.js
  45. 2 67
      packages/ckeditor5-engine/src/model/delta/mergedelta.js
  46. 2 43
      packages/ckeditor5-engine/src/model/delta/movedelta.js
  47. 2 40
      packages/ckeditor5-engine/src/model/delta/removedelta.js
  48. 2 39
      packages/ckeditor5-engine/src/model/delta/renamedelta.js
  49. 30 0
      packages/ckeditor5-engine/src/model/delta/rootattributedelta.js
  50. 2 57
      packages/ckeditor5-engine/src/model/delta/splitdelta.js
  51. 1 1
      packages/ckeditor5-engine/src/model/delta/transform.js
  52. 2 53
      packages/ckeditor5-engine/src/model/delta/unwrapdelta.js
  53. 2 42
      packages/ckeditor5-engine/src/model/delta/weakinsertdelta.js
  54. 2 67
      packages/ckeditor5-engine/src/model/delta/wrapdelta.js
  55. 987 0
      packages/ckeditor5-engine/src/model/differ.js
  56. 180 309
      packages/ckeditor5-engine/src/model/document.js
  57. 75 58
      packages/ckeditor5-engine/src/model/documentfragment.js
  58. 582 230
      packages/ckeditor5-engine/src/model/documentselection.js
  59. 89 66
      packages/ckeditor5-engine/src/model/element.js
  60. 2 2
      packages/ckeditor5-engine/src/model/history.js
  61. 2 2
      packages/ckeditor5-engine/src/model/item.jsdoc
  62. 33 13
      packages/ckeditor5-engine/src/model/liveposition.js
  63. 29 14
      packages/ckeditor5-engine/src/model/liverange.js
  64. 169 56
      packages/ckeditor5-engine/src/model/markercollection.js
  65. 435 0
      packages/ckeditor5-engine/src/model/model.js
  66. 0 8
      packages/ckeditor5-engine/src/model/model.jsdoc
  67. 55 42
      packages/ckeditor5-engine/src/model/node.js
  68. 11 6
      packages/ckeditor5-engine/src/model/nodelist.js
  69. 12 9
      packages/ckeditor5-engine/src/model/operation/attributeoperation.js
  70. 84 0
      packages/ckeditor5-engine/src/model/operation/detachoperation.js
  71. 27 9
      packages/ckeditor5-engine/src/model/operation/insertoperation.js
  72. 6 7
      packages/ckeditor5-engine/src/model/operation/markeroperation.js
  73. 11 10
      packages/ckeditor5-engine/src/model/operation/moveoperation.js
  74. 1 5
      packages/ckeditor5-engine/src/model/operation/nooperation.js
  75. 27 7
      packages/ckeditor5-engine/src/model/operation/operation.js
  76. 1 1
      packages/ckeditor5-engine/src/model/operation/operationfactory.js
  77. 17 1
      packages/ckeditor5-engine/src/model/operation/reinsertoperation.js
  78. 20 1
      packages/ckeditor5-engine/src/model/operation/removeoperation.js
  79. 11 10
      packages/ckeditor5-engine/src/model/operation/renameoperation.js
  80. 33 8
      packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js
  81. 2 2
      packages/ckeditor5-engine/src/model/operation/transform.js
  82. 297 0
      packages/ckeditor5-engine/src/model/operation/utils.js
  83. 3 2
      packages/ckeditor5-engine/src/model/position.js
  84. 4 2
      packages/ckeditor5-engine/src/model/range.js
  85. 1 1
      packages/ckeditor5-engine/src/model/rootelement.js
  86. 1296 502
      packages/ckeditor5-engine/src/model/schema.js
  87. 189 218
      packages/ckeditor5-engine/src/model/selection.js
  88. 28 9
      packages/ckeditor5-engine/src/model/text.js
  89. 3 3
      packages/ckeditor5-engine/src/model/textproxy.js
  90. 6 5
      packages/ckeditor5-engine/src/model/treewalker.js
  91. 81 67
      packages/ckeditor5-engine/src/model/utils/deletecontent.js
  92. 148 0
      packages/ckeditor5-engine/src/model/utils/getselectedcontent.js
  93. 130 132
      packages/ckeditor5-engine/src/model/utils/insertcontent.js
  94. 95 15
      packages/ckeditor5-engine/src/model/utils/modifyselection.js
  95. 1253 247
      packages/ckeditor5-engine/src/model/writer.js
  96. 141 0
      packages/ckeditor5-engine/src/utils/bindtwostepcarettoattribute.js
  97. 32 18
      packages/ckeditor5-engine/src/view/attributeelement.js
  98. 6 4
      packages/ckeditor5-engine/src/view/containerelement.js
  99. 52 262
      packages/ckeditor5-engine/src/view/document.js
  100. 28 14
      packages/ckeditor5-engine/src/view/documentfragment.js

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 15 - 11
packages/ckeditor5-engine/.travis.yml

@@ -1,24 +1,28 @@
 sudo: required
 dist: trusty
 addons:
+  firefox: "latest"
   apt:
     sources:
-      - google-chrome
+    - google-chrome
     packages:
-      - google-chrome-stable
+    - google-chrome-stable
 language: node_js
 node_js:
-  - "6"
+- '6'
 cache:
-  - node_modules
+- node_modules
 before_install:
-  - export DISPLAY=:99.0
-  - sh -e /etc/init.d/xvfb start
+- export DISPLAY=:99.0
+- sh -e /etc/init.d/xvfb start
 install:
-  - npm install @ckeditor/ckeditor5-dev-tests
-  - ckeditor5-dev-tests-install-dependencies
+- npm install @ckeditor/ckeditor5-dev-tests
+- ckeditor5-dev-tests-install-dependencies
 script:
-  - ckeditor5-dev-tests-travis
+- ckeditor5-dev-tests-travis
 after_success:
-  - codeclimate-test-reporter < coverage/lcov.info
-  - ckeditor5-dev-tests-save-revision
+- ckeditor5-dev-tests-save-revision
+env:
+  global:
+  - secure: CSFKXnTb2oQW8YIjh+SWXYSYzx1jcIg1wGcl6jWrbaOU06JHEO77YLKlhMxLfRniFtilBEksK4ugIcn+lihykhHleMxI27+kifILdLLZKvwWlZiDrtpJSzLk08SpRNDwwthSlHVKrxbHVFE1K9SYpX4s3qTpxullDMPgc13OP4c=
+  - secure: WGdwDEavTOIE/yw8pdpWmx3Cehm2/GN8gZMimuot8sWPbOZRuPh+2lnVWpASLe25qJUKKeLWYLRHccpMLKbyweiordSEBwzNrOGOFyUuaDPITNZZrp5ekh2vHBp1+mwhRpRekdyWdzzVsm01RkFrnJqQlaXxX9i/r9Tu4EsK54M=

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

@@ -1,6 +1,98 @@
 Changelog
 =========
 
+## [1.0.0-beta.1](https://github.com/ckeditor/ckeditor5-engine/compare/v1.0.0-alpha.2...v1.0.0-beta.1) (2018-03-15)
+
+### Major refactoring
+
+In 1.0.0-beta.1 the engine's API has underwent a thorough review which resulted in a deep refactoring. Most of the underlying concepts and architecture remained untouched. The API, though, is brand new. The changes are huge and, in this package exclusively, resulted in changing 40.000 LOC. Therefore, the list of changes below is neither complete nor will explain you how the engine is structured now and how to should migrate to this version.
+
+Instead, we recommend reading https://docs.ckeditor.com/ckeditor5/latest/framework/guides/architecture/editing-engine.html once more (it will be updated in a couple of days after the release).
+
+The good news is that the our focus when designing the new API was on developer experience. APIs which were dangerous or confusing were removed or hidden and new APIs were added in their place. The engine is now safer and more useful library and we hope you'll enjoy it :).
+
+### Features
+
+* Add support for the `'word'` unit in the `modifySelection()` helper. ([f37a97a](https://github.com/ckeditor/ckeditor5-engine/commit/f37a97a))
+* Allowed passing `true` as `view.Matcher`'s attribute match to check if that attribute is set. Closes [#1239](https://github.com/ckeditor/ckeditor5-engine/issues/1239). ([bc1c3e5](https://github.com/ckeditor/ckeditor5-engine/commit/bc1c3e5))
+* Consumable type name is now normalized inside `conversion.ModelConsumable` methods. Closes [#1214](https://github.com/ckeditor/ckeditor5-engine/issues/1214). ([131e9c8](https://github.com/ckeditor/ckeditor5-engine/commit/131e9c8))
+* Convert view to model using position. Closes [#1213](https://github.com/ckeditor/ckeditor5-engine/issues/1213). Closes [#1250](https://github.com/ckeditor/ckeditor5-engine/issues/1250). ([1961395](https://github.com/ckeditor/ckeditor5-engine/commit/1961395))
+
+  Feature: `Schema#findAllowedParent()` has been introduced.
+  Feature: `SchemaContext#concat()` has been introduced.
+* Engine debug tools can be easily disabled using disableEngineDebug() function. Closes [#1193](https://github.com/ckeditor/ckeditor5-engine/issues/1193). ([0934496](https://github.com/ckeditor/ckeditor5-engine/commit/0934496))
+* Introduced `ViewElementDefinition` and `definition-based-converters` module with a set of utils allowing to turn element definitions to converters. Closes [#1198](https://github.com/ckeditor/ckeditor5-engine/issues/1198). ([d2e9f06](https://github.com/ckeditor/ckeditor5-engine/commit/d2e9f06))
+* Introduced composition observer. Closes [#1329](https://github.com/ckeditor/ckeditor5-engine/issues/1329). ([a0ad8fe](https://github.com/ckeditor/ckeditor5-engine/commit/a0ad8fe))
+* Introduced decorable DataController#init metohd. Closes [ckeditor/ckeditor5-core#120](https://github.com/ckeditor/ckeditor5-core/issues/120). ([d20d660](https://github.com/ckeditor/ckeditor5-engine/commit/d20d660))
+* Introduced two-step caret movement mechanism. Closes [#1289](https://github.com/ckeditor/ckeditor5-engine/issues/1289). ([88bb94c](https://github.com/ckeditor/ckeditor5-engine/commit/88bb94c))
+
+### Bug fixes
+
+* [Firefox] Added fix for typing space on the edge of inline elements. Closes [ckeditor/ckeditor5#692](https://github.com/ckeditor/ckeditor5/issues/692). ([3ea70f3](https://github.com/ckeditor/ckeditor5-engine/commit/3ea70f3))
+* `DocumenSelection#change:range` event will be fired only once after multiple selection live ranges have changed. Closes [#1281](https://github.com/ckeditor/ckeditor5-engine/issues/1281). ([b26935c](https://github.com/ckeditor/ckeditor5-engine/commit/b26935c))
+* `model.DocumentSelection` should update it's attributes after each change, including external changes. Closes [#1267](https://github.com/ckeditor/ckeditor5-engine/issues/1267). ([b91d967](https://github.com/ckeditor/ckeditor5-engine/commit/b91d967))
+* `Model#insertContent()` will not merge nodes if the model after the merge would violate schema rules. Closes [ckeditor/ckeditor5#730](https://github.com/ckeditor/ckeditor5/issues/730). ([2a73830](https://github.com/ckeditor/ckeditor5-engine/commit/2a73830))
+* `Schema#getLimitElement()` will return a proper limit element (the root element) if one of the selection's ranges have the root element as the limit element. Closes [#1275](https://github.com/ckeditor/ckeditor5-engine/issues/1275). ([050a415](https://github.com/ckeditor/ckeditor5-engine/commit/050a415))
+* Added a 50ms timeout after `Document#focus` event before rendering to be sure that selection changes are processed on Firefox and Safari. Closes [ckeditor/ckeditor5#676](https://github.com/ckeditor/ckeditor5/issues/676). Closes [#1157](https://github.com/ckeditor/ckeditor5-engine/issues/1157). Closes [#1155](https://github.com/ckeditor/ckeditor5-engine/issues/1155). Closes [#1153](https://github.com/ckeditor/ckeditor5-engine/issues/1153). ([aba8e68](https://github.com/ckeditor/ckeditor5-engine/commit/aba8e68))
+* Added missing parse context in `DataController#set()`. Closes [#1278](https://github.com/ckeditor/ckeditor5-engine/issues/1278). ([8c56dce](https://github.com/ckeditor/ckeditor5-engine/commit/8c56dce))
+* Corrected how change items in `model.Differ` are dismissed if they are in inserted/removed parent. Closes https://github.com/ckeditor/ckeditor5/issues/733. ([e70ab96](https://github.com/ckeditor/ckeditor5-engine/commit/e70ab96))
+* Corrected offsets transformation in `model.Differ` when multiple change items interfere with each other. Closes [#1309](https://github.com/ckeditor/ckeditor5-engine/issues/1309). Closes https://github.com/ckeditor/ckeditor5/issues/849. ([30dcf6c](https://github.com/ckeditor/ckeditor5-engine/commit/30dcf6c))
+* Fixed a bug where Firefox would throw an `NS_ERROR_FAILURE` error when moving selection from a nested editable to the root editable. Closes [ckeditor/ckeditor5#721](https://github.com/ckeditor/ckeditor5/issues/721). ([4b7d435](https://github.com/ckeditor/ckeditor5-engine/commit/4b7d435))
+* Fixed memory leak in `DocumentSelection`. Closes [#903](https://github.com/ckeditor/ckeditor5-engine/issues/903). ([7e352e3](https://github.com/ckeditor/ckeditor5-engine/commit/7e352e3))
+* Improved how `model.Differ` checks whether the operation should be buffered or not. Closes [#1326](https://github.com/ckeditor/ckeditor5-engine/issues/1326). ([3e9f81b](https://github.com/ckeditor/ckeditor5-engine/commit/3e9f81b))
+* It should not be possible to move a `model.Node` from a `model.Document` to a `model.DocumentFragment`. Closes [#1337](https://github.com/ckeditor/ckeditor5-engine/issues/1337). ([24b97f5](https://github.com/ckeditor/ckeditor5-engine/commit/24b97f5))
+* Registered $marker element in Schema. Closes [#1317](https://github.com/ckeditor/ckeditor5-engine/issues/1317). ([2d1d62f](https://github.com/ckeditor/ckeditor5-engine/commit/2d1d62f))
+* The fake selection container will not leak into the viewport. Closes [ckeditor/ckeditor5#752](https://github.com/ckeditor/ckeditor5/issues/752). ([3f059a7](https://github.com/ckeditor/ckeditor5-engine/commit/3f059a7))
+* View stringify utility now sorts CSS classes and values in `style` attribute. Closes [#1179](https://github.com/ckeditor/ckeditor5-engine/issues/1179). ([fc7da80](https://github.com/ckeditor/ckeditor5-engine/commit/fc7da80))
+
+### Other changes
+
+* Cleaned up the model, document and controllers API. Closes [#1208](https://github.com/ckeditor/ckeditor5-engine/issues/1208). ([aea6119](https://github.com/ckeditor/ckeditor5-engine/commit/aea6119))
+* Conversion utilities refactor. Closes [#1236](https://github.com/ckeditor/ckeditor5-engine/issues/1236). ([fd128a1](https://github.com/ckeditor/ckeditor5-engine/commit/fd128a1))
+* Fix `render()` and `change()` flow. Introduce postfixers in view. Closes [#1312](https://github.com/ckeditor/ckeditor5-engine/issues/1312). ([63b9d14](https://github.com/ckeditor/ckeditor5-engine/commit/63b9d14))
+* Introduced several improvements to conversion helpers. Closes [#1295](https://github.com/ckeditor/ckeditor5-engine/issues/1295). Closes [#1293](https://github.com/ckeditor/ckeditor5-engine/issues/1293). Closes [#1292](https://github.com/ckeditor/ckeditor5-engine/issues/1292). Closes [#1291](https://github.com/ckeditor/ckeditor5-engine/issues/1291). Closes [#1290](https://github.com/ckeditor/ckeditor5-engine/issues/1290). Closes [#1305](https://github.com/ckeditor/ckeditor5-engine/issues/1305). ([809ea24](https://github.com/ckeditor/ckeditor5-engine/commit/809ea24))
+* Keep the same marker instance when marker is updated. ([8eba5e9](https://github.com/ckeditor/ckeditor5-engine/commit/8eba5e9))
+* Make `Position` and `Range` immutable in model and view. Closes [#897](https://github.com/ckeditor/ckeditor5-engine/issues/897). ([836dfd8](https://github.com/ckeditor/ckeditor5-engine/commit/836dfd8))
+* Manual test for [#475](https://github.com/ckeditor/ckeditor5-engine/issues/475) now works correctly. Closes [#1271](https://github.com/ckeditor/ckeditor5-engine/issues/1271). ([c2d4cec](https://github.com/ckeditor/ckeditor5-engine/commit/c2d4cec))
+* Methods which modify the model's and view's tree are now protected and shouldn't be used directly in the code. Iinstance of `Writer` should be used instead. Closes [#738](https://github.com/ckeditor/ckeditor5-engine/issues/738). ([a4f3dad](https://github.com/ckeditor/ckeditor5-engine/commit/a4f3dad))
+* Migrated package styles to PostCSS. Moved visual styles to ckeditor5-theme-lark (see [ckeditor/ckeditor5-ui#144](https://github.com/ckeditor/ckeditor5-ui/issues/144)). ([5f65823](https://github.com/ckeditor/ckeditor5-engine/commit/5f65823))
+* Moved `consumable` parameter to `conversionApi` parameter in downcast. Closes [#1294](https://github.com/ckeditor/ckeditor5-engine/issues/1294). Closes [#1261](https://github.com/ckeditor/ckeditor5-engine/issues/1261). ([731db37](https://github.com/ckeditor/ckeditor5-engine/commit/731db37))
+* Moved `Document#getNearesetSelectionRange` to `Schema`. Closes [#1227](https://github.com/ckeditor/ckeditor5-engine/issues/1227). ([d1838a4](https://github.com/ckeditor/ckeditor5-engine/commit/d1838a4))
+* Moved selection methods to `Writer`, introduced `LiveSelection`. Closes [#1209](https://github.com/ckeditor/ckeditor5-engine/issues/1209). ([7db1fee](https://github.com/ckeditor/ckeditor5-engine/commit/7db1fee))
+* Operations that do not operate on a document should have `baseVersion` set to `null`. Closes [#1211](https://github.com/ckeditor/ckeditor5-engine/issues/1211). ([b527d7f](https://github.com/ckeditor/ckeditor5-engine/commit/b527d7f))
+
+  Fixed: Markers again are properly converted in `engine.controller.DataController`.
+  Fixed: Markers are cleared now before an operation is applied to `model.Document` tree to fix scenarios where marker range could not be converted to the view after the model changed.
+* Prevented `Writer` from usage outside of the `change` block. Closes [#1212](https://github.com/ckeditor/ckeditor5-engine/issues/1212). ([2592bf1](https://github.com/ckeditor/ckeditor5-engine/commit/2592bf1))
+* Provided one API for two types of markers, improved docs. Closes [#1086](https://github.com/ckeditor/ckeditor5-engine/issues/1086). ([bfe23c9](https://github.com/ckeditor/ckeditor5-engine/commit/bfe23c9))
+* Refactor: engine/model reorganization, introducing new chnage and enqueueChange block, split batch/writer. Related: [#1186](https://github.com/ckeditor/ckeditor5-engine/issues/1186). ([5be1ad6](https://github.com/ckeditor/ckeditor5-engine/commit/5be1ad6))
+* Refactored events fired by model classes. Closes [#1207](https://github.com/ckeditor/ckeditor5-engine/issues/1207). ([f56bddf](https://github.com/ckeditor/ckeditor5-engine/commit/f56bddf))
+* Refactoring of the view API. Closes [#1210](https://github.com/ckeditor/ckeditor5-engine/issues/1210). ([dd9ae51](https://github.com/ckeditor/ckeditor5-engine/commit/dd9ae51))
+* Refactoring: Conversion refactoring. Introduced `model.Differ`. Changes now will be converted after all changes in a change block are done. Closes [#1172](https://github.com/ckeditor/ckeditor5-engine/issues/1172). ([6479bfd](https://github.com/ckeditor/ckeditor5-engine/commit/6479bfd))
+* Refactoring: make writer a protected operations util. ([440dfc7](https://github.com/ckeditor/ckeditor5-engine/commit/440dfc7))
+* Rewritten the Schema API. Closes [#532](https://github.com/ckeditor/ckeditor5-engine/issues/532). ([4e4f5c3](https://github.com/ckeditor/ckeditor5-engine/commit/4e4f5c3))
+* Simplified model to view selection conversion. Closes [#1238](https://github.com/ckeditor/ckeditor5-engine/issues/1238). ([9a53251](https://github.com/ckeditor/ckeditor5-engine/commit/9a53251))
+* UIElement custom `render()` method can be now provided without using inheritance. Closes [#1254](https://github.com/ckeditor/ckeditor5-engine/issues/1254). ([e05b8b1](https://github.com/ckeditor/ckeditor5-engine/commit/e05b8b1))
+
+### BREAKING CHANGES
+
+* **Note:** See the "Major refactoring" section above.
+* `view.Writer` is no longer an object literal with functions but a class.
+* Introduced new method of creating custom UIElements.
+* View document is now separated from the DOM. `view.Renderer`, `view.DomConverter` and observers are moved to `view.View`.
+* `view#event:render` is introduced to indicate a moment when all changes are applied and document may be rendered to the DOM.
+* Downcast converter helpers no longer accepts view elements instances as constructors are now protected. Callbacks using view writer should be used.
+* Writer should be now used to set or remove markers, instead of MarkerCollection.
+* View controller `view.View` is introduced. Changes to the view document tree structure should be done by using writer provided to callback in `view.change()` method.
+* `ViewConversionApi#splitToAllowedParent` has been introduced.
+* `ViewConversionApi#storage` has been introduced.
+* `ViewConsumable` has been merged to `ViewConversionApi`.
+* Format od data object passed across conversion callback has been changed.
+Feature: `Schema#findAllowedParent` has been introduced.
+Feature: `SchemaContext#concat` has been introduced.
+* `DataController#parse`, `DataController#toModel`, `ViewConversionDispatcher#convert` gets `SchemaContextDefinition` as a contex instead of `String`.
+
+
 ## [1.0.0-alpha.2](https://github.com/ckeditor/ckeditor5-engine/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2017-11-14)
 
 ### Bug fixes

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

@@ -2,7 +2,7 @@ Software License Agreement
 ==========================
 
 **CKEditor 5 Editing Engine** – https://github.com/ckeditor/ckeditor5-engine <br>
-Copyright (c) 2003-2017, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
+Copyright (c) 2003-2018, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
 
 Licensed under the terms of any of the following licenses at your choice:
 

+ 3 - 1
packages/ckeditor5-engine/README.md

@@ -4,7 +4,9 @@ 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)
-[![Test Coverage](https://codeclimate.com/github/ckeditor/ckeditor5-engine/badges/coverage.svg)](https://codeclimate.com/github/ckeditor/ckeditor5-engine/coverage)
+[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=d3hvenZqQVZERFQ5d09FWXdyT0ozVXhLaVltRFRjTTUyZGpvQWNmWVhUUT0tLUZqNlJ1YWRUd0RvdEVOaEptM1B2Q0E9PQ==--c9d3dee40b9b4471ff3fb516d9ecf8d09292c7e0)](https://www.browserstack.com/automate/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)
 [![devDependency Status](https://david-dm.org/ckeditor/ckeditor5-engine/dev-status.svg)](https://david-dm.org/ckeditor/ckeditor5-engine?type=dev)
 

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

@@ -12,7 +12,7 @@ Together with the {@link api/core core editor architecture} and the {@link api/u
 
 ## Documentation
 
-See the introduction to the {@link framework/guides/architecture/intro#Editing-engine editing engine's architecture}.
+See the introduction to the {@link framework/guides/architecture/intro#editing-engine editing engine's architecture}.
 
 You can also browse the API documentation of this package by using the module tree on the left.
 

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

@@ -1,30 +1,30 @@
 {
   "name": "@ckeditor/ckeditor5-engine",
-  "version": "1.0.0-alpha.2",
+  "version": "1.0.0-beta.1",
   "description": "CKEditor 5 editing engine.",
   "keywords": [
     "ckeditor5",
     "ckeditor5-lib"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-utils": "^1.0.0-alpha.2"
+    "@ckeditor/ckeditor5-utils": "^1.0.0-beta.1"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-core": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-editor-classic": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-enter": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-essentials": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-heading": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-list": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-paragraph": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-typing": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-undo": "^1.0.0-alpha.2",
-    "@ckeditor/ckeditor5-widget": "^1.0.0-alpha.2",
-    "eslint": "^4.8.0",
-    "eslint-config-ckeditor5": "^1.0.6",
+    "@ckeditor/ckeditor5-basic-styles": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-core": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-editor-classic": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-enter": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-essentials": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-heading": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-list": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-paragraph": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-typing": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-undo": "^1.0.0-beta.1",
+    "@ckeditor/ckeditor5-widget": "^1.0.0-beta.1",
+    "eslint": "^4.15.0",
+    "eslint-config-ckeditor5": "^1.0.7",
     "husky": "^0.14.3",
-    "lint-staged": "^4.2.3"
+    "lint-staged": "^6.0.0"
   },
   "engines": {
     "node": ">=6.0.0",

+ 135 - 198
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -9,25 +9,21 @@
 
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 import Mapper from '../conversion/mapper';
 
-import ModelConversionDispatcher from '../conversion/modelconversiondispatcher';
-import { insertText } from '../conversion/model-to-view-converters';
+import DowncastDispatcher from '../conversion/downcastdispatcher';
+import { insertText } from '../conversion/downcast-converters';
 
-import ViewConversionDispatcher from '../conversion/viewconversiondispatcher';
-import { convertText, convertToModelFragment } from '../conversion/view-to-model-converters';
+import UpcastDispatcher from '../conversion/upcastdispatcher';
+import { convertText, convertToModelFragment } from '../conversion/upcast-converters';
 
 import ViewDocumentFragment from '../view/documentfragment';
+import ViewDocument from '../view/document';
+import ViewWriter from '../view/writer';
 
 import ModelRange from '../model/range';
-import ModelPosition from '../model/position';
-import ModelElement from '../model/element';
-
-import insertContent from './insertcontent';
-import deleteContent from './deletecontent';
-import modifySelection from './modifyselection';
-import getSelectedContent from './getselectedcontent';
 
 /**
  * Controller for the data pipeline. The data pipeline controls how data is retrieved from the document
@@ -36,24 +32,25 @@ import getSelectedContent from './getselectedcontent';
  * using given:
  *
  * * {@link module:engine/dataprocessor/dataprocessor~DataProcessor data processor},
- * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher model to view} and
- * * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher view to model} converters.
+ * * downcast converters,
+ * * upcast converters.
  *
- * @mixes module:utils/emittermixin~ObservableMixin
+ * @mixes module:utils/observablemixin~ObservableMixin
  */
 export default class DataController {
 	/**
-	 * Creates data controller instance.
+	 * Creates a data controller instance.
 	 *
-	 * @param {module:engine/model/document~Document} model Document model.
-	 * @param {module:engine/dataprocessor/dataprocessor~DataProcessor} [dataProcessor] Data processor which should used by the controller.
+	 * @param {module:engine/model/model~Model} model Data model.
+	 * @param {module:engine/dataprocessor/dataprocessor~DataProcessor} [dataProcessor] Data processor that should be used
+	 * by the controller.
 	 */
 	constructor( model, dataProcessor ) {
 		/**
-		 * Document model.
+		 * Data model.
 		 *
 		 * @readonly
-		 * @member {module:engine/model/document~Document}
+		 * @member {module:engine/model/model~Model}
 		 */
 		this.model = model;
 
@@ -67,45 +64,32 @@ export default class DataController {
 
 		/**
 		 * Mapper used for the conversion. It has no permanent bindings, because they are created when getting data and
-		 * cleared directly after data are converted. However, the mapper is defined as class property, because
-		 * it needs to be passed to the `ModelConversionDispatcher` as a conversion API.
+		 * cleared directly after the data are converted. However, the mapper is defined as a class property, because
+		 * it needs to be passed to the `DowncastDispatcher` as a conversion API.
 		 *
+		 * @readonly
 		 * @member {module:engine/conversion/mapper~Mapper}
 		 */
 		this.mapper = new Mapper();
 
 		/**
-		 * Model to view conversion dispatcher used by the {@link #get get method}.
-		 * To attach model to view converter to the data pipeline you need to add lister to this property:
-		 *
-		 *		data.modelToView( 'insert:$element', customInsertConverter );
-		 *
-		 * Or use {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder}:
-		 *
-		 *		buildModelConverter().for( data.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
+		 * Downcast dispatcher used by the {@link #get get method}. Downcast converters should be attached to it.
 		 *
 		 * @readonly
-		 * @member {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}
+		 * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher}
 		 */
-		this.modelToView = new ModelConversionDispatcher( this.model, {
+		this.downcastDispatcher = new DowncastDispatcher( {
 			mapper: this.mapper
 		} );
-		this.modelToView.on( 'insert:$text', insertText(), { priority: 'lowest' } );
+		this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
 
 		/**
-		 * View to model conversion dispatcher used by the {@link #set set method}.
-		 * To attach view to model converter to the data pipeline you need to add lister to this property:
-		 *
-		 *		data.viewToModel( 'element', customElementConverter );
-		 *
-		 * Or use {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder}:
-		 *
-		 *		buildViewConverter().for( data.viewToModel ).fromElement( 'b' ).toAttribute( 'bold', 'true' );
+		 * Upcast dispatcher used by the {@link #set set method}. Upcast converters should be attached to it.
 		 *
 		 * @readonly
-		 * @member {module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}
+		 * @member {module:engine/conversion/upcastdispatcher~UpcastDispatcher}
 		 */
-		this.viewToModel = new ViewConversionDispatcher( {
+		this.upcastDispatcher = new UpcastDispatcher( {
 			schema: model.schema
 		} );
 
@@ -114,16 +98,15 @@ export default class DataController {
 		// Note that if there is no default converter for the element it will be skipped, for instance `<b>foo</b>` will be
 		// converted to nothing. We add `convertToModelFragment` as a last converter so it converts children of that
 		// element to the document fragment so `<b>foo</b>` will be converted to `foo` if there is no converter for `<b>`.
-		this.viewToModel.on( 'text', convertText(), { priority: 'lowest' } );
-		this.viewToModel.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
-		this.viewToModel.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
+		this.upcastDispatcher.on( 'text', convertText(), { priority: 'lowest' } );
+		this.upcastDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
+		this.upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
 
-		[ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent' ]
-			.forEach( methodName => this.decorate( methodName ) );
+		this.decorate( 'init' );
 	}
 
 	/**
-	 * Returns model's data converted by the {@link #modelToView model to view converters} and
+	 * Returns the model's data converted by downcast dispatchers attached to {@link #downcastDispatcher} and
 	 * formatted by the {@link #processor data processor}.
 	 *
 	 * @param {String} [rootName='main'] Root name.
@@ -131,55 +114,103 @@ export default class DataController {
 	 */
 	get( rootName = 'main' ) {
 		// Get model range.
-		return this.stringify( this.model.getRoot( rootName ) );
+		return this.stringify( this.model.document.getRoot( rootName ) );
 	}
 
 	/**
 	 * Returns the content of the given {@link module:engine/model/element~Element model's element} or
-	 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the
-	 * {@link #modelToView model to view converters} and formatted by the
-	 * {@link #processor data processor}.
+	 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the downcast converters
+	 * attached to {@link #downcastDispatcher} and formatted by the {@link #processor data processor}.
 	 *
 	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
-	 * Element which content will be stringified.
+	 * Element whose content will be stringified.
 	 * @returns {String} Output data.
 	 */
 	stringify( modelElementOrFragment ) {
-		// model -> view
+		// Model -> view.
 		const viewDocumentFragment = this.toView( modelElementOrFragment );
 
-		// view -> data
+		// View -> data.
 		return this.processor.toData( viewDocumentFragment );
 	}
 
 	/**
 	 * Returns the content of the given {@link module:engine/model/element~Element model element} or
-	 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the
-	 * {@link #modelToView model to view converters} to a
+	 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} converted by the downcast
+	 * converters attached to {@link #downcastDispatcher} to a
 	 * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}.
 	 *
 	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
-	 * Element or document fragment which content will be converted.
+	 * Element or document fragment whose content will be converted.
 	 * @returns {module:engine/view/documentfragment~DocumentFragment} Output view DocumentFragment.
 	 */
 	toView( modelElementOrFragment ) {
+		// First, convert elements.
 		const modelRange = ModelRange.createIn( modelElementOrFragment );
 
 		const viewDocumentFragment = new ViewDocumentFragment();
+
+		// Create separate ViewWriter just for data conversion purposes.
+		// We have no view controller and rendering do DOM in DataController so view.change() block is not used here.
+		const viewWriter = new ViewWriter( new ViewDocument() );
 		this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment );
 
-		this.modelToView.convertInsertion( modelRange );
+		this.downcastDispatcher.convertInsert( modelRange, viewWriter );
+
+		if ( !modelElementOrFragment.is( 'documentFragment' ) ) {
+			// Then, if a document element is converted, convert markers.
+			// From all document markers, get those, which "intersect" with the converter element.
+			const markers = _getMarkersRelativeToElement( modelElementOrFragment );
 
+			for ( const [ name, range ] of markers ) {
+				this.downcastDispatcher.convertMarkerAdd( name, range, viewWriter );
+			}
+		}
+
+		// Clear bindings so the next call to this method gives correct results.
 		this.mapper.clearBindings();
 
 		return viewDocumentFragment;
 	}
 
+	/**
+	 * Sets initial input data parsed by the {@link #processor data processor} and
+	 * converted by the {@link #upcastDispatcher view-to-model converters}.
+	 * Initial data can be set only to document that {@link module:engine/model/document~Document#version} is equal 0.
+	 *
+	 * **Note** This method is {@link module:utils/observablemixin~ObservableMixin#decorate decorated} which is
+	 * used by e.g. collaborative editing plugin that syncs remote data on init.
+	 *
+	 * @fires init
+	 * @param {String} data Input data.
+	 * @param {String} [rootName='main'] Root name.
+	 */
+	init( data, rootName = 'main' ) {
+		if ( this.model.document.version ) {
+			/**
+			 * Cannot set initial data to not empty {@link module:engine/model/document~Document}.
+			 * Initial data should be set once, during {@link module:core/editor/editor~Editor} initialization,
+			 * when the {@link module:engine/model/document~Document#version} is equal 0.
+			 *
+			 * @error datacontroller-init-document-not-empty
+			 */
+			throw new CKEditorError( 'datacontroller-init-document-not-empty: Trying to set initial data to not empty document.' );
+		}
+
+		const modelRoot = this.model.document.getRoot( rootName );
+
+		this.model.enqueueChange( 'transparent', writer => {
+			writer.insert( this.parse( data, modelRoot ), modelRoot );
+		} );
+	}
+
 	/**
 	 * Sets input data parsed by the {@link #processor data processor} and
-	 * converted by the {@link #viewToModel view to model converters}.
+	 * converted by the {@link #upcastDispatcher view-to-model converters}.
+	 * This method can be used any time to replace existing editor data by the new one without clearing the
+	 * {@link module:engine/model/document~Document#history document history}.
 	 *
-	 * This method also creates a batch with all the changes applied. If all you need is to parse data use
+	 * This method also creates a batch with all the changes applied. If all you need is to parse data, use
 	 * the {@link #parse} method.
 	 *
 	 * @param {String} data Input data.
@@ -187,29 +218,25 @@ export default class DataController {
 	 */
 	set( data, rootName = 'main' ) {
 		// Save to model.
-		const modelRoot = this.model.getRoot( rootName );
-
-		this.model.enqueueChanges( () => {
-			// Clearing selection is a workaround for ticket #569 (LiveRange loses position after removing data from document).
-			// After fixing it this code should be removed.
-			this.model.selection.removeAllRanges();
-			this.model.selection.clearAttributes();
-
-			// Initial batch should be ignored by features like undo, etc.
-			this.model.batch( 'transparent' )
-				.remove( ModelRange.createIn( modelRoot ) )
-				.insert( ModelPosition.createAt( modelRoot, 0 ), this.parse( data ) );
+		const modelRoot = this.model.document.getRoot( rootName );
+
+		this.model.enqueueChange( 'transparent', writer => {
+			writer.setSelection( null );
+			writer.removeSelectionAttribute( this.model.document.selection.getAttributeKeys() );
+
+			writer.remove( ModelRange.createIn( modelRoot ) );
+			writer.insert( this.parse( data, modelRoot ), modelRoot );
 		} );
 	}
 
 	/**
-	 * Returns data parsed by the {@link #processor data processor} and then
-	 * converted by the {@link #viewToModel view to model converters}.
+	 * Returns the data parsed by the {@link #processor data processor} and then converted by upcast converters
+	 * attached to the {@link #upcastDispatcher}.
 	 *
 	 * @see #set
 	 * @param {String} data Data to parse.
-	 * @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
-	 * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
+	 * @param {module:engine/model/schema~SchemaContextDefinition} [context='$root'] Base context in which the view will
+	 * be converted to the model. See: {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#convert}.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Parsed data.
 	 */
 	parse( data, context = '$root' ) {
@@ -221,22 +248,23 @@ export default class DataController {
 	}
 
 	/**
-	 * Returns wrapped by {module:engine/model/documentfragment~DocumentFragment} result of the given
-	 * {@link module:engine/view/element~Element view element} or
+	 * Returns the result of the given {@link module:engine/view/element~Element view element} or
 	 * {@link module:engine/view/documentfragment~DocumentFragment view document fragment} converted by the
-	 * {@link #viewToModel view to model converters}.
+	 * {@link #upcastDispatcher view-to-model converters}, wrapped by {module:engine/model/documentfragment~DocumentFragment}.
 	 *
-	 * When marker elements were converted during conversion process then will be set as DocumentFragment's
+	 * When marker elements were converted during the conversion process, it will be set as a document fragment's
 	 * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
 	 *
 	 * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment
-	 * Element or document fragment which content will be converted.
-	 * @param {String} [context='$root'] Base context in which the view will be converted to the model. See:
-	 * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}.
+	 * Element or document fragment whose content will be converted.
+	 * @param {module:engine/model/schema~SchemaContextDefinition} [context='$root'] Base context in which the view will
+	 * be converted to the model. See: {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#convert}.
 	 * @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment.
 	 */
 	toModel( viewElementOrFragment, context = '$root' ) {
-		return this.viewToModel.convert( viewElementOrFragment, { context: [ context ] } );
+		return this.model.change( writer => {
+			return this.upcastDispatcher.convert( viewElementOrFragment, writer, context );
+		} );
 	}
 
 	/**
@@ -245,127 +273,36 @@ export default class DataController {
 	destroy() {}
 
 	/**
-	 * See {@link module:engine/controller/insertcontent.insertContent}.
+	 * Event fired by decorated {@link #init} method.
+	 * See {@link module:utils/observablemixin~ObservableMixin.decorate} for more information and samples.
 	 *
-	 * @fires insertContent
-	 * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
-	 * @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted.
-	 * @param {module:engine/model/batch~Batch} [batch] Batch to which deltas will be added. If not specified, then
-	 * changes will be added to a new batch.
+	 * @event init
 	 */
-	insertContent( content, selection, batch ) {
-		insertContent( this, content, selection, batch );
-	}
+}
 
-	/**
-	 * See {@link module:engine/controller/deletecontent.deleteContent}.
-	 *
-	 * Note: For the sake of predictability, the resulting selection should always be collapsed.
-	 * In cases where a feature wants to modify deleting behavior so selection isn't collapsed
-	 * (e.g. a table feature may want to keep row selection after pressing <kbd>Backspace</kbd>),
-	 * then that behavior should be implemented in the view's listener. At the same time, the table feature
-	 * will need to modify this method's behavior too, e.g. to "delete contents and then collapse
-	 * the selection inside the last selected cell" or "delete the row and collapse selection somewhere near".
-	 * That needs to be done in order to ensure that other features which use `deleteContent()` will work well with tables.
-	 *
-	 * @fires deleteContent
-	 * @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted.
-	 * @param {module:engine/model/batch~Batch} batch Batch to which deltas will be added.
-	 * @param {Object} options See {@link module:engine/controller/deletecontent~deleteContent}'s options.
-	 */
-	deleteContent( selection, batch, options ) {
-		deleteContent( selection, batch, options );
-	}
+mix( DataController, ObservableMixin );
 
-	/**
-	 * See {@link module:engine/controller/modifyselection.modifySelection}.
-	 *
-	 * @fires modifySelection
-	 * @param {module:engine/model/selection~Selection} selection The selection to modify.
-	 * @param {Object} options See {@link module:engine/controller/modifyselection~modifySelection}'s options.
-	 */
-	modifySelection( selection, options ) {
-		modifySelection( this, selection, options );
-	}
+// Helper function for downcast conversion.
+//
+// Takes a document element (element that is added to a model document) and checks which markers are inside it
+// and which markers are containing it. If the marker is intersecting with element, the intersection is returned.
+function _getMarkersRelativeToElement( element ) {
+	const result = [];
+	const doc = element.root.document;
 
-	/**
-	 * See {@link module:engine/controller/getselectedcontent.getSelectedContent}.
-	 *
-	 * @fires module:engine/controller/datacontroller~DataController#getSelectedContent
-	 * @param {module:engine/model/selection~Selection} selection The selection of which content will be retrieved.
-	 * @returns {module:engine/model/documentfragment~DocumentFragment} Document fragment holding the clone of the selected content.
-	 */
-	getSelectedContent( selection ) {
-		return getSelectedContent( selection );
+	if ( !doc ) {
+		return [];
 	}
 
-	/**
-	 * Checks whether given {@link module:engine/model/range~Range range} or {@link module:engine/model/element~Element element}
-	 * has any content.
-	 *
-	 * Content is any text node or element which is registered in {@link module:engine/model/schema~Schema schema}.
-	 *
-	 * @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
-	 * @returns {Boolean}
-	 */
-	hasContent( rangeOrElement ) {
-		if ( rangeOrElement instanceof ModelElement ) {
-			rangeOrElement = ModelRange.createIn( rangeOrElement );
-		}
+	const elementRange = ModelRange.createIn( element );
 
-		if ( rangeOrElement.isCollapsed ) {
-			return false;
-		}
+	for ( const marker of doc.model.markers ) {
+		const intersection = elementRange.getIntersection( marker.getRange() );
 
-		for ( const item of rangeOrElement.getItems() ) {
-			// Remember, `TreeWalker` returns always `textProxy` nodes.
-			if ( item.is( 'textProxy' ) || this.model.schema.objects.has( item.name ) ) {
-				return true;
-			}
+		if ( intersection ) {
+			result.push( [ marker.name, intersection ] );
 		}
-
-		return false;
 	}
-}
 
-mix( DataController, ObservableMixin );
-
-/**
- * Event fired when {@link #insertContent} method is called.
- *
- * The {@link #insertContent default action of that method} is implemented as a
- * listener to this event so it can be fully customized by the features.
- *
- * @event insertContent
- * @param {Array} args The arguments passed to the original method.
- */
-
-/**
- * Event fired when {@link #deleteContent} method is called.
- *
- * The {@link #deleteContent default action of that method} is implemented as a
- * listener to this event so it can be fully customized by the features.
- *
- * @event deleteContent
- * @param {Array} args The arguments passed to the original method.
- */
-
-/**
- * Event fired when {@link #modifySelection} method is called.
- *
- * The {@link #modifySelection default action of that method} is implemented as a
- * listener to this event so it can be fully customized by the features.
- *
- * @event modifySelection
- * @param {Array} args The arguments passed to the original method.
- */
-
-/**
- * Event fired when {@link #getSelectedContent} method is called.
- *
- * The {@link #getSelectedContent default action of that method} is implemented as a
- * listener to this event so it can be fully customized by the features.
- *
- * @event getSelectedContent
- * @param {Array} args The arguments passed to the original method.
- */
+	return result;
+}

+ 121 - 84
packages/ckeditor5-engine/src/controller/editingcontroller.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -7,56 +7,57 @@
  * @module engine/controller/editingcontroller
  */
 
-import ViewDocument from '../view/document';
+import RootEditableElement from '../view/rooteditableelement';
+import View from '../view/view';
+import ViewWriter from '../view/writer';
 import Mapper from '../conversion/mapper';
-import ModelConversionDispatcher from '../conversion/modelconversiondispatcher';
+import DowncastDispatcher from '../conversion/downcastdispatcher';
 import {
 	insertText,
 	remove
-} from '../conversion/model-to-view-converters';
-import { convertSelectionChange } from '../conversion/view-selection-to-model-converters';
+} from '../conversion/downcast-converters';
+import { convertSelectionChange } from '../conversion/upcast-selection-converters';
 import {
 	convertRangeSelection,
 	convertCollapsedSelection,
-	clearAttributes,
-	clearFakeSelection
-} from '../conversion/model-selection-to-view-converters';
+	clearAttributes
+} from '../conversion/downcast-selection-converters';
 
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 /**
  * Controller for the editing pipeline. The editing pipeline controls {@link ~EditingController#model model} rendering,
- * including selection handling. It also creates {@link ~EditingController#view view document} which build a
- * browser-independent virtualization over the DOM elements. Editing controller also attach default converters.
+ * including selection handling. It also creates the {@link ~EditingController#view view document} which builds a
+ * browser-independent virtualization over the DOM elements. The editing controller also attaches default converters.
  *
  * @mixes module:utils/observablemixin~ObservableMixin
  */
 export default class EditingController {
 	/**
-	 * Creates editing controller instance.
+	 * Creates an editing controller instance.
 	 *
-	 * @param {module:engine/model/document~Document} model Document model.
+	 * @param {module:engine/model/model~Model} model Editing model.
 	 */
 	constructor( model ) {
 		/**
-		 * Document model.
+		 * Editing model.
 		 *
 		 * @readonly
-		 * @member {module:engine/model/document~Document}
+		 * @member {module:engine/model/model~Model}
 		 */
 		this.model = model;
 
 		/**
-		 * View document.
+		 * Editing view controller.
 		 *
 		 * @readonly
-		 * @member {module:engine/view/document~Document}
+		 * @member {module:engine/view/view~View}
 		 */
-		this.view = new ViewDocument();
+		this.view = new View();
 
 		/**
-		 * Mapper which describes model-view binding.
+		 * Mapper which describes the model-view binding.
 		 *
 		 * @readonly
 		 * @member {module:engine/conversion/mapper~Mapper}
@@ -64,86 +65,102 @@ export default class EditingController {
 		this.mapper = new Mapper();
 
 		/**
-		 * Model to view conversion dispatcher, which converts changes from the model to
-		 * {@link #view editing view}.
-		 *
-		 * To attach model to view converter to the editing pipeline you need to add lister to this property:
-		 *
-		 *		editing.modelToView( 'insert:$element', customInsertConverter );
-		 *
-		 * Or use {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder}:
-		 *
-		 *		buildModelConverter().for( editing.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
+		 * Downcast dispatcher that converts changes from the model to {@link #view the editing view}.
 		 *
 		 * @readonly
-		 * @member {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} #modelToView
+		 * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #downcastDispatcher
 		 */
-		this.modelToView = new ModelConversionDispatcher( this.model, {
-			mapper: this.mapper,
-			viewSelection: this.view.selection
+		this.downcastDispatcher = new DowncastDispatcher( {
+			mapper: this.mapper
 		} );
 
-		// Convert changes in model to view.
-		this.listenTo( this.model, 'change', ( evt, type, changes ) => {
-			this.modelToView.convertChange( type, changes );
-		}, { priority: 'low' } );
-
-		// Convert model selection to view.
-		this.listenTo( this.model, 'changesDone', () => {
-			const selection = this.model.selection;
+		const doc = this.model.document;
+		const selection = doc.selection;
+		const markers = this.model.markers;
 
-			this.modelToView.convertSelection( selection );
-			this.view.render();
+		this.listenTo( doc, 'change', () => {
+			this.view.change( writer => {
+				this.downcastDispatcher.convertChanges( doc.differ, writer );
+				this.downcastDispatcher.convertSelection( selection, markers, writer );
+			} );
 		}, { priority: 'low' } );
 
-		// Convert model markers changes.
-		this.listenTo( this.model.markers, 'add', ( evt, marker ) => {
-			this.modelToView.convertMarker( 'addMarker', marker.name, marker.getRange() );
+		// Convert selection from view to model.
+		this.listenTo( this.view.document, 'selectionChange', convertSelectionChange( this.model, this.mapper ) );
+
+		// Attach default model converters.
+		this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
+		this.downcastDispatcher.on( 'remove', remove(), { priority: 'low' } );
+
+		// Attach default model selection converters.
+		this.downcastDispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
+		this.downcastDispatcher.on( 'selection', convertRangeSelection(), { priority: 'low' } );
+		this.downcastDispatcher.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );
+
+		// Convert markers removal.
+		//
+		// Markers should be removed from the view before changes to the model are applied. This is because otherwise
+		// it would be impossible to map some markers to the view (if, for example, the marker's boundary parent got removed).
+		//
+		// `removedMarkers` keeps information which markers already has been removed to prevent removing them twice.
+		const removedMarkers = new Set();
+
+		// We don't want to render view when markers are converted, so we need to create view writer
+		// manually instead of using `View#change` block. See https://github.com/ckeditor/ckeditor5-engine/issues/1323.
+		const viewWriter = new ViewWriter( this.view.document );
+
+		this.listenTo( model, 'applyOperation', ( evt, args ) => {
+			// Before operation is applied...
+			const operation = args[ 0 ];
+
+			for ( const marker of model.markers ) {
+				// Check all markers, that aren't already removed...
+				if ( removedMarkers.has( marker.name ) ) {
+					continue;
+				}
+
+				const markerRange = marker.getRange();
+
+				if ( _operationAffectsMarker( operation, marker ) ) {
+					// And if the operation in any way modifies the marker, remove the marker from the view.
+					removedMarkers.add( marker.name );
+					this.downcastDispatcher.convertMarkerRemove( marker.name, markerRange, viewWriter );
+					// TODO: This stinks but this is the safest place to have this code.
+					this.model.document.differ.bufferMarkerChange( marker.name, markerRange, markerRange );
+				}
+			}
+		}, { priority: 'high' } );
+
+		// If an existing marker is updated through `model.Model#markers` directly (not through operation), just remove it.
+		this.listenTo( model.markers, 'update', ( evt, marker, oldRange ) => {
+			if ( oldRange && !removedMarkers.has( marker.name ) ) {
+				removedMarkers.add( marker.name );
+				this.downcastDispatcher.convertMarkerRemove( marker.name, oldRange, viewWriter );
+			}
 		} );
 
-		this.listenTo( this.model.markers, 'remove', ( evt, marker ) => {
-			this.modelToView.convertMarker( 'removeMarker', marker.name, marker.getRange() );
-		} );
+		// When all changes are done, clear `removedMarkers` set.
+		this.listenTo( model, '_change', () => {
+			removedMarkers.clear();
+		}, { priority: 'low' } );
 
-		// Convert view selection to model.
-		this.listenTo( this.view, 'selectionChange', convertSelectionChange( this.model, this.mapper ) );
+		// Binds {@link module:engine/view/document~Document#roots view roots collection} to
+		// {@link module:engine/model/document~Document#roots model roots collection} so creating
+		// model root automatically creates corresponding view root.
+		this.view.document.roots.bindTo( this.model.document.roots ).using( root => {
+			// $graveyard is a special root that has no reflection in the view.
+			if ( root.rootName == '$graveyard' ) {
+				return null;
+			}
 
-		// Attach default content converters.
-		this.modelToView.on( 'insert:$text', insertText(), { priority: 'lowest' } );
-		this.modelToView.on( 'remove', remove(), { priority: 'low' } );
+			const viewRoot = new RootEditableElement( root.name );
 
-		// Attach default selection converters.
-		this.modelToView.on( 'selection', clearAttributes(), { priority: 'low' } );
-		this.modelToView.on( 'selection', clearFakeSelection(), { priority: 'low' } );
-		this.modelToView.on( 'selection', convertRangeSelection(), { priority: 'low' } );
-		this.modelToView.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );
-	}
+			viewRoot.rootName = root.rootName;
+			viewRoot._document = this.view.document;
+			this.mapper.bindElements( root, viewRoot );
 
-	/**
-	 * {@link module:engine/view/document~Document#createRoot Creates} a view root
-	 * and {@link module:engine/conversion/mapper~Mapper#bindElements binds}
-	 * the model root with view root and and view root with DOM element:
-	 *
-	 *		editing.createRoot( document.querySelector( div#editor ) );
-	 *
-	 * If the DOM element is not available at the time you want to create a view root, for instance it is iframe body
-	 * element, it is possible to create view element and bind the DOM element later:
-	 *
-	 *		editing.createRoot( 'body' );
-	 *		editing.view.attachDomRoot( iframe.contentDocument.body );
-	 *
-	 * @param {Element|String} domRoot DOM root element or the name of view root element if the DOM element will be
-	 * attached later.
-	 * @param {String} [name='main'] Root name.
-	 * @returns {module:engine/view/containerelement~ContainerElement} View root element.
-	 */
-	createRoot( domRoot, name = 'main' ) {
-		const viewRoot = this.view.createRoot( domRoot, name );
-		const modelRoot = this.model.getRoot( name );
-
-		this.mapper.bindElements( modelRoot, viewRoot );
-
-		return viewRoot;
+			return viewRoot;
+		} );
 	}
 
 	/**
@@ -157,3 +174,23 @@ export default class EditingController {
 }
 
 mix( EditingController, ObservableMixin );
+
+// Helper function which checks whether given operation will affect given marker after the operation is applied.
+function _operationAffectsMarker( operation, marker ) {
+	const range = marker.getRange();
+
+	if ( operation.type == 'insert' || operation.type == 'rename' ) {
+		return _positionAffectsRange( operation.position, range );
+	} else if ( operation.type == 'move' || operation.type == 'remove' || operation.type == 'reinsert' ) {
+		return _positionAffectsRange( operation.targetPosition, range ) || _positionAffectsRange( operation.sourcePosition, range );
+	} else if ( operation.type == 'marker' && operation.name == marker.name ) {
+		return true;
+	}
+
+	return false;
+}
+
+// Helper function which checks whether change at given position affects given range.
+function _positionAffectsRange( position, range ) {
+	return range.containsPosition( position ) || !range.start._getTransformedByInsertion( position, 1, true ).isEqual( range.start );
+}

+ 0 - 147
packages/ckeditor5-engine/src/controller/getselectedcontent.js

@@ -1,147 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/controller/getselectedcontent
- */
-
-import DocumentFragment from '../model/documentfragment';
-import Range from '../model/range';
-import Position from '../model/position';
-import Text from '../model/text';
-import { remove } from '../model/writer';
-
-/**
- * Gets a clone of the selected content.
- *
- * For example, for the following selection:
- *
- *		<p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p>
- *
- * It will return a document fragment with such a content:
- *
- *		<quote><h>st</h></quote><p>se</p>
- *
- * @param {module:engine/model/selection~Selection} selection The selection of which content will be returned.
- * @returns {module:engine/model/documentfragment~DocumentFragment}
- */
-export default function getSelectedContent( selection ) {
-	const frag = new DocumentFragment();
-	const range = selection.getFirstRange();
-
-	if ( !range || range.isCollapsed ) {
-		return frag;
-	}
-
-	const root = range.start.root;
-	const commonPath = range.start.getCommonPath( range.end );
-	const commonParent = root.getNodeByPath( commonPath );
-
-	// ## 1st step
-	//
-	// First, we'll clone a fragment represented by a minimal flat range
-	// containing the original range to be cloned.
-	// E.g. let's consider such a range:
-	//
-	// <p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p>
-	//
-	// A minimal flat range containing this one is:
-	//
-	// <p>x</p>[<quote><p>y</p><h>first</h></quote><p>second</p>]<p>z</p>
-	//
-	// We can easily clone this structure, preserving e.g. the <quote> element.
-	let flatSubtreeRange;
-
-	if ( range.start.parent == range.end.parent ) {
-		// The original range is flat, so take it.
-		flatSubtreeRange = range;
-	} else {
-		flatSubtreeRange = Range.createFromParentsAndOffsets(
-			commonParent, range.start.path[ commonPath.length ],
-			commonParent, range.end.path[ commonPath.length ] + 1
-		);
-	}
-
-	const howMany = flatSubtreeRange.end.offset - flatSubtreeRange.start.offset;
-
-	// Clone the whole contents.
-	for ( const item of flatSubtreeRange.getItems( { shallow: true } ) ) {
-		if ( item.is( 'textProxy' ) ) {
-			frag.appendChildren( new Text( item.data, item.getAttributes() ) );
-		} else {
-			frag.appendChildren( item.clone( true ) );
-		}
-	}
-
-	// ## 2nd step
-	//
-	// If the original range wasn't flat, then we need to remove the excess nodes from the both ends of the cloned fragment.
-	//
-	// For example, for the range shown in the 1st step comment, we need to remove these pieces:
-	//
-	// <quote>[<p>y</p>]<h>[fir]st</h></quote><p>se[cond]</p>
-	//
-	// So this will be the final copied content:
-	//
-	// <quote><h>st</h></quote><p>se</p>
-	//
-	// In order to do that, we remove content from these two ranges:
-	//
-	// [<quote><p>y</p><h>fir]st</h></quote><p>se[cond</p>]
-	if ( flatSubtreeRange != range ) {
-		// Find the position of the original range in the cloned fragment.
-		const newRange = range._getTransformedByMove( flatSubtreeRange.start, Position.createAt( frag, 0 ), howMany )[ 0 ];
-
-		const leftExcessRange = new Range( Position.createAt( frag ), newRange.start );
-		const rightExcessRange = new Range( newRange.end, Position.createAt( frag, 'end' ) );
-
-		removeRangeContent( rightExcessRange );
-		removeRangeContent( leftExcessRange );
-	}
-
-	return frag;
-}
-
-// After https://github.com/ckeditor/ckeditor5-engine/issues/690 is fixed,
-// this function will, most likely, be able to rewritten using getMinimalFlatRanges().
-function removeRangeContent( range ) {
-	const parentsToCheck = [];
-
-	Array.from( range.getItems( { direction: 'backward' } ) )
-		// We should better store ranges because text proxies will lose integrity
-		// with the text nodes when we'll start removing content.
-		.map( item => Range.createOn( item ) )
-		// Filter only these items which are fully contained in the passed range.
-		//
-		// E.g. for the following range: [<quote><p>y</p><h>fir]st</h>
-		// the walker will return the entire <h> element, when only the "fir" item inside it is fully contained.
-		.filter( itemRange => {
-			// We should be able to use Range.containsRange, but https://github.com/ckeditor/ckeditor5-engine/issues/691.
-			const contained =
-				( itemRange.start.isAfter( range.start ) || itemRange.start.isEqual( range.start ) ) &&
-				( itemRange.end.isBefore( range.end ) || itemRange.end.isEqual( range.end ) );
-
-			return contained;
-		} )
-		.forEach( itemRange => {
-			parentsToCheck.push( itemRange.start.parent );
-
-			remove( itemRange );
-		} );
-
-	// Remove ancestors of the removed items if they turned to be empty now
-	// (their whole content was contained in the range).
-	parentsToCheck.forEach( parentToCheck => {
-		let parent = parentToCheck;
-
-		while ( parent.parent && parent.isEmpty ) {
-			const removeRange = Range.createOn( parent );
-
-			parent = parent.parent;
-
-			remove( removeRange );
-		}
-	} );
-}

+ 0 - 432
packages/ckeditor5-engine/src/conversion/buildmodelconverter.js

@@ -1,432 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/conversion/buildmodelconverter
- */
-
-import {
-	insertElement,
-	insertUIElement,
-	setAttribute,
-	removeAttribute,
-	removeUIElement,
-	wrapItem,
-	unwrapItem,
-	highlightText,
-	highlightElement
-} from './model-to-view-converters';
-
-import { convertSelectionAttribute, convertSelectionMarker } from './model-selection-to-view-converters';
-
-import ViewAttributeElement from '../view/attributeelement';
-import ViewContainerElement from '../view/containerelement';
-import ViewUIElement from '../view/uielement';
-
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
-/**
- * Provides chainable, high-level API to easily build basic model-to-view converters that are appended to given
- * dispatchers. In many cases, this is the API that should be used to specify how abstract model elements and
- * attributes should be represented in the view (and then later in DOM). Instances of this class are created by
- * {@link module:engine/conversion/buildmodelconverter~buildModelConverter}.
- *
- * If you need more complex converters, see {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher},
- * {@link module:engine/conversion/model-to-view-converters}, {@link module:engine/conversion/modelconsumable~ModelConsumable},
- * {@link module:engine/conversion/mapper~Mapper}.
- *
- * Using this API it is possible to create five kinds of converters:
- *
- * 1. Model element to view element converter. This is a converter that takes the model element and represents it
- * in the view.
- *
- *		buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
- *		buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( 'img' );
- *
- * 2. Model attribute to view attribute converter. This is a converter that operates on model element attributes
- * and converts them to view element attributes. It is suitable for elements like `image` (`src`, `title` attributes).
- *
- *		buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( 'img' );
- *		buildModelConverter().for( dispatcher ).fromAttribute( 'src' ).toAttribute();
- *
- * 3. Model attribute to view element converter. This is a converter that takes model attributes and represents them
- * as view elements. Elements created by this kind of converter are wrapping other view elements. Wrapped view nodes
- * correspond to model nodes had converter attribute. It is suitable for attributes like `bold`, where `bold` attribute
- * set on model text nodes is converter to `strong` view element.
- *
- *		buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( 'strong' );
- *
- * 4. Model marker to view highlight converter. This is a converter that converts model markers to view highlight
- * described by {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object passed to
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toHighlight} method.
- *
- *		buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toHighlight( {
- *			class: 'search',
- *			priority: 20
- *		} );
- *
- * 5. Model marker to element converter. This is a converter that takes model marker and creates separate elements at
- * the beginning and at the end of the marker's range. For more information see
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement} method.
- *
- *		buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
- *
- * It is possible to provide various different parameters for
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement},
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toAttribute} and
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toHighlight} methods.
- * See their descriptions to learn more.
- *
- * It is also possible to {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority change default priority}
- * of created converters to decide which converter should be fired earlier and which later. This is useful if you have
- * a general converter but also want to provide different special-case converters (i.e. given model element is converted
- * always to given view element, but if it has given attribute it is converter to other view element). For this,
- * use {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority withPriority} right after `from...` method.
- *
- * Note that `to...` methods are "terminators", which means that should be the last one used in building converter.
- *
- * You can use {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder}
- * to create "opposite" converters - from view to model.
- */
-class ModelConverterBuilder {
-	/**
-	 * Creates `ModelConverterBuilder` with given `dispatchers` registered to it.
-	 */
-	constructor() {
-		/**
-		 * Dispatchers to which converters will be attached.
-		 *
-		 * @type {Array.<module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher>}
-		 * @private
-		 */
-		this._dispatchers = [];
-
-		/**
-		 * Contains data about registered "from" query.
-		 *
-		 * @type {Object}
-		 * @private
-		 */
-		this._from = null;
-	}
-
-	/**
-	 * Set one or more dispatchers which the built converter will be attached to.
-	 *
-	 * @chainable
-	 * @param {...module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} dispatchers One or more dispatchers.
-	 * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
-	 */
-	for( ...dispatchers ) {
-		this._dispatchers = dispatchers;
-
-		return this;
-	}
-
-	/**
-	 * Registers what model element should be converted.
-	 *
-	 * @chainable
-	 * @param {String} elementName Name of element to convert.
-	 * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
-	 */
-	fromElement( elementName ) {
-		this._from = {
-			type: 'element',
-			name: elementName,
-			priority: null
-		};
-
-		return this;
-	}
-
-	/**
-	 * Registers what model attribute should be converted.
-	 *
-	 * @chainable
-	 * @param {String} key Key of attribute to convert.
-	 * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
-	 */
-	fromAttribute( key ) {
-		this._from = {
-			type: 'attribute',
-			key,
-			priority: null
-		};
-
-		return this;
-	}
-
-	/**
-	 * Registers what type of marker should be converted.
-	 *
-	 * @chainable
-	 * @param {String} markerName Name of marker to convert.
-	 * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
-	 */
-	fromMarker( markerName ) {
-		this._from = {
-			type: 'marker',
-			name: markerName,
-			priority: null
-		};
-
-		return this;
-	}
-
-	/**
-	 * Changes default priority for built converter. The lower the number, the earlier converter will be fired.
-	 * Default priority is `10`.
-	 *
-	 * **Note:** Keep in mind that event priority, that is set by this modifier, is used for attribute priority
-	 * when {@link module:engine/view/writer~writer} is used. This changes how view elements are ordered,
-	 * i.e.: `<strong><em>foo</em></strong>` vs `<em><strong>foo</strong></em>`. Using priority you can also
-	 * prevent node merging, i.e.: `<span class="bold"><span class="theme">foo</span><span>` vs `<span class="bold theme">foo</span>`.
-	 * If you want to prevent merging, just set different priority for both converters.
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).withPriority( 2 ).toElement( 'strong' );
-	 *		buildModelConverter().for( dispatcher ).fromAttribute( 'italic' ).withPriority( 3 ).toElement( 'em' );
-	 *
-	 * @chainable
-	 * @param {Number} priority Converter priority.
-	 * @returns {module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
-	 */
-	withPriority( priority ) {
-		this._from.priority = priority;
-
-		return this;
-	}
-
-	/**
-	 * Registers what view element will be created by converter.
-	 *
-	 * Method accepts various ways of providing how the view element will be created. You can pass view element name as
-	 * `string`, view element instance which will be cloned and used, or creator function which returns view element that
-	 * will be used. Keep in mind that when you view element instance or creator function, it has to be/return a
-	 * proper type of view element: {@link module:engine/view/containerelement~ContainerElement ViewContainerElement} if you convert
-	 * from element, {@link module:engine/view/attributeelement~AttributeElement ViewAttributeElement} if you convert
-	 * from attribute and {@link module:engine/view/uielement~UIElement ViewUIElement} if you convert from marker.
-	 *
-	 * **Note:** When converting from model's marker, separate elements will be created at the beginning and at the end of the
-	 * marker's range. If range is collapsed then only one element will be created. See how markers
-	 * {module:engine/model/buildviewconverter~ViewConverterBuilder#toMarker serialization from view to model}
-	 * works to find out what view element format is the best for you.
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' );
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromElement( 'image' ).toElement( new ViewContainerElement( 'img' ) );
-	 *
-	 *		buildModelConverter().for( dispatcher )
-	 *			.fromElement( 'header' )
-	 *			.toElement( ( data ) => new ViewContainerElement( 'h' + data.item.getAttribute( 'level' ) ) );
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( new ViewAttributeElement( 'strong' ) );
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( 'span' );
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toElement( new ViewUIElement( 'span' ) );
-	 *
-	 * Creator function will be passed different values depending whether conversion is from element or from attribute:
-	 *
-	 * * from element: dispatcher's
-	 * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert event}
-	 * parameters will be passed,
-	 * * from attribute: there is one parameter and it is attribute value,
-	 * * from marker: {@link module:engine/conversion/buildmodelconverter~MarkerViewElementCreatorData}.
-	 *
-	 * This method also registers model selection to view selection converter, if conversion is from attribute.
-	 *
-	 * This method creates the converter and adds it as a callback to a proper
-	 * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher conversion dispatcher} event.
-	 *
-	 * @param {String|module:engine/view/element~Element|Function} element Element created by converter or
-	 * a function that returns view element.
-	 */
-	toElement( element ) {
-		const priority = this._from.priority === null ? 'normal' : this._from.priority;
-
-		for ( const dispatcher of this._dispatchers ) {
-			if ( this._from.type == 'element' ) {
-				// From model element to view element -> insert element.
-				element = typeof element == 'string' ? new ViewContainerElement( element ) : element;
-
-				dispatcher.on( 'insert:' + this._from.name, insertElement( element ), { priority } );
-			} else if ( this._from.type == 'attribute' ) {
-				// From model attribute to view element -> wrap and unwrap.
-				element = typeof element == 'string' ? new ViewAttributeElement( element ) : element;
-
-				dispatcher.on( 'addAttribute:' + this._from.key, wrapItem( element ), { priority } );
-				dispatcher.on( 'changeAttribute:' + this._from.key, wrapItem( element ), { priority } );
-				dispatcher.on( 'removeAttribute:' + this._from.key, unwrapItem( element ), { priority } );
-
-				dispatcher.on( 'selectionAttribute:' + this._from.key, convertSelectionAttribute( element ), { priority } );
-			} else { // From marker to element.
-				const priority = this._from.priority === null ? 'normal' : this._from.priority;
-
-				element = typeof element == 'string' ? new ViewUIElement( element ) : element;
-
-				dispatcher.on( 'addMarker:' + this._from.name, insertUIElement( element ), { priority } );
-				dispatcher.on( 'removeMarker:' + this._from.name, removeUIElement( element ), { priority } );
-			}
-		}
-	}
-
-	/**
-	 * Registers that marker should be converted to view highlight. Markers, basically,
-	 * are {@link module:engine/model/liverange~LiveRange} instances, that are named. View highlight is
-	 * a representation of the model marker in the view:
-	 * * each {@link module:engine/view/text~Text view text node} in the marker's range will be wrapped with `span`
-	 * {@link module:engine/view/attributeelement~AttributeElement},
-	 * * each {@link module:engine/view/containerelement~ContainerElement container view element} in the marker's
-	 * range can handle highlighting individually by providing `addHighlight` and `removeHighlight`
-	 * custom properties:
-	 *
-	 *		viewElement.setCustomProperty( 'addHighlight', ( element, descriptor ) => {} );
-	 *		viewElement.setCustomProperty( 'removeHighlight', ( element, descriptorId ) => {} );
-	 *
-	 * {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} will be used to create
-	 * spans over text nodes and also will be provided to `addHighlight` and `removeHighlight` methods
-	 * each time highlight should be set or removed from view elements.
-	 *
-	 * **Note:** When `addHighlight` and `removeHighlight` custom properties are present, converter assumes
-	 * that element itself is taking care of presenting highlight on its child nodes, so it won't convert them.
-	 *
-	 * Highlight descriptor can be provided as plain object:
-	 *
-	 *		buildModelConverter.for( dispatcher ).fromMarker( 'search' ).toHighlight( { class: 'search-highlight' } );
- 	 *
-	 * Also, descriptor creator function can be provided:
-	 *
-	 *		buildModelConverter.for( dispatcher ).fromMarker( 'search:blue' ).toHighlight( data => {
-	 *			const color = data.markerName.split( ':' )[ 1 ];
-	 *
-	 *			return { class: 'search-' + color };
-	 *		} );
-	 *
-	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
-	 * `build-model-converter-non-marker-to-highlight` when trying to convert not from marker.
-	 *
-	 * @param {function|module:engine/conversion/model-to-view-converters~HighlightDescriptor} highlightDescriptor
-	 */
-	toHighlight( highlightDescriptor ) {
-		const priority = this._from.priority === null ? 'normal' : this._from.priority;
-
-		if ( this._from.type != 'marker' ) {
-			/**
-			 * Conversion to a highlight is supported only from model markers.
-			 *
-			 * @error build-model-converter-non-marker-to-highlight
-			 */
-			throw new CKEditorError(
-				'build-model-converter-non-marker-to-highlight: Conversion to a highlight is supported ' +
-				'only from model markers.'
-			);
-		}
-
-		for ( const dispatcher of this._dispatchers ) {
-			// Separate converters for converting texts and elements inside marker's range.
-			dispatcher.on( 'addMarker:' + this._from.name, highlightText( highlightDescriptor ), { priority } );
-			dispatcher.on( 'addMarker:' + this._from.name, highlightElement( highlightDescriptor ), { priority } );
-
-			dispatcher.on( 'removeMarker:' + this._from.name, highlightText( highlightDescriptor ), { priority } );
-			dispatcher.on( 'removeMarker:' + this._from.name, highlightElement( highlightDescriptor ), { priority } );
-
-			dispatcher.on( 'selectionMarker:' + this._from.name, convertSelectionMarker( highlightDescriptor ), { priority } );
-		}
-	}
-
-	/**
-	 * Registers what view attribute will be created by converter. Keep in mind, that only model attribute to
-	 * view attribute conversion is supported.
-	 *
-	 * Method accepts various ways of providing how the view attribute will be created:
-	 *
-	 * * for no passed parameter, attribute key and value will be converted 1-to-1 to view attribute,
-	 * * if you pass one `string`, it will be used as new attribute key while attribute value will be copied,
-	 * * if you pass two `string`s, first one will be used as new attribute key and second one as new attribute value,
-	 * * if you pass a function, it is expected to return an object with `key` and `value` properties representing attribute key and value.
-	 * This function will be passed model attribute value and model attribute key as first two parameters and then
-	 * all dispatcher's
-	 * {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute changeAttribute event}
-	 * parameters.
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromAttribute( 'class' ).toAttribute( '' );
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromAttribute( 'linkTitle' ).toAttribute( 'title' );
-	 *
-	 *		buildModelConverter().for( dispatcher ).fromAttribute( 'highlighted' ).toAttribute( 'style', 'background:yellow' );
-	 *
-	 *		buildModelConverter().for( dispatcher )
-	 *			.fromAttribute( 'theme' )
-	 *			.toAttribute( ( value ) => ( { key: 'class', value: value + '-theme' } ) );
-	 *
-	 * This method creates the converter and adds it as a callback to a proper
-	 * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher conversion dispatcher} event.
-	 *
-	 * @param {String|Function} [keyOrCreator] Attribute key or a creator function.
-	 * @param {*} [value] Attribute value.
-	 */
-	toAttribute( keyOrCreator, value ) {
-		if ( this._from.type != 'attribute' ) {
-			/**
-			 * To-attribute conversion is supported only for model attributes.
-			 *
-			 * @error build-model-converter-element-to-attribute
-			 */
-			throw new CKEditorError( 'build-model-converter-non-attribute-to-attribute: ' +
-				'To-attribute conversion is supported only from model attributes.' );
-		}
-
-		let attributeCreator;
-
-		if ( !keyOrCreator ) {
-			// If `keyOrCreator` is not set, we assume default behavior which is 1:1 attribute re-write.
-			// This is also a default behavior for `setAttribute` converter when no attribute creator is passed.
-			attributeCreator = undefined;
-		} else if ( typeof keyOrCreator == 'string' ) {
-			// `keyOrCreator` is an attribute key.
-
-			if ( value ) {
-				// If value is set, create "dumb" creator that always returns the same object.
-				attributeCreator = function() {
-					return { key: keyOrCreator, value };
-				};
-			} else {
-				// If value is not set, take it from the passed parameter.
-				attributeCreator = function( value ) {
-					return { key: keyOrCreator, value };
-				};
-			}
-		} else {
-			// `keyOrCreator` is an attribute creator function.
-			attributeCreator = keyOrCreator;
-		}
-
-		for ( const dispatcher of this._dispatchers ) {
-			const options = { priority: this._from.priority || 'normal' };
-
-			dispatcher.on( 'addAttribute:' + this._from.key, setAttribute( attributeCreator ), options );
-			dispatcher.on( 'changeAttribute:' + this._from.key, setAttribute( attributeCreator ), options );
-			dispatcher.on( 'removeAttribute:' + this._from.key, removeAttribute( attributeCreator ), options );
-		}
-	}
-}
-
-/**
- * Entry point for model-to-view converters builder. This chainable API makes it easy to create basic, most common
- * model-to-view converters and attach them to provided dispatchers. The method returns an instance of
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder}.
- */
-export default function buildModelConverter() {
-	return new ModelConverterBuilder();
-}
-
-/**
- * @typedef {Object} module:engine/conversion/buildmodelconverter~MarkerViewElementCreatorData
- *
- * @param {String} markerName Marker name.
- * @param {module:engine/model/range~Range} markerRange Marker range.
- * @param {Boolean} isOpening Defines if currently converted element is a beginning or end of the marker range.
- */

+ 0 - 539
packages/ckeditor5-engine/src/conversion/buildviewconverter.js

@@ -1,539 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/conversion/buildviewconverter
- */
-
-import Matcher from '../view/matcher';
-import ModelElement from '../model/element';
-import ModelPosition from '../model/position';
-import modelWriter from '../model/writer';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
-
-/**
- * Provides chainable, high-level API to easily build basic view-to-model converters that are appended to given
- * dispatchers. View-to-model converters are used when external data is added to the editor, i.e. when a user pastes
- * HTML content to the editor. Then, converters are used to translate this structure, possibly removing unknown/incorrect
- * nodes, and add it to the model. Also multiple, different elements might be translated into the same thing in the
- * model, i.e. `<b>` and `<strong>` elements might be converted to `bold` attribute (even though `bold` attribute will
- * be then converted only to `<strong>` tag). Instances of this class are created by
- * {@link module:engine/conversion/buildviewconverter~buildViewConverter}.
- *
- * If you need more complex converters, see {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher},
- * {@link module:engine/conversion/view-to-model-converters}, {@link module:engine/conversion/viewconsumable~ViewConsumable}.
- *
- * Using this API it is possible to create various kind of converters:
- *
- * 1. View element to model element:
- *
- *		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
- *
- * 2. View element to model attribute:
- *
- *		buildViewConverter().for( dispatcher ).fromElement( 'b' ).fromElement( 'strong' ).toAttribute( 'bold', 'true' );
- *
- * 3. View attribute to model attribute:
- *
- *		buildViewConverter().for( dispatcher ).fromAttribute( 'style', { 'font-weight': 'bold' } ).toAttribute( 'bold', 'true' );
- *		buildViewConverter().for( dispatcher )
- *			.fromAttribute( 'class' )
- *			.toAttribute( ( viewElement ) => ( { class: viewElement.getAttribute( 'class' ) } ) );
- *
- * 4. View elements and attributes to model attribute:
- *
- *		buildViewConverter().for( dispatcher )
- *			.fromElement( 'b' ).fromElement( 'strong' ).fromAttribute( 'style', { 'font-weight': 'bold' } )
- *			.toAttribute( 'bold', 'true' );
- *
- * 5. View {@link module:engine/view/matcher~Matcher view element matcher instance} or
- * {@link module:engine/view/matcher~Matcher#add matcher pattern}
- * to model element or attribute:
- *
- *		const matcher = new ViewMatcher();
- *		matcher.add( 'div', { class: 'quote' } );
- *		buildViewConverter().for( dispatcher ).from( matcher ).toElement( 'quote' );
- *
- *		buildViewConverter().for( dispatcher ).from( { name: 'span', class: 'bold' } ).toAttribute( 'bold', 'true' );
- *
- * Note, that converters built using `ViewConverterBuilder` automatically check {@link module:engine/model/schema~Schema schema}
- * if created model structure is valid. If given conversion would be invalid according to schema, it is ignored.
- *
- * It is possible to provide creator functions as parameters for {@link ~ViewConverterBuilder#toElement}
- * and {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder#toAttribute} methods. See their descriptions to learn more.
- *
- * By default, converter will {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consume} every value specified in
- * given `from...` query, i.e. `.from( { name: 'span', class: 'bold' } )` will make converter consume both `span` name
- * and `bold` class. It is possible to change this behavior using {@link ~ViewConverterBuilder#consuming consuming}
- * modifier. The modifier alters the last `fromXXX` query used before it. To learn more about consuming values,
- * see {@link module:engine/conversion/viewconsumable~ViewConsumable}.
- *
- * It is also possible to {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder#withPriority change default priority}
- * of created converters to decide which converter should be fired earlier and which later. This is useful if you provide
- * a general converter but want to provide different converter for a specific-case (i.e. given view element is converted
- * always to given model element, but if it has given class it is converter to other model element). For this,
- * use {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder#withPriority withPriority} modifier. The modifier alters
- * the last `from...` query used before it.
- *
- * Note that `to...` methods are "terminators", which means that should be the last one used in building converter.
- *
- * You can use {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder}
- * to create "opposite" converters - from model to view.
- */
-class ViewConverterBuilder {
-	/**
-	 * Creates `ViewConverterBuilder` with given `dispatchers` registered to it.
-	 */
-	constructor() {
-		/**
-		 * Dispatchers to which converters will be attached.
-		 *
-		 * @type {Array.<module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher>}
-		 * @private
-		 */
-		this._dispatchers = [];
-
-		/**
-		 * Stores "from" queries.
-		 *
-		 * @type {Array}
-		 * @private
-		 */
-		this._from = [];
-	}
-
-	/**
-	 * Set one or more dispatchers which the built converter will be attached to.
-	 *
-	 * @chainable
-	 * @param {...module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher} dispatchers One or more dispatchers.
-	 * @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
-	 */
-	for( ...dispatchers ) {
-		this._dispatchers = dispatchers;
-
-		return this;
-	}
-
-	/**
-	 * Registers what view element should be converted.
-	 *
-	 *		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-	 *
-	 * @chainable
-	 * @param {String} elementName View element name.
-	 * @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
-	 */
-	fromElement( elementName ) {
-		return this.from( { name: elementName } );
-	}
-
-	/**
-	 * Registers what view attribute should be converted.
-	 *
-	 *		buildViewConverter().for( dispatcher ).fromAttribute( 'style', { 'font-weight': 'bold' } ).toAttribute( 'bold', 'true' );
-	 *
-	 * @chainable
-	 * @param {String|RegExp} key View attribute key.
-	 * @param {String|RegExp} [value] View attribute value.
-	 * @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
-	 */
-	fromAttribute( key, value = /.*/ ) {
-		const pattern = {};
-
-		if ( key === 'style' || key === 'class' ) {
-			pattern[ key ] = value;
-		} else {
-			pattern.attribute = {};
-			pattern.attribute[ key ] = value;
-		}
-
-		const matcher = new Matcher( pattern );
-
-		this._from.push( {
-			matcher,
-			consume: false,
-			priority: null,
-			attributeKey: key
-		} );
-
-		return this;
-	}
-
-	/**
-	 * Registers what view pattern should be converted. The method accepts either {@link module:engine/view/matcher~Matcher view matcher}
-	 * or view matcher pattern.
-	 *
-	 *		const matcher = new ViewMatcher();
-	 *		matcher.add( 'div', { class: 'quote' } );
-	 *		buildViewConverter().for( dispatcher ).from( matcher ).toElement( 'quote' );
-	 *
-	 *		buildViewConverter().for( dispatcher ).from( { name: 'span', class: 'bold' } ).toAttribute( 'bold', 'true' );
-	 *
-	 * @chainable
-	 * @param {Object|module:engine/view/matcher~Matcher} matcher View matcher or view matcher pattern.
-	 * @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
-	 */
-	from( matcher ) {
-		if ( !( matcher instanceof Matcher ) ) {
-			matcher = new Matcher( matcher );
-		}
-
-		this._from.push( {
-			matcher,
-			consume: false,
-			priority: null
-		} );
-
-		return this;
-	}
-
-	/**
-	 * Modifies which consumable values will be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}
-	 * by built converter.
-	 * It modifies the last `from...` query. Can be used after each `from...` query in given chain. Useful for providing
-	 * more specific matches.
-	 *
-	 *		// This converter will only handle class bold conversion (to proper attribute) but span element
-	 *		// conversion will have to be done in separate converter.
-	 *		// Without consuming modifier, the converter would consume both class and name, so a converter for
-	 *		// span element would not be fired.
-	 *		buildViewConverter().for( dispatcher )
-	 *			.from( { name: 'span', class: 'bold' } ).consuming( { class: 'bold' } )
-	 *			.toAttribute( 'bold', 'true' } );
-	 *
-	 *		buildViewConverter().for( dispatcher )
-	 *			.fromElement( 'img' ).consuming( { name: true, attribute: [ 'src', 'title' ] } )
-	 *			.toElement( ( viewElement ) => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ),
-	 *			                                                            title: viewElement.getAttribute( 'title' ) } );
-	 *
-	 * **Note:** All and only values from passed object has to be consumable on converted view element. This means that
-	 * using `consuming` method, you can either make looser conversion conditions (like in first example) or tighter
-	 * conversion conditions (like in second example). So, the view element, to be converter, has to match query of
-	 * `from...` method and then have to have enough consumable values to consume.
-	 *
-	 * @see module:engine/conversion/viewconsumable~ViewConsumable
-	 * @chainable
-	 * @param {Object} consume Values to consume.
-	 * @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
-	 */
-	consuming( consume ) {
-		const lastFrom = this._from[ this._from.length - 1 ];
-		lastFrom.consume = consume;
-
-		return this;
-	}
-
-	/**
-	 * Changes default priority for built converter. It modifies the last `from...` query. Can be used after each
-	 * `from...` query in given chain. Useful for overwriting converters. The lower the number, the earlier converter will be fired.
-	 *
-	 *		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-	 *		// Register converter with proper priority, otherwise "p" element would get consumed by first
-	 *		// converter and the second converter would not be fired.
-	 *		buildViewConverter().for( dispatcher )
-	 *			.from( { name: 'p', class: 'custom' } ).withPriority( 9 )
-	 *			.toElement( 'customParagraph' );
-	 *
-	 * **Note:** `ViewConverterBuilder` takes care of applying all `toElement()` conversions before all `toAttribute()`
-	 * conversions. This is done by setting default `toElement()` priority to `normal` and `toAttribute()` priority to `low`.
-	 * It is recommended to set converter priority for `toElement()` around `0` (the value of `normal` priority)
-	 * and `toAttribute()` priority around `-1000` (the value of `low` priority).
-	 * It is important that model elements are created before attributes, otherwise attributes would
-	 * not be applied or other errors may occur.
-	 *
-	 * @chainable
-	 * @param {Number} priority Converter priority.
-	 * @returns {module:engine/conversion/buildviewconverter~ViewConverterBuilder}
-	 */
-	withPriority( priority ) {
-		const lastFrom = this._from[ this._from.length - 1 ];
-		lastFrom.priority = priority;
-
-		return this;
-	}
-
-	/**
-	 * Registers what model element will be created by converter.
-	 *
-	 * Method accepts two ways of providing what kind of model element will be created. You can pass model element
-	 * name as a `string` or a function that will return model element instance. If you provide creator function,
-	 * it will be passed converted view element as first and only parameter.
-	 *
-	 *		buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
-	 *		buildViewConverter().for( dispatcher )
-	 *			.fromElement( 'img' )
-	 *			.toElement( ( viewElement ) => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ) } );
-	 *
-	 * @param {String|Function} element Model element name or model element creator function.
-	 */
-	toElement( element ) {
-		function eventCallbackGen( from ) {
-			return ( evt, data, consumable, conversionApi ) => {
-				// There is one callback for all patterns in the matcher.
-				// This will be usually just one pattern but we support matchers with many patterns too.
-				const matchAll = from.matcher.matchAll( data.input );
-
-				// If there is no match, this callback should not do anything.
-				if ( !matchAll ) {
-					return;
-				}
-
-				// Now, for every match between matcher and actual element, we will try to consume the match.
-				for ( const match of matchAll ) {
-					// Create model element basing on creator function or element name.
-					const modelElement = element instanceof Function ? element( data.input ) : new ModelElement( element );
-
-					// Do not convert if element building function returned falsy value.
-					if ( !modelElement ) {
-						continue;
-					}
-
-					// Check whether generated structure is okay with `Schema`.
-					const keys = Array.from( modelElement.getAttributeKeys() );
-
-					if ( !conversionApi.schema.check( { name: modelElement.name, attributes: keys, inside: data.context } ) ) {
-						continue;
-					}
-
-					// Try to consume appropriate values from consumable values list.
-					if ( !consumable.consume( data.input, from.consume || match.match ) ) {
-						continue;
-					}
-
-					// If everything is fine, we are ready to start the conversion.
-					// Add newly created `modelElement` to the parents stack.
-					data.context.push( modelElement );
-
-					// Convert children of converted view element and append them to `modelElement`.
-					const modelChildren = conversionApi.convertChildren( data.input, consumable, data );
-					const insertPosition = ModelPosition.createAt( modelElement, 'end' );
-					modelWriter.insert( insertPosition, modelChildren );
-
-					// Remove created `modelElement` from the parents stack.
-					data.context.pop();
-
-					// Add `modelElement` as a result.
-					data.output = modelElement;
-
-					// Prevent multiple conversion if there are other correct matches.
-					break;
-				}
-			};
-		}
-
-		this._setCallback( eventCallbackGen, 'normal' );
-	}
-
-	/**
-	 * Registers what model attribute will be created by converter.
-	 *
-	 * Method accepts two ways of providing what kind of model attribute will be created. You can either pass two strings
-	 * representing attribute key and attribute value or a function that returns an object with `key` and `value` properties.
-	 * If you provide creator function, it will be passed converted view element as first and only parameter.
-	 *
-	 *		buildViewConverter().for( dispatcher ).fromAttribute( 'alt' ).toAttribute( 'alt' );
-	 *		buildViewConverter().for( dispatcher ).fromAttribute( 'style', { 'font-weight': 'bold' } ).toAttribute( 'bold', true );
-	 *		buildViewConverter().for( dispatcher )
-	 *			.fromAttribute( 'class' )
-	 *			.toAttribute( ( viewElement ) => ( { key: 'class', value: 'class-' + viewElement.getAttribute( 'class' ) } ) );
-	 *
-	 * @param {String|Function} keyOrCreator Attribute key or a creator function.
-	 * @param {String} [value] Attribute value. Ignored if `keyOrCreator` is not a `string`. If `keyOrCreator` is `string`,
-	 * if `value` is not set, attribute value from converted element will be used.
-	 */
-	toAttribute( keyOrCreator, value ) {
-		function eventCallbackGen( from ) {
-			return ( evt, data, consumable, conversionApi ) => {
-				// There is one callback for all patterns in the matcher.
-				// This will be usually just one pattern but we support matchers with many patterns too.
-				const matchAll = from.matcher.matchAll( data.input );
-
-				// If there is no match, this callback should not do anything.
-				if ( !matchAll ) {
-					return;
-				}
-
-				// Now, for every match between matcher and actual element, we will try to consume the match.
-				for ( const match of matchAll ) {
-					// Try to consume appropriate values from consumable values list.
-					if ( !consumable.consume( data.input, from.consume || match.match ) ) {
-						continue;
-					}
-
-					// Since we are converting to attribute we need an output on which we will set the attribute.
-					// If the output is not created yet, we will create it.
-					if ( !data.output ) {
-						data.output = conversionApi.convertChildren( data.input, consumable, data );
-					}
-
-					// Use attribute creator function, if provided.
-					let attribute;
-
-					if ( keyOrCreator instanceof Function ) {
-						attribute = keyOrCreator( data.input );
-
-						if ( !attribute ) {
-							return;
-						}
-					} else {
-						attribute = {
-							key: keyOrCreator,
-							value: value ? value : data.input.getAttribute( from.attributeKey )
-						};
-					}
-
-					// Set attribute on current `output`. `Schema` is checked inside this helper function.
-					setAttributeOn( data.output, attribute, data, conversionApi );
-
-					// Prevent multiple conversion if there are other correct matches.
-					break;
-				}
-			};
-		}
-
-		this._setCallback( eventCallbackGen, 'low' );
-	}
-
-	/**
-	 * Registers how model element marking marker range will be created by converter.
-	 *
-	 * Created element has to match the following pattern:
-	 *
-	 * 		{ name: '$marker', attribute: { data-name: /^\w/ } }
-	 *
-	 * There are two ways of creating this element:
-	 *
-	 * 1. Makes sure that converted view element will have property `data-name` then converter will
-	 * automatically take this property value. In this case there is no need to provide creator function.
-	 * For the following view:
-	 *
-	 *		<marker data-name="search"></marker>foo<marker data-name="search"></marker>
-	 *
-	 * converter should look like this:
-	 *
-	 *		buildViewConverter().for( dispatcher ).fromElement( 'marker' ).toMarker();
-	 *
-	 * 2. Creates element by creator:
-	 *
-	 * For the following view:
-	 *
-	 * 		<span foo="search"></span>foo<span foo="search"></span>
-	 *
-	 * converter should look like this:
-	 *
-	 * 		buildViewConverter().for( dispatcher ).from( { name: 'span', { attribute: foo: /^\w/ } } ).toMarker( ( data ) => {
-	 * 			return new Element( '$marker', { 'data-name': data.getAttribute( 'foo' ) } );
-	 * 		} );
-	 *
-	 * @param {Function} [creator] Creator function.
-	 */
-	toMarker( creator ) {
-		function eventCallbackGen( from ) {
-			return ( evt, data, consumable ) => {
-				// There is one callback for all patterns in the matcher.
-				// This will be usually just one pattern but we support matchers with many patterns too.
-				const matchAll = from.matcher.matchAll( data.input );
-
-				// If there is no match, this callback should not do anything.
-				if ( !matchAll ) {
-					return;
-				}
-
-				let modelElement;
-
-				// When creator is provided then create model element basing on creator function.
-				if ( creator instanceof Function ) {
-					modelElement = creator( data.input );
-				// When there is no creator then create model element basing on data from view element.
-				} else {
-					modelElement = new ModelElement( '$marker', { 'data-name': data.input.getAttribute( 'data-name' ) } );
-				}
-
-				// Check if model element is correct (has proper name and property).
-				if ( modelElement.name != '$marker' || typeof modelElement.getAttribute( 'data-name' ) != 'string' ) {
-					throw new CKEditorError(
-						'build-view-converter-invalid-marker: Invalid model element to mark marker range.'
-					);
-				}
-
-				// Now, for every match between matcher and actual element, we will try to consume the match.
-				for ( const match of matchAll ) {
-					// Try to consume appropriate values from consumable values list.
-					if ( !consumable.consume( data.input, from.consume || match.match ) ) {
-						continue;
-					}
-
-					data.output = modelElement;
-
-					// Prevent multiple conversion if there are other correct matches.
-					break;
-				}
-			};
-		}
-
-		this._setCallback( eventCallbackGen, 'normal' );
-	}
-
-	/**
-	 * Helper function that uses given callback generator to created callback function and sets it on registered dispatchers.
-	 *
-	 * @param eventCallbackGen
-	 * @param defaultPriority
-	 * @private
-	 */
-	_setCallback( eventCallbackGen, defaultPriority ) {
-		// We will add separate event callback for each registered `from` entry.
-		for ( const from of this._from ) {
-			// We have to figure out event name basing on matcher's patterns.
-			// If there is exactly one pattern and it has `name` property we will used that name.
-			const matcherElementName = from.matcher.getElementName();
-			const eventName = matcherElementName ? 'element:' + matcherElementName : 'element';
-			const eventCallback = eventCallbackGen( from );
-
-			const priority = from.priority === null ? defaultPriority : from.priority;
-
-			// Add event to each registered dispatcher.
-			for ( const dispatcher of this._dispatchers ) {
-				dispatcher.on( eventName, eventCallback, { priority } );
-			}
-		}
-	}
-}
-
-// Helper function that sets given attributes on given `module:engine/model/node~Node` or
-// `module:engine/model/documentfragment~DocumentFragment`.
-function setAttributeOn( toChange, attribute, data, conversionApi ) {
-	if ( isIterable( toChange ) ) {
-		for ( const node of toChange ) {
-			setAttributeOn( node, attribute, data, conversionApi );
-		}
-
-		return;
-	}
-
-	const keys = Array.from( toChange.getAttributeKeys() );
-	keys.push( attribute.key );
-
-	const schemaQuery = {
-		name: toChange.name || '$text',
-		attributes: keys,
-		inside: data.context
-	};
-
-	if ( conversionApi.schema.check( schemaQuery ) ) {
-		toChange.setAttribute( attribute.key, attribute.value );
-	}
-}
-
-/**
- * Entry point for view-to-model converters builder. This chainable API makes it easy to create basic, most common
- * view-to-model converters and attach them to provided dispatchers. The method returns an instance of
- * {@link module:engine/conversion/buildviewconverter~ViewConverterBuilder}.
- */
-export default function buildViewConverter() {
-	return new ViewConverterBuilder();
-}

+ 567 - 0
packages/ckeditor5-engine/src/conversion/conversion.js

@@ -0,0 +1,567 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/conversion/conversion
+ */
+
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+import {
+	downcastElementToElement,
+	downcastAttributeToElement,
+	downcastAttributeToAttribute
+} from './downcast-converters';
+
+import {
+	upcastElementToElement,
+	upcastElementToAttribute,
+	upcastAttributeToAttribute
+} from './upcast-converters';
+
+/**
+ * An utility class that helps organizing dispatchers and adding converters to them.
+ */
+export default class Conversion {
+	/**
+	 * Creates new Conversion instance.
+	 */
+	constructor() {
+		/**
+		 * @private
+		 * @member {Map}
+		 */
+		this._dispatchersGroups = new Map();
+	}
+
+	/**
+	 * Registers one or more converters under given group name. Then, group name can be used to assign a converter
+	 * to multiple dispatchers at once.
+	 *
+	 * If given group name is used for a second time,
+	 * {@link module:utils/ckeditorerror~CKEditorError conversion-register-group-exists} error is thrown.
+	 *
+	 * @param {String} groupName A name for dispatchers group.
+	 * @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers Dispatchers to register
+	 * under given name.
+	 */
+	register( groupName, dispatchers ) {
+		if ( this._dispatchersGroups.has( groupName ) ) {
+			/**
+			 * Trying to register a group name that was already registered.
+			 *
+			 * @error conversion-register-group-exists
+			 */
+			throw new CKEditorError( 'conversion-register-group-exists: Trying to register a group name that was already registered.' );
+		}
+
+		this._dispatchersGroups.set( groupName, dispatchers );
+	}
+
+	/**
+	 * Provides chainable API to assign converters to dispatchers registered under given group name. Converters are added
+	 * by calling `.add()` method of an object returned by this function.
+	 *
+	 *		conversion.for( 'downcast' )
+	 *			.add( conversionHelperA )
+	 *			.add( conversionHelperB );
+	 *
+	 * In above example, `conversionHelperA` and `conversionHelperB` will be called for all dispatchers from `'model'` group.
+	 *
+	 * `.add()` takes exactly one parameter, which is a function. That function should accept one parameter, which
+	 * is a dispatcher instance. The function should add an actual converter to passed dispatcher instance.
+	 *
+	 * Conversion helpers for most common cases are already provided. They are flexible enough to cover most use cases.
+	 * See documentation to learn how they can be configured.
+	 *
+	 * For downcast (model to view conversion), these are:
+	 *
+	 * * {@link module:engine/conversion/downcast-converters~downcastElementToElement downcast element to element converter},
+	 * * {@link module:engine/conversion/downcast-converters~downcastAttributeToElement downcast attribute to element converter},
+	 * * {@link module:engine/conversion/downcast-converters~downcastAttributeToAttribute downcast attribute to attribute converter}.
+	 *
+	 * For upcast (view to model conversion), these are:
+	 *
+	 * * {@link module:engine/conversion/upcast-converters~upcastElementToElement upcast element to element converter},
+	 * * {@link module:engine/conversion/upcast-converters~upcastElementToAttribute upcast attribute to element converter},
+	 * * {@link module:engine/conversion/upcast-converters~upcastAttributeToAttribute upcast attribute to attribute converter}.
+	 *
+	 * An example of using conversion helpers to convert `paragraph` model element to `p` view element (and back):
+	 *
+	 *		// Define conversion configuration - model element 'paragraph' should be converted to view element 'p'.
+	 *		const config = { model: 'paragraph', view: 'p' };
+	 *
+	 *		// Add converters to proper dispatchers using conversion helpers.
+	 *		conversion.for( 'downcast' ).add( downcastElementToElement( config ) );
+	 *		conversion.for( 'upcast' ).add( upcastElementToElement( config ) );
+	 *
+	 * An example of providing custom conversion helper that uses custom converter function:
+	 *
+	 *		// Adding custom `myConverter` converter for 'paragraph' element insertion, with default priority ('normal').
+	 *		conversion.for( 'downcast' ).add( conversion.customConverter( 'insert:paragraph', myConverter ) );
+	 *
+	 * @param {String} groupName Name of dispatchers group to add converters to.
+	 * @returns {Object} Object with `.add()` method, providing a way to add converters.
+	 */
+	for( groupName ) {
+		const dispatchers = this._getDispatchers( groupName );
+
+		return {
+			add( conversionHelper ) {
+				_addToDispatchers( dispatchers, conversionHelper );
+
+				return this;
+			}
+		};
+	}
+
+	/**
+	 * Sets up converters between the model and the view which convert a model element to a view element (and vice versa).
+	 * For example, model `<paragraph>Foo</paragraph>` is `<p>Foo</p>` in the view.
+	 *
+	 *		// Simple conversion from `paragraph` model element to `<p>` view element (and vice versa).
+	 *		conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+	 *
+	 *		// Override other converters by specifying converter definition with higher priority.
+	 *		conversion.elementToElement( { model: 'paragraph', view: 'div', priority: 'high' } );
+	 *
+	 *		// View specified as an object instead of a string.
+	 *		conversion.elementToElement( {
+	 *			model: 'fancyParagraph',
+	 *			view: {
+	 *				name: 'p',
+	 *				class: 'fancy'
+	 *			}
+	 *		} );
+	 *
+	 *		// Use `upcastAlso` to define other view elements that should be also converted to `paragraph` element.
+	 *		conversion.elementToElement( {
+	 *			model: 'paragraph',
+	 *			view: 'p',
+	 *			upcastAlso: [
+	 *				'div',
+	 *				{
+	 *					// Any element with `display: block` style.
+	 *					style: {
+	 *						display: 'block'
+	 *					}
+	 *				}
+	 *			]
+	 *		} );
+	 *
+	 *		// `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
+	 *		conversion.elementToElement( {
+	 *			model: 'heading',
+	 *			view: 'h2',
+	 *			// Convert "headling-like" paragraphs to headings.
+	 *			upcastAlso: viewElement => {
+	 *				const fontSize = viewElement.getStyle( 'font-size' );
+	 *
+	 *				if ( !fontSize ) {
+	 *					return null;
+	 *				}
+	 *
+	 *				const match = fontSize.match( /(\d+)\s*px/ );
+	 *
+	 *				if ( !match ) {
+	 *					return null;
+	 *				}
+	 *
+	 *				const size = Number( match[ 1 ] );
+	 *
+	 *				if ( size > 26 ) {
+	 *					// Returned value be an object with the matched properties.
+	 *					// Those properties will be "consumed" during conversion.
+	 *					// See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
+	 *
+	 *					return { name: true, style: [ 'font-size' ] };
+	 *				}
+	 *
+	 *				return null;
+	 *			}
+	 *		} );
+	 *
+	 * `definition.model` is a `String` with a model element name to converter from/to.
+	 * See {@link module:engine/conversion/conversion~ConverterDefinition} to learn about other parameters.
+	 *
+	 * @param {module:engine/conversion/conversion~ConverterDefinition} definition Converter definition.
+	 */
+	elementToElement( definition ) {
+		// Set up downcast converter.
+		this.for( 'downcast' ).add( downcastElementToElement( definition ) );
+
+		// Set up upcast converter.
+		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
+			this.for( 'upcast' ).add(
+				upcastElementToElement( {
+					model,
+					view,
+					priority: definition.priority
+				} )
+			);
+		}
+	}
+
+	/**
+	 * Sets up converters between the model and the view which convert a model attribute to a view element (and vice versa).
+	 * For example, model text node with data `"Foo"` and `bold` attribute is `<strong>Foo</strong>` in the view.
+	 *
+	 *		// Simple conversion from `bold=true` attribute to `<strong>` view element (and vice versa).
+	 *		conversion.attributeToElement( { model: 'bold', view: 'strong' } );
+	 *
+	 *		// Override other converters by specifying converter definition with higher priority.
+	 *		conversion.attributeToElement( { model: 'bold', view: 'b', priority: 'high' } );
+	 *
+	 *		// View specified as an object instead of a string.
+	 *		conversion.attributeToElement( {
+	 *			model: 'bold',
+	 *			view: {
+	 *				name: 'span',
+	 *				class: 'bold'
+	 *			}
+	 *		} );
+	 *
+	 *		// Use `upcastAlso` to define other view elements that should be also converted to `bold` attribute.
+	 *		conversion.attributeToElement( {
+	 *			model: 'bold',
+	 *			view: 'strong',
+	 *			upcastAlso: [
+	 *				'b',
+	 *				{
+	 *					name: 'span',
+	 *					class: 'bold'
+	 *				},
+	 *				{
+	 *					name: 'span',
+	 *					style: {
+	 *						'font-weight': 'bold'
+	 *					}
+	 *				},
+	 *				viewElement => {
+	 *					const fontWeight = viewElement.getStyle( 'font-weight' );
+	 *
+	 *					if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test() && Number( fontWeight ) > 500 ) {
+	 *						// Returned value be an object with the matched properties.
+	 *						// Those properties will be "consumed" during conversion.
+	 *						// See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
+	 *
+	 *						return {
+	 *							name: true,
+	 *							style: [ 'font-weight' ]
+	 *						};
+	 *					}
+	 *				}
+	 *			]
+	 *		} );
+	 *
+	 *		// Conversion from/to a model attribute key which value is an enum (`fontSize=big|small`).
+	 *		// `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
+	 *		conversion.attributeToElement( {
+	 *			model: {
+	 *				key: 'fontSize',
+	 *				values: [ 'big', 'small' ]
+	 *			},
+	 *			view: {
+	 *				big: {
+	 *					name: 'span',
+	 *					style: {
+	 *						'font-size': '1.2em'
+	 *					}
+	 *				},
+	 *				small: {
+	 *					name: 'span',
+	 *					style: {
+	 *						'font-size': '0.8em'
+	 *					}
+	 *				}
+	 *			},
+	 *			upcastAlso: {
+	 *				big: viewElement => {
+	 *					const fontSize = viewElement.getStyle( 'font-size' );
+	 *
+	 *					if ( !fontSize ) {
+	 *						return null;
+	 *					}
+	 *
+	 *					const match = fontSize.match( /(\d+)\s*px/ );
+	 *
+	 *					if ( !match ) {
+	 *						return null;
+	 *					}
+	 *
+	 *					const size = Number( match[ 1 ] );
+	 *
+	 *					if ( viewElement.is( 'span' ) && size > 10 ) {
+	 *						// Returned value be an object with the matched properties.
+	 *						// Those properties will be "consumed" during conversion.
+	 *						// See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
+	 *
+	 *						return { name: true, style: [ 'font-size' ] };
+	 *					}
+	 *
+	 *					return null;
+	 *				},
+	 *				small: viewElement => {
+	 *					const fontSize = viewElement.getStyle( 'font-size' );
+	 *
+	 *					if ( !fontSize ) {
+	 *						return null;
+	 *					}
+	 *
+	 *					const match = fontSize.match( /(\d+)\s*px/ );
+	 *
+	 *					if ( !match ) {
+	 *						return null;
+	 *					}
+	 *
+	 *					const size = Number( match[ 1 ] );
+	 *
+	 *					if ( viewElement.is( 'span' ) && size < 10 ) {
+	 *						// Returned value be an object with the matched properties.
+	 *						// Those properties will be "consumed" during conversion.
+	 *						// See `engine.view.Matcher~MatcherPattern` and `engine.view.Matcher#match` for more.
+	 *
+	 *						return { name: true, style: [ 'font-size' ] };
+	 *					}
+	 *
+	 *					return null;
+	 *				}
+	 *			}
+	 *		} );
+	 *
+	 * `definition.model` parameter specifies what model attribute should be converted from/to. It can be a `{ key, value }` object
+	 * describing attribute key and value to convert or a `String` specifying just attribute key (then `value` is set to `true`).
+	 * See {@link module:engine/conversion/conversion~ConverterDefinition} to learn about other parameters.
+	 *
+	 * @param {module:engine/conversion/conversion~ConverterDefinition} definition Converter definition.
+	 */
+	attributeToElement( definition ) {
+		// Set up downcast converter.
+		this.for( 'downcast' ).add( downcastAttributeToElement( definition ) );
+
+		// Set up upcast converter.
+		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
+			this.for( 'upcast' ).add(
+				upcastElementToAttribute( {
+					view,
+					model,
+					priority: definition.priority
+				} )
+			);
+		}
+	}
+
+	/**
+	 * Sets up converters between the model and the view which convert a model attribute to a view attribute (and vice versa).
+	 * For example, `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>` (same attribute key and value).
+	 *
+	 *		// Simple conversion from `source` model attribute to `src` view attribute (and vice versa).
+	 *		conversion.attributeToAttribute( { model: 'source', view: 'src' } );
+	 *
+	 *		// Attributes values are strictly specified.
+	 *		conversion.attributeToAttribute( {
+	 *			model: {
+	 *				name: 'image',
+	 *				key: 'aside',
+	 *				values: [ 'aside' ]
+	 *			},
+	 *			view: {
+	 *				aside: {
+	 *					name: 'img',
+	 *					key: 'class',
+	 *					value: [ 'aside', 'half-size' ]
+	 *				}
+	 *			}
+	 *		} );
+	 *
+	 *		// Set style attribute.
+	 *		conversion.attributeToAttribute( {
+	 *			model: {
+	 *				name: 'image',
+	 *				key: 'aside',
+	 *				values: [ 'aside' ]
+	 *			},
+	 *			view: {
+	 *				aside: {
+	 *					name: 'img',
+	 *					key: 'style',
+	 *					value: {
+	 *						float: 'right',
+	 *						width: '50%',
+	 *						margin: '5px'
+	 *					}
+	 *				}
+	 *			}
+	 *		} );
+	 *
+	 *		// Conversion from/to a model attribute key which value is an enum (`align=right|center`).
+	 *		// Use `upcastAlso` to define other view elements that should be also converted to `align=right` attribute.
+	 *		conversion.attributeToAttribute( {
+	 *			model: {
+	 *				key: 'align',
+	 *				values: [ 'right', 'center' ]
+	 *			},
+	 *			view: {
+	 *				right: {
+	 *					key: 'class',
+	 *					value: 'align-right'
+	 *				},
+	 *				center: {
+	 *					key: 'class',
+	 *					value: 'align-center'
+	 *				}
+	 *			},
+	 *			upcastAlso: {
+	 *				right: {
+	 *					style: {
+	 *						'text-align': 'right'
+	 *					}
+	 *				},
+	 *				center: {
+	 *					style: {
+	 *						'text-align': 'center'
+	 *					}
+	 *				}
+	 *			}
+	 *		} );
+	 *
+	 * `definition.model` parameter specifies what model attribute should be converted from/to.
+	 * It can be a `{ key, [ values ], [ name ] }` object or a `String`, which will be treated like `{ key: definition.model }`.
+	 * `key` property is the model attribute key to convert from/to.
+	 * `values` are the possible model attribute values. If `values` is not set, model attribute value will be the same as the
+	 * view attribute value.
+	 * If `name` is set, conversion will be set up only for model elements with the given name.
+	 *
+	 * `definition.view` parameter specifies what view attribute should be converted from/to.
+	 * It can be a `{ key, value, [ name ] }` object or a `String`, which will be treated like `{ key: definition.view }`.
+	 * `key` property is the view attribute key to convert from/to.
+	 * `value` is the view attribute value to convert from/to. If `definition.value` is not set, view attribute value will be
+	 * the same as the model attribute value.
+	 * If `key` is `'class'`, `value` can be a `String` or an array of `String`s.
+	 * If `key` is `'style'`, `value` is an object with key-value pairs.
+	 * In other cases, `value` is a `String`.
+	 * If `name` is set, conversion will be set up only for model elements with the given name.
+	 * If `definition.model.values` is set, `definition.view` is an object which assigns values from `definition.model.values`
+	 * to `{ key, value, [ name ] }` objects.
+	 *
+	 * `definition.upcastAlso` specifies which other matching view elements should be also upcast to given model configuration.
+	 * If `definition.model.values` is set, `definition.upcastAlso` should be an object assigning values from `definition.model.values`
+	 * to {@link module:engine/view/matcher~MatcherPattern}s or arrays of {@link module:engine/view/matcher~MatcherPattern}s.
+	 *
+	 * **Note:** `definition.model` and `definition.view` form should be mirrored, that is the same type of parameters should
+	 * be given in both parameters.
+	 *
+	 * @param {Object} definition Converter definition.
+	 * @param {String|Object} definition.model Model attribute to convert from/to.
+	 * @param {String|Object} definition.view View attribute to convert from/to.
+	 * @param {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [definition.upcastAlso]
+	 * Any view element matching `definition.upcastAlso` will also be converted to the given model attribute. `definition.upcastAlso`
+	 * is used only if `config.model.values` is specified.
+	 */
+	attributeToAttribute( definition ) {
+		// Set up downcast converter.
+		this.for( 'downcast' ).add( downcastAttributeToAttribute( definition ) );
+
+		// Set up upcast converter.
+		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
+			this.for( 'upcast' ).add(
+				upcastAttributeToAttribute( {
+					view,
+					model
+				} )
+			);
+		}
+	}
+
+	/**
+	 * Returns dispatchers registered under given group name.
+	 *
+	 * If given group name has not been registered,
+	 * {@link module:utils/ckeditorerror~CKEditorError conversion-for-unknown-group} error is thrown.
+	 *
+	 * @private
+	 * @param {String} groupName
+	 * @returns {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>}
+	 */
+	_getDispatchers( groupName ) {
+		const dispatchers = this._dispatchersGroups.get( groupName );
+
+		if ( !dispatchers ) {
+			/**
+			 * Trying to add a converter to an unknown dispatchers group.
+			 *
+			 * @error conversion-for-unknown-group
+			 */
+			throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.' );
+		}
+
+		return dispatchers;
+	}
+}
+
+/**
+ * Defines how the model should be converted from/to the view.
+ *
+ * @typedef {Object} module:engine/conversion/conversion~ConverterDefinition
+ *
+ * @property {*} [model] Model conversion definition. Describes model element or model attribute to convert. This parameter differs
+ * for different functions that accepts `ConverterDefinition`. See the description of a function to learn how to set it.
+ * @property {module:engine/view/elementdefinition~ElementDefinition|Object} view Definition of a view element to convert from/to.
+ * If `model` describes multiple values, `view` is an object that assigns those values (`view` object keys) to view element definitions
+ * (`view` object values).
+ * @property {module:engine/view/matcher~MatcherPattern|Array.<module:engine/view/matcher~MatcherPattern>} [upcastAlso]
+ * Any view element matching `upcastAlso` will also be converted to model. If `model` describes multiple values, `upcastAlso`
+ * is an object that assigns those values (`upcastAlso` object keys) to {@link module:engine/view/matcher~MatcherPattern}s
+ * (`upcastAlso` object values).
+ * @property {module:utils/priorities~PriorityString} [priority] Conversion priority.
+ */
+
+// Helper function for `Conversion` `.add()` method.
+//
+// Calls `conversionHelper` on each dispatcher from the group specified earlier in `.for()` call, effectively
+// adding converters to all specified dispatchers.
+//
+// @private
+// @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+// module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers
+// @param {Function} conversionHelper
+function _addToDispatchers( dispatchers, conversionHelper ) {
+	for ( const dispatcher of dispatchers ) {
+		conversionHelper( dispatcher );
+	}
+}
+
+// Helper function that creates a joint array out of an item passed in `definition.view` and items passed in
+// `definition.upcastAlso`.
+//
+// @param {module:engine/conversion/conversion~ConverterDefinition} definition
+// @returns {Array} Array containing view definitions.
+function* _getAllUpcastDefinitions( definition ) {
+	if ( definition.model.values ) {
+		for ( const value of definition.model.values ) {
+			const model = { key: definition.model.key, value };
+			const view = definition.view[ value ];
+			const upcastAlso = definition.upcastAlso ? definition.upcastAlso[ value ] : undefined;
+
+			yield* _getUpcastDefinition( model, view, upcastAlso );
+		}
+	} else {
+		yield* _getUpcastDefinition( definition.model, definition.view, definition.upcastAlso );
+	}
+}
+
+function* _getUpcastDefinition( model, view, upcastAlso ) {
+	yield { model, view };
+
+	if ( upcastAlso ) {
+		upcastAlso = Array.isArray( upcastAlso ) ? upcastAlso : [ upcastAlso ];
+
+		for ( const upcastAlsoItem of upcastAlso ) {
+			yield { model, view: upcastAlsoItem };
+		}
+	}
+}

+ 1036 - 0
packages/ckeditor5-engine/src/conversion/downcast-converters.js

@@ -0,0 +1,1036 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ModelRange from '../model/range';
+import ModelSelection from '../model/selection';
+import ModelElement from '../model/element';
+
+import ViewAttributeElement from '../view/attributeelement';
+import ViewRange from '../view/range';
+import DocumentSelection from '../model/documentselection';
+
+import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
+
+/**
+ * Contains downcast (model to view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
+ *
+ * @module engine/conversion/downcast-converters
+ */
+
+/**
+ * Model element to view element conversion helper.
+ *
+ * This conversion results in creating a view element. For example, model `<paragraph>Foo</paragraph>` becomes `<p>Foo</p>` in the view.
+ *
+ *		downcastElementToElement( { model: 'paragraph', view: 'p' } );
+ *
+ *		downcastElementToElement( { model: 'paragraph', view: 'div', priority: 'high' } );
+ *
+ *		downcastElementToElement( {
+ *			model: 'fancyParagraph',
+ *			view: {
+ *				name: 'p',
+ *				class: 'fancy'
+ *			}
+ *		} );
+ *
+ * 		downcastElementToElement( {
+ * 			model: 'heading',
+ * 			view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
+ * 		} );
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
+ *
+ * @param {Object} config Conversion configuration.
+ * @param {String} config.model Name of the model element to convert.
+ * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element definition or a function
+ * that takes model element and view writer as a parameters and returns a view container element.
+ * @returns {Function} Conversion helper.
+ */
+export function downcastElementToElement( config ) {
+	config = cloneDeep( config );
+
+	config.view = _normalizeToElementConfig( config.view, 'container' );
+
+	return dispatcher => {
+		dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.priority || 'normal' } );
+	};
+}
+
+/**
+ * Model attribute to view element conversion helper.
+ *
+ * This conversion results in wrapping view nodes in a view attribute element. For example, model text node with data
+ * `"Foo"` and `bold` attribute becomes `<strong>Foo</strong>` in the view.
+ *
+ *		downcastAttributeToElement( { model: 'bold', view: 'strong' } );
+ *
+ *		downcastAttributeToElement( { model: 'bold', view: 'b', priority: 'high' } );
+ *
+ *		downcastAttributeToElement( {
+ *			model: 'invert',
+ *			view: {
+ *				name: 'span',
+ *				class: [ 'font-light', 'bg-dark' ]
+ *			}
+ *		} );
+ *
+ *		downcastAttributeToElement( {
+ *			model: {
+ *				key: 'fontSize',
+ *				values: [ 'big', 'small' ]
+ *			},
+ *			view: {
+ *				big: {
+ *					name: 'span',
+ *					style: {
+ *						'font-size': '1.2em'
+ *					}
+ *				},
+ *				small: {
+ *					name: 'span',
+ *					style: {
+ *						'font-size': '0.8em'
+ *					}
+ *				}
+ *			}
+ *		} );
+ *
+ * 		downcastAttributeToElement( {
+ * 			model: 'bold',
+ * 			view: ( modelAttributeValue, viewWriter ) => {
+ * 				return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } );
+ * 			}
+ * 		} );
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
+ *
+ * @param {Object} config Conversion configuration.
+ * @param {String|Object} config.model Key of the attribute to convert from or a `{ key, values }` object. `values` is an array
+ * of `String`s with possible values if the model attribute is enumerable.
+ * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view View element definition or a function
+ * that takes model attribute value and view writer as parameters and returns a view attribute element. If `config.model.values` is
+ * given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
+ * @returns {Function} Conversion helper.
+ */
+export function downcastAttributeToElement( config ) {
+	config = cloneDeep( config );
+
+	const modelKey = config.model.key ? config.model.key : config.model;
+
+	if ( config.model.values ) {
+		for ( const modelValue of config.model.values ) {
+			config.view[ modelValue ] = _normalizeToElementConfig( config.view[ modelValue ], 'attribute' );
+		}
+	} else {
+		config.view = _normalizeToElementConfig( config.view, 'attribute' );
+	}
+
+	const elementCreator = _getFromAttributeCreator( config );
+
+	return dispatcher => {
+		dispatcher.on( 'attribute:' + modelKey, wrap( elementCreator ), { priority: config.priority || 'normal' } );
+	};
+}
+
+/**
+ * Model attribute to view attribute conversion helper.
+ *
+ * This conversion results in adding an attribute on a view node, basing on an attribute from a model node. For example,
+ * `<image src='foo.jpg'></image>` is converted to `<img src='foo.jpg'></img>`.
+ *
+ *		downcastAttributeToAttribute( { model: 'source', view: 'src' } );
+ *
+ *		downcastAttributeToAttribute( { model: 'source', view: 'href', priority: 'high' } );
+ *
+ *		downcastAttributeToAttribute( {
+ *			model: {
+ *				name: 'image',
+ *				key: 'source'
+ *			},
+ *			view: 'src'
+ *		} );
+ *
+ *		downcastAttributeToAttribute( {
+ *			model: {
+ *				name: 'styled',
+ *				values: [ 'dark', 'light' ]
+ *			},
+ *			view: {
+ *				dark: {
+ *					key: 'class',
+ *					value: [ 'styled', 'styled-dark' ]
+ *				},
+ *				light: {
+ *					key: 'class',
+ *					value: [ 'styled', 'styled-light' ]
+ *				}
+ *			}
+ *		} );
+ *
+ *		downcastAttributeToAttribute( {
+ *			model: 'styled',
+ *			view: modelAttributeValue => ( { key: 'class', value: 'styled-' + modelAttributeValue } )
+ *		} );
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
+ *
+ * @param {Object} config Conversion configuration.
+ * @param {String|Object} config.model Key of the attribute to convert from or a `{ key, values, [ name ] }` object describing
+ * the attribute key, possible values and, optionally, an element name to convert from.
+ * @param {String|Object|Function} config.view View attribute key, or a `{ key, value }` object or a function that takes
+ * model attribute value and returns a `{ key, value }` object. If `key` is `'class'`, `value` can be a `String` or an
+ * array of `String`s. If `key` is `'style'`, `value` is an object with key-value pairs. In other cases, `value` is a `String`.
+ * If `config.model.values` is set, `config.view` should be an object assigning values from `config.model.values` to
+ * `{ key, value }` objects or a functions.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
+ * @returns {Function} Conversion helper.
+ */
+export function downcastAttributeToAttribute( config ) {
+	config = cloneDeep( config );
+
+	const modelKey = config.model.key ? config.model.key : config.model;
+	let eventName = 'attribute:' + modelKey;
+
+	if ( config.model.name ) {
+		eventName += ':' + config.model.name;
+	}
+
+	if ( config.model.values ) {
+		for ( const modelValue of config.model.values ) {
+			config.view[ modelValue ] = _normalizeToAttributeConfig( config.view[ modelValue ] );
+		}
+	} else {
+		config.view = _normalizeToAttributeConfig( config.view );
+	}
+
+	const elementCreator = _getFromAttributeCreator( config );
+
+	return dispatcher => {
+		dispatcher.on( eventName, changeAttribute( elementCreator ), { priority: config.priority || 'normal' } );
+	};
+}
+
+/**
+ * Model marker to view element conversion helper.
+ *
+ * This conversion results in creating a view element on the boundaries of the converted marker. If converted marker
+ * is collapsed, only one element is created. For example, model marker set like this `<paragraph>F[oo b]ar</paragraph>`
+ * becomes `<p>F<span data-marker="search"></span>oo b<span data-marker="search"></span>ar</p>` in the view.
+ *
+ *		downcastMarkerToElement( { model: 'search', view: 'marker-search' } );
+ *
+ *		downcastMarkerToElement( { model: 'search', view: 'search-result', priority: 'high' } );
+ *
+ *		downcastMarkerToElement( {
+ *			model: 'search',
+ *			view: {
+ *				name: 'span',
+ *				attribute: {
+ *					'data-marker': 'search'
+ *				}
+ *			}
+ *		} );
+ *
+ * 		downcastMarkerToElement( {
+ * 			model: 'search',
+ * 			view: ( markerData, viewWriter ) => {
+ *	 			return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': markerData.isOpening } );
+ * 			}
+ * 		} );
+ *
+ * If function is passed as `config.view` parameter, it will be used to generate both boundary elements. The function
+ * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}.
+ * The `data` and `conversionApi` objects are passed from
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
+ * `data.isOpening` parameter is passed, which is set to `true` for marker start boundary element, and `false` to
+ * marker end boundary element.
+ *
+ * This kind of conversion is useful for saving data into data base, so it should be used in data conversion pipeline.
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
+ *
+ * @param {Object} config Conversion configuration.
+ * @param {String} config.model Name of the model marker (or model marker group) to convert.
+ * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element definition or a function
+ * that takes model marker data as a parameter and returns view ui element.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
+ * @returns {Function} Conversion helper.
+ */
+export function downcastMarkerToElement( config ) {
+	config = cloneDeep( config );
+
+	config.view = _normalizeToElementConfig( config.view, 'ui' );
+
+	return dispatcher => {
+		dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority: config.priority || 'normal' } );
+		dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority: config.priority || 'normal' } );
+	};
+}
+
+/**
+ * Model marker to highlight conversion helper.
+ *
+ * This conversion results in creating a highlight on view nodes. For this kind of conversion,
+ * {@link module:engine/conversion/downcast-converters~HighlightDescriptor} should be provided.
+ *
+ * For text nodes, a `span` {@link module:engine/view/attributeelement~AttributeElement} is created and it wraps all text nodes
+ * in the converted marker range. For example, model marker set like this `<paragraph>F[oo b]ar</paragraph>` becomes
+ * `<p>F<span class="comment">oo b</span>ar</p>` in the view.
+ *
+ * {@link module:engine/view/containerelement~ContainerElement} may provide custom way of handling highlight. Most often,
+ * the element itself is given classes and attributes described in the highlight descriptor (instead of being wrapped in `span`).
+ * For example, model marker set like this `[<image src="foo.jpg"></image>]` becomes `<img src="foo.jpg" class="comment"></img>`
+ * in the view.
+ *
+ * For container elements, the conversion is two-step. While the converter processes highlight descriptor and passes it
+ * to a container element, it is the container element instance itself which applies values from highlight descriptor.
+ * So, in a sense, converter takes care of stating what should be applied on what, while element decides how to apply that.
+ *
+ *		downcastMarkerToHighlight( { model: 'comment', view: { class: 'comment' } } );
+ *
+ *		downcastMarkerToHighlight( { model: 'comment', view: { class: 'new-comment' }, priority: 'high' } );
+ *
+ * 		downcastMarkerToHighlight( {
+ * 			model: 'comment',
+ * 			view: data => {
+ * 				// Assuming that marker name is in a form of comment:commentType.
+ *	 			const commentType = data.markerName.split( ':' )[ 1 ];
+ *
+ *	 			return {
+ *	 				class: [ 'comment', 'comment-' + commentType ]
+ *	 			};
+ * 			}
+ * 		} );
+ *
+ * If function is passed as `config.view` parameter, it will be used to generate highlight descriptor. The function
+ * receives `data` object as parameter and should return a {@link module:engine/conversion/downcast-converters~HighlightDescriptor}.
+ * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
+ *
+ * @param {Object} config Conversion configuration.
+ * @param {String} config.model Name of the model marker (or model marker group) to convert.
+ * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view Highlight descriptor
+ * which will be used for highlighting or a function that takes model marker data as a parameter and returns a highlight descriptor.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
+ * @returns {Function} Conversion helper.
+ */
+export function downcastMarkerToHighlight( config ) {
+	return dispatcher => {
+		dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority: config.priority || 'normal' } );
+		dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority: config.priority || 'normal' } );
+		dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority: config.priority || 'normal' } );
+	};
+}
+
+// Takes `config.view`, and if it is a {@link module:engine/view/elementdefinition~ElementDefinition}, converts it
+// to a function (because lower level converters accepts only element creator functions).
+//
+// @param {module:engine/view/elementdefinition~ElementDefinition|Function} view View configuration.
+// @param {'container'|'attribute'|'ui'} viewElementType View element type to create.
+// @returns {Function} Element creator function to use in lower level converters.
+function _normalizeToElementConfig( view, viewElementType ) {
+	if ( typeof view == 'function' ) {
+		// If `view` is already a function, don't do anything.
+		return view;
+	}
+
+	return ( modelData, viewWriter ) => _createViewElementFromDefinition( view, viewWriter, viewElementType );
+}
+
+// Creates view element instance from provided viewElementDefinition and class.
+//
+// @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition
+// @param {module:engine/view/writer~Writer} viewWriter
+// @param {'container'|'attribute'|'ui'} viewElementType
+// @returns {module:engine/view/element~Element}
+function _createViewElementFromDefinition( viewElementDefinition, viewWriter, viewElementType ) {
+	if ( typeof viewElementDefinition == 'string' ) {
+		// If `viewElementDefinition` is given as a `String`, normalize it to an object with `name` property.
+		viewElementDefinition = { name: viewElementDefinition };
+	}
+
+	let element;
+
+	if ( viewElementType == 'container' ) {
+		element = viewWriter.createContainerElement( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
+	} else if ( viewElementType == 'attribute' ) {
+		element = viewWriter.createAttributeElement( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
+	} else {
+		// 'ui'.
+		element = viewWriter.createUIElement( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) );
+	}
+
+	if ( viewElementDefinition.style ) {
+		const keys = Object.keys( viewElementDefinition.style );
+
+		for ( const key of keys ) {
+			viewWriter.setStyle( key, viewElementDefinition.style[ key ], element );
+		}
+	}
+
+	if ( viewElementDefinition.class ) {
+		const classes = viewElementDefinition.class;
+
+		if ( typeof classes == 'string' ) {
+			viewWriter.addClass( classes, element );
+		} else {
+			for ( const className of classes ) {
+				viewWriter.addClass( className, element );
+			}
+		}
+	}
+
+	return element;
+}
+
+function _getFromAttributeCreator( config ) {
+	if ( config.model.values ) {
+		return ( modelAttributeValue, viewWriter ) => {
+			const view = config.view[ modelAttributeValue ];
+
+			if ( view ) {
+				return view( modelAttributeValue, viewWriter );
+			}
+
+			return null;
+		};
+	} else {
+		return config.view;
+	}
+}
+
+// Takes config and adds default parameters if they don't exist and normalizes other parameters to be used in downcast converters
+// for generating view attribute.
+//
+// @param {Object} view View configuration.
+function _normalizeToAttributeConfig( view ) {
+	if ( typeof view == 'string' ) {
+		return modelAttributeValue => ( { key: view, value: modelAttributeValue } );
+	} else if ( typeof view == 'object' ) {
+		return () => view;
+	} else {
+		// function.
+		return view;
+	}
+}
+
+/**
+ * Function factory, creates a converter that converts node insertion changes from the model to the view.
+ * Passed function will be provided with all the parameters of the dispatcher's
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert event}.
+ * It's expected that the function returns a {@link module:engine/view/element~Element}.
+ * The result of the function will be inserted to the view.
+ *
+ * The converter automatically consumes corresponding value from consumables list, stops the event (see
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and bind model and view elements.
+ *
+ *		downcastDispatcher.on(
+ *			'insert:myElem',
+ *			insertElement( ( modelItem, viewWriter ) => {
+ *				const text = viewWriter.createText( 'myText' );
+ *				const myElem = viewWriter.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text );
+ *
+ *				// Do something fancy with myElem using `modelItem` or other parameters.
+ *
+ *				return myElem;
+ *			}
+ *		) );
+ *
+ * @param {Function} elementCreator Function returning a view element, which will be inserted.
+ * @returns {Function} Insert element event converter.
+ */
+export function insertElement( elementCreator ) {
+	return ( evt, data, conversionApi ) => {
+		const viewElement = elementCreator( data.item, conversionApi.writer );
+
+		if ( !viewElement ) {
+			return;
+		}
+
+		if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
+			return;
+		}
+
+		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
+
+		conversionApi.mapper.bindElements( data.item, viewElement );
+		conversionApi.writer.insert( viewPosition, viewElement );
+	};
+}
+
+/**
+ * Function factory, creates a default downcast converter for text insertion changes.
+ *
+ * The converter automatically consumes corresponding value from consumables list and stops the event (see
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
+ *
+ *		modelDispatcher.on( 'insert:$text', insertText() );
+ *
+ * @returns {Function} Insert text event converter.
+ */
+export function insertText() {
+	return ( evt, data, conversionApi ) => {
+		if ( !conversionApi.consumable.consume( data.item, 'insert' ) ) {
+			return;
+		}
+
+		const viewWriter = conversionApi.writer;
+		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
+		const viewText = viewWriter.createText( data.item.data );
+
+		viewWriter.insert( viewPosition, viewText );
+	};
+}
+
+/**
+ * Function factory, creates a default downcast converter for node remove changes.
+ *
+ *		modelDispatcher.on( 'remove', remove() );
+ *
+ * @returns {Function} Remove event converter.
+ */
+export function remove() {
+	return ( evt, data, conversionApi ) => {
+		// Find view range start position by mapping model position at which the remove happened.
+		const viewStart = conversionApi.mapper.toViewPosition( data.position );
+
+		const modelEnd = data.position.getShiftedBy( data.length );
+		const viewEnd = conversionApi.mapper.toViewPosition( modelEnd, { isPhantom: true } );
+
+		const viewRange = new ViewRange( viewStart, viewEnd );
+
+		// Trim the range to remove in case some UI elements are on the view range boundaries.
+		const removed = conversionApi.writer.remove( viewRange.getTrimmed() );
+
+		// After the range is removed, unbind all view elements from the model.
+		// Range inside view document fragment is used to unbind deeply.
+		for ( const child of ViewRange.createIn( removed ).getItems() ) {
+			conversionApi.mapper.unbindViewElement( child );
+		}
+	};
+}
+
+/**
+ * Function factory, creates a converter that converts marker adding change to the view ui element.
+ * The view ui element that will be added to the view depends on passed parameter. See {@link ~insertElement}.
+ * In a case of collapsed range element will not wrap range but separate elements will be placed at the beginning
+ * and at the end of the range.
+ *
+ * **Note:** unlike {@link ~insertElement}, the converter does not bind view element to model, because this converter
+ * uses marker as "model source of data". This means that view ui element does not have corresponding model element.
+ *
+ * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning a view element, which
+ * will be inserted.
+ * @returns {Function} Insert element event converter.
+ */
+export function insertUIElement( elementCreator ) {
+	return ( evt, data, conversionApi ) => {
+		// Create two view elements. One will be inserted at the beginning of marker, one at the end.
+		// If marker is collapsed, only "opening" element will be inserted.
+		data.isOpening = true;
+		const viewStartElement = elementCreator( data, conversionApi.writer );
+
+		data.isOpening = false;
+		const viewEndElement = elementCreator( data, conversionApi.writer );
+
+		if ( !viewStartElement || !viewEndElement ) {
+			return;
+		}
+
+		const markerRange = data.markerRange;
+
+		// Marker that is collapsed has consumable build differently that non-collapsed one.
+		// For more information see `addMarker` event description.
+		// If marker's range is collapsed - check if it can be consumed.
+		if ( markerRange.isCollapsed && !conversionApi.consumable.consume( markerRange, evt.name ) ) {
+			return;
+		}
+
+		// If marker's range is not collapsed - consume all items inside.
+		for ( const value of markerRange ) {
+			if ( !conversionApi.consumable.consume( value.item, evt.name ) ) {
+				return;
+			}
+		}
+
+		const mapper = conversionApi.mapper;
+		const viewWriter = conversionApi.writer;
+
+		// Add "opening" element.
+		viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
+
+		// Add "closing" element only if range is not collapsed.
+		if ( !markerRange.isCollapsed ) {
+			viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
+		}
+
+		evt.stop();
+	};
+}
+
+/**
+ * Function factory, creates a default downcast converter for removing {@link module:engine/view/uielement~UIElement ui element}
+ * basing on marker remove change.
+ *
+ * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning
+ * a view ui element, which will be used as a pattern when look for element to remove at the marker start position.
+ * @returns {Function} Remove ui element converter.
+ */
+export function removeUIElement( elementCreator ) {
+	return ( evt, data, conversionApi ) => {
+		// Create two view elements. One will be used to remove "opening element", the other for "closing element".
+		// If marker was collapsed, only "opening" element will be removed.
+		data.isOpening = true;
+		const viewStartElement = elementCreator( data, conversionApi.writer );
+
+		data.isOpening = false;
+		const viewEndElement = elementCreator( data, conversionApi.writer );
+
+		if ( !viewStartElement || !viewEndElement ) {
+			return;
+		}
+
+		const markerRange = data.markerRange;
+		const viewWriter = conversionApi.writer;
+
+		// When removing the ui elements, we map the model range to view twice, because that view range
+		// may change after the first clearing.
+		if ( !markerRange.isCollapsed ) {
+			viewWriter.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewEndElement );
+		}
+
+		// Remove "opening" element.
+		viewWriter.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewStartElement );
+
+		evt.stop();
+	};
+}
+
+/**
+ * Function factory, creates a converter that converts set/change/remove attribute changes from the model to the view.
+ *
+ * Attributes from model are converted to the view element attributes in the view. You may provide a custom function to generate
+ * a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view elements
+ * attributes on 1-to-1 basis.
+ *
+ * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
+ *
+ * The converter automatically consumes corresponding value from consumables list and stops the event (see
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
+ *
+ *		modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( value, data ) => {
+ *			// Change attribute key from `customAttr` to `class` in view.
+ *			const key = 'class';
+ *			let value = data.attributeNewValue;
+ *
+ *			// Force attribute value to 'empty' if the model element is empty.
+ *			if ( data.item.childCount === 0 ) {
+ *				value = 'empty';
+ *			}
+ *
+ *			// Return key-value pair.
+ *			return { key, value };
+ *		} ) );
+ *
+ * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
+ * represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
+ * The function is passed model attribute value as first parameter and additional data about the change as a second parameter.
+ * @returns {Function} Set/change attribute converter.
+ */
+export function changeAttribute( attributeCreator ) {
+	attributeCreator = attributeCreator || ( ( value, data ) => ( { value, key: data.attributeKey } ) );
+
+	return ( evt, data, conversionApi ) => {
+		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
+			return;
+		}
+
+		const viewElement = conversionApi.mapper.toViewElement( data.item );
+		const viewWriter = conversionApi.writer;
+
+		// First remove the old attribute if there was one.
+		const oldAttribute = attributeCreator( data.attributeOldValue, data );
+
+		if ( data.attributeOldValue !== null && oldAttribute ) {
+			if ( oldAttribute.key == 'class' ) {
+				const classes = Array.isArray( oldAttribute.value ) ? oldAttribute.value : [ oldAttribute.value ];
+
+				for ( const className of classes ) {
+					viewWriter.removeClass( className, viewElement );
+				}
+			} else if ( oldAttribute.key == 'style' ) {
+				const keys = Object.keys( oldAttribute.value );
+
+				for ( const key of keys ) {
+					viewWriter.removeStyle( key, viewElement );
+				}
+			} else {
+				viewWriter.removeAttribute( oldAttribute.key, viewElement );
+			}
+		}
+
+		// Then, if conversion was successful, set the new attribute.
+		const newAttribute = attributeCreator( data.attributeNewValue, data );
+
+		if ( data.attributeNewValue !== null && newAttribute ) {
+			if ( newAttribute.key == 'class' ) {
+				const classes = Array.isArray( newAttribute.value ) ? newAttribute.value : [ newAttribute.value ];
+
+				for ( const className of classes ) {
+					viewWriter.addClass( className, viewElement );
+				}
+			} else if ( newAttribute.key == 'style' ) {
+				const keys = Object.keys( newAttribute.value );
+
+				for ( const key of keys ) {
+					viewWriter.setStyle( key, newAttribute.value[ key ], viewElement );
+				}
+			} else {
+				viewWriter.setAttribute( newAttribute.key, newAttribute.value, viewElement );
+			}
+		}
+	};
+}
+
+/**
+ * Function factory, creates a converter that converts set/change/remove attribute changes from the model to the view.
+ * Also can be used to convert selection attributes. In that case, an empty attribute element will be created and the
+ * selection will be put inside it.
+ *
+ * Attributes from model are converted to a view element that will be wrapping those view nodes that are bound to
+ * model elements having given attribute. This is useful for attributes like `bold`, which may be set on text nodes in model
+ * but are represented as an element in the view:
+ *
+ *		[paragraph]              MODEL ====> VIEW        <p>
+ *			|- a {bold: true}                             |- <b>
+ *			|- b {bold: true}                             |   |- ab
+ *			|- c                                          |- c
+ *
+ * Passed `Function` will be provided with attribute value and then all the parameters of the
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute event}.
+ * It's expected that the function returns a {@link module:engine/view/element~Element}.
+ * The result of the function will be the wrapping element.
+ * When provided `Function` does not return element, then will be no conversion.
+ *
+ * The converter automatically consumes corresponding value from consumables list, stops the event (see
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
+ *
+ *		modelDispatcher.on( 'attribute:bold', wrapItem( ( modelAttributeValue, viewWriter ) => {
+ *			return viewWriter.createAttributeElement( 'strong' );
+ *		} );
+ *
+ * @param {Function} elementCreator Function returning a view element, which will be used for wrapping.
+ * @returns {Function} Set/change attribute converter.
+ */
+export function wrap( elementCreator ) {
+	return ( evt, data, conversionApi ) => {
+		// Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
+		// or the attribute was removed.
+		const oldViewElement = elementCreator( data.attributeOldValue, conversionApi.writer );
+
+		// Create node to wrap with.
+		const newViewElement = elementCreator( data.attributeNewValue, conversionApi.writer );
+
+		if ( !oldViewElement && !newViewElement ) {
+			return;
+		}
+
+		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
+			return;
+		}
+
+		const viewWriter = conversionApi.writer;
+		const viewSelection = viewWriter.document.selection;
+
+		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
+			// Selection attribute conversion.
+			viewWriter.wrap( viewSelection.getFirstRange(), newViewElement );
+		} else {
+			// Node attribute conversion.
+			let viewRange = conversionApi.mapper.toViewRange( data.range );
+
+			// First, unwrap the range from current wrapper.
+			if ( data.attributeOldValue !== null && oldViewElement ) {
+				viewRange = viewWriter.unwrap( viewRange, oldViewElement );
+			}
+
+			if ( data.attributeNewValue !== null && newViewElement ) {
+				viewWriter.wrap( viewRange, newViewElement );
+			}
+		}
+	};
+}
+
+/**
+ * Function factory, creates converter that converts text inside marker's range. Converter wraps the text with
+ * {@link module:engine/view/attributeelement~AttributeElement} created from provided descriptor.
+ * See {link module:engine/conversion/downcast-converters~createViewElementFromHighlightDescriptor}.
+ *
+ * Also can be used to convert selection that is inside a marker. In that case, an empty attribute element will be
+ * created and the selection will be put inside it.
+ *
+ * If the highlight descriptor will not provide `priority` property, `10` will be used.
+ *
+ * If the highlight descriptor will not provide `id` property, name of the marker will be used.
+ *
+ * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} highlightDescriptor
+ * @return {Function}
+ */
+export function highlightText( highlightDescriptor ) {
+	return ( evt, data, conversionApi ) => {
+		if ( data.markerRange.isCollapsed ) {
+			return;
+		}
+
+		if ( !( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) && !data.item.is( 'textProxy' ) ) {
+			return;
+		}
+
+		const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
+
+		if ( !descriptor ) {
+			return;
+		}
+
+		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
+			return;
+		}
+
+		const viewElement = createViewElementFromHighlightDescriptor( descriptor );
+		const viewWriter = conversionApi.writer;
+		const viewSelection = viewWriter.document.selection;
+
+		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
+			viewWriter.wrap( viewSelection.getFirstRange(), viewElement, viewSelection );
+		} else {
+			const viewRange = conversionApi.mapper.toViewRange( data.range );
+			viewWriter.wrap( viewRange, viewElement );
+		}
+	};
+}
+
+/**
+ * Converter function factory. Creates a function which applies the marker's highlight to an element inside the marker's range.
+ *
+ * The converter checks if an element has `addHighlight` function stored as
+ * {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight.
+ * In such case converter will consume all element's children, assuming that they were handled by element itself.
+ *
+ * When `addHighlight` custom property is not present, element is not converted in any special way.
+ * This means that converters will proceed to convert element's child nodes.
+ *
+ * If the highlight descriptor will not provide `priority` property, `10` will be used.
+ *
+ * If the highlight descriptor will not provide `id` property, name of the marker will be used.
+ *
+ * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} highlightDescriptor
+ * @return {Function}
+ */
+export function highlightElement( highlightDescriptor ) {
+	return ( evt, data, conversionApi ) => {
+		if ( data.markerRange.isCollapsed ) {
+			return;
+		}
+
+		if ( !( data.item instanceof ModelElement ) ) {
+			return;
+		}
+
+		const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
+
+		if ( !descriptor ) {
+			return;
+		}
+
+		if ( !conversionApi.consumable.test( data.item, evt.name ) ) {
+			return;
+		}
+
+		const viewElement = conversionApi.mapper.toViewElement( data.item );
+
+		if ( viewElement && viewElement.getCustomProperty( 'addHighlight' ) ) {
+			// Consume element itself.
+			conversionApi.consumable.consume( data.item, evt.name );
+
+			// Consume all children nodes.
+			for ( const value of ModelRange.createIn( data.item ) ) {
+				conversionApi.consumable.consume( value.item, evt.name );
+			}
+
+			viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor, conversionApi.writer );
+		}
+	};
+}
+
+/**
+ * Function factory, creates a converter that converts model marker remove to the view.
+ *
+ * Both text nodes and elements are handled by this converter by they are handled a bit differently.
+ *
+ * Text nodes are unwrapped using {@link module:engine/view/attributeelement~AttributeElement} created from provided
+ * highlight descriptor. See {link module:engine/conversion/downcast-converters~highlightDescriptorToAttributeElement}.
+ *
+ * For elements, the converter checks if an element has `removeHighlight` function stored as
+ * {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight.
+ * In such case, children of that element will not be converted.
+ *
+ * When `removeHighlight` is not present, element is not converted in any special way.
+ * Instead converter will proceed to convert element's child nodes.
+ *
+ * If the highlight descriptor will not provide `priority` property, `10` will be used.
+ *
+ * If the highlight descriptor will not provide `id` property, name of the marker will be used.
+ *
+ * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} highlightDescriptor
+ * @return {Function}
+ */
+export function removeHighlight( highlightDescriptor ) {
+	return ( evt, data, conversionApi ) => {
+		// This conversion makes sense only for non-collapsed range.
+		if ( data.markerRange.isCollapsed ) {
+			return;
+		}
+
+		const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
+
+		if ( !descriptor ) {
+			return;
+		}
+
+		const viewRange = conversionApi.mapper.toViewRange( data.markerRange );
+
+		// Retrieve all items in the affected range. We will process them and remove highlight from them appropriately.
+		const items = new Set( viewRange.getItems() );
+
+		// First, iterate through all items and remove highlight from those container elements that have custom highlight handling.
+		for ( const item of items ) {
+			if ( item.is( 'containerElement' ) && item.getCustomProperty( 'removeHighlight' ) ) {
+				item.getCustomProperty( 'removeHighlight' )( item, descriptor.id, conversionApi.writer );
+
+				// If container element had custom handling, remove all it's children from further processing.
+				for ( const descendant of ViewRange.createIn( item ) ) {
+					items.delete( descendant );
+				}
+			}
+		}
+
+		// Then, iterate through all other items. Look for text nodes and unwrap them. Start from the end
+		// to prevent errors when view structure changes when unwrapping (and, for example, some attributes are merged).
+		const viewHighlightElement = createViewElementFromHighlightDescriptor( descriptor );
+		const viewWriter = conversionApi.writer;
+
+		for ( const item of Array.from( items ).reverse() ) {
+			if ( item.is( 'textProxy' ) ) {
+				viewWriter.unwrap( ViewRange.createOn( item ), viewHighlightElement );
+			}
+		}
+	};
+}
+
+// Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter.
+function _prepareDescriptor( highlightDescriptor, data, conversionApi ) {
+	// If passed descriptor is a creator function, call it. If not, just use passed value.
+	const descriptor = typeof highlightDescriptor == 'function' ?
+		highlightDescriptor( data, conversionApi ) :
+		highlightDescriptor;
+
+	if ( !descriptor ) {
+		return null;
+	}
+
+	// Apply default descriptor priority.
+	if ( !descriptor.priority ) {
+		descriptor.priority = 10;
+	}
+
+	// Default descriptor id is marker name.
+	if ( !descriptor.id ) {
+		descriptor.id = data.markerName;
+	}
+
+	return descriptor;
+}
+
+/**
+ * Creates `span` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from information
+ * provided by {@link module:engine/conversion/downcast-converters~HighlightDescriptor} object. If priority
+ * is not provided in descriptor - default priority will be used.
+ *
+ * @param {module:engine/conversion/downcast-converters~HighlightDescriptor} descriptor
+ * @return {module:engine/conversion/downcast-converters~HighlightAttributeElement}
+ */
+export function createViewElementFromHighlightDescriptor( descriptor ) {
+	const viewElement = new HighlightAttributeElement( 'span', descriptor.attributes );
+
+	if ( descriptor.class ) {
+		viewElement._addClass( descriptor.class );
+	}
+
+	if ( descriptor.priority ) {
+		viewElement._priority = descriptor.priority;
+	}
+
+	viewElement._setCustomProperty( 'highlightDescriptorId', descriptor.id );
+
+	return viewElement;
+}
+
+/**
+ * Special kind of {@link module:engine/view/attributeelement~AttributeElement} that is created and used in
+ * marker-to-highlight conversion.
+ *
+ * The difference between `HighlightAttributeElement` and {@link module:engine/view/attributeelement~AttributeElement}
+ * is {@link module:engine/view/attributeelement~AttributeElement#isSimilar} method.
+ *
+ * For `HighlightAttributeElement` it checks just `highlightDescriptorId` custom property, that is set during marker-to-highlight
+ * conversion basing on {@link module:engine/conversion/downcast-converters~HighlightDescriptor} object.
+ * `HighlightAttributeElement`s with same `highlightDescriptorId` property are considered similar.
+ */
+class HighlightAttributeElement extends ViewAttributeElement {
+	isSimilar( otherElement ) {
+		if ( otherElement.is( 'attributeElement' ) ) {
+			return this.getCustomProperty( 'highlightDescriptorId' ) === otherElement.getCustomProperty( 'highlightDescriptorId' );
+		}
+
+		return false;
+	}
+}
+
+/**
+ * Object describing how the marker highlight should be represented in the view.
+ *
+ * Each text node contained in a highlighted range will be wrapped in a `span` {@link module:engine/view/attributeelement~AttributeElement}
+ * with CSS class(es), attributes and priority described by this object.
+ *
+ * Additionally, each {@link module:engine/view/containerelement~ContainerElement} can handle displaying the highlight separately
+ * by providing `addHighlight` and `removeHighlight` custom properties. In this case:
+ *
+ *  * `HighlightDescriptor` object is passed to the `addHighlight` function upon conversion and should be used to apply the highlight to
+ *  the element,
+ *  * descriptor `id` is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight of given
+ *  id from the element.
+ *
+ * @typedef {Object} module:engine/conversion/downcast-converters~HighlightDescriptor
+ *
+ * @property {String|Array.<String>} class CSS class or array of classes to set. If descriptor is used to
+ * create {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those classes will be set
+ * on that {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element,
+ * usually those class will be set on that element, however this depends on how the element converts the descriptor.
+ *
+ * @property {String} [id] Descriptor identifier. If not provided, defaults to converted marker's name.
+ *
+ * @property {Number} [priority] Descriptor priority. If not provided, defaults to `10`. If descriptor is used to create
+ * {@link module:engine/view/attributeelement~AttributeElement}, it will be that element's
+ * {@link module:engine/view/attributeelement~AttributeElement#priority}. If descriptor is applied to an element,
+ * the priority will be used to determine which descriptor is more important.
+ *
+ * @property {Object} [attributes] Attributes to set. If descriptor is used to create
+ * {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those attributes will be set on that
+ * {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element, usually those
+ * attributes will be set on that element, however this depends on how the element converts the descriptor.
+ */

+ 129 - 0
packages/ckeditor5-engine/src/conversion/downcast-selection-converters.js

@@ -0,0 +1,129 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * Contains {@link module:engine/model/selection~Selection model selection} to
+ * {@link module:engine/view/selection~Selection view selection} converters for
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
+ *
+ * @module engine/conversion/downcast-selection-converters
+ */
+
+/**
+ * Function factory, creates a converter that converts non-collapsed {@link module:engine/model/selection~Selection model selection} to
+ * {@link module:engine/view/selection~Selection view selection}. The converter consumes appropriate value from `consumable` object
+ * and maps model positions from selection to view positions.
+ *
+ *		modelDispatcher.on( 'selection', convertRangeSelection() );
+ *
+ * @returns {Function} Selection converter.
+ */
+export function convertRangeSelection() {
+	return ( evt, data, conversionApi ) => {
+		const selection = data.selection;
+
+		if ( selection.isCollapsed ) {
+			return;
+		}
+
+		if ( !conversionApi.consumable.consume( selection, 'selection' ) ) {
+			return;
+		}
+
+		const viewRanges = [];
+
+		for ( const range of selection.getRanges() ) {
+			const viewRange = conversionApi.mapper.toViewRange( range );
+			viewRanges.push( viewRange );
+		}
+
+		conversionApi.writer.setSelection( viewRanges, { backward: selection.isBackward } );
+	};
+}
+
+/**
+ * Function factory, creates a converter that converts collapsed {@link module:engine/model/selection~Selection model selection} to
+ * {@link module:engine/view/selection~Selection view selection}. The converter consumes appropriate value from `consumable` object,
+ * maps model selection position to view position and breaks {@link module:engine/view/attributeelement~AttributeElement attribute elements}
+ * at the selection position.
+ *
+ *		modelDispatcher.on( 'selection', convertCollapsedSelection() );
+ *
+ * Example of view state before and after converting collapsed selection:
+ *
+ *		   <p><strong>f^oo<strong>bar</p>
+ *		-> <p><strong>f</strong>^<strong>oo</strong>bar</p>
+ *
+ * By breaking attribute elements like `<strong>`, selection is in correct element. Then, when selection attribute is
+ * converted, the broken attributes might be merged again, or the position where the selection is may be wrapped
+ * in different, appropriate attribute elements.
+ *
+ * See also {@link module:engine/conversion/downcast-selection-converters~clearAttributes} which does a clean-up
+ * by merging attributes.
+ *
+ * @returns {Function} Selection converter.
+ */
+export function convertCollapsedSelection() {
+	return ( evt, data, conversionApi ) => {
+		const selection = data.selection;
+
+		if ( !selection.isCollapsed ) {
+			return;
+		}
+
+		if ( !conversionApi.consumable.consume( selection, 'selection' ) ) {
+			return;
+		}
+
+		const viewWriter = conversionApi.writer;
+		const modelPosition = selection.getFirstPosition();
+		const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
+		const brokenPosition = viewWriter.breakAttributes( viewPosition );
+
+		viewWriter.setSelection( brokenPosition );
+	};
+}
+
+/**
+ * Function factory, creates a converter that clears artifacts after the previous
+ * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty
+ * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merge sibling attributes at all start and end
+ * positions of all ranges.
+ *
+ *		   <p><strong>^</strong></p>
+ *		-> <p>^</p>
+ *
+ *		   <p><strong>foo</strong>^<strong>bar</strong>bar</p>
+ *		-> <p><strong>foo^bar<strong>bar</p>
+ *
+ *		   <p><strong>foo</strong><em>^</em><strong>bar</strong>bar</p>
+ *		-> <p><strong>foo^bar<strong>bar</p>
+ *
+ * This listener should be assigned before any converter for the new selection:
+ *
+ *		modelDispatcher.on( 'selection', clearAttributes() );
+ *
+ * See {@link module:engine/conversion/downcast-selection-converters~convertCollapsedSelection}
+ * which do the opposite by breaking attributes in the selection position.
+ *
+ * @returns {Function} Selection converter.
+ */
+export function clearAttributes() {
+	return ( evt, data, conversionApi ) => {
+		const viewWriter = conversionApi.writer;
+		const viewSelection = viewWriter.document.selection;
+
+		for ( const range of viewSelection.getRanges() ) {
+			// Not collapsed selection should not have artifacts.
+			if ( range.isCollapsed ) {
+				// Position might be in the node removed by the view writer.
+				if ( range.end.parent.document ) {
+					conversionApi.writer.mergeAttributes( range.start );
+				}
+			}
+		}
+		viewWriter.setSelection( null );
+	};
+}

+ 605 - 0
packages/ckeditor5-engine/src/conversion/downcastdispatcher.js

@@ -0,0 +1,605 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/conversion/downcastdispatcher
+ */
+
+import Consumable from './modelconsumable';
+import Range from '../model/range';
+import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
+
+/**
+ * `DowncastDispatcher` is a central point of downcasting (conversion from model to view), which is a process of reacting to changes
+ * in the model and firing a set of events. Callbacks listening to those events are called converters. Those
+ * converters role is to convert the model changes to changes in view (for example, adding view nodes or
+ * changing attributes on view elements).
+ *
+ * During conversion process, `DowncastDispatcher` fires events, basing on state of the model and prepares
+ * data for those events. It is important to understand that those events are connected with changes done on model,
+ * for example: "node has been inserted" or "attribute has changed". This is in a contrary to upcasting (view to model conversion),
+ * where we convert view state (view nodes) to a model tree.
+ *
+ * The events are prepared basing on a diff created by {@link module:engine/model/differ~Differ Differ}, which buffers them
+ * and then passes to `DowncastDispatcher` as a diff between old model state and new model state.
+ *
+ * Note, that because changes are converted there is a need to have a mapping between model structure and view structure.
+ * To map positions and elements during downcast (model to view conversion) use {@link module:engine/conversion/mapper~Mapper}.
+ *
+ * `DowncastDispatcher` fires following events for model tree changes:
+ *
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert}
+ * if a range of nodes has been inserted to the model tree,
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove remove}
+ * if a range of nodes has been removed from the model tree,
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute}
+ * if attribute has been added, changed or removed from a model node.
+ *
+ * For {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert}
+ * and {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute},
+ * `DowncastDispatcher` generates {@link module:engine/conversion/modelconsumable~ModelConsumable consumables}.
+ * These are used to have a control over which changes has been already consumed. It is useful when some converters
+ * overwrite other or converts multiple changes (for example converts insertion of an element and also converts that
+ * element's attributes during insertion).
+ *
+ * Additionally, `DowncastDispatcher` fires events for {@link module:engine/model/markercollection~Marker marker} changes:
+ *
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} if a marker has been added,
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} if a marker has been removed.
+ *
+ * Note, that changing a marker is done through removing the marker from the old range, and adding on the new range,
+ * so both those events are fired.
+ *
+ * Finally, `DowncastDispatcher` also handles firing events for {@link module:engine/model/selection model selection}
+ * conversion:
+ *
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:selection}
+ * which converts selection from model to view,
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute}
+ * which is fired for every selection attribute,
+ * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}
+ * which is fired for every marker which contains selection.
+ *
+ * Unlike model tree and markers, events for selection are not fired for changes but for selection state.
+ *
+ * When providing custom listeners for `DowncastDispatcher` remember to check whether given change has not been
+ * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} yet.
+ *
+ * When providing custom listeners for `DowncastDispatcher` keep in mind that any callback that had
+ * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} a value from a consumable and
+ * converted the change should also stop the event (for efficiency purposes).
+ *
+ * When providing custom listeners for `DowncastDispatcher` remember to use provided
+ * {@link module:engine/view/writer~Writer view writer} to apply changes to the view document.
+ *
+ * Example of a custom converter for `DowncastDispatcher`:
+ *
+ *		// We will convert inserting "paragraph" model element into the model.
+ *		downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
+ *			// Remember to check whether the change has not been consumed yet and consume it.
+ *			if ( conversionApi.consumable.consume( data.item, 'insert' ) ) {
+ *				return;
+ *			}
+ *
+ *			// Translate position in model to position in view.
+ *			const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
+ *
+ *			// Create <p> element that will be inserted in view at `viewPosition`.
+ *			const viewElement = conversionApi.writer.createContainerElement( 'p' );
+ *
+ *			// Bind the newly created view element to model element so positions will map accordingly in future.
+ *			conversionApi.mapper.bindElements( data.item, viewElement );
+ *
+ *			// Add the newly created view element to the view.
+ *			conversionApi.writer.insert( viewPosition, viewElement );
+ *
+ *			// Remember to stop the event propagation.
+ *			evt.stop();
+ *		} );
+ */
+export default class DowncastDispatcher {
+	/**
+	 * Creates a `DowncastDispatcher` instance.
+	 *
+	 * @param {Object} [conversionApi] Interface passed by dispatcher to the events calls.
+	 */
+	constructor( conversionApi = {} ) {
+		/**
+		 * Interface passed by dispatcher to the events callbacks.
+		 *
+		 * @member {Object}
+		 */
+		this.conversionApi = extend( { dispatcher: this }, conversionApi );
+	}
+
+	/**
+	 * 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/view/writer~Writer} writer View writer that should be used to modify view document.
+	 */
+	convertChanges( differ, writer ) {
+		// Convert changes that happened on model tree.
+		for ( const entry of differ.getChanges() ) {
+			if ( entry.type == 'insert' ) {
+				this.convertInsert( Range.createFromPositionAndShift( entry.position, entry.length ), writer );
+			} else if ( entry.type == 'remove' ) {
+				this.convertRemove( entry.position, entry.length, entry.name, writer );
+			} else {
+				// entry.type == 'attribute'.
+				this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue, writer );
+			}
+		}
+
+		// After the view is updated, convert markers which has changed.
+		for ( const change of differ.getMarkersToAdd() ) {
+			this.convertMarkerAdd( change.name, change.range, writer );
+		}
+	}
+
+	/**
+	 * Starts conversion of a range insertion.
+	 *
+	 * For each node in the range, {@link #event:insert insert event is fired}. For each attribute on each node,
+	 * {@link #event:attribute attribute event is fired}.
+	 *
+	 * @fires insert
+	 * @fires attribute
+	 * @param {module:engine/model/range~Range} range Inserted range.
+	 * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
+	 */
+	convertInsert( range, writer ) {
+		this.conversionApi.writer = writer;
+
+		// Create a list of things that can be consumed, consisting of nodes and their attributes.
+		this.conversionApi.consumable = this._createInsertConsumable( range );
+
+		// Fire a separate insert event for each node and text fragment contained in the range.
+		for ( const value of range ) {
+			const item = value.item;
+			const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
+			const data = {
+				item,
+				range: itemRange
+			};
+
+			this._testAndFire( 'insert', data );
+
+			// Fire a separate addAttribute event for each attribute that was set on inserted items.
+			// This is important because most attributes converters will listen only to add/change/removeAttribute events.
+			// If we would not add this part, attributes on inserted nodes would not be converted.
+			for ( const key of item.getAttributeKeys() ) {
+				data.attributeKey = key;
+				data.attributeOldValue = null;
+				data.attributeNewValue = item.getAttribute( key );
+
+				this._testAndFire( `attribute:${ key }`, data );
+			}
+		}
+
+		this._clearConversionApi();
+	}
+
+	/**
+	 * Fires conversion of a single node removal. Fires {@link #event:remove remove event} with provided data.
+	 *
+	 * @param {module:engine/model/position~Position} position Position from which node was removed.
+	 * @param {Number} length Offset size of removed node.
+	 * @param {String} name Name of removed node.
+	 * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
+	 */
+	convertRemove( position, length, name, writer ) {
+		this.conversionApi.writer = writer;
+
+		this.fire( 'remove:' + name, { position, length }, this.conversionApi );
+
+		this._clearConversionApi();
+	}
+
+	/**
+	 * Starts conversion of attribute change on given `range`.
+	 *
+	 * For each node in the given `range`, {@link #event:attribute attribute event} is fired with the passed data.
+	 *
+	 * @fires attribute
+	 * @param {module:engine/model/range~Range} range Changed range.
+	 * @param {String} key Key of the attribute that has changed.
+	 * @param {*} oldValue Attribute value before the change or `null` if the attribute has not been set before.
+	 * @param {*} newValue New attribute value or `null` if the attribute has been removed.
+	 * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
+	 */
+	convertAttribute( range, key, oldValue, newValue, writer ) {
+		this.conversionApi.writer = writer;
+
+		// Create a list with attributes to consume.
+		this.conversionApi.consumable = this._createConsumableForRange( range, `attribute:${ key }` );
+
+		// Create a separate attribute event for each node in the range.
+		for ( const value of range ) {
+			const item = value.item;
+			const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
+			const data = {
+				item,
+				range: itemRange,
+				attributeKey: key,
+				attributeOldValue: oldValue,
+				attributeNewValue: newValue
+			};
+
+			this._testAndFire( `attribute:${ key }`, data );
+		}
+
+		this._clearConversionApi();
+	}
+
+	/**
+	 * Starts model selection conversion.
+	 *
+	 * Fires events for given {@link module:engine/model/selection~Selection selection} to start selection conversion.
+	 *
+	 * @fires selection
+	 * @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/view/writer~Writer} writer View writer that should be used to modify view document.
+	 */
+	convertSelection( selection, markers, writer ) {
+		const markersAtSelection = Array.from( markers.getMarkersAtPosition( selection.getFirstPosition() ) );
+
+		this.conversionApi.writer = writer;
+		this.conversionApi.consumable = this._createSelectionConsumable( selection, markersAtSelection );
+
+		this.fire( 'selection', { selection }, this.conversionApi );
+
+		if ( !selection.isCollapsed ) {
+			return;
+		}
+
+		for ( const marker of markersAtSelection ) {
+			const markerRange = marker.getRange();
+
+			if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
+				continue;
+			}
+
+			const data = {
+				item: selection,
+				markerName: marker.name,
+				markerRange
+			};
+
+			if ( this.conversionApi.consumable.test( selection, 'addMarker:' + marker.name ) ) {
+				this.fire( 'addMarker:' + marker.name, data, this.conversionApi );
+			}
+		}
+
+		for ( const key of selection.getAttributeKeys() ) {
+			const data = {
+				item: selection,
+				range: selection.getFirstRange(),
+				attributeKey: key,
+				attributeOldValue: null,
+				attributeNewValue: selection.getAttribute( key )
+			};
+
+			// Do not fire event if the attribute has been consumed.
+			if ( this.conversionApi.consumable.test( selection, 'attribute:' + data.attributeKey ) ) {
+				this.fire( 'attribute:' + data.attributeKey, data, this.conversionApi );
+			}
+		}
+
+		this._clearConversionApi();
+	}
+
+	/**
+	 * Converts added marker. Fires {@link #event:addMarker addMarker} event for each item
+	 * in marker's range. If range is collapsed single event is dispatched. See event description for more details.
+	 *
+	 * @fires addMarker
+	 * @param {String} markerName Marker name.
+	 * @param {module:engine/model/range~Range} markerRange Marker range.
+	 * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
+	 */
+	convertMarkerAdd( markerName, markerRange, writer ) {
+		// Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
+		if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
+			return;
+		}
+
+		this.conversionApi.writer = writer;
+
+		// In markers' case, event name == consumable name.
+		const eventName = 'addMarker:' + markerName;
+
+		// When range is collapsed - fire single event with collapsed range in consumable.
+		if ( markerRange.isCollapsed ) {
+			const consumable = new Consumable();
+			consumable.add( markerRange, eventName );
+
+			this.conversionApi.consumable = consumable;
+
+			this.fire( eventName, { markerName, markerRange }, this.conversionApi );
+
+			return;
+		}
+
+		// Create consumable for each item in range.
+		this.conversionApi.consumable = this._createConsumableForRange( markerRange, eventName );
+
+		// Create separate event for each node in the range.
+		for ( const item of markerRange.getItems() ) {
+			// Do not fire event for already consumed items.
+			if ( !this.conversionApi.consumable.test( item, eventName ) ) {
+				continue;
+			}
+
+			const data = { item, range: Range.createOn( item ), markerName, markerRange };
+
+			this.fire( eventName, data, this.conversionApi );
+		}
+
+		this._clearConversionApi();
+	}
+
+	/**
+	 * Fires conversion of marker removal. Fires {@link #event:removeMarker removeMarker} event with provided data.
+	 *
+	 * @fires removeMarker
+	 * @param {String} markerName Marker name.
+	 * @param {module:engine/model/range~Range} markerRange Marker range.
+	 * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
+	 */
+	convertMarkerRemove( markerName, markerRange, writer ) {
+		// Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
+		if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
+			return;
+		}
+
+		this.conversionApi.writer = writer;
+
+		this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi );
+
+		this._clearConversionApi();
+	}
+
+	/**
+	 * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from given range,
+	 * assuming that the range has just been inserted to the model.
+	 *
+	 * @private
+	 * @param {module:engine/model/range~Range} range Inserted range.
+	 * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
+	 */
+	_createInsertConsumable( range ) {
+		const consumable = new Consumable();
+
+		for ( const value of range ) {
+			const item = value.item;
+
+			consumable.add( item, 'insert' );
+
+			for ( const key of item.getAttributeKeys() ) {
+				consumable.add( item, 'attribute:' + key );
+			}
+		}
+
+		return consumable;
+	}
+
+	/**
+	 * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume for given range.
+	 *
+	 * @private
+	 * @param {module:engine/model/range~Range} range Affected range.
+	 * @param {String} type Consumable type.
+	 * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
+	 */
+	_createConsumableForRange( range, type ) {
+		const consumable = new Consumable();
+
+		for ( const item of range.getItems() ) {
+			consumable.add( item, type );
+		}
+
+		return consumable;
+	}
+
+	/**
+	 * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values.
+	 *
+	 * @private
+	 * @param {module:engine/model/selection~Selection} selection Selection to create consumable from.
+	 * @param {Iterable.<module:engine/model/markercollection~Marker>} markers Markers which contains selection.
+	 * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
+	 */
+	_createSelectionConsumable( selection, markers ) {
+		const consumable = new Consumable();
+
+		consumable.add( selection, 'selection' );
+
+		for ( const marker of markers ) {
+			consumable.add( selection, 'addMarker:' + marker.name );
+		}
+
+		for ( const key of selection.getAttributeKeys() ) {
+			consumable.add( selection, 'attribute:' + key );
+		}
+
+		return consumable;
+	}
+
+	/**
+	 * Tests passed `consumable` to check whether given event can be fired and if so, fires it.
+	 *
+	 * @private
+	 * @fires insert
+	 * @fires attribute
+	 * @param {String} type Event type.
+	 * @param {Object} data Event data.
+	 */
+	_testAndFire( type, data ) {
+		if ( !this.conversionApi.consumable.test( data.item, type ) ) {
+			// Do not fire event if the item was consumed.
+			return;
+		}
+
+		const name = data.item.name || '$text';
+
+		this.fire( type + ':' + name, data, this.conversionApi );
+	}
+
+	/**
+	 * Clears conversion API object.
+	 *
+	 * @private
+	 */
+	_clearConversionApi() {
+		delete this.conversionApi.writer;
+		delete this.conversionApi.consumable;
+	}
+
+	/**
+	 * Fired for inserted nodes.
+	 *
+	 * `insert` is a namespace for a class of events. Names of actually called events follow this pattern:
+	 * `insert:name`. `name` is either `'$text'`, when {@link module:engine/model/text~Text a text node} has been inserted,
+	 * or {@link module:engine/model/element~Element#name name} of inserted element.
+	 *
+	 * This way listeners can either listen to a general `insert` event or specific event (for example `insert:paragraph`).
+	 *
+	 * @event insert
+	 * @param {Object} data Additional information about the change.
+	 * @param {module:engine/model/item~Item} data.item Inserted item.
+	 * @param {module:engine/model/range~Range} data.range Range spanning over inserted item.
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
+	 */
+
+	/**
+	 * Fired for removed nodes.
+	 *
+	 * `remove` is a namespace for a class of events. Names of actually called events follow this pattern:
+	 * `remove:name`. `name` is either `'$text'`, when {@link module:engine/model/text~Text a text node} has been removed,
+	 * or the {@link module:engine/model/element~Element#name name} of removed element.
+	 *
+	 * This way listeners can either listen to a general `remove` event or specific event (for example `remove:paragraph`).
+	 *
+	 * @event remove
+	 * @param {Object} data Additional information about the change.
+	 * @param {module:engine/model/position~Position} data.sourcePosition Position from where the range has been removed.
+	 * @param {module:engine/model/range~Range} data.range Removed range (in {@link module:engine/model/document~Document#graveyard
+	 * graveyard root}).
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
+	 */
+
+	/**
+	 * Fired when attribute has been added/changed/removed from a node. Also fired when collapsed model selection attribute is converted.
+	 *
+	 * `attribute` is a namespace for a class of events. Names of actually called events follow this pattern:
+	 * `attribute:attributeKey:name`. `attributeKey` is the key of added/changed/removed attribute.
+	 * `name` is either `'$text'` if change was on {@link module:engine/model/text~Text a text node},
+	 * or the {@link module:engine/model/element~Element#name name} of element which attribute has changed.
+	 *
+	 * This way listeners can either listen to a general `attribute:bold` event or specific event (for example `attribute:src:image`).
+	 *
+	 * @event attribute
+	 * @param {Object} data Additional information about the change.
+	 * @param {module:engine/model/item~Item|module:engine/model/documentselection~DocumentSelection} data.item Changed item
+	 * or converted selection.
+	 * @param {module:engine/model/range~Range} data.range Range spanning over changed item or selection range.
+	 * @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 {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
+	 */
+
+	/**
+	 * Fired for {@link module:engine/model/selection~Selection selection} changes.
+	 *
+	 * @event selection
+	 * @param {module:engine/model/selection~Selection} selection Selection that is converted.
+	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
+	 */
+
+	/**
+	 * Fired when a new marker is added to the model. Also fired when collapsed model selection that is inside marker is converted.
+	 *
+	 * `addMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
+	 * `addMarker:markerName`. By specifying certain marker names, you can make the events even more gradual. For example,
+	 * if markers are named `foo:abc`, `foo:bar`, then it is possible to listen to `addMarker:foo` or `addMarker:foo:abc` and
+	 * `addMarker:foo:bar` events.
+	 *
+	 * 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.
+	 *
+	 * If the marker range is collapsed:
+	 *
+	 * * there is only one event,
+	 * * consumables object 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.
+	 *
+	 * @event addMarker
+	 * @param {Object} data Additional information about the change.
+	 * @param {module:engine/model/item~Item|module:engine/model/selection~Selection} data.item Item inside the new marker or
+	 * the selection that is being converted.
+	 * @param {module:engine/model/range~Range} [data.range] Range spanning over converted item. Available only in marker conversion, if
+	 * 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 {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
+	 */
+
+	/**
+	 * Fired when marker is removed from the model.
+	 *
+	 * `removeMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
+	 * `removeMarker:markerName`. By specifying certain marker names, you can make the events even more gradual. For example,
+	 * if markers are named `foo:abc`, `foo:bar`, then it is possible to listen to `removeMarker:foo` or `removeMarker:foo:abc` and
+	 * `removeMarker:foo:bar` events.
+	 *
+	 * @event removeMarker
+	 * @param {Object} data Additional information about the change.
+	 * @param {module:engine/model/range~Range} data.markerRange Marker range.
+	 * @param {String} data.markerName Marker name.
+	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
+	 */
+}
+
+mix( DowncastDispatcher, EmitterMixin );
+
+// Helper function, checks whether change of `marker` at `modelPosition` should be converted. Marker changes are not
+// converted if they happen inside an element with custom conversion method.
+//
+// @param {module:engine/model/position~Position} modelPosition
+// @param {module:engine/model/markercollection~Marker} marker
+// @param {module:engine/conversion/mapper~Mapper} mapper
+// @returns {Boolean}
+function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
+	const range = marker.getRange();
+	const ancestors = Array.from( modelPosition.getAncestors() );
+	ancestors.shift(); // Remove root element. It cannot be passed to `model.Range#containsItem`.
+	ancestors.reverse();
+
+	const hasCustomHandling = ancestors.some( element => {
+		if ( range.containsItem( element ) ) {
+			const viewElement = mapper.toViewElement( element );
+
+			return !!viewElement.getCustomProperty( 'addHighlight' );
+		}
+	} );
+
+	return !hasCustomHandling;
+}

+ 54 - 28
packages/ckeditor5-engine/src/conversion/mapper.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -110,23 +110,43 @@ export default class Mapper {
 	/**
 	 * Unbinds given {@link module:engine/view/element~Element view element} from the map.
 	 *
+	 * **Note:** view-to-model binding will be removed, if it existed. However, corresponding model-to-view binding
+	 * will be removed only if model element is still bound to passed `viewElement`.
+	 *
+	 * This behavior lets for re-binding model element to another view element without fear of losing the new binding
+	 * when the previously bound view element is unbound.
+	 *
 	 * @param {module:engine/view/element~Element} viewElement View element to unbind.
 	 */
 	unbindViewElement( viewElement ) {
 		const modelElement = this.toModelElement( viewElement );
 
-		this._unbindElements( modelElement, viewElement );
+		this._viewToModelMapping.delete( viewElement );
+
+		if ( this._modelToViewMapping.get( modelElement ) == viewElement ) {
+			this._modelToViewMapping.delete( modelElement );
+		}
 	}
 
 	/**
 	 * Unbinds given {@link module:engine/model/element~Element model element} from the map.
 	 *
+	 * **Note:** model-to-view binding will be removed, if it existed. However, corresponding view-to-model binding
+	 * will be removed only if view element is still bound to passed `modelElement`.
+	 *
+	 * This behavior lets for re-binding view element to another model element without fear of losing the new binding
+	 * when the previously bound model element is unbound.
+	 *
 	 * @param {module:engine/model/element~Element} modelElement Model element to unbind.
 	 */
 	unbindModelElement( modelElement ) {
 		const viewElement = this.toViewElement( modelElement );
 
-		this._unbindElements( modelElement, viewElement );
+		this._modelToViewMapping.delete( modelElement );
+
+		if ( this._viewToModelMapping.get( viewElement ) == modelElement ) {
+			this._viewToModelMapping.delete( viewElement );
+		}
 	}
 
 	/**
@@ -202,12 +222,16 @@ export default class Mapper {
 	 *
 	 * @fires modelToViewPosition
 	 * @param {module:engine/model/position~Position} modelPosition Model position.
+	 * @param {Object} [options] Additional options for position mapping process.
+	 * @param {Boolean} [options.isPhantom=false] Should be set to `true` if the model position to map is pointing to a place
+	 * in model tree which no longer exists. For example, it could be an end of a removed model range.
 	 * @returns {module:engine/view/position~Position} Corresponding view position.
 	 */
-	toViewPosition( modelPosition ) {
+	toViewPosition( modelPosition, options = { isPhantom: false } ) {
 		const data = {
 			modelPosition,
-			mapper: this
+			mapper: this,
+			isPhantom: options.isPhantom
 		};
 
 		this.fire( 'modelToViewPosition', data );
@@ -292,18 +316,6 @@ export default class Mapper {
 		return modelOffset;
 	}
 
-	/**
-	 * Removes binding between given elements.
-	 *
-	 * @private
-	 * @param {module:engine/model/element~Element} modelElement Model element to unbind.
-	 * @param {module:engine/view/element~Element} viewElement View element to unbind.
-	 */
-	_unbindElements( modelElement, viewElement ) {
-		this._viewToModelMapping.delete( viewElement );
-		this._modelToViewMapping.delete( modelElement );
-	}
-
 	/**
 	 * Gets the length of the view element in the model.
 	 *
@@ -460,19 +472,33 @@ export default class Mapper {
 	 *			}
 	 *		} );
 	 *
-	 * **Note:** keep in mind that custom callback provided for this event should use provided `data.modelPosition` only to check
-	 * what is before the position (or position's parent). This is important when model-to-view position mapping is used in
-	 * remove change conversion. Model after the removed position (that is being mapped) does not correspond to view, so it cannot be used:
+	 * **Note:** keep in mind that sometimes a "phantom" model position is being converted. "Phantom" model position is
+	 * a position that points to a non-existing place in model. Such position might still be valid for conversion, though
+	 * (it would point to a correct place in view when converted). One example of such situation is when a range is
+	 * removed from model, there may be a need to map the range's end (which is no longer valid model position). To
+	 * handle such situation, check `data.isPhantom` flag:
+	 *
+	 * 		// Assume that there is "customElement" model element and whenever position is before it, we want to move it
+	 * 		// to the inside of the view element bound to "customElement".
+	 *		mapper.on( 'modelToViewPosition', ( evt, data ) => {
+	 *			if ( data.isPhantom ) {
+	 *				return;
+	 *			}
 	 *
-	 *		// Incorrect:
-	 *		const modelElement = data.modelPosition.nodeAfter;
-	 *		const viewElement = data.mapper.toViewElement( modelElement );
-	 *		// ... Do something with `viewElement` and set `data.viewPosition`.
+	 *			// Below line might crash for phantom position that does not exist in model.
+	 *			const sibling = data.modelPosition.nodeBefore;
+	 *
+	 *			// Check if this is the element we are interested in.
+	 *			if ( !sibling.is( 'customElement' ) ) {
+	 *				return;
+	 *			}
 	 *
-	 *		// Correct:
-	 *		const prevModelElement = data.modelPosition.nodeBefore;
-	 *		const prevViewElement = data.mapper.toViewElement( prevModelElement );
-	 *		// ... Use `prevViewElement` to find correct `data.viewPosition`.
+	 *			const viewElement = data.mapper.toViewElement( sibling );
+	 *
+	 *			data.viewPosition = new ViewPosition( sibling, 0 );
+	 *
+	 *			evt.stop();
+	 *		} );
 	 *
 	 * **Note:** default mapping callback is provided with `low` priority setting and does not cancel the event, so it is possible to
 	 * attach a custom callback after default callback and also use `data.viewPosition` calculated by default callback

+ 0 - 276
packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js

@@ -1,276 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import ViewElement from '../view/element';
-import ViewRange from '../view/range';
-import viewWriter from '../view/writer';
-import { createViewElementFromHighlightDescriptor } from './model-to-view-converters';
-
-/**
- * Contains {@link module:engine/model/selection~Selection model selection} to
- * {@link module:engine/view/selection~Selection view selection} converters for
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}.
- *
- * @module engine/conversion/model-selection-to-view-converters
- */
-
-/**
- * Function factory, creates a converter that converts non-collapsed {@link module:engine/model/selection~Selection model selection} to
- * {@link module:engine/view/selection~Selection view selection}. The converter consumes appropriate value from `consumable` object
- * and maps model positions from selection to view positions.
- *
- *		modelDispatcher.on( 'selection', convertRangeSelection() );
- *
- * @returns {Function} Selection converter.
- */
-export function convertRangeSelection() {
-	return ( evt, data, consumable, conversionApi ) => {
-		const selection = data.selection;
-
-		if ( selection.isCollapsed ) {
-			return;
-		}
-
-		if ( !consumable.consume( selection, 'selection' ) ) {
-			return;
-		}
-
-		conversionApi.viewSelection.removeAllRanges();
-
-		for ( const range of selection.getRanges() ) {
-			const viewRange = conversionApi.mapper.toViewRange( range );
-			conversionApi.viewSelection.addRange( viewRange, selection.isBackward );
-		}
-	};
-}
-
-/**
- * Function factory, creates a converter that converts collapsed {@link module:engine/model/selection~Selection model selection} to
- * {@link module:engine/view/selection~Selection view selection}. The converter consumes appropriate value from `consumable` object,
- * maps model selection position to view position and breaks {@link module:engine/view/attributeelement~AttributeElement attribute elements}
- * at the selection position.
- *
- *		modelDispatcher.on( 'selection', convertCollapsedSelection() );
- *
- * Example of view state before and after converting collapsed selection:
- *
- *		   <p><strong>f^oo<strong>bar</p>
- *		-> <p><strong>f</strong>^<strong>oo</strong>bar</p>
- *
- * By breaking attribute elements like `<strong>`, selection is in correct element. See also complementary
- * {@link module:engine/conversion/model-selection-to-view-converters~convertSelectionAttribute attribute converter}
- * for selection attributes,
- * which wraps collapsed selection into view elements. Those converters together ensure, that selection ends up in
- * appropriate attribute elements.
- *
- * See also {@link module:engine/conversion/model-selection-to-view-converters~clearAttributes} which does a clean-up
- * by merging attributes.
- *
- * @returns {Function} Selection converter.
- */
-export function convertCollapsedSelection() {
-	return ( evt, data, consumable, conversionApi ) => {
-		const selection = data.selection;
-
-		if ( !selection.isCollapsed ) {
-			return;
-		}
-
-		if ( !consumable.consume( selection, 'selection' ) ) {
-			return;
-		}
-
-		const modelPosition = selection.getFirstPosition();
-		const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
-		const brokenPosition = viewWriter.breakAttributes( viewPosition );
-
-		conversionApi.viewSelection.removeAllRanges();
-		conversionApi.viewSelection.addRange( new ViewRange( brokenPosition, brokenPosition ) );
-	};
-}
-
-/**
- * Function factory, creates a converter that converts {@link module:engine/model/selection~Selection model selection} attributes to
- * {@link module:engine/view/attributeelement~AttributeElement view attribute elements}. The converter works only for collapsed selection.
- * The converter consumes appropriate value from `consumable` object, maps model selection position to view position and
- * wraps that position into a view attribute element.
- *
- * The wrapping node depends on passed parameter. If {@link module:engine/view/element~Element} was passed, it will be cloned and
- * the copy will become the wrapping element. If `Function` is provided, it is passed all the parameters of the
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:selectionAttribute selectionAttribute event}.
- * It's expected that the function returns a {@link module:engine/view/attributeelement~AttributeElement}.
- * The result of the function will be the wrapping element.
- *
- *		modelDispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
- *
- *		function styleElementCreator( styleValue ) {
- *			if ( styleValue == 'important' ) {
- *				return new ViewAttributeElement( 'strong', { style: 'text-transform:uppercase;' } );
- *			} else if ( styleValue == 'gold' ) {
- *				return new ViewAttributeElement( 'span', { style: 'color:yellow;' } );
- *			}
- *		}
- *		modelDispatcher.on( 'selectionAttribute:style', convertSelectionAttribute( styleCreator ) );
- *		modelDispatcher.on( 'selection', convertCollapsedSelection() );
- *		modelDispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
- *		modelDispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'strong' ) ) );
- *
- * Example of view states before and after converting collapsed selection:
- *
- *		   <p><em>f^oo</em>bar</p>
- *		-> <p><em>f</em>^<em>oo</em>bar</p>
- *		-> <p><em>f^oo</em>bar</p>
- *
- * Example of view state after converting collapsed selection. The scenario is: selection is inside bold text (`<strong>` element)
- * but it does not have bold attribute itself and has italic attribute instead (let's assume that user turned off bold and turned
- * on italic with selection collapsed):
- *
- *		   <p><strong>f^oo<strong>bar</p>
- *		-> <p><strong>f</strong>^<strong>oo<strong>bar</p>
- *		-> <p><strong>f</strong><em>^</em><strong>oo</strong>bar</p>
- *
- * In first example, nothing has changed, because first `<em>` element got broken by `convertCollapsedSelection()` converter,
- * but then it got wrapped-back by `convertSelectionAttribute()` converter. In second example, notice how `<strong>` element
- * is broken to prevent putting selection in it, since selection has no `bold` attribute.
- *
- * @param {module:engine/view/attributeelement~AttributeElement|Function} elementCreator View element,
- * or function returning a view element, which will be used for wrapping.
- * @returns {Function} Selection converter.
- */
-export function convertSelectionAttribute( elementCreator ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const viewElement = elementCreator instanceof ViewElement ?
-			elementCreator.clone( true ) :
-			elementCreator( data.value, data, data.selection, consumable, conversionApi );
-
-		if ( !viewElement ) {
-			return;
-		}
-
-		const consumableName = 'selectionAttribute:' + data.key;
-
-		wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName );
-	};
-}
-
-/**
- * Performs similar conversion as {@link ~convertSelectionAttribute}, but depends on a marker name of a marker in which
- * collapsed selection is placed.
- *
- *		modelDispatcher.on( 'selectionMarker:searchResult', convertSelectionMarker( { class: 'search' } ) );
- *
- * @see module:engine/conversion/model-selection-to-view-converters~convertSelectionAttribute
- * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor Highlight
- * descriptor object or function returning a descriptor object.
- * @returns {Function} Selection converter.
- */
-export function convertSelectionMarker( highlightDescriptor ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const descriptor = typeof highlightDescriptor == 'function' ?
-			highlightDescriptor( data, consumable, conversionApi ) :
-			highlightDescriptor;
-
-		if ( !descriptor ) {
-			return;
-		}
-
-		if ( !descriptor.id ) {
-			descriptor.id = data.markerName;
-		}
-
-		const viewElement = createViewElementFromHighlightDescriptor( descriptor );
-		const consumableName = 'selectionMarker:' + data.markerName;
-
-		wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName );
-	};
-}
-
-// Helper function for `convertSelectionAttribute` and `convertSelectionMarker`, which perform similar task.
-function wrapCollapsedSelectionPosition( modelSelection, viewSelection, viewElement, consumable, consumableName ) {
-	if ( !modelSelection.isCollapsed ) {
-		return;
-	}
-
-	if ( !consumable.consume( modelSelection, consumableName ) ) {
-		return;
-	}
-
-	let viewPosition = viewSelection.getFirstPosition();
-
-	// This hack is supposed to place attribute element *after* all ui elements if the attribute element would be
-	// the only non-ui child and thus receive a block filler.
-	// This is needed to properly render ui elements. Block filler is a <br /> element. If it is placed before
-	// UI element, the ui element will most probably be incorrectly rendered (in next line). #1072.
-	if ( shouldPushAttributeElement( viewPosition.parent ) ) {
-		viewPosition = viewPosition.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
-	}
-	// End of hack.
-
-	viewPosition = viewWriter.wrapPosition( viewPosition, viewElement );
-
-	viewSelection.removeAllRanges();
-	viewSelection.addRange( new ViewRange( viewPosition, viewPosition ) );
-}
-
-function shouldPushAttributeElement( parent ) {
-	if ( !parent.is( 'element' ) ) {
-		return false;
-	}
-
-	for ( const child of parent.getChildren() ) {
-		if ( !child.is( 'uiElement' ) ) {
-			return false;
-		}
-	}
-
-	return true;
-}
-
-/**
- * Function factory, creates a converter that clears artifacts after the previous
- * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty
- * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merge sibling attributes at all start and end
- * positions of all ranges.
- *
- *		   <p><strong>^</strong></p>
- *		-> <p>^</p>
- *
- *		   <p><strong>foo</strong>^<strong>bar</strong>bar</p>
- *		-> <p><strong>foo^bar<strong>bar</p>
- *
- *		   <p><strong>foo</strong><em>^</em><strong>bar</strong>bar</p>
- *		-> <p><strong>foo^bar<strong>bar</p>
- *
- * This listener should be assigned before any converter for the new selection:
- *
- *		modelDispatcher.on( 'selection', clearAttributes() );
- *
- * See {@link module:engine/conversion/model-selection-to-view-converters~convertCollapsedSelection}
- * which do the opposite by breaking attributes in the selection position.
- *
- * @returns {Function} Selection converter.
- */
-export function clearAttributes() {
-	return ( evt, data, consumable, conversionApi ) => {
-		for ( const range of conversionApi.viewSelection.getRanges() ) {
-			// Not collapsed selection should not have artifacts.
-			if ( range.isCollapsed ) {
-				// Position might be in the node removed by the view writer.
-				if ( range.end.parent.document ) {
-					viewWriter.mergeAttributes( range.start );
-				}
-			}
-		}
-		conversionApi.viewSelection.removeAllRanges();
-	};
-}
-
-/**
- * Function factory, creates a converter that clears fake selection marking after the previous
- * {@link module:engine/model/selection~Selection model selection} conversion.
- */
-export function clearFakeSelection() {
-	return ( evt, data, consumable, conversionApi ) => conversionApi.viewSelection.setFake( false );
-}

+ 0 - 667
packages/ckeditor5-engine/src/conversion/model-to-view-converters.js

@@ -1,667 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import ViewElement from '../view/element';
-import ViewAttributeElement from '../view/attributeelement';
-import ViewText from '../view/text';
-import ViewRange from '../view/range';
-import ViewPosition from '../view/position';
-import ViewTreeWalker from '../view/treewalker';
-import viewWriter from '../view/writer';
-import ModelRange from '../model/range';
-
-/**
- * Contains {@link module:engine/model/model model} to {@link module:engine/view/view view} converters for
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}.
- *
- * @module engine/conversion/model-to-view-converters
- */
-
-/**
- * Function factory, creates a converter that converts node insertion changes from the model to the view.
- * The view element that will be added to the view depends on passed parameter. If {@link module:engine/view/element~Element} was passed,
- * it will be cloned and the copy will be inserted. If `Function` is provided, it is passed all the parameters of the
- * dispatcher's {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert event}.
- * It's expected that the function returns a {@link module:engine/view/element~Element}.
- * The result of the function will be inserted to the view.
- *
- * The converter automatically consumes corresponding value from consumables list, stops the event (see
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements.
- *
- *		modelDispatcher.on( 'insert:paragraph', insertElement( new ViewElement( 'p' ) ) );
- *
- *		modelDispatcher.on(
- *			'insert:myElem',
- *			insertElement( ( data, consumable, conversionApi ) => {
- *				let myElem = new ViewElement( 'myElem', { myAttr: true }, new ViewText( 'myText' ) );
- *
- *				// Do something fancy with myElem using data/consumable/conversionApi ...
- *
- *				return myElem;
- *			}
- *		) );
- *
- * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which
- * will be inserted.
- * @returns {Function} Insert element event converter.
- */
-export function insertElement( elementCreator ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const viewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data, consumable, conversionApi );
-
-		if ( !viewElement ) {
-			return;
-		}
-
-		if ( !consumable.consume( data.item, 'insert' ) ) {
-			return;
-		}
-
-		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
-
-		conversionApi.mapper.bindElements( data.item, viewElement );
-		viewWriter.insert( viewPosition, viewElement );
-	};
-}
-
-/**
- * Function factory, creates a default model-to-view converter for text insertion changes.
- *
- * The converter automatically consumes corresponding value from consumables list and stops the event (see
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
- *
- *		modelDispatcher.on( 'insert:$text', insertText() );
- *
- * @returns {Function} Insert text event converter.
- */
-export function insertText() {
-	return ( evt, data, consumable, conversionApi ) => {
-		if ( !consumable.consume( data.item, 'insert' ) ) {
-			return;
-		}
-
-		const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
-		const viewText = new ViewText( data.item.data );
-
-		viewWriter.insert( viewPosition, viewText );
-	};
-}
-
-/**
- * Function factory, creates a converter that converts marker adding change to the view ui element.
- * The view ui element that will be added to the view depends on passed parameter. See {@link ~insertElement}.
- * In a case of collapsed range element will not wrap range but separate elements will be placed at the beginning
- * and at the end of the range.
- *
- * **Note:** unlike {@link ~insertElement}, the converter does not bind view element to model, because this converter
- * uses marker as "model source of data". This means that view ui element does not have corresponding model element.
- *
- * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning a view element, which
- * will be inserted.
- * @returns {Function} Insert element event converter.
- */
-export function insertUIElement( elementCreator ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		let viewStartElement, viewEndElement;
-
-		if ( elementCreator instanceof ViewElement ) {
-			viewStartElement = elementCreator.clone( true );
-			viewEndElement = elementCreator.clone( true );
-		} else {
-			data.isOpening = true;
-			viewStartElement = elementCreator( data, consumable, conversionApi );
-
-			data.isOpening = false;
-			viewEndElement = elementCreator( data, consumable, conversionApi );
-		}
-
-		if ( !viewStartElement || !viewEndElement ) {
-			return;
-		}
-
-		const markerRange = data.markerRange;
-		const eventName = evt.name;
-
-		// Marker that is collapsed has consumable build differently that non-collapsed one.
-		// For more information see `addMarker` and `removeMarker` events description.
-		// If marker's range is collapsed - check if it can be consumed.
-		if ( markerRange.isCollapsed && !consumable.consume( markerRange, eventName ) ) {
-			return;
-		}
-
-		// If marker's range is not collapsed - consume all items inside.
-		for ( const value of markerRange ) {
-			if ( !consumable.consume( value.item, eventName ) ) {
-				return;
-			}
-		}
-
-		const mapper = conversionApi.mapper;
-
-		viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
-
-		if ( !markerRange.isCollapsed ) {
-			viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
-		}
-	};
-}
-
-/**
- * Function factory, creates a converter that converts set/change attribute changes from the model to the view. Attributes
- * from model are converted to the view element attributes in the view. You may provide a custom function to generate a
- * key-value attribute pair to add/change. If not provided, model attributes will be converted to view elements attributes
- * on 1-to-1 basis.
- *
- * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
- *
- * The converter automatically consumes corresponding value from consumables list and stops the event (see
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
- *
- *		modelDispatcher.on( 'addAttribute:customAttr:myElem', setAttribute( ( data ) => {
- *			// Change attribute key from `customAttr` to `class` in view.
- *			const key = 'class';
- *			let value = data.attributeNewValue;
- *
- *			// Force attribute value to 'empty' if the model element is empty.
- *			if ( data.item.childCount === 0 ) {
- *				value = 'empty';
- *			}
- *
- *			// Return key-value pair.
- *			return { key, value };
- *		} ) );
- *
- * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
- * represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
- * The function is passed all the parameters of the
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute}
- * or {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute} event.
- * @returns {Function} Set/change attribute converter.
- */
-export function setAttribute( attributeCreator ) {
-	attributeCreator = attributeCreator || ( ( value, key ) => ( { value, key } ) );
-
-	return ( evt, data, consumable, conversionApi ) => {
-		if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
-			return;
-		}
-
-		const { key, value } = attributeCreator( data.attributeNewValue, data.attributeKey, data, consumable, conversionApi );
-
-		conversionApi.mapper.toViewElement( data.item ).setAttribute( key, value );
-	};
-}
-
-/**
- * Function factory, creates a converter that converts remove attribute changes from the model to the view. Removes attributes
- * that were converted to the view element attributes in the view. You may provide a custom function to generate a
- * key-value attribute pair to remove. If not provided, model attributes will be removed from view elements on 1-to-1 basis.
- *
- * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
- *
- * **Note:** You can use the same attribute creator as in {@link module:engine/conversion/model-to-view-converters~setAttribute}.
- *
- * The converter automatically consumes corresponding value from consumables list and stops the event (see
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
- *
- *		modelDispatcher.on( 'removeAttribute:customAttr:myElem', removeAttribute( ( data ) => {
- *			// Change attribute key from `customAttr` to `class` in view.
- *			const key = 'class';
- *			let value = data.attributeNewValue;
- *
- *			// Force attribute value to 'empty' if the model element is empty.
- *			if ( data.item.childCount === 0 ) {
- *				value = 'empty';
- *			}
- *
- *			// Return key-value pair.
- *			return { key, value };
- *		} ) );
- *
- * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
- * represents attribute key and attribute value to be removed from {@link module:engine/view/element~Element view element}.
- * The function is passed all the parameters of the
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}
- * or {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute changeAttribute event}.
- * @returns {Function} Remove attribute converter.
- */
-export function removeAttribute( attributeCreator ) {
-	attributeCreator = attributeCreator || ( ( value, key ) => ( { key } ) );
-
-	return ( evt, data, consumable, conversionApi ) => {
-		if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
-			return;
-		}
-
-		const { key } = attributeCreator( data.attributeOldValue, data.attributeKey, data, consumable, conversionApi );
-
-		conversionApi.mapper.toViewElement( data.item ).removeAttribute( key );
-	};
-}
-
-/**
- * Function factory, creates a converter that converts set/change attribute changes from the model to the view. In this case,
- * model attributes are converted to a view element that will be wrapping view nodes which corresponding model nodes had
- * the attribute set. This is useful for attributes like `bold`, which may be set on text nodes in model but are
- * represented as an element in the view:
- *
- *		[paragraph]              MODEL ====> VIEW        <p>
- *			|- a {bold: true}                             |- <b>
- *			|- b {bold: true}                             |   |- ab
- *			|- c                                          |- c
- *
- * The wrapping node depends on passed parameter. If {@link module:engine/view/element~Element} was passed, it will be cloned and
- * the copy will become the wrapping element. If `Function` is provided, it is passed attribute value and then all the parameters of the
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}.
- * It's expected that the function returns a {@link module:engine/view/element~Element}.
- * The result of the function will be the wrapping element.
- * When provided `Function` does not return element, then will be no conversion.
- *
- * The converter automatically consumes corresponding value from consumables list, stops the event (see
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
- *
- *		modelDispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
- *
- * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
- * be used for wrapping.
- * @returns {Function} Set/change attribute converter.
- */
-export function wrapItem( elementCreator ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const viewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data.attributeNewValue, data, consumable, conversionApi );
-
-		if ( !viewElement ) {
-			return;
-		}
-
-		if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
-			return;
-		}
-
-		let viewRange = conversionApi.mapper.toViewRange( data.range );
-
-		// If this is a change event (because old value is not empty) and the creator is a function (so
-		// it may create different view elements basing on attribute value) we have to create
-		// view element basing on old value and unwrap it before wrapping with a newly created view element.
-		if ( data.attributeOldValue !== null && !( elementCreator instanceof ViewElement ) ) {
-			const oldViewElement = elementCreator( data.attributeOldValue, data, consumable, conversionApi );
-			viewRange = viewWriter.unwrap( viewRange, oldViewElement );
-		}
-
-		viewWriter.wrap( viewRange, viewElement );
-	};
-}
-
-/**
- * Function factory, creates a converter that converts remove attribute changes from the model to the view. It assumes, that
- * attributes from model were converted to elements in the view. This converter will unwrap view nodes from corresponding
- * view element if given attribute was removed.
- *
- * The view element type that will be unwrapped depends on passed parameter.
- * If {@link module:engine/view/element~Element} was passed, it will be used to look for similar element in the view for unwrapping.
- * If `Function` is provided, it is passed all the parameters of the
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}.
- * It's expected that the function returns a {@link module:engine/view/element~Element}.
- * The result of the function will be used to look for similar element in the view for unwrapping.
- *
- * The converter automatically consumes corresponding value from consumables list, stops the event (see
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements.
- *
- *		modelDispatcher.on( 'removeAttribute:bold', unwrapItem( new ViewAttributeElement( 'strong' ) ) );
- *
- * @see module:engine/conversion/model-to-view-converters~wrapItem
- * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
- * be used for unwrapping.
- * @returns {Function} Remove attribute converter.
- */
-export function unwrapItem( elementCreator ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const viewElement = ( elementCreator instanceof ViewElement ) ?
-			elementCreator.clone( true ) :
-			elementCreator( data.attributeOldValue, data, consumable, conversionApi );
-
-		if ( !viewElement ) {
-			return;
-		}
-
-		if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
-			return;
-		}
-
-		const viewRange = conversionApi.mapper.toViewRange( data.range );
-
-		viewWriter.unwrap( viewRange, viewElement );
-	};
-}
-
-/**
- * Function factory, creates a default model-to-view converter for node remove changes.
- *
- *		modelDispatcher.on( 'remove', remove() );
- *
- * @returns {Function} Remove event converter.
- */
-export function remove() {
-	return ( evt, data, consumable, conversionApi ) => {
-		if ( !consumable.consume( data.item, 'remove' ) ) {
-			return;
-		}
-
-		// We cannot map non-existing positions from model to view. Since a range was removed
-		// from the model, we cannot recreate that range and map it to view, because
-		// end of that range is incorrect.
-		// Instead we will use `data.sourcePosition` as this is the last correct model position and
-		// it is a position before the removed item. Then, we will calculate view range to remove "manually".
-		let viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition );
-		let viewRange;
-
-		if ( data.item.is( 'element' ) ) {
-			// Note: in remove conversion we cannot use model-to-view element mapping because `data.item` may be
-			// already mapped to another element (this happens when move change is converted).
-			// In this case however, `viewPosition` is the position before view element that corresponds to removed model element.
-			//
-			// First, fix the position. Traverse the tree forward until the container element is found. The `viewPosition`
-			// may be before a ui element, before attribute element or at the end of text element.
-			viewPosition = viewPosition.getLastMatchingPosition( value => !value.item.is( 'containerElement' ) );
-
-			if ( viewPosition.parent.is( 'text' ) && viewPosition.isAtEnd ) {
-				viewPosition = ViewPosition.createAfter( viewPosition.parent );
-			}
-
-			viewRange = ViewRange.createOn( viewPosition.nodeAfter );
-		} else {
-			// If removed item is a text node, we need to traverse view tree to find the view range to remove.
-			// Range to remove will start `viewPosition` and should contain amount of characters equal to the amount of removed characters.
-			const viewRangeEnd = _shiftViewPositionByCharacters( viewPosition, data.item.offsetSize );
-			viewRange = new ViewRange( viewPosition, viewRangeEnd );
-		}
-
-		// Trim the range to remove in case some UI elements are on the view range boundaries.
-		viewWriter.remove( viewRange.getTrimmed() );
-
-		// Unbind this element only if it was moved to graveyard.
-		// The dispatcher#remove event will also be fired if the element was moved to another place (remove+insert are fired).
-		// Let's say that <b> is moved before <a>. The view will be changed like this:
-		//
-		// 1) start:    <a></a><b></b>
-		// 2) insert:   <b (new)></b><a></a><b></b>
-		// 3) remove:   <b (new)></b><a></a>
-		//
-		// If we'll unbind the <b> element in step 3 we'll also lose binding of the <b (new)> element in the view,
-		// because unbindModelElement() cancels both bindings – (model <b> => view <b (new)>) and (view <b (new)> => model <b>).
-		// We can't lose any of these.
-		//
-		// See #847.
-		if ( data.item.root.rootName == '$graveyard' ) {
-			conversionApi.mapper.unbindModelElement( data.item );
-		}
-	};
-}
-
-/**
- * Function factory, creates converter that converts all texts inside marker's range. Converter wraps each text with
- * {@link module:engine/view/attributeelement~AttributeElement} created from provided descriptor.
- * See {link module:engine/conversion/model-to-view-converters~highlightDescriptorToAttributeElement}.
- *
- * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor
- * @return {Function}
- */
-export function highlightText( highlightDescriptor ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const descriptor = typeof highlightDescriptor == 'function' ?
-			highlightDescriptor( data, consumable, conversionApi ) :
-			highlightDescriptor;
-
-		const modelItem = data.item;
-
-		if ( !descriptor || data.markerRange.isCollapsed || !modelItem.is( 'textProxy' ) ) {
-			return;
-		}
-
-		if ( !consumable.consume( modelItem, evt.name ) ) {
-			return;
-		}
-
-		if ( !descriptor.id ) {
-			descriptor.id = data.markerName;
-		}
-
-		const viewElement = createViewElementFromHighlightDescriptor( descriptor );
-		const viewRange = conversionApi.mapper.toViewRange( data.range );
-
-		if ( evt.name.split( ':' )[ 0 ] == 'addMarker' ) {
-			viewWriter.wrap( viewRange, viewElement );
-		} else {
-			viewWriter.unwrap( viewRange, viewElement );
-		}
-	};
-}
-
-/**
- * Converter function factory. Creates a function which applies the marker's highlight to all elements inside a marker's range.
- * The converter checks if an element has the addHighlight and removeHighlight functions stored as
- * {@link module:engine/view/element~Element#setCustomProperty custom properties} and if so use them to apply the highlight.
- * In such case converter will consume all element's children, assuming that they were handled by element itself.
- * If the highlight descriptor will not provide priority, priority `10` will be used as default, to be compliant with
- * {@link module:engine/conversion/model-to-view-converters~highlightText} method which uses default priority of
- * {@link module:engine/view/attributeelement~AttributeElement}.
- *
- * If the highlight descriptor will not provide `id` property, name of the marker will be used.
- * When `addHighlight` and `removeHighlight` custom properties are not present, element is not converted
- * in any special way. This means that converters will proceed to convert element's child nodes.
- *
- * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor
- * @return {Function}
- */
-export function highlightElement( highlightDescriptor ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const descriptor = typeof highlightDescriptor == 'function' ?
-			highlightDescriptor( data, consumable, conversionApi ) :
-			highlightDescriptor;
-
-		const modelItem = data.item;
-
-		if ( !descriptor || data.markerRange.isCollapsed || !modelItem.is( 'element' ) ) {
-			return;
-		}
-
-		if ( !consumable.test( data.item, evt.name ) ) {
-			return;
-		}
-
-		if ( !descriptor.priority ) {
-			descriptor.priority = 10;
-		}
-
-		if ( !descriptor.id ) {
-			descriptor.id = data.markerName;
-		}
-
-		const viewElement = conversionApi.mapper.toViewElement( modelItem );
-		const addMarker = evt.name.split( ':' )[ 0 ] == 'addMarker';
-		const highlightHandlingMethod = addMarker ? 'addHighlight' : 'removeHighlight';
-
-		if ( viewElement && viewElement.getCustomProperty( highlightHandlingMethod ) ) {
-			// Consume element itself.
-			consumable.consume( data.item, evt.name );
-
-			// Consume all children nodes.
-			for ( const value of ModelRange.createIn( modelItem ) ) {
-				consumable.consume( value.item, evt.name );
-			}
-
-			viewElement.getCustomProperty( highlightHandlingMethod )( viewElement, addMarker ? descriptor : descriptor.id );
-		}
-	};
-}
-
-/**
- * Function factory, creates a default model-to-view converter for removing {@link module:engine/view/uielement~UIElement ui element}
- * basing on marker remove change.
- *
- * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning
- * a view ui element, which will be used as a pattern when look for element to remove at the marker start position.
- * @returns {Function} Remove ui element converter.
- */
-export function removeUIElement( elementCreator ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		let viewStartElement, viewEndElement;
-
-		if ( elementCreator instanceof ViewElement ) {
-			viewStartElement = elementCreator.clone( true );
-			viewEndElement = elementCreator.clone( true );
-		} else {
-			data.isOpening = true;
-			viewStartElement = elementCreator( data, consumable, conversionApi );
-
-			data.isOpening = false;
-			viewEndElement = elementCreator( data, consumable, conversionApi );
-		}
-
-		if ( !viewStartElement || !viewEndElement ) {
-			return;
-		}
-
-		const markerRange = data.markerRange;
-		const eventName = evt.name;
-
-		// If marker's range is collapsed - check if it can be consumed.
-		if ( markerRange.isCollapsed && !consumable.consume( markerRange, eventName ) ) {
-			return;
-		}
-
-		// Check if all items in the range can be consumed, and consume them.
-		for ( const value of markerRange ) {
-			if ( !consumable.consume( value.item, eventName ) ) {
-				return;
-			}
-		}
-
-		const viewRange = conversionApi.mapper.toViewRange( markerRange );
-
-		// First remove closing element.
-		viewWriter.clear( viewRange.getEnlarged(), viewEndElement );
-
-		// If closing and opening elements are not the same then remove opening element.
-		if ( !viewStartElement.isSimilar( viewEndElement ) ) {
-			viewWriter.clear( viewRange.getEnlarged(), viewStartElement );
-		}
-	};
-}
-
-/**
- * Returns the consumable type that is to be consumed in an event, basing on that event name.
- *
- * @param {String} evtName Event name.
- * @returns {String} Consumable type.
- */
-export function eventNameToConsumableType( evtName ) {
-	const parts = evtName.split( ':' );
-
-	return parts[ 0 ] + ':' + parts[ 1 ];
-}
-
-// Helper function that shifts given view `position` in a way that returned position is after `howMany` characters compared
-// to the original `position`.
-// Because in view there might be view ui elements splitting text nodes, we cannot simply use `ViewPosition#getShiftedBy()`.
-function _shiftViewPositionByCharacters( position, howMany ) {
-	// Create a walker that will walk the view tree starting from given position and walking characters one-by-one.
-	const walker = new ViewTreeWalker( { startPosition: position, singleCharacters: true } );
-	// We will count visited characters and return the position after `howMany` characters.
-	let charactersFound = 0;
-
-	for ( const value of walker ) {
-		if ( value.type == 'text' ) {
-			charactersFound++;
-
-			if ( charactersFound == howMany ) {
-				return walker.position;
-			}
-		}
-	}
-}
-
-/**
- * Creates `span` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from information
- * provided by {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object. If priority
- * is not provided in descriptor - default priority will be used.
- *
- * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} descriptor
- * @return {module:engine/conversion/model-to-view-converters~HighlightAttributeElement}
- */
-export function createViewElementFromHighlightDescriptor( descriptor ) {
-	const viewElement = new HighlightAttributeElement( 'span', descriptor.attributes );
-
-	if ( descriptor.class ) {
-		const cssClasses = Array.isArray( descriptor.class ) ? descriptor.class : [ descriptor.class ];
-		viewElement.addClass( ...cssClasses );
-	}
-
-	if ( descriptor.priority ) {
-		viewElement.priority = descriptor.priority;
-	}
-
-	viewElement.setCustomProperty( 'highlightDescriptorId', descriptor.id );
-
-	return viewElement;
-}
-
-/**
- * Special kind of {@link module:engine/view/attributeelement~AttributeElement} that is created and used in
- * marker-to-highlight conversion.
- *
- * The difference between `HighlightAttributeElement` and {@link module:engine/view/attributeelement~AttributeElement}
- * is {@link module:engine/view/attributeelement~AttributeElement#isSimilar} method.
- *
- * For `HighlightAttributeElement` it checks just `highlightDescriptorId` custom property, that is set during marker-to-highlight
- * conversion basing on {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object.
- * `HighlightAttributeElement`s with same `highlightDescriptorId` property are considered similar.
- */
-class HighlightAttributeElement extends ViewAttributeElement {
-	isSimilar( otherElement ) {
-		if ( otherElement.is( 'attributeElement' ) ) {
-			return this.getCustomProperty( 'highlightDescriptorId' ) === otherElement.getCustomProperty( 'highlightDescriptorId' );
-		}
-
-		return false;
-	}
-}
-
-/**
- * Object describing how the content highlight should be created in the view.
- *
- * Each text node contained in the highlight will be wrapped with `span` element with CSS class(es), attributes and priority
- * described by this object.
- *
- * Each element can handle displaying the highlight separately by providing `addHighlight` and `removeHighlight` custom
- * properties:
- *  * `HighlightDescriptor` is passed to the `addHighlight` function upon conversion and should be used to apply the highlight to
- *  the element,
- *  * descriptor id is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight of given
- *  id from the element.
- *
- * @typedef {Object} module:engine/conversion/model-to-view-converters~HighlightDescriptor
- *
- * @property {String|Array.<String>} class CSS class or array of classes to set. If descriptor is used to
- * create {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those classes will be set
- * on that {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element,
- * usually those class will be set on that element, however this depends on how the element converts the descriptor.
- *
- * @property {String} [id] Descriptor identifier. If not provided, defaults to converted marker's name.
- *
- * @property {Number} [priority] Descriptor priority. If not provided, defaults to `10`. If descriptor is used to create
- * {@link module:engine/view/attributeelement~AttributeElement}, it will be that element's
- * {@link module:engine/view/attributeelement~AttributeElement#priority}. If descriptor is applied to an element,
- * the priority will be used to determine which descriptor is more important.
- *
- * @property {Object} [attributes] Attributes to set. If descriptor is used to create
- * {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those attributes will be set on that
- * {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element, usually those
- * attributes will be set on that element, however this depends on how the element converts the descriptor.
- */

+ 34 - 15
packages/ckeditor5-engine/src/conversion/modelconsumable.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -15,7 +15,7 @@ import TextProxy from '../model/textproxy';
  * Consumables are various aspects of the model. A model item can be broken down into singular properties that might be
  * taken into consideration when converting that item.
  *
- * `ModelConsumable` is used by {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} while analyzing changed
+ * `ModelConsumable` is used by {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} while analyzing changed
  * parts of {@link module:engine/model/document~Document the document}. The added / changed / removed model items are broken down
  * into singular properties (the item itself and it's attributes). All those parts are saved in `ModelConsumable`. Then,
  * during conversion, when given part of model item is converted (i.e. the view element has been inserted into the view,
@@ -24,12 +24,12 @@ import TextProxy from '../model/textproxy';
  * For model items, `ModelConsumable` stores consumable values of one of following types: `insert`, `addAttribute:<attributeKey>`,
  * `changeAttribute:<attributeKey>`, `removeAttribute:<attributeKey>`.
  *
- * In most cases, it is enough to let {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}
+ * In most cases, it is enough to let {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}
  * gather consumable values, so there is no need to use
- * @link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly.
+ * {@link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly.
  * However, it is important to understand how consumable values can be
  * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}.
- * See {@link module:engine/conversion/model-selection-to-view-converters default model to view converters} for more information.
+ * See {@link module:engine/conversion/downcast-selection-converters default downcast converters} for more information.
  *
  * Keep in mind, that one conversion event may have multiple callbacks (converters) attached to it. Each of those is
  * able to convert one or more parts of the model. However, when one of those callbacks actually converts
@@ -53,9 +53,9 @@ import TextProxy from '../model/textproxy';
  *		//   ├─ <img />
  *		//   └─ <caption>
  *		//       └─ foo
- *		modelConversionDispatcher.on( 'insert:image', ( evt, data, consumable, conversionApi ) => {
+ *		modelConversionDispatcher.on( 'insert:image', ( evt, data, conversionApi ) => {
  *			// First, consume the `image` element.
- *			consumable.consume( data.item, 'insert' );
+ *			conversionApi.consumable.consume( data.item, 'insert' );
  *
  *			// Just create normal image element for the view.
  *			// Maybe it will be "decorated" later.
@@ -69,7 +69,7 @@ import TextProxy from '../model/textproxy';
  *				// `modelCaption` insertion change is consumed from consumable values.
  *				// It will not be converted by other converters, but it's children (probably some text) will be.
  *				// Through mapping, converters for text will know where to insert contents of `modelCaption`.
- *				if ( consumable.consume( modelCaption, 'insert' ) ) {
+ *				if ( conversionApi.consumable.consume( modelCaption, 'insert' ) ) {
  *					const viewCaption = new ViewElement( 'figcaption' );
  *
  *					const viewImageHolder = new ViewElement( 'figure', null, [ viewImage, viewCaption ] );
@@ -120,14 +120,16 @@ export default class ModelConsumable {
 	 *		modelConsumable.add( modelElement, 'addAttribute:bold' ); // Add `bold` attribute insertion on `modelElement` change.
 	 *		modelConsumable.add( modelElement, 'removeAttribute:bold' ); // Add `bold` attribute removal on `modelElement` change.
 	 *		modelConsumable.add( modelSelection, 'selection' ); // Add `modelSelection` to consumable values.
-	 *		modelConsumable.add( modelSelection, 'selectionAttribute:bold' ); // Add `bold` attribute on `modelSelection` to consumables.
 	 *		modelConsumable.add( modelRange, 'range' ); // Add `modelRange` to consumable values.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item
 	 * Model item, range or selection that has the consumable.
-	 * @param {String} type Consumable type.
+	 * @param {String} type Consumable type. Will be normalized to a proper form, that is either `<word>` or `<part>:<part>`.
+	 * Second colon and everything after will be cut. Passing event name is a safe and good practice.
 	 */
 	add( item, type ) {
+		type = _normalizeConsumableType( type );
+
 		if ( item instanceof TextProxy ) {
 			item = this._getSymbolForTextProxy( item );
 		}
@@ -146,15 +148,17 @@ export default class ModelConsumable {
 	 *		modelConsumable.consume( modelElement, 'addAttribute:bold' ); // Remove `bold` attribute insertion on `modelElement` change.
 	 *		modelConsumable.consume( modelElement, 'removeAttribute:bold' ); // Remove `bold` attribute removal on `modelElement` change.
 	 *		modelConsumable.consume( modelSelection, 'selection' ); // Remove `modelSelection` from consumable values.
-	 *		modelConsumable.consume( modelSelection, 'selectionAttribute:bold' ); // Remove `bold` on `modelSelection` from consumables.
 	 *		modelConsumable.consume( modelRange, 'range' ); // Remove 'modelRange' from consumable values.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item
 	 * Model item, range or selection from which consumable will be consumed.
-	 * @param {String} type Consumable type.
+	 * @param {String} type Consumable type. Will be normalized to a proper form, that is either `<word>` or `<part>:<part>`.
+	 * Second colon and everything after will be cut. Passing event name is a safe and good practice.
 	 * @returns {Boolean} `true` if consumable value was available and was consumed, `false` otherwise.
 	 */
 	consume( item, type ) {
+		type = _normalizeConsumableType( type );
+
 		if ( item instanceof TextProxy ) {
 			item = this._getSymbolForTextProxy( item );
 		}
@@ -175,16 +179,18 @@ export default class ModelConsumable {
 	 *		modelConsumable.test( modelElement, 'addAttribute:bold' ); // Check for `bold` attribute insertion on `modelElement` change.
 	 *		modelConsumable.test( modelElement, 'removeAttribute:bold' ); // Check for `bold` attribute removal on `modelElement` change.
 	 *		modelConsumable.test( modelSelection, 'selection' ); // Check if `modelSelection` is consumable.
-	 *		modelConsumable.test( modelSelection, 'selectionAttribute:bold' ); // Check if `bold` on `modelSelection` is consumable.
 	 *		modelConsumable.test( modelRange, 'range' ); // Check if `modelRange` is consumable.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item
 	 * Model item, range or selection to be tested.
-	 * @param {String} type Consumable type.
+	 * @param {String} type Consumable type. Will be normalized to a proper form, that is either `<word>` or `<part>:<part>`.
+	 * Second colon and everything after will be cut. Passing event name is a safe and good practice.
 	 * @returns {null|Boolean} `null` if such consumable was never added, `false` if the consumable values was
 	 * already consumed or `true` if it was added and not consumed yet.
 	 */
 	test( item, type ) {
+		type = _normalizeConsumableType( type );
+
 		if ( item instanceof TextProxy ) {
 			item = this._getSymbolForTextProxy( item );
 		}
@@ -211,7 +217,6 @@ export default class ModelConsumable {
 	 *		modelConsumable.revert( modelElement, 'addAttribute:bold' ); // Revert consuming `bold` attribute insert from `modelElement`.
 	 *		modelConsumable.revert( modelElement, 'removeAttribute:bold' ); // Revert consuming `bold` attribute remove from `modelElement`.
 	 *		modelConsumable.revert( modelSelection, 'selection' ); // Revert consuming `modelSelection`.
-	 *		modelConsumable.revert( modelSelection, 'selectionAttribute:bold' ); // Revert consuming `bold` from `modelSelection`.
 	 *		modelConsumable.revert( modelRange, 'range' ); // Revert consuming `modelRange`.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/selection~Selection|module:engine/model/range~Range} item
@@ -221,6 +226,8 @@ export default class ModelConsumable {
 	 * never been added.
 	 */
 	revert( item, type ) {
+		type = _normalizeConsumableType( type );
+
 		if ( item instanceof TextProxy ) {
 			item = this._getSymbolForTextProxy( item );
 		}
@@ -302,3 +309,15 @@ export default class ModelConsumable {
 		return symbol;
 	}
 }
+
+// Returns a normalized consumable type name from given string. A normalized consumable type name is a string that has
+// at most one colon, for example: `insert` or `addMarker:highlight`. If string to normalize has more "parts" (more colons),
+// the other parts are dropped, for example: `addAttribute:bold:$text` -> `addAttribute:bold`.
+//
+// @param {String} type Consumable type.
+// @returns {String} Normalized consumable type.
+function _normalizeConsumableType( type ) {
+	const parts = type.split( ':' );
+
+	return parts.length > 1 ? parts[ 0 ] + ':' + parts[ 1 ] : parts[ 0 ];
+}

+ 0 - 745
packages/ckeditor5-engine/src/conversion/modelconversiondispatcher.js

@@ -1,745 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/conversion/modelconversiondispatcher
- */
-
-import Consumable from './modelconsumable';
-import Range from '../model/range';
-import Position from '../model/position';
-import DocumentFragment from '../model/documentfragment';
-import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
-import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
-
-/**
- * `ModelConversionDispatcher` is a central point of {@link module:engine/model/model model} conversion, which is
- * a process of reacting to changes in the model and reflecting them by listeners that listen to those changes.
- * In default application, {@link module:engine/model/model model} is converted to {@link module:engine/view/view view}. This means
- * that changes in the model are reflected by changing the view (i.e. adding view nodes or changing attributes on view elements).
- *
- * During conversion process, `ModelConversionDispatcher` fires data-manipulation events, basing on state of the model and prepares
- * data for those events. It is important to note that the events are connected with "change actions" like "inserting"
- * or "removing" so one might say that we are converting "changes". This is in contrary to view to model conversion,
- * where we convert view nodes (the structure, not "changes" to the view). Note, that because changes are converted
- * and not the structure itself, there is a need to have a mapping between model and the structure on which changes are
- * reflected. To map elements during model to view conversion use {@link module:engine/conversion/mapper~Mapper}.
- *
- * The main use for this class is to listen to {@link module:engine/model/document~Document#event:change Document change event}, process it
- * and then fire specific events telling what exactly has changed. For those events, `ModelConversionDispatcher`
- * creates {@link module:engine/conversion/modelconsumable~ModelConsumable list of consumable values} that should be handled by event
- * callbacks. Those events are listened to by model-to-view converters which convert changes done in the
- * {@link module:engine/model/model model} to changes in the {@link module:engine/view/view view}. `ModelConversionController` also checks
- * the current state of consumables, so it won't fire events for parts of model that were already consumed. This is
- * especially important in callbacks that consume multiple values. See {@link module:engine/conversion/modelconsumable~ModelConsumable}
- * for an example of such callback.
- *
- * Although the primary usage for this class is the model-to-view conversion, `ModelConversionDispatcher` can be used
- * to build custom data processing pipelines that converts model to anything that is needed. Existing model structure can
- * be used to generate events (listening to {@link module:engine/model/document~Document#event:change Document change event} is not
- * required)
- * and custom callbacks can be added to the events (these does not have to be limited to changes in the view).
- *
- * When providing your own event listeners for `ModelConversionDispatcher` keep in mind that any callback that had
- * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} a value from consumable (and did some changes, i.e. to
- * the view) should also stop the event. This is because whenever a callback is fired it is assumed that there is something
- * to be consumed. Thanks to that approach, you do not have to test whether there is anything to consume at the beginning
- * of your listener callback.
- *
- * Example of providing a converter for `ModelConversionDispatcher`:
- *
- *		// We will convert inserting "paragraph" model element into the model.
- *		modelDispatcher.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => {
- *			// Remember to consume the part of consumable.
- *			consumable.consume( data.item, 'insert' );
- *
- *			// Translate position in model to position in the view.
- *			const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
- *
- *			// Create a P element (note that this converter is for inserting P elements -> 'insert:paragraph').
- *			const viewElement = new ViewElement( 'p' );
- *
- *			// Bind the newly created view element to model element so positions will map accordingly in future.
- *			conversionApi.mapper.bindElements( data.item, viewElement );
- *
- *			// Add the newly created view element to the view.
- *			viewWriter.insert( viewPosition, viewElement );
- *
- *			// Remember to stop the event propagation if the data.item was consumed.
- *			evt.stop();
- *		} );
- *
- * Callback that "overrides" other callback:
- *
- *		// Special converter for `linkHref` attribute added on custom `quote` element. Note, that this
- *		// attribute may be the same as the attribute added by other features (link feature in this case).
- *		// It might be even added by that feature! It makes sense that a part of content that is a quote is linked
- *		// to an external source so it makes sense that link feature works on the custom quote element.
- *		// However, we have to make sure that the attributes added by link feature are correctly converted.
- *		// To block default `linkHref` conversion we have to:
- *		// 1) add this callback with higher priority than link feature callback,
- *		// 2) consume `linkHref` attribute add change.
- *		modelConversionDispatcher.on( 'addAttribute:linkHref:quote', ( evt, data, consumable, conversionApi ) => {
- *			consumable.consume( data.item, 'addAttribute:linkHref' );
- *
- *			// Create a button that will represent the `linkHref` attribute.
- *			let viewSourceBtn = new ViewElement( 'a', {
- *				href: data.item.getAttribute( 'linkHref' ),
- *				title: 'source'
- *			} );
- *
- *			// Add a class for the button.
- *			viewSourceBtn.addClass( 'source' );
- *
- *			// Insert the button using writer API.
- *			// If `addAttribute` event is fired by
- *			// `module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#convertInsert` it is fired
- *			// after `data.item` insert conversion was done. If the event is fired due to attribute insertion coming from
- *			// different source, `data.item` already existed. This means we are safe to get `viewQuote` from mapper.
- *			const viewQuote = conversionApi.mapper.toViewElement( data.item );
- *			const position = new ViewPosition( viewQuote, viewQuote.childCount );
- *			viewWriter.insert( position, viewSourceBtn );
- *
- *			evt.stop();
- *		}, { priority: 'high' } );
- */
-export default class ModelConversionDispatcher {
-	/**
-	 * Creates a `ModelConversionDispatcher` that operates using passed API.
-	 *
-	 * @param {module:engine/model/document~Document} modelDocument Model document instance bound with this dispatcher.
-	 * @param {Object} [conversionApi] Interface passed by dispatcher to the events callbacks.
-	 */
-	constructor( modelDocument, conversionApi = {} ) {
-		/**
-		 * Model document instance bound with this dispatcher.
-		 *
-		 * @private
-		 * @member {module:engine/model/document~Document}
-		 */
-		this._modelDocument = modelDocument;
-
-		/**
-		 * Interface passed by dispatcher to the events callbacks.
-		 *
-		 * @member {Object}
-		 */
-		this.conversionApi = extend( { dispatcher: this }, conversionApi );
-	}
-
-	/**
-	 * Prepares data and fires a proper event.
-	 *
-	 * The method is crafted to take use of parameters passed in {@link module:engine/model/document~Document#event:change Document change
-	 * event}.
-	 *
-	 * @see module:engine/model/document~Document#event:change
-	 * @fires insert
-	 * @fires remove
-	 * @fires addAttribute
-	 * @fires removeAttribute
-	 * @fires changeAttribute
-	 * @fires addMarker
-	 * @param {String} type Change type.
-	 * @param {Object} data Additional information about the change.
-	 */
-	convertChange( type, data ) {
-		// Do not convert changes if they happen in graveyard.
-		// Graveyard is a special root that has no view / no other representation and changes done in it should not be converted.
-		if ( type !== 'remove' && data.range && data.range.root.rootName == '$graveyard' ) {
-			return;
-		}
-
-		if ( type == 'remove' && data.sourcePosition.root.rootName == '$graveyard' ) {
-			return;
-		}
-
-		if ( type == 'rename' && data.element.root.rootName == '$graveyard' ) {
-			return;
-		}
-
-		// We can safely dispatch changes.
-		if ( type == 'insert' || type == 'reinsert' ) {
-			this.convertInsertion( data.range );
-		} else if ( type == 'move' ) {
-			this.convertMove( data.sourcePosition, data.range );
-		} else if ( type == 'remove' ) {
-			this.convertRemove( data.sourcePosition, data.range );
-		} else if ( type == 'addAttribute' || type == 'removeAttribute' || type == 'changeAttribute' ) {
-			this.convertAttribute( type, data.range, data.key, data.oldValue, data.newValue );
-		} else if ( type == 'rename' ) {
-			this.convertRename( data.element, data.oldName );
-		}
-	}
-
-	/**
-	 * Starts conversion of insertion-change on given `range`.
-	 *
-	 * Analyzes given range and fires insertion-connected events with data based on that range.
-	 *
-	 * **Note**: This method will fire separate events for node insertion and attributes insertion. All
-	 * attributes that are set on inserted nodes are treated like they were added just after node insertion.
-	 *
-	 * @fires insert
-	 * @fires addAttribute
-	 * @fires addMarker
-	 * @param {module:engine/model/range~Range} range Inserted range.
-	 */
-	convertInsertion( range ) {
-		// Create a list of things that can be consumed, consisting of nodes and their attributes.
-		const consumable = this._createInsertConsumable( range );
-
-		// Fire a separate insert event for each node and text fragment contained in the range.
-		for ( const value of range ) {
-			const item = value.item;
-			const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
-			const data = {
-				item,
-				range: itemRange
-			};
-
-			this._testAndFire( 'insert', data, consumable );
-
-			// Fire a separate addAttribute event for each attribute that was set on inserted items.
-			// This is important because most attributes converters will listen only to add/change/removeAttribute events.
-			// If we would not add this part, attributes on inserted nodes would not be converted.
-			for ( const key of item.getAttributeKeys() ) {
-				data.attributeKey = key;
-				data.attributeOldValue = null;
-				data.attributeNewValue = item.getAttribute( key );
-
-				this._testAndFire( `addAttribute:${ key }`, data, consumable );
-			}
-		}
-
-		for ( const marker of this._modelDocument.markers ) {
-			const markerRange = marker.getRange();
-			const intersection = markerRange.getIntersection( range );
-
-			// Check if inserted content is inserted into a marker.
-			if ( intersection && shouldMarkerChangeBeConverted( range.start, marker, this.conversionApi.mapper ) ) {
-				this.convertMarker( 'addMarker', marker.name, intersection );
-			}
-		}
-	}
-
-	/**
-	 * Starts conversion of move-change of given `range`, that was moved from given `sourcePosition`.
-	 *
-	 * Fires {@link ~#event:remove remove event} and {@link ~#event:insert insert event} based on passed parameters.
-	 *
-	 * @fires remove
-	 * @fires insert
-	 * @param {module:engine/model/position~Position} sourcePosition The original position from which the range was moved.
-	 * @param {module:engine/model/range~Range} range The range containing the moved content.
-	 */
-	convertMove( sourcePosition, range ) {
-		// Move left – convert insertion first (#847).
-		if ( range.start.isBefore( sourcePosition ) ) {
-			this.convertInsertion( range );
-
-			const sourcePositionAfterInsertion =
-				sourcePosition._getTransformedByInsertion( range.start, range.end.offset - range.start.offset );
-
-			this.convertRemove( sourcePositionAfterInsertion, range );
-		} else {
-			this.convertRemove( sourcePosition, range );
-			this.convertInsertion( range );
-		}
-	}
-
-	/**
-	 * Starts conversion of remove-change of given `range`, that was removed from given `sourcePosition`.
-	 *
-	 * Fires {@link ~#event:remove remove event} with data based on passed values.
-	 *
-	 * @fires remove
-	 * @param {module:engine/model/position~Position} sourcePosition Position from where the range has been removed.
-	 * @param {module:engine/model/range~Range} range Removed range (after remove, in
-	 * {@link module:engine/model/document~Document#graveyard graveyard root}).
-	 */
-	convertRemove( sourcePosition, range ) {
-		const consumable = this._createConsumableForRange( range, 'remove' );
-
-		for ( const item of range.getItems( { shallow: true } ) ) {
-			const data = {
-				sourcePosition,
-				item
-			};
-
-			this._testAndFire( 'remove', data, consumable );
-		}
-	}
-
-	/**
-	 * Starts conversion of attribute-change on given `range`.
-	 *
-	 * Analyzes given attribute change and fires attributes-connected events with data based on passed values.
-	 *
-	 * @fires addAttribute
-	 * @fires removeAttribute
-	 * @fires changeAttribute
-	 * @param {String} type Change type. Possible values: `addAttribute`, `removeAttribute`, `changeAttribute`.
-	 * @param {module:engine/model/range~Range} range Changed range.
-	 * @param {String} key Attribute key.
-	 * @param {*} oldValue Attribute value before the change or `null` if attribute has not been set.
-	 * @param {*} newValue New attribute value or `null` if attribute has been removed.
-	 */
-	convertAttribute( type, range, key, oldValue, newValue ) {
-		if ( oldValue == newValue ) {
-			// Do not convert if the attribute did not change.
-			return;
-		}
-
-		// Create a list with attributes to consume.
-		const consumable = this._createConsumableForRange( range, type + ':' + key );
-
-		// Create a separate attribute event for each node in the range.
-		for ( const value of range ) {
-			const item = value.item;
-			const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
-			const data = {
-				item,
-				range: itemRange,
-				attributeKey: key,
-				attributeOldValue: oldValue,
-				attributeNewValue: newValue
-			};
-
-			this._testAndFire( `${ type }:${ key }`, data, consumable );
-		}
-	}
-
-	/**
-	 * Starts conversion of rename-change of given `element` that had given `oldName`.
-	 *
-	 * Fires {@link ~#event:remove remove event} and {@link ~#event:insert insert event} based on passed values.
-	 *
-	 * @fires remove
-	 * @fires insert
-	 * @param {module:engine/model/element~Element} element Renamed element.
-	 * @param {String} oldName Name of the renamed element before it was renamed.
-	 */
-	convertRename( element, oldName ) {
-		if ( element.name == oldName ) {
-			// Do not convert if the name did not change.
-			return;
-		}
-
-		// Create fake element that will be used to fire remove event. The fake element will have the old element name.
-		const fakeElement = element.clone( true );
-		fakeElement.name = oldName;
-
-		// Bind fake element with original view element so the view element will be removed.
-		this.conversionApi.mapper.bindElements(
-			fakeElement,
-			this.conversionApi.mapper.toViewElement( element )
-		);
-
-		// Create fake document fragment so a range can be created on fake element.
-		const fakeDocumentFragment = new DocumentFragment();
-		fakeDocumentFragment.appendChildren( fakeElement );
-
-		this.convertRemove( Position.createBefore( element ), Range.createOn( fakeElement ) );
-		this.convertInsertion( Range.createOn( element ) );
-	}
-
-	/**
-	 * Starts selection conversion.
-	 *
-	 * Fires events for given {@link module:engine/model/selection~Selection selection} to start selection conversion.
-	 *
-	 * @fires selection
-	 * @fires selectionAttribute
-	 * @param {module:engine/model/selection~Selection} selection Selection to convert.
-	 */
-	convertSelection( selection ) {
-		const markers = Array.from( this._modelDocument.markers.getMarkersAtPosition( selection.getFirstPosition() ) );
-		const consumable = this._createSelectionConsumable( selection, markers );
-
-		this.fire( 'selection', { selection }, consumable, this.conversionApi );
-
-		for ( const marker of markers ) {
-			const markerRange = marker.getRange();
-
-			if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
-				continue;
-			}
-
-			const data = {
-				selection,
-				markerName: marker.name,
-				markerRange
-			};
-
-			if ( consumable.test( selection, 'selectionMarker:' + marker.name ) ) {
-				this.fire( 'selectionMarker:' + marker.name, data, consumable, this.conversionApi );
-			}
-		}
-
-		for ( const key of selection.getAttributeKeys() ) {
-			const data = {
-				selection,
-				key,
-				value: selection.getAttribute( key )
-			};
-
-			// Do not fire event if the attribute has been consumed.
-			if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) {
-				this.fire( 'selectionAttribute:' + data.key, data, consumable, this.conversionApi );
-			}
-		}
-	}
-
-	/**
-	 * Starts marker conversion.
-	 *
-	 * Fires {@link ~#event:addMarker addMarker} or {@link ~#event:removeMarker removeMarker} events for each item
-	 * in marker's range. If range is collapsed single event is dispatched. See events description for more details.
-	 *
-	 * @fires addMarker
-	 * @fires removeMarker
-	 * @param {'addMarker'|'removeMarker'} type Change type.
-	 * @param {String} name Marker name.
-	 * @param {module:engine/model/range~Range} range Marker range.
-	 */
-	convertMarker( type, name, range ) {
-		// Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
-		if ( !range.root.document || range.root.rootName == '$graveyard' ) {
-			return;
-		}
-
-		// In markers case, event name == consumable name.
-		const eventName = type + ':' + name;
-
-		// When range is collapsed - fire single event with collapsed range in consumable.
-		if ( range.isCollapsed ) {
-			const consumable = new Consumable();
-			consumable.add( range, eventName );
-
-			this.fire( eventName, {
-				markerName: name,
-				markerRange: range
-			}, consumable, this.conversionApi );
-
-			return;
-		}
-
-		// Create consumable for each item in range.
-		const consumable = this._createConsumableForRange( range, eventName );
-
-		// Create separate event for each node in the range.
-		for ( const value of range ) {
-			const item = value.item;
-
-			// Do not fire event for already consumed items.
-			if ( !consumable.test( item, eventName ) ) {
-				continue;
-			}
-
-			const data = {
-				item,
-				range: Range.createFromPositionAndShift( value.previousPosition, value.length ),
-				markerName: name,
-				markerRange: range
-			};
-
-			this.fire( eventName, data, consumable, this.conversionApi );
-		}
-	}
-
-	/**
-	 * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from given range, assuming that
-	 * given range has just been inserted to the model.
-	 *
-	 * @private
-	 * @param {module:engine/model/range~Range} range Inserted range.
-	 * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
-	 */
-	_createInsertConsumable( range ) {
-		const consumable = new Consumable();
-
-		for ( const value of range ) {
-			const item = value.item;
-
-			consumable.add( item, 'insert' );
-
-			for ( const key of item.getAttributeKeys() ) {
-				consumable.add( item, 'addAttribute:' + key );
-			}
-		}
-
-		return consumable;
-	}
-
-	/**
-	 * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values of given `type`
-	 * for each item from given `range`.
-	 *
-	 * @private
-	 * @param {module:engine/model/range~Range} range Affected range.
-	 * @param {String} type Consumable type.
-	 * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
-	 */
-	_createConsumableForRange( range, type ) {
-		const consumable = new Consumable();
-
-		for ( const item of range.getItems() ) {
-			consumable.add( item, type );
-		}
-
-		return consumable;
-	}
-
-	/**
-	 * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values.
-	 *
-	 * @private
-	 * @param {module:engine/model/selection~Selection} selection Selection to create consumable from.
-	 * @param {Iterable.<module:engine/model/markercollection~Marker>} markers Markers which contains selection.
-	 * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
-	 */
-	_createSelectionConsumable( selection, markers ) {
-		const consumable = new Consumable();
-
-		consumable.add( selection, 'selection' );
-
-		for ( const marker of markers ) {
-			consumable.add( selection, 'selectionMarker:' + marker.name );
-		}
-
-		for ( const key of selection.getAttributeKeys() ) {
-			consumable.add( selection, 'selectionAttribute:' + key );
-		}
-
-		return consumable;
-	}
-
-	/**
-	 * Tests passed `consumable` to check whether given event can be fired and if so, fires it.
-	 *
-	 * @private
-	 * @fires insert
-	 * @fires remove
-	 * @fires addAttribute
-	 * @fires removeAttribute
-	 * @fires changeAttribute
-	 * @param {String} type Event type.
-	 * @param {Object} data Event data.
-	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
-	 */
-	_testAndFire( type, data, consumable ) {
-		if ( !consumable.test( data.item, type ) ) {
-			// Do not fire event if the item was consumed.
-			return;
-		}
-
-		const name = data.item.name || '$text';
-
-		this.fire( type + ':' + name, data, consumable, this.conversionApi );
-	}
-
-	/**
-	 * Fired for inserted nodes.
-	 *
-	 * `insert` is a namespace for a class of events. Names of actually called events follow this pattern:
-	 * `insert:<name>`. `name` is either `'$text'` when one or more characters has been inserted or
-	 * {@link module:engine/model/element~Element#name name} of inserted element.
-	 *
-	 * This way listeners can either listen to a general `insert` event or specific event (for example `insert:paragraph`).
-	 *
-	 * @event insert
-	 * @param {Object} data Additional information about the change.
-	 * @param {module:engine/model/item~Item} data.item Inserted item.
-	 * @param {module:engine/model/range~Range} data.range Range spanning over inserted item.
-	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
-	 */
-
-	/**
-	 * Fired for removed nodes.
-	 *
-	 * `remove` is a namespace for a class of events. Names of actually called events follow this pattern:
-	 * `remove:<name>`. `name` is either `'$text'` when one or more characters has been removed or the
-	 * {@link module:engine/model/element~Element#name name} of removed element.
-	 *
-	 * This way listeners can either listen to a general `remove` event or specific event (for example `remove:paragraph`).
-	 *
-	 * @event remove
-	 * @param {Object} data Additional information about the change.
-	 * @param {module:engine/model/position~Position} data.sourcePosition Position from where the range has been removed.
-	 * @param {module:engine/model/range~Range} data.range Removed range (in {@link module:engine/model/document~Document#graveyard
-	 * graveyard root}).
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
-	 */
-
-	/**
-	 * Fired when attribute has been added on a node.
-	 *
-	 * `addAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
-	 * `addAttribute:<attributeKey>:<name>`. `attributeKey` is the key of added attribute. `name` is either `'$text'`
-	 * if attribute was added on one or more characters, or the {@link module:engine/model/element~Element#name name} of
-	 * the element on which attribute was added.
-	 *
-	 * This way listeners can either listen to a general `addAttribute:bold` event or specific event
-	 * (for example `addAttribute:link:image`).
-	 *
-	 * @event addAttribute
-	 * @param {Object} data Additional information about the change.
-	 * @param {module:engine/model/item~Item} data.item Changed item.
-	 * @param {module:engine/model/range~Range} data.range Range spanning over changed item.
-	 * @param {String} data.attributeKey Attribute key.
-	 * @param {null} data.attributeOldValue Attribute value before the change - always `null`. Kept for the sake of unifying events.
-	 * @param {*} data.attributeNewValue New attribute value.
-	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
-	 */
-
-	/**
-	 * Fired when attribute has been removed from a node.
-	 *
-	 * `removeAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
-	 * `removeAttribute:<attributeKey>:<name>`. `attributeKey` is the key of removed attribute. `name` is either `'$text'`
-	 * if attribute was removed from one or more characters, or the {@link module:engine/model/element~Element#name name} of
-	 * the element from which attribute was removed.
-	 *
-	 * This way listeners can either listen to a general `removeAttribute:bold` event or specific event
-	 * (for example `removeAttribute:link:image`).
-	 *
-	 * @event removeAttribute
-	 * @param {Object} data Additional information about the change.
-	 * @param {module:engine/model/item~Item} data.item Changed item.
-	 * @param {module:engine/model/range~Range} data.range Range spanning over changed item.
-	 * @param {String} data.attributeKey Attribute key.
-	 * @param {*} data.attributeOldValue Attribute value before it was removed.
-	 * @param {null} data.attributeNewValue New attribute value - always `null`. Kept for the sake of unifying events.
-	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
-	 */
-
-	/**
-	 * Fired when attribute of a node has been changed.
-	 *
-	 * `changeAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
-	 * `changeAttribute:<attributeKey>:<name>`. `attributeKey` is the key of changed attribute. `name` is either `'$text'`
-	 * if attribute was changed on one or more characters, or the {@link module:engine/model/element~Element#name name} of
-	 * the element on which attribute was changed.
-	 *
-	 * This way listeners can either listen to a general `changeAttribute:link` event or specific event
-	 * (for example `changeAttribute:link:image`).
-	 *
-	 * @event changeAttribute
-	 * @param {Object} data Additional information about the change.
-	 * @param {module:engine/model/item~Item} data.item Changed item.
-	 * @param {module:engine/model/range~Range} data.range Range spanning over changed item.
-	 * @param {String} data.attributeKey Attribute key.
-	 * @param {*} data.attributeOldValue Attribute value before the change.
-	 * @param {*} data.attributeNewValue New attribute value.
-	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
-	 */
-
-	/**
-	 * Fired for {@link module:engine/model/selection~Selection selection} changes.
-	 *
-	 * @event selection
-	 * @param {module:engine/model/selection~Selection} selection `Selection` instance that is converted.
-	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
-	 */
-
-	/**
-	 * Fired for {@link module:engine/model/selection~Selection selection} attributes changes.
-	 *
-	 * `selectionAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
-	 * `selectionAttribute:<attributeKey>`. `attributeKey` is the key of selection attribute. This way listen can listen to
-	 * certain attribute, i.e. `addAttribute:bold`.
-	 *
-	 * @event selectionAttribute
-	 * @param {Object} data Additional information about the change.
-	 * @param {module:engine/model/selection~Selection} data.selection Selection that is converted.
-	 * @param {String} data.attributeKey Key of changed attribute.
-	 * @param {*} data.attributeValue Value of changed attribute.
-	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
-	 */
-
-	/**
-	 * Fired when a new marker is added to the model.
-	 * * If marker's range is not collapsed, event is fired separately for each item contained in that range. In this
-	 * situation, consumable contains all items from that range.
-	 * * If marker's range is collapsed, single event is fired. In this situation, consumable contains only the collapsed range.
-	 *
-	 * `addMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
-	 * `addMarker:<markerName>`. By specifying certain marker names, you can make the events even more gradual. For example,
-	 * markers can be named `foo:abc`, `foo:bar`, then it is possible to listen to `addMarker:foo` or `addMarker:foo:abc` and
-	 * `addMarker:foo:bar` events.
-	 *
-	 * @event addMarker
-	 * @param {Object} data Additional information about the change.
-	 * @param {module:engine/model/item~Item} [data.item] Item contained in marker's range. Not present if collapsed range
-	 * is being converted.
-	 * @param {module:engine/model/range~Range} [data.range] Range spanning over item. Not present if collapsed range
-	 * is being converted.
-	 * @param {String} data.markerName Name of the marker.
-	 * @param {module:engine/model/range~Range} data.markerRange Marker's range spanning on all items.
-	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. When non-collapsed
-	 * marker is being converted then consumable contains all items in marker's range. For collapsed markers it contains
-	 * only marker's range to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
-	 */
-
-	/**
-	 * Fired when marker is removed from the model.
-	 * * If marker's range is not collapsed, event is fired separately for each item contained in that range. In this
-	 * situation, consumable contains all items from that range.
-	 * * If marker's range is collapsed, single event is fired. In this situation, consumable contains only the collapsed range.
-	 *
-	 * `removeMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
-	 * `removeMarker:<markerName>`. By specifying certain marker names, you can make the events even more gradual. For example,
-	 * markers can be named `foo:abc`, `foo:bar`, then it is possible to listen to `removeMarker:foo` or `removeMarker:foo:abc` and
-	 * `removeMarker:foo:bar` events.
-	 *
-	 * @event removeMarker
-	 * @param {Object} data Additional information about the change.
-	 * @param {module:engine/model/item~Item} [data.item] Item contained in marker's range. Not present if collapsed range
-	 * is being converted.
-	 * @param {module:engine/model/range~Range} [data.range] Range spanning over item. Not present if collapsed range
-	 * is being converted.
-	 * @param {String} data.markerName Name of the marker.
-	 * @param {module:engine/model/range~Range} data.markerRange Whole marker's range.
-	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. When non-collapsed
-	 * marker is being converted then consumable contains all items in marker's range. For collapsed markers it contains
-	 * only marker's range to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
-	 */
-}
-
-mix( ModelConversionDispatcher, EmitterMixin );
-
-// Helper function, checks whether change of `marker` at `modelPosition` should be converted. Marker changes are not
-// converted if they happen inside an element with custom conversion method.
-//
-// @param {module:engine/model/position~Position} modelPosition
-// @param {module:engine/model/markercollection~Marker} marker
-// @param {module:engine/conversion/mapper~Mapper} mapper
-// @returns {Boolean}
-function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
-	const range = marker.getRange();
-	const ancestors = Array.from( modelPosition.getAncestors() );
-	ancestors.shift(); // Remove root element. It cannot be passed to `model.Range#containsItem`.
-	ancestors.reverse();
-
-	const hasCustomHandling = ancestors.some( element => {
-		if ( range.containsItem( element ) ) {
-			const viewElement = mapper.toViewElement( element );
-
-			return !!viewElement.getCustomProperty( 'addHighlight' );
-		}
-	} );
-
-	return !hasCustomHandling;
-}

+ 573 - 0
packages/ckeditor5-engine/src/conversion/upcast-converters.js

@@ -0,0 +1,573 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Matcher from '../view/matcher';
+
+import ModelRange from '../model/range';
+import ModelPosition from '../model/position';
+
+import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep';
+
+/**
+ * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
+ * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}.
+ *
+ * @module engine/conversion/upcast-converters
+ */
+
+/**
+ * View element to model element conversion helper.
+ *
+ * This conversion results in creating a model element. For example, view `<p>Foo</p>` becomes `<paragraph>Foo</paragraph>` in the model.
+ *
+ * Keep in mind that the element will be inserted only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
+ *
+ *		upcastElementToElement( { view: 'p', model: 'paragraph' } );
+ *
+ *		upcastElementToElement( { view: 'p', model: 'paragraph', priority: 'high' } );
+ *
+ *		upcastElementToElement( {
+ *			view: {
+ *				name: 'p',
+ *				class: 'fancy'
+ *			},
+ *			model: 'fancyParagraph'
+ *		} );
+ *
+ *		upcastElementToElement( {
+ * 			view: {
+ *				name: 'p',
+ *				class: 'heading'
+ * 			},
+ * 			model: ( viewElement, modelWriter ) => {
+ * 				return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } );
+ * 			}
+ * 		} );
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
+ *
+ * @param {Object} config Conversion configuration.
+ * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
+ * @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element
+ * instance or a function that takes a view element and returns a model element. The model element will be inserted in the model.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
+ * @returns {Function} Conversion helper.
+ */
+export function upcastElementToElement( config ) {
+	config = cloneDeep( config );
+
+	const converter = _prepareToElementConverter( config );
+
+	const elementName = _getViewElementNameFromConfig( config );
+	const eventName = elementName ? 'element:' + elementName : 'element';
+
+	return dispatcher => {
+		dispatcher.on( eventName, converter, { priority: config.priority || 'normal' } );
+	};
+}
+
+/**
+ * View element to model attribute conversion helper.
+ *
+ * This conversion results in setting an attribute on a model node. For example, view `<strong>Foo</strong>` becomes
+ * `Foo` {@link module:engine/model/text~Text model text node} with `bold` attribute set to `true`.
+ *
+ * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
+ *
+ *		upcastElementToAttribute( { view: 'strong', model: 'bold' } );
+ *
+ *		upcastElementToAttribute( { view: 'strong', model: 'bold', priority: 'high' } );
+ *
+ *		upcastElementToAttribute( {
+ *			view: {
+ *				name: 'span',
+ *				class: 'bold'
+ *			},
+ *			model: 'bold'
+ *		} );
+ *
+ *		upcastElementToAttribute( {
+ *			view: {
+ *				name: 'span',
+ *				class: [ 'styled', 'styled-dark' ]
+ *			},
+ *			model: {
+ *				key: 'styled',
+ *				value: 'dark'
+ *			}
+ *		} );
+ *
+ * 		upcastElementToAttribute( {
+ *			view: {
+ *				name: 'span',
+ *				style: {
+ *					'font-size': /[\s\S]+/
+ *				}
+ *			},
+ *			model: {
+ *				key: 'fontSize',
+ *				value: viewElement => {
+ *					const fontSize = viewElement.getStyle( 'font-size' );
+ *					const value = fontSize.substr( 0, fontSize.length - 2 );
+ *
+ *					if ( value <= 10 ) {
+ *						return 'small';
+ *					} else if ( value > 12 ) {
+ *						return 'big';
+ *					}
+ *
+ *					return null;
+ *				}
+ *			}
+ *		} );
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
+ *
+ * @param {Object} config Conversion configuration.
+ * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
+ * @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.priority='normal'] Converter priority.
+ * @returns {Function} Conversion helper.
+ */
+export function upcastElementToAttribute( config ) {
+	config = cloneDeep( config );
+
+	_normalizeModelAttributeConfig( config );
+
+	const converter = _prepareToAttributeConverter( config, true );
+
+	const elementName = _getViewElementNameFromConfig( config );
+	const eventName = elementName ? 'element:' + elementName : 'element';
+
+	return dispatcher => {
+		dispatcher.on( eventName, converter, { priority: config.priority || 'normal' } );
+	};
+}
+
+/**
+ * View attribute to model attribute conversion helper.
+ *
+ * This conversion results in setting an attribute on a model node. For example, view `<img src="foo.jpg"></img>` becomes
+ * `<image source="foo.jpg"></image>` in the model.
+ *
+ * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
+ *
+ *		upcastAttributeToAttribute( { view: 'src', model: 'source' } );
+ *
+ *		upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source' } );
+ *
+ *		upcastAttributeToAttribute( { view: { key: 'src' }, model: 'source', priority: 'normal' } );
+ *
+ *		upcastAttributeToAttribute( {
+ *			view: {
+ *				key: 'data-style',
+ *				value: /[\s\S]+/
+ *			},
+ *			model: 'styled'
+ *		} );
+ *
+ *		upcastAttributeToAttribute( {
+ *			view: {
+ *				name: 'img',
+ *				key: 'class',
+ *				value: 'styled-dark'
+ *			},
+ *			model: {
+ *				key: 'styled',
+ *				value: 'dark'
+ *			}
+ *		} );
+ *
+ *		upcastAttributeToAttribute( {
+ *			view: {
+ *				key: 'class',
+ *				value: /styled-[\S]+/
+ *			},
+ *			model: {
+ *				key: 'styled'
+ *				value: viewElement => {
+ *					const regexp = /styled-([\S]+)/;
+ *					const match = viewElement.getAttribute( 'class' ).match( regexp );
+ *
+ *					return match[ 1 ];
+ *				}
+ *			}
+ *		} );
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
+ *
+ * @param {Object} config Conversion configuration.
+ * @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed,
+ * attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property,
+ * specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name`
+ * property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`,
+ * a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`.
+ * @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 same as view attribute value.
+ * @param {module:utils/priorities~PriorityString} [config.priority='low'] Converter priority.
+ * @returns {Function} Conversion helper.
+ */
+export function upcastAttributeToAttribute( config ) {
+	config = cloneDeep( config );
+
+	let viewKey = null;
+
+	if ( typeof config.view == 'string' || config.view.key ) {
+		viewKey = _normalizeViewAttributeKeyValueConfig( config );
+	}
+
+	_normalizeModelAttributeConfig( config, viewKey );
+
+	const converter = _prepareToAttributeConverter( config, false );
+
+	return dispatcher => {
+		dispatcher.on( 'element', converter, { priority: config.priority || 'low' } );
+	};
+}
+
+/**
+ * View element to model marker conversion helper.
+ *
+ * This conversion results in creating a model marker. For example, if the marker was stored in a view as an element:
+ * `<p>Fo<span data-marker="comment" data-comment-id="7"></span>o</p><p>B<span data-marker="comment" data-comment-id="7"></span>ar</p>`,
+ * after the conversion is done, the marker will be available in
+ * {@link module:engine/model/model~Model#markers model document markers}.
+ *
+ *		upcastElementToMarker( { view: 'marker-search', model: 'search' } );
+ *
+ *		upcastElementToMarker( { view: 'marker-search', model: 'search', priority: 'high' } );
+ *
+ *		upcastElementToMarker( {
+ *			view: 'marker-search',
+ *			model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
+ *		} );
+ *
+ *		upcastElementToMarker( {
+ *			view: {
+ *				name: 'span',
+ *				attribute: {
+ *					'data-marker': 'search'
+ *				}
+ *			},
+ *			model: 'search'
+ *		} );
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
+ *
+ * @param {Object} config Conversion configuration.
+ * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
+ * @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns
+ * a model marker name.
+ * @param {module:utils/priorities~PriorityString} [config.priority='normal'] Converter priority.
+ * @returns {Function} Conversion helper.
+ */
+export function upcastElementToMarker( config ) {
+	config = cloneDeep( config );
+
+	_normalizeToMarkerConfig( config );
+
+	return upcastElementToElement( 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.
+// @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;
+	}
+
+	if ( typeof config.view == 'object' && typeof config.view.name == 'string' ) {
+		return config.view.name;
+	}
+
+	return null;
+}
+
+// Helper for to-model-element conversion. Takes a config object and returns a proper converter function.
+//
+// @param {Object} config Conversion configuration.
+// @returns {Function} View to model converter.
+function _prepareToElementConverter( config ) {
+	const matcher = new Matcher( config.view );
+
+	return ( evt, data, conversionApi ) => {
+		// This will be usually just one pattern but we support matchers with many patterns too.
+		const match = matcher.match( data.viewItem );
+
+		// If there is no match, this callback should not do anything.
+		if ( !match ) {
+			return;
+		}
+
+		// Force consuming element's name.
+		match.match.name = true;
+
+		// Create model element basing on config.
+		const modelElement = _getModelElement( config.model, data.viewItem, conversionApi.writer );
+
+		// Do not convert if element building function returned falsy value.
+		if ( !modelElement ) {
+			return;
+		}
+
+		// When element was already consumed then skip it.
+		if ( !conversionApi.consumable.test( data.viewItem, match.match ) ) {
+			return;
+		}
+
+		// Find allowed parent for element that we are going to insert.
+		// If current parent does not allow to insert element but one of the ancestors does
+		// then split nodes to allowed parent.
+		const splitResult = conversionApi.splitToAllowedParent( modelElement, data.modelCursor );
+
+		// When there is no split result it means that we can't insert element to model tree, so let's skip it.
+		if ( !splitResult ) {
+			return;
+		}
+
+		// Insert element on allowed position.
+		conversionApi.writer.insert( modelElement, splitResult.position );
+
+		// Convert children and insert to element.
+		const childrenResult = conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( modelElement ) );
+
+		// Consume appropriate value from consumable values list.
+		conversionApi.consumable.consume( data.viewItem, match.match );
+
+		// Set conversion result range.
+		data.modelRange = new ModelRange(
+			// Range should start before inserted element
+			ModelPosition.createBefore( modelElement ),
+			// Should end after but we need to take into consideration that children could split our
+			// element, so we need to move range after parent of the last converted child.
+			// before: <allowed>[]</allowed>
+			// after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
+			ModelPosition.createAfter( childrenResult.modelCursor.parent )
+		);
+
+		// Now we need to check where the modelCursor should be.
+		// If we had to split parent to insert our element then we want to continue conversion inside split parent.
+		//
+		// before: <allowed><notAllowed>[]</notAllowed></allowed>
+		// after:  <allowed><notAllowed></notAllowed><converted></converted><notAllowed>[]</notAllowed></allowed>
+		if ( splitResult.cursorParent ) {
+			data.modelCursor = ModelPosition.createAt( splitResult.cursorParent );
+
+			// Otherwise just continue after inserted element.
+		} else {
+			data.modelCursor = data.modelRange.end;
+		}
+	};
+}
+
+// Helper function for upcasting-to-element converter. Takes the model configuration, the converted view element
+// and a writer instance and returns a model element instance to be inserted in the model.
+//
+// @param {String|Function|module:engine/model/element~Element} model Model conversion configuration.
+// @param {module:engine/view/node~Node} input The converted view node.
+// @param {module:engine/model/writer~Writer} writer A writer instance to use to create the model element.
+function _getModelElement( model, input, writer ) {
+	if ( model instanceof Function ) {
+		return model( input, writer );
+	} else {
+		return writer.createElement( model );
+	}
+}
+
+// Helper function view-attribute-to-model-attribute helper. Normalizes `config.view` which was set as `String` or
+// as an `Object` with `key`, `value` and `name` properties. Normalized `config.view` has is compatible with
+// {@link module:engine/view/matcher~MatcherPattern}.
+//
+// @param {Object} config Conversion config.
+// @returns {String} Key of the converted view attribute.
+function _normalizeViewAttributeKeyValueConfig( config ) {
+	if ( typeof config.view == 'string' ) {
+		config.view = { key: config.view };
+	}
+
+	const key = config.view.key;
+	let normalized;
+
+	if ( key == 'class' || key == 'style' ) {
+		normalized = {
+			[ key ]: config.view.value
+		};
+	} else {
+		const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value;
+
+		normalized = {
+			attribute: {
+				[ key ]: value
+			}
+		};
+	}
+
+	if ( config.view.name ) {
+		normalized.name = config.view.name;
+	}
+
+	config.view = normalized;
+
+	return key;
+}
+
+// Helper function that normalizes `config.model` in from-model-attribute conversion. `config.model` can be set
+// as a `String`, an `Object` with only `key` property or an `Object` with `key` and `value` properties. Normalized
+// `config.model` is an `Object` with `key` and `value` properties.
+//
+// @param {Object} config Conversion config.
+// @param {String} viewAttributeKeyToCopy Key of the  converted view attribute. If it is set, model attribute value
+// will be equal to view attribute value.
+function _normalizeModelAttributeConfig( config, viewAttributeKeyToCopy = null ) {
+	const defaultModelValue = viewAttributeKeyToCopy === null ? true : viewElement => viewElement.getAttribute( viewAttributeKeyToCopy );
+
+	const key = typeof config.model != 'object' ? config.model : config.model.key;
+	const value = typeof config.model != 'object' ? defaultModelValue : config.model.value;
+
+	config.model = { key, value };
+}
+
+// Helper for to-model-attribute conversion. Takes the model attribute name and conversion configuration and returns
+// a proper converter function.
+//
+// @param {String} modelAttributeKey The key of the model attribute to set on a model node.
+// @param {Object|Array.<Object>} config Conversion configuration. It is possible to provide multiple configurations in an array.
+// @param {Boolean} consumeName If set to `true` converter will try to consume name. If set to `false` converter will not try to
+// consume name. This flag overwrites parameter returned by `Matcher#match`.
+function _prepareToAttributeConverter( config, consumeName ) {
+	const matcher = new Matcher( config.view );
+
+	return ( evt, data, conversionApi ) => {
+		const match = matcher.match( data.viewItem );
+
+		// If there is no match, this callback should not do anything.
+		if ( !match ) {
+			return;
+		}
+
+		const modelKey = config.model.key;
+		const modelValue = typeof config.model.value == 'function' ? config.model.value( data.viewItem ) : config.model.value;
+
+		// Do not convert if attribute building function returned falsy value.
+		if ( modelValue === null ) {
+			return;
+		}
+
+		if ( !consumeName ) {
+			// Do not test or consume `name` consumable.
+			delete match.match.name;
+		} else {
+			match.match.name = true;
+		}
+
+		// Try to consume appropriate values from consumable values list.
+		if ( !conversionApi.consumable.test( data.viewItem, match.match ) ) {
+			return;
+		}
+
+		// Since we are converting to attribute we need an range on which we will set the attribute.
+		// If the range is not created yet, we will create it.
+		if ( !data.modelRange ) {
+			// Convert children and set conversion result as a current data.
+			data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) );
+		}
+
+		// Set attribute on current `output`. `Schema` is checked inside this helper function.
+		const attributeWasSet = _setAttributeOn( data.modelRange, { key: modelKey, value: modelValue }, conversionApi );
+
+		if ( attributeWasSet ) {
+			conversionApi.consumable.consume( data.viewItem, match.match );
+		}
+	};
+}
+
+// Helper function for to-model-attribute converter. Sets model attribute on given range. Checks {@link module:engine/model/schema~Schema}
+// to ensure proper model structure.
+//
+// @param {module:engine/model/range~Range} modelRange Model range on which attribute should be set.
+// @param {Object} modelAttribute Model attribute to set.
+// @param {Object} conversionApi Conversion API.
+// @returns {Boolean} `true` if attribute was set on at least one node from given `modelRange`.
+function _setAttributeOn( modelRange, modelAttribute, conversionApi ) {
+	let result = false;
+
+	// Set attribute on each item in range according to Schema.
+	for ( const node of Array.from( modelRange.getItems() ) ) {
+		if ( conversionApi.schema.checkAttribute( node, modelAttribute.key ) ) {
+			conversionApi.writer.setAttribute( modelAttribute.key, modelAttribute.value, node );
+
+			result = true;
+		}
+	}
+
+	return result;
+}
+
+// Helper function for upcasting-to-marker conversion. Takes the config in a format requested by `upcastElementToMarker()`
+// function and converts it to a format that is supported by `upcastElementToElement()` function.
+//
+// @param {Object} config Conversion configuration.
+function _normalizeToMarkerConfig( config ) {
+	const oldModel = config.model;
+
+	config.model = ( viewElement, modelWriter ) => {
+		const markerName = typeof oldModel == 'string' ? oldModel : oldModel( viewElement );
+
+		return modelWriter.createElement( '$marker', { 'data-name': markerName } );
+	};
+}
+
+/**
+ * Function factory, creates a converter that converts {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
+ * or all children of {@link module:engine/view/element~Element} into
+ * {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
+ * This is the "entry-point" converter for upcast (view to model conversion). This converter starts the conversion of all children
+ * of passed view document fragment. Those children {@link module:engine/view/node~Node view nodes} are then handled by other converters.
+ *
+ * This also a "default", last resort converter for all view elements that has not been converted by other converters.
+ * When a view element is being converted to the model but it does not have converter specified, that view element
+ * will be converted to {@link module:engine/model/documentfragment~DocumentFragment model document fragment} and returned.
+ *
+ * @returns {Function} Universal converter for view {@link module:engine/view/documentfragment~DocumentFragment fragments} and
+ * {@link module:engine/view/element~Element elements} that returns
+ * {@link module:engine/model/documentfragment~DocumentFragment model fragment} with children of converted view item.
+ */
+export function convertToModelFragment() {
+	return ( evt, data, conversionApi ) => {
+		// Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement.
+		if ( !data.modelRange && conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
+			const { modelRange, modelCursor } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
+
+			data.modelRange = modelRange;
+			data.modelCursor = modelCursor;
+		}
+	};
+}
+
+/**
+ * Function factory, creates a converter that converts {@link module:engine/view/text~Text} to {@link module:engine/model/text~Text}.
+ *
+ * @returns {Function} {@link module:engine/view/text~Text View text} converter.
+ */
+export function convertText() {
+	return ( evt, data, conversionApi ) => {
+		if ( conversionApi.schema.checkChild( data.modelCursor, '$text' ) ) {
+			if ( conversionApi.consumable.consume( data.viewItem ) ) {
+				const text = conversionApi.writer.createText( data.viewItem.data );
+
+				conversionApi.writer.insert( text, data.modelCursor );
+
+				data.modelRange = ModelRange.createFromPositionAndShift( data.modelCursor, text.offsetSize );
+				data.modelCursor = data.modelRange.end;
+			}
+		}
+	};
+}

+ 8 - 8
packages/ckeditor5-engine/src/conversion/view-selection-to-model-converters.js → packages/ckeditor5-engine/src/conversion/upcast-selection-converters.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -7,7 +7,7 @@
  * Contains {@link module:engine/view/selection~Selection view selection}
  * to {@link module:engine/model/selection~Selection model selection} conversion helpers.
  *
- * @module engine/conversion/view-selection-to-model-converters
+ * @module engine/conversion/upcast-selection-converters
  */
 
 import ModelSelection from '../model/selection';
@@ -22,11 +22,11 @@ import ModelSelection from '../model/selection';
  *
  *		view.document.on( 'selectionChange', convertSelectionChange( modelDocument, mapper ) );
  *
- * @param {module:engine/model/document~Document} modelDocument Model document on which selection should be updated.
+ * @param {module:engine/model/model~Model} model Data model.
  * @param {module:engine/conversion/mapper~Mapper} mapper Conversion mapper.
  * @returns {Function} {@link module:engine/view/document~Document#event:selectionChange} callback function.
  */
-export function convertSelectionChange( modelDocument, mapper ) {
+export function convertSelectionChange( model, mapper ) {
 	return ( evt, data ) => {
 		const viewSelection = data.newSelection;
 		const modelSelection = new ModelSelection();
@@ -37,11 +37,11 @@ export function convertSelectionChange( modelDocument, mapper ) {
 			ranges.push( mapper.toModelRange( viewRange ) );
 		}
 
-		modelSelection.setRanges( ranges, viewSelection.isBackward );
+		modelSelection.setTo( ranges, { backward: viewSelection.isBackward } );
 
-		if ( !modelSelection.isEqual( modelDocument.selection ) ) {
-			modelDocument.enqueueChanges( () => {
-				modelDocument.selection.setTo( modelSelection );
+		if ( !modelSelection.isEqual( model.document.selection ) ) {
+			model.change( writer => {
+				writer.setSelection( modelSelection );
 			} );
 		}
 	};

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

@@ -0,0 +1,518 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/conversion/upcastdispatcher
+ */
+
+import ViewConsumable from './viewconsumable';
+import ModelRange from '../model/range';
+import ModelPosition from '../model/position';
+import { SchemaContext } from '../model/schema';
+
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+
+/**
+ * `UpcastDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of
+ * converting given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
+ * {@link module:engine/view/element~Element} into another structure.
+ * In default application, {@link module:engine/view/view view} is converted to {@link module:engine/model/model}.
+ *
+ * During conversion process, for all {@link module:engine/view/node~Node view nodes} from the converted view document fragment,
+ * `UpcastDispatcher` fires corresponding events. Special callbacks called "converters" should listen to
+ * `UpcastDispatcher` for those events.
+ *
+ * Each callback, as the second argument, is passed a special object `data` that has `viewItem`, `modelCursor` and
+ * `modelRange` properties. `viewItem` property contains {@link module:engine/view/node~Node view node} or
+ * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
+ * that is converted at the moment and might be handled by the callback. `modelRange` property should be used to save the result
+ * of conversion and is always a {@link module:engine/model/range~Range} when conversion result is correct.
+ * `modelCursor` property is a {@link module:engine/model/position~Position position} on which conversion result will be inserted
+ * and is a context according to {@link module:engine/model/schema~Schema schema} will be checked before the conversion.
+ * See also {@link ~UpcastDispatcher#convert}. It is also shared by reference by all callbacks listening to given event.
+ *
+ * The third parameter passed to a callback is an instance of {@link ~UpcastDispatcher}
+ * which provides additional tools for converters.
+ *
+ * Examples of providing callbacks for `UpcastDispatcher`:
+ *
+ *		// Converter for links (<a>).
+ *		upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
+ *			if ( conversionApi.consumable.consume( data.viewItem, { name: true, attributes: [ 'href' ] } ) ) {
+ *				// <a> element is inline and is represented by an attribute in the model.
+ *				// This is why we need to convert only children.
+ *				const { modelRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
+ *
+ *				for ( let item of modelRange.getItems() ) {
+ *					if ( conversionApi.schema.checkAttribute( item, 'linkHref' ) ) {
+ *						conversionApi.writer.setAttribute( 'linkHref', data.viewItem.getAttribute( 'href' ), item );
+ *					}
+ *				}
+ *			}
+ *		} );
+ *
+ *		// Convert all elements which have no custom converter into paragraph (autoparagraphing).
+ *  	data.viewToModel.on( 'element', ( evt, data, conversionApi ) => {
+ *  	 	// When element is already consumed by higher priority converters then do nothing.
+ *  	 	if ( conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
+ *  	 			const paragraph = conversionApi.writer.createElement( 'paragraph' );
+ *
+ *  	 			// Find allowed parent for paragraph that we are going to insert. If current parent does not allow
+ *  	 			// to insert paragraph but one of the ancestors does then split nodes to allowed parent.
+ *  	 			const splitResult = conversionApi.splitToAllowedParent( paragraph, data.modelCursor );
+ *
+ *  	 			// When there is no split result it means that we can't insert paragraph in this position.
+ *  	 			if ( splitResult ) {
+ *  	 				// Insert paragraph in allowed position.
+ *  	 				conversionApi.writer.insert( paragraph, splitResult.position );
+ *
+ *  	 				// Convert children to paragraph.
+ *  	 				const { modelRange } = conversionApi.convertChildren( data.viewItem, Position.createAt( paragraph ) );
+ *
+ * 						// Set as conversion result, attribute converters may use this property.
+ *  	 				data.modelRange = new Range( Position.createBefore( paragraph ), modelRange.end );
+ *
+ *  	 				// Continue conversion inside paragraph.
+ *  	 				data.modelCursor = data.modelRange.end;
+ *  	 			}
+ *  	 		}
+ *  	 	}
+ *  	 }, { priority: 'low' } );
+ *
+ * Before each conversion process, `UpcastDispatcher` fires {@link ~UpcastDispatcher#event:viewCleanup}
+ * event which can be used to prepare tree view for conversion.
+ *
+ * @mixes module:utils/emittermixin~EmitterMixin
+ * @fires viewCleanup
+ * @fires element
+ * @fires text
+ * @fires documentFragment
+ */
+export default class UpcastDispatcher {
+	/**
+	 * Creates a `UpcastDispatcher` that operates using passed API.
+	 *
+	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi
+	 * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
+	 * by `UpcastDispatcher`.
+	 */
+	constructor( conversionApi = {} ) {
+		/**
+		 * List of elements that will be checked after conversion process and if element in the list will be empty it
+		 * will be removed from conversion result.
+		 *
+		 * After conversion process list is cleared.
+		 *
+		 * @protected
+		 * @type {Set<module:engine/model/element~Element>}
+		 */
+		this._removeIfEmpty = new Set();
+
+		/**
+		 * Position in the temporary structure where the converted content is inserted. The structure reflect the context of
+		 * the target position where the content will be inserted. This property is build based on the context parameter of the
+		 * convert method.
+		 *
+		 * @private
+		 * @type {module:engine/model/position~Position|null}
+		 */
+		this._modelCursor = null;
+
+		/**
+		 * Interface passed by dispatcher to the events callbacks.
+		 *
+		 * @member {module:engine/conversion/upcastdispatcher~ViewConversionApi}
+		 */
+		this.conversionApi = Object.assign( {}, conversionApi );
+
+		// `convertItem`, `convertChildren` and `splitToAllowedParent` are bound to this `UpcastDispatcher`
+		// instance and set on `conversionApi`. This way only a part of `UpcastDispatcher` API is exposed.
+		this.conversionApi.convertItem = this._convertItem.bind( this );
+		this.conversionApi.convertChildren = this._convertChildren.bind( this );
+		this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.bind( this );
+	}
+
+	/**
+	 * Starts the conversion process. The entry point for the conversion.
+	 *
+	 * @fires element
+	 * @fires text
+	 * @fires documentFragment
+	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
+	 * Part of the view to be converted.
+	 * @param {module:engine/model/writer~Writer} writer Instance of model writer.
+	 * @param {module:engine/model/schema~SchemaContextDefinition} [context=['$root']] Elements will be converted according to this context.
+	 * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process
+	 * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's
+	 * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
+	 */
+	convert( viewItem, writer, context = [ '$root' ] ) {
+		this.fire( 'viewCleanup', viewItem );
+
+		// Create context tree and set position in the top element.
+		// Items will be converted according to this position.
+		this._modelCursor = createContextTree( context, writer );
+
+		// Store writer in conversion as a conversion API
+		// to be sure that conversion process will use the same batch.
+		this.conversionApi.writer = writer;
+
+		// Create consumable values list for conversion process.
+		this.conversionApi.consumable = ViewConsumable.createFrom( viewItem );
+
+		// Custom data stored by converter for conversion process.
+		this.conversionApi.store = {};
+
+		// Do the conversion.
+		const { modelRange } = this._convertItem( viewItem, this._modelCursor );
+
+		// Conversion result is always a document fragment so let's create this fragment.
+		const documentFragment = writer.createDocumentFragment();
+
+		// When there is a conversion result.
+		if ( modelRange ) {
+			// Remove all empty elements that was added to #_removeIfEmpty list.
+			this._removeEmptyElements();
+
+			// Move all items that was converted to context tree to document fragment.
+			for ( const item of Array.from( this._modelCursor.parent.getChildren() ) ) {
+				writer.append( item, documentFragment );
+			}
+
+			// Extract temporary markers elements from model and set as static markers collection.
+			documentFragment.markers = extractMarkersFromModelFragment( documentFragment, writer );
+		}
+
+		// Clear context position.
+		this._modelCursor = null;
+
+		// Clear split elements.
+		this._removeIfEmpty.clear();
+
+		// Clear conversion API.
+		this.conversionApi.writer = null;
+		this.conversionApi.store = null;
+
+		// Return fragment as conversion result.
+		return documentFragment;
+	}
+
+	/**
+	 * @private
+	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#convertItem
+	 */
+	_convertItem( viewItem, modelCursor ) {
+		const data = Object.assign( { viewItem, modelCursor, modelRange: null } );
+
+		if ( viewItem.is( 'element' ) ) {
+			this.fire( 'element:' + viewItem.name, data, this.conversionApi );
+		} else if ( viewItem.is( 'text' ) ) {
+			this.fire( 'text', data, this.conversionApi );
+		} else {
+			this.fire( 'documentFragment', data, this.conversionApi );
+		}
+
+		// Handle incorrect conversion result.
+		if ( data.modelRange && !( data.modelRange instanceof ModelRange ) ) {
+			/**
+			 * Incorrect conversion result was dropped.
+			 *
+			 * {@link module:engine/model/range~Range Model range} should be a conversion result.
+			 *
+			 * @error view-conversion-dispatcher-incorrect-result
+			 */
+			throw new CKEditorError( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.' );
+		}
+
+		return { modelRange: data.modelRange, modelCursor: data.modelCursor };
+	}
+
+	/**
+	 * @private
+	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#convertChildren
+	 */
+	_convertChildren( viewItem, modelCursor ) {
+		const modelRange = new ModelRange( modelCursor );
+		let nextModelCursor = modelCursor;
+
+		for ( const viewChild of Array.from( viewItem.getChildren() ) ) {
+			const result = this._convertItem( viewChild, nextModelCursor );
+
+			if ( result.modelRange instanceof ModelRange ) {
+				modelRange.end = result.modelRange.end;
+				nextModelCursor = result.modelCursor;
+			}
+		}
+
+		return { modelRange, modelCursor: nextModelCursor };
+	}
+
+	/**
+	 * @private
+	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#splitToAllowedParent
+	 */
+	_splitToAllowedParent( node, modelCursor ) {
+		// Try to find allowed parent.
+		const allowedParent = this.conversionApi.schema.findAllowedParent( node, modelCursor );
+
+		// When there is no parent that allows to insert node then return `null`.
+		if ( !allowedParent ) {
+			return null;
+		}
+
+		// When current position parent allows to insert node then return this position.
+		if ( allowedParent === modelCursor.parent ) {
+			return { position: modelCursor };
+		}
+
+		// When allowed parent is in context tree.
+		if ( this._modelCursor.parent.getAncestors().includes( allowedParent ) ) {
+			return null;
+		}
+
+		// Split element to allowed parent.
+		const splitResult = this.conversionApi.writer.split( modelCursor, allowedParent );
+
+		// Remember all elements that are created as a result of split.
+		// This is important because at the end of conversion we want to remove all empty split elements.
+		//
+		// Loop through positions between elements in range (except split result position) and collect parents.
+		// <notSplit><split1><split2>[pos]</split2>[pos]</split1>[omit]<split1>[pos]<split2>[pos]</split2></split1></notSplit>
+		for ( const position of splitResult.range.getPositions() ) {
+			if ( !position.isEqual( splitResult.position ) ) {
+				this._removeIfEmpty.add( position.parent );
+			}
+		}
+
+		return {
+			position: splitResult.position,
+			cursorParent: splitResult.range.end.parent
+		};
+	}
+
+	/**
+	 * Checks if {@link #_removeIfEmpty} contains empty elements and remove them.
+	 * We need to do it smart because there could be elements that are not empty because contains
+	 * other empty elements and after removing its children they become available to remove.
+	 * We need to continue iterating over split elements as long as any element will be removed.
+	 *
+	 * @private
+	 */
+	_removeEmptyElements() {
+		let removed = false;
+
+		for ( const element of this._removeIfEmpty ) {
+			if ( element.isEmpty ) {
+				this.conversionApi.writer.remove( element );
+				this._removeIfEmpty.delete( element );
+				removed = true;
+			}
+		}
+
+		if ( removed ) {
+			this._removeEmptyElements();
+		}
+	}
+
+	/**
+	 * Fired before the first conversion event, at the beginning of upcast (view to model conversion) process.
+	 *
+	 * @event viewCleanup
+	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
+	 * viewItem Part of the view to be converted.
+	 */
+
+	/**
+	 * Fired when {@link module:engine/view/element~Element} is converted.
+	 *
+	 * `element` is a namespace event for a class of events. Names of actually called events follow this pattern:
+	 * `element:<elementName>` where `elementName` is the name of converted element. This way listeners may listen to
+	 * all elements conversion or to conversion of specific elements.
+	 *
+	 * @event element
+	 * @param {Object} data Conversion data. Keep in mind that this object is shared by reference between all
+	 * callbacks that will be called. This means that callbacks can override values if needed, and those values will
+	 * be available in other callbacks.
+	 * @param {module:engine/view/item~Item} data.viewItem Converted item.
+	 * @param {module:engine/model/position~Position} data.modelCursor Position where a converter should start changes.
+	 * Change this value for the next converter to tell where the conversion should continue.
+	 * @param {module:engine/model/range~Range} data.modelRange The current state of conversion result. Every change to
+	 * converted element should be reflected by setting or modifying this property.
+	 * @param {ViewConversionApi} conversionApi Conversion utilities to be used by callback.
+	 */
+
+	/**
+	 * Fired when {@link module:engine/view/text~Text} is converted.
+	 *
+	 * @event text
+	 * @see #event:element
+	 */
+
+	/**
+	 * Fired when {@link module:engine/view/documentfragment~DocumentFragment} is converted.
+	 *
+	 * @event documentFragment
+	 * @see #event:element
+	 */
+}
+
+mix( UpcastDispatcher, EmitterMixin );
+
+// Traverses given model item and searches elements which marks marker range. Found element is removed from
+// DocumentFragment but path of this element is stored in a Map which is then returned.
+//
+// @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
+// @returns {Map<String, module:engine/model/range~Range>} List of static markers.
+function extractMarkersFromModelFragment( modelItem, writer ) {
+	const markerElements = new Set();
+	const markers = new Map();
+
+	// Create ModelTreeWalker.
+	const range = ModelRange.createIn( modelItem ).getItems();
+
+	// Walk through DocumentFragment and collect marker elements.
+	for ( const item of range ) {
+		// Check if current element is a marker.
+		if ( item.name == '$marker' ) {
+			markerElements.add( item );
+		}
+	}
+
+	// Walk through collected marker elements store its path and remove its from the DocumentFragment.
+	for ( const markerElement of markerElements ) {
+		const markerName = markerElement.getAttribute( 'data-name' );
+		const currentPosition = ModelPosition.createBefore( markerElement );
+
+		// When marker of given name is not stored it means that we have found the beginning of the range.
+		if ( !markers.has( markerName ) ) {
+			markers.set( markerName, new ModelRange( ModelPosition.createFromPosition( currentPosition ) ) );
+		// Otherwise is means that we have found end of the marker range.
+		} else {
+			markers.get( markerName ).end = ModelPosition.createFromPosition( currentPosition );
+		}
+
+		// Remove marker element from DocumentFragment.
+		writer.remove( markerElement );
+	}
+
+	return markers;
+}
+
+// Creates model fragment according to given context and returns position in top element.
+function createContextTree( contextDefinition, writer ) {
+	let position;
+
+	for ( const item of new SchemaContext( contextDefinition ) ) {
+		const attributes = {};
+
+		for ( const key of item.getAttributeKeys() ) {
+			attributes[ key ] = item.getAttribute( key );
+		}
+
+		const current = writer.createElement( item.name, attributes );
+
+		if ( position ) {
+			writer.append( current, position );
+		}
+
+		position = ModelPosition.createAt( current );
+	}
+
+	return position;
+}
+
+/**
+ * Conversion interface that is registered for given {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}
+ * and is passed as one of parameters when {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher dispatcher}
+ * fires it's events.
+ *
+ * @interface ViewConversionApi
+ */
+
+/**
+ * Starts conversion of given item by firing an appropriate event.
+ *
+ * Every fired event is passed (as first parameter) an object with `modelRange` property. Every event may set and/or
+ * modify that property. When all callbacks are done, the final value of `modelRange` property is returned by this method.
+ * The `modelRange` must be {@link module:engine/model/range~Range model range} or `null` (as set by default).
+ *
+ * @method #convertItem
+ * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
+ * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
+ * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
+ * @param {module:engine/view/item~Item} viewItem Item to convert.
+ * @param {module:engine/model/position~Position} modelCursor Position of conversion.
+ * @returns {Object} result Conversion result.
+ * @returns {module:engine/model/range~Range|null} result.modelRange Model range containing result of item conversion,
+ * created and modified by callbacks attached to fired event, or `null` if the conversion result was incorrect.
+ * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
+ */
+
+/**
+ * Starts conversion of all children of given item by firing appropriate events for all those children.
+ *
+ * @method #convertChildren
+ * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
+ * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
+ * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
+ * @param {module:engine/view/item~Item} viewItem Item to convert.
+ * @param {module:engine/model/position~Position} modelCursor Position of conversion.
+ * @returns {Object} result Conversion result.
+ * @returns {module:engine/model/range~Range} result.modelRange Model range containing results of conversion of all children of given item.
+ * When no children was converted then range is collapsed.
+ * @returns {module:engine/model/position~Position} result.modelCursor Position where conversion should be continued.
+ */
+
+/**
+ * Checks {@link module:engine/model/schema~Schema schema} to find allowed parent for element that we are going to insert
+ * starting from given position. If current parent does not allow to insert element but one of the ancestors does then
+ * split nodes to allowed parent.
+ *
+ * If schema allows to insert node in given position, nothing is split and object with that position is returned.
+ *
+ * If it was not possible to find allowed parent, `null` is returned, nothing is split.
+ *
+ * Otherwise, ancestors are split and object with position and the copy of the split element is returned.
+ *
+ * For instance, if `<image>` is not allowed in `<paragraph>` but is allowed in `$root`:
+ *
+ *		<paragraph>foo[]bar</paragraph>
+ *
+ *  	-> split for `<image>` ->
+ *
+ *  	<paragraph>foo</paragraph>[]<paragraph>bar</paragraph>
+ *
+ * In the sample above position between `<paragraph>` elements will be returned as `position` and the second `paragraph`
+ * as `cursorParent`.
+ *
+ * @method #splitToAllowedParent
+ * @param {module:engine/model/position~Position} position Position on which element is going to be inserted.
+ * @param {module:engine/model/node~Node} node Node to insert.
+ * @returns {Object|null} Split result. If it was not possible to find allowed position `null` is returned.
+ * @returns {module:engine/model/position~Position} position between split elements.
+ * @returns {module:engine/model/element~Element} [cursorParent] Element inside which cursor should be placed to
+ * continue conversion. When element is not defined it means that there was no split.
+ */
+
+/**
+ * Instance of {@link module:engine/conversion/viewconsumable~ViewConsumable}. It stores
+ * information about what parts of processed view item are still waiting to be handled. After a piece of view item
+ * was converted, appropriate consumable value should be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
+ *
+ * @param {Object} #consumable
+ */
+
+/**
+ * Custom data stored by converters for conversion process. Custom properties of this object can be defined and use to
+ * pass parameters between converters.
+ *
+ * The difference between this property and `data` parameter of
+ * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element} is that `data` parameters allows you
+ * to pass parameters within a single event and `store` within the whole conversion.
+ *
+ * @param {Object} #store
+ */

+ 0 - 61
packages/ckeditor5-engine/src/conversion/view-to-model-converters.js

@@ -1,61 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import ModelDocumentFragment from '../model/documentfragment';
-import ModelText from '../model/text';
-import { normalizeNodes } from '../model/writer';
-
-/**
- * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
- * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}.
- *
- * @module engine/conversion/view-to-model-converters
- */
-
-/**
- * Function factory, creates a converter that converts {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
- * or all children of {@link module:engine/view/element~Element} into
- * {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
- * This is the "entry-point" converter for view to model conversion. This converter starts the conversion of all children
- * of passed view document fragment. Those children {@link module:engine/view/node~Node view nodes} are then handled by other converters.
- *
- * This also a "default", last resort converter for all view elements that has not been converted by other converters.
- * When a view element is being converted to the model but it does not have converter specified, that view element
- * will be converted to {@link module:engine/model/documentfragment~DocumentFragment model document fragment} and returned.
- *
- * @returns {Function} Universal converter for view {@link module:engine/view/documentfragment~DocumentFragment fragments} and
- * {@link module:engine/view/element~Element elements} that returns
- * {@link module:engine/model/documentfragment~DocumentFragment model fragment} with children of converted view item.
- */
-export function convertToModelFragment() {
-	return ( evt, data, consumable, conversionApi ) => {
-		// Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement.
-		if ( !data.output && consumable.consume( data.input, { name: true } ) ) {
-			const convertedChildren = conversionApi.convertChildren( data.input, consumable, data );
-
-			data.output = new ModelDocumentFragment( normalizeNodes( convertedChildren ) );
-		}
-	};
-}
-
-/**
- * Function factory, creates a converter that converts {@link module:engine/view/text~Text} to {@link module:engine/model/text~Text}.
- *
- * @returns {Function} {@link module:engine/view/text~Text View text} converter.
- */
-export function convertText() {
-	return ( evt, data, consumable, conversionApi ) => {
-		const schemaQuery = {
-			name: '$text',
-			inside: data.context
-		};
-
-		if ( conversionApi.schema.check( schemaQuery ) ) {
-			if ( consumable.consume( data.input ) ) {
-				data.output = new ModelText( data.input.data );
-			}
-		}
-	};
-}

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

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

@@ -1,365 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/conversion/viewconversiondispatcher
- */
-
-import ViewConsumable from './viewconsumable';
-import ModelRange from '../model/range';
-import ModelPosition from '../model/position';
-import ModelTreeWalker from '../model/treewalker';
-import ModelNode from '../model/node';
-import ModelDocumentFragment from '../model/documentfragment';
-import { remove } from '../model/writer';
-
-import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
-import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
-import log from '@ckeditor/ckeditor5-utils/src/log';
-
-/**
- * `ViewConversionDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of
- * converting given {@link module:engine/view/documentfragment~DocumentFragment view document fragment} or
- * {@link module:engine/view/element~Element}
- * into another structure. In default application, {@link module:engine/view/view view} is converted to {@link module:engine/model/model}.
- *
- * During conversion process, for all {@link module:engine/view/node~Node view nodes} from the converted view document fragment,
- * `ViewConversionDispatcher` fires corresponding events. Special callbacks called "converters" should listen to
- * `ViewConversionDispatcher` for those events.
- *
- * Each callback, as a first argument, is passed a special object `data` that has `input` and `output` properties.
- * `input` property contains {@link module:engine/view/node~Node view node} or
- * {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
- * that is converted at the moment and might be handled by the callback. `output` property should be used to save the result
- * of conversion. Keep in mind that the `data` parameter is customizable and may contain other values - see
- * {@link ~ViewConversionDispatcher#convert}. It is also shared by reference by all callbacks
- * listening to given event. **Note**: in view to model conversion - `data` contains `context` property that is an array
- * of {@link module:engine/model/element~Element model elements}. These are model elements that will be the parent of currently
- * converted view item. `context` property is used in examples below.
- *
- * The second parameter passed to a callback is an instance of {@link module:engine/conversion/viewconsumable~ViewConsumable}. It stores
- * information about what parts of processed view item are still waiting to be handled. After a piece of view item
- * was converted, appropriate consumable value should be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
- *
- * The third parameter passed to a callback is an instance of {@link ~ViewConversionDispatcher}
- * which provides additional tools for converters.
- *
- * Examples of providing callbacks for `ViewConversionDispatcher`:
- *
- *		// Converter for paragraphs (<p>).
- *		viewDispatcher.on( 'element:p', ( evt, data, consumable, conversionApi ) => {
- *			const paragraph = new ModelElement( 'paragraph' );
- *			const schemaQuery = {
- *				name: 'paragraph',
- *				inside: data.context
- *			};
- *
- *			if ( conversionApi.schema.check( schemaQuery ) ) {
- *				if ( !consumable.consume( data.input, { name: true } ) ) {
- *					// Before converting this paragraph's children we have to update their context by this paragraph.
- *					data.context.push( paragraph );
- *					const children = conversionApi.convertChildren( data.input, consumable, data );
- *					data.context.pop();
- *					paragraph.appendChildren( children );
- *					data.output = paragraph;
- *				}
- *			}
- *		} );
- *
- *		// Converter for links (<a>).
- *		viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => {
- *			if ( consumable.consume( data.input, { name: true, attributes: [ 'href' ] } ) ) {
- *				// <a> element is inline and is represented by an attribute in the model.
- *				// This is why we are not updating `context` property.
- *				data.output = conversionApi.convertChildren( data.input, consumable, data );
- *
- *				for ( let item of Range.createFrom( data.output ) ) {
- *					const schemaQuery = {
- *						name: item.name || '$text',
- *						attribute: 'link',
- *						inside: data.context
- *					};
- *
- *					if ( conversionApi.schema.check( schemaQuery ) ) {
- *						item.setAttribute( 'link', data.input.getAttribute( 'href' ) );
- *					}
- *				}
- *			}
- *		} );
- *
- *		// Fire conversion.
- *		// Always take care where the converted model structure will be appended to. If this `viewDocumentFragment`
- *		// is going to be appended directly to a '$root' element, use that in `context`.
- *		viewDispatcher.convert( viewDocumentFragment, { context: [ '$root' ] } );
- *
- * Before each conversion process, `ViewConversionDispatcher` fires {@link ~ViewConversionDispatcher#event:viewCleanup}
- * event which can be used to prepare tree view for conversion.
- *
- * @mixes module:utils/emittermixin~EmitterMixin
- * @fires viewCleanup
- * @fires element
- * @fires text
- * @fires documentFragment
- */
-export default class ViewConversionDispatcher {
-	/**
-	 * Creates a `ViewConversionDispatcher` that operates using passed API.
-	 *
-	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi
-	 * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
-	 * by `ViewConversionDispatcher`.
-	 */
-	constructor( conversionApi = {} ) {
-		/**
-		 * Interface passed by dispatcher to the events callbacks.
-		 *
-		 * @member {module:engine/conversion/viewconversiondispatcher~ViewConversionApi}
-		 */
-		this.conversionApi = extend( {}, conversionApi );
-
-		// `convertItem` and `convertChildren` are bound to this `ViewConversionDispatcher` instance and
-		// set on `conversionApi`. This way only a part of `ViewConversionDispatcher` API is exposed.
-		this.conversionApi.convertItem = this._convertItem.bind( this );
-		this.conversionApi.convertChildren = this._convertChildren.bind( this );
-	}
-
-	/**
-	 * Starts the conversion process. The entry point for the conversion.
-	 *
-	 * @fires element
-	 * @fires text
-	 * @fires documentFragment
-	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
-	 * Part of the view to be converted.
-	 * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
-	 * events. See also {@link ~ViewConversionDispatcher#event:element element event}.
-	 * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process
-	 * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's
-	 * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
-	 */
-	convert( viewItem, additionalData = {} ) {
-		this.fire( 'viewCleanup', viewItem );
-
-		const consumable = ViewConsumable.createFrom( viewItem );
-		let conversionResult = this._convertItem( viewItem, consumable, additionalData );
-
-		// We can get a null here if conversion failed (see _convertItem())
-		// or simply if an item could not be converted (e.g. due to the schema).
-		if ( !conversionResult ) {
-			return new ModelDocumentFragment();
-		}
-
-		// When conversion result is not a document fragment we need to wrap it in document fragment.
-		if ( !conversionResult.is( 'documentFragment' ) ) {
-			conversionResult = new ModelDocumentFragment( [ conversionResult ] );
-		}
-
-		// Extract temporary markers elements from model and set as static markers collection.
-		conversionResult.markers = extractMarkersFromModelFragment( conversionResult );
-
-		return conversionResult;
-	}
-
-	/**
-	 * @private
-	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertItem
-	 */
-	_convertItem( input, consumable, additionalData = {} ) {
-		const data = extend( {}, additionalData, {
-			input,
-			output: null
-		} );
-
-		if ( input.is( 'element' ) ) {
-			this.fire( 'element:' + input.name, data, consumable, this.conversionApi );
-		} else if ( input.is( 'text' ) ) {
-			this.fire( 'text', data, consumable, this.conversionApi );
-		} else {
-			this.fire( 'documentFragment', data, consumable, this.conversionApi );
-		}
-
-		// Handle incorrect `data.output`.
-		if ( data.output && !( data.output instanceof ModelNode || data.output instanceof ModelDocumentFragment ) ) {
-			/**
-			 * Incorrect conversion result was dropped.
-			 *
-			 * Item may be converted to either {@link module:engine/model/node~Node model node} or
-			 * {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
-			 *
-			 * @error view-conversion-dispatcher-incorrect-result
-			 */
-			log.warn( 'view-conversion-dispatcher-incorrect-result: Incorrect conversion result was dropped.', [ input, data.output ] );
-
-			return null;
-		}
-
-		return data.output;
-	}
-
-	/**
-	 * @private
-	 * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertChildren
-	 */
-	_convertChildren( input, consumable, additionalData = {} ) {
-		// Get all children of view input item.
-		const viewChildren = Array.from( input.getChildren() );
-
-		// 1. Map those children to model.
-		// 2. Filter out items that has not been converted or for which conversion returned wrong result (for those warning is logged).
-		// 3. Extract children from document fragments to flatten results.
-		const convertedChildren = viewChildren
-			.map( viewChild => this._convertItem( viewChild, consumable, additionalData ) )
-			.filter( converted => converted instanceof ModelNode || converted instanceof ModelDocumentFragment )
-			.reduce( ( result, filtered ) => {
-				return result.concat(
-					filtered.is( 'documentFragment' ) ? Array.from( filtered.getChildren() ) : filtered
-				);
-			}, [] );
-
-		// Normalize array to model document fragment.
-		return new ModelDocumentFragment( convertedChildren );
-	}
-
-	/**
-	 * Fired before the first conversion event, at the beginning of view to model conversion process.
-	 *
-	 * @event viewCleanup
-	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
-	 * viewItem Part of the view to be converted.
-	 */
-
-	/**
-	 * Fired when {@link module:engine/view/element~Element} is converted.
-	 *
-	 * `element` is a namespace event for a class of events. Names of actually called events follow this pattern:
-	 * `element:<elementName>` where `elementName` is the name of converted element. This way listeners may listen to
-	 * all elements conversion or to conversion of specific elements.
-	 *
-	 * @event element
-	 * @param {Object} data Object containing conversion input and a placeholder for conversion output and possibly other
-	 * values (see {@link #convert}).
-	 * Keep in mind that this object is shared by reference between all callbacks that will be called.
-	 * This means that callbacks can add their own values if needed,
-	 * and those values will be available in other callbacks.
-	 * @param {module:engine/view/element~Element} data.input Converted element.
-	 * @param {*} data.output The current state of conversion result. Every change to converted element should
-	 * be reflected by setting or modifying this property.
-	 * @param {module:engine/model/schema~SchemaPath} data.context The conversion context.
-	 * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ViewConversionDispatcher` constructor.
-	 * Besides of properties passed in constructor, it also has `convertItem` and `convertChildren` methods which are references
-	 * to {@link #_convertItem} and
-	 * {@link ~ViewConversionDispatcher#_convertChildren}. Those methods are needed to convert
-	 * the whole view-tree they were exposed in `conversionApi` for callbacks.
-	 */
-
-	/**
-	 * Fired when {@link module:engine/view/text~Text} is converted.
-	 *
-	 * @event text
-	 * @see #event:element
-	 */
-
-	/**
-	 * Fired when {@link module:engine/view/documentfragment~DocumentFragment} is converted.
-	 *
-	 * @event documentFragment
-	 * @see #event:element
-	 */
-}
-
-mix( ViewConversionDispatcher, EmitterMixin );
-
-// Traverses given model item and searches elements which marks marker range. Found element is removed from
-// DocumentFragment but path of this element is stored in a Map which is then returned.
-//
-// @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
-// @returns {Map<String, module:engine/model/range~Range>} List of static markers.
-function extractMarkersFromModelFragment( modelItem ) {
-	const markerElements = new Set();
-	const markers = new Map();
-
-	// Create ModelTreeWalker.
-	const walker = new ModelTreeWalker( {
-		startPosition: ModelPosition.createAt( modelItem, 0 ),
-		ignoreElementEnd: true
-	} );
-
-	// Walk through DocumentFragment and collect marker elements.
-	for ( const value of walker ) {
-		// Check if current element is a marker.
-		if ( value.item.name == '$marker' ) {
-			markerElements.add( value.item );
-		}
-	}
-
-	// Walk through collected marker elements store its path and remove its from the DocumentFragment.
-	for ( const markerElement of markerElements ) {
-		const markerName = markerElement.getAttribute( 'data-name' );
-		const currentPosition = ModelPosition.createBefore( markerElement );
-
-		// When marker of given name is not stored it means that we have found the beginning of the range.
-		if ( !markers.has( markerName ) ) {
-			markers.set( markerName, new ModelRange( ModelPosition.createFromPosition( currentPosition ) ) );
-		// Otherwise is means that we have found end of the marker range.
-		} else {
-			markers.get( markerName ).end = ModelPosition.createFromPosition( currentPosition );
-		}
-
-		// Remove marker element from DocumentFragment.
-		remove( ModelRange.createOn( markerElement ) );
-	}
-
-	return markers;
-}
-
-/**
- * Conversion interface that is registered for given {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}
- * and is passed as one of parameters when {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher dispatcher}
- * fires it's events.
- *
- * `ViewConversionApi` object is built by {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher} constructor.
- * The exact list of properties of this object is determined by the object passed to the constructor.
- *
- * @interface ViewConversionApi
- */
-
-/**
- * Starts conversion of given item by firing an appropriate event.
- *
- * Every fired event is passed (as first parameter) an object with `output` property. Every event may set and/or
- * modify that property. When all callbacks are done, the final value of `output` property is returned by this method.
- * The `output` must be either {@link module:engine/model/node~Node model node} or
- * {@link module:engine/model/documentfragment~DocumentFragment model document fragment} or `null` (as set by default).
- *
- * @method #convertItem
- * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
- * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:text
- * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:documentFragment
- * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element|module:engine/view/text~Text}
- * input Item to convert.
- * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
- * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
- * events. See also {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element element event}.
- * @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment|null} The result of item conversion,
- * created and modified by callbacks attached to fired event, or `null` if the conversion result was incorrect.
- */
-
-/**
- * Starts conversion of all children of given item by firing appropriate events for all those children.
- *
- * @method #convertChildren
- * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
- * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:text
- * @fires module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:documentFragment
- * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
- * input Item which children will be converted.
- * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
- * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
- * events. See also {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element element event}.
- * @returns {module:engine/model/documentfragment~DocumentFragment} Model document fragment containing results of conversion
- * of all children of given item.
- */

+ 4 - 4
packages/ckeditor5-engine/src/dataprocessor/basichtmlwriter.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -10,14 +10,14 @@
 /* globals document */
 
 /**
- * Basic HTML writer, it uses the native `innerHTML` property for basic conversion
- * from DocumentFragment to an HTML string.
+ * Basic HTML writer. It uses the native `innerHTML` property for basic conversion
+ * from a document fragment to an HTML string.
  *
  * @implements module:engine/dataprocessor/htmlwriter~HtmlWriter
  */
 export default class BasicHtmlWriter {
 	/**
-	 * Returns HTML string created from DocumentFragment.
+	 * Returns an HTML string created from the document fragment.
 	 *
 	 * @param {DocumentFragment} fragment
 	 * @returns {String}

+ 8 - 8
packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,25 +8,25 @@
  */
 
 /**
- * DataProcessor interface. It should be implemented by actual DataProcessors.
- * Each data processor implements a certain format of the data. E.g. `MarkdownDataProcessor` will convert the data
- * (Markdown string) to a {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} and back.
+ * The data processor interface. It should be implemented by actual data processors.
+ * Each data processor implements a certain format of the data. For example, Markdown data processor will convert the data
+ * (a Markdown string) to a {@link module:engine/view/documentfragment~DocumentFragment document fragment} and back.
  *
  * @interface DataProcessor
  */
 
 /**
- * Converts a {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} to data.
+ * Converts a {@link module:engine/view/documentfragment~DocumentFragment document fragment} to data.
  *
  * @method #toData
- * @param {module:engine/view/documentfragment~DocumentFragment} fragment DocumentFragment to be processed.
+ * @param {module:engine/view/documentfragment~DocumentFragment} fragment The document fragment to be processed.
  * @returns {*}
  */
 
 /**
- * Converts data to a {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment}.
+ * Converts the data to a {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
  *
  * @method #toView
- * @param {*} data Data to be processed.
+ * @param {*} data The data to be processed.
  * @returns {module:engine/view/documentfragment~DocumentFragment}
  */

+ 15 - 15
packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -14,18 +14,18 @@ import DomConverter from '../view/domconverter';
 import { NBSP_FILLER } from '../view/filler';
 
 /**
- * HtmlDataProcessor class.
- * This data processor implementation uses HTML as input/output data.
+ * The HTML data processor class.
+ * This data processor implementation uses HTML as input and output data.
  *
  * @implements module:engine/dataprocessor/dataprocessor~DataProcessor
  */
 export default class HtmlDataProcessor {
 	/**
-	 * Creates a new instance of the HtmlDataProcessor class.
+	 * Creates a new instance of the HTML data processor class.
 	 */
 	constructor() {
 		/**
-		 * DOMParser instance used to parse HTML string to HTMLDocument.
+		 * A DOM parser instance used to parse an HTML string to an HTML document.
 		 *
 		 * @private
 		 * @member {DOMParser}
@@ -33,15 +33,15 @@ export default class HtmlDataProcessor {
 		this._domParser = new DOMParser();
 
 		/**
-		 * DOM converter used to convert DOM elements to view elements.
+		 * A DOM converter used to convert DOM elements to view elements.
 		 *
 		 * @private
-		 * @member
+		 * @member {module:engine/view/domconverter~DomConverter}
 		 */
 		this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } );
 
 		/**
-		 * BasicHtmlWriter instance used to convert DOM elements to HTML string.
+		 * A basic HTML writer instance used to convert DOM elements to an HTML string.
 		 *
 		 * @private
 		 * @member {module:engine/dataprocessor/basichtmlwriter~BasicHtmlWriter}
@@ -50,8 +50,8 @@ export default class HtmlDataProcessor {
 	}
 
 	/**
-	 * Converts provided {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment}
-	 * to data format - in this case HTML string.
+	 * Converts a provided {@link module:engine/view/documentfragment~DocumentFragment document fragment}
+	 * to data format &mdash; in this case to an HTML string.
 	 *
 	 * @param {module:engine/view/documentfragment~DocumentFragment} viewFragment
 	 * @returns {String} HTML string.
@@ -65,10 +65,10 @@ export default class HtmlDataProcessor {
 	}
 
 	/**
-	 * Converts provided HTML string to view tree.
+	 * Converts the provided HTML string to a view tree.
 	 *
-	 * @param {String} data HTML string.
-	 * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} Converted view element.
+	 * @param {String} data An HTML string.
+	 * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} A converted view element.
 	 */
 	toView( data ) {
 		// Convert input HTML data to DOM DocumentFragment.
@@ -79,8 +79,8 @@ export default class HtmlDataProcessor {
 	}
 
 	/**
-	 * Converts HTML String to its DOM representation. Returns DocumentFragment, containing nodes parsed from
-	 * provided data.
+	 * Converts an HTML string to its DOM representation. Returns a document fragment containing nodes parsed from
+	 * the provided data.
 	 *
 	 * @private
 	 * @param {String} data

+ 3 - 3
packages/ckeditor5-engine/src/dataprocessor/htmlwriter.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,13 +8,13 @@
  */
 
 /**
- * HTML writer interface.
+ * The HTML writer interface.
  *
  * @interface module:engine/dataprocessor/htmlwriter~HtmlWriter
  */
 
 /**
- * Returns HTML string created from DocumentFragment.
+ * Returns an HTML string created from a document fragment.
  *
  * @method module:engine/dataprocessor/htmlwriter~HtmlWriter#getHtml
  * @param {DocumentFragment} fragment

+ 20 - 20
packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -14,26 +14,26 @@ import DomConverter from '../view/domconverter';
 import { NBSP_FILLER } from '../view/filler';
 
 /**
- * XmlDataProcessor class.
- * This data processor implementation uses XML as input/output data.
+ * The XML data processor class.
+ * This data processor implementation uses XML as input and output data.
  * This class is needed because unlike HTML, XML allows to use any tag with any value.
- * E.g. `<link>Text</link>` is a valid XML but invalid HTML.
+ * For example, `<link>Text</link>` is a valid XML but invalid HTML.
  *
  * @implements module:engine/dataprocessor/dataprocessor~DataProcessor
  */
 export default class XmlDataProcessor {
 	/**
-	 * Creates a new instance of the XmlDataProcessor class.
+	 * Creates a new instance of the XML data processor class.
 	 *
 	 * @param {Object} options Configuration options.
-	 * @param {Array<String>} [options.namespaces=[]] List of namespaces allowed to use in XML input.
+	 * @param {Array<String>} [options.namespaces=[]] A list of namespaces allowed to use in the XML input.
 	 */
 	constructor( options = {} ) {
 		/**
-		 * List of namespaces allowed to use in XML input.
+		 * A list of namespaces allowed to use in the XML input.
 		 *
-		 * E.g. Registering namespaces [ 'attribute', 'container' ] allows to use `<attirbute:tagName></attribute:tagName>` and
-		 * `<container:tagName></container:tagName>` input. It is mainly for debugging.
+		 * For example, registering namespaces [ 'attribute', 'container' ] allows to use `<attirbute:tagName></attribute:tagName>`
+		 * and `<container:tagName></container:tagName>` input. It is mainly for debugging.
 		 *
 		 * @public
 		 * @member {DOMParser}
@@ -41,7 +41,7 @@ export default class XmlDataProcessor {
 		this.namespaces = options.namespaces || [];
 
 		/**
-		 * DOMParser instance used to parse XML string to XMLDocument.
+		 * DOM parser instance used to parse an XML string to an XML document.
 		 *
 		 * @private
 		 * @member {DOMParser}
@@ -57,8 +57,8 @@ export default class XmlDataProcessor {
 		this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } );
 
 		/**
-		 * BasicHtmlWriter instance used to convert DOM elements to XML string.
-		 * There is no need to use dedicated for XML writer because BasicHtmlWriter works well in this case.
+		 * A basic HTML writer instance used to convert DOM elements to an XML string.
+		 * There is no need to use a dedicated XML writer because the basic HTML writer works well in this case.
 		 *
 		 * @private
 		 * @member {module:engine/dataprocessor/basichtmlwriter~BasicHtmlWriter}
@@ -67,11 +67,11 @@ export default class XmlDataProcessor {
 	}
 
 	/**
-	 * Converts provided {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment}
-	 * to data format- in this case XML string.
+	 * Converts the provided {@link module:engine/view/documentfragment~DocumentFragment document fragment}
+	 * to data format &mdash; in this case an XML string.
 	 *
 	 * @param {module:engine/view/documentfragment~DocumentFragment} viewFragment
-	 * @returns {String} XML string.
+	 * @returns {String} An XML string.
 	 */
 	toData( viewFragment ) {
 		// Convert view DocumentFragment to DOM DocumentFragment.
@@ -83,10 +83,10 @@ export default class XmlDataProcessor {
 	}
 
 	/**
-	 * Converts provided XML string to view tree.
+	 * Converts the provided XML string to a view tree.
 	 *
-	 * @param {String} data XML string.
-	 * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} Converted view element.
+	 * @param {String} data An XML string.
+	 * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} A converted view element.
 	 */
 	toView( data ) {
 		// Convert input XML data to DOM DocumentFragment.
@@ -97,8 +97,8 @@ export default class XmlDataProcessor {
 	}
 
 	/**
-	 * Converts XML String to its DOM representation. Returns DocumentFragment, containing nodes parsed from
-	 * provided data.
+	 * Converts an XML string to its DOM representation. Returns a document fragment containing nodes parsed from
+	 * the provided data.
 	 *
 	 * @private
 	 * @param {String} data

+ 15 - 16
packages/ckeditor5-engine/src/dev-utils/deltareplayer.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -12,22 +12,22 @@
 import DeltaFactory from '../model/delta/deltafactory';
 
 /**
- * DeltaReplayer is a dev-tool created for easily replaying operations on the document from stringified deltas.
+ * Delta replayer is a development tool created for easy replaying of operations on the document from stringified deltas.
  */
 export default class DeltaReplayer {
 	/**
-	 * @param {module:engine/model/document~Document} document Document to replay deltas on.
+	 * @param {module:engine/model/model~Model} model Data model.
 	 * @param {String} logSeparator Separator between deltas.
 	 * @param {String} stringifiedDeltas Deltas to replay.
 	 */
-	constructor( document, logSeparator, stringifiedDeltas ) {
-		this._document = document;
+	constructor( model, logSeparator, stringifiedDeltas ) {
+		this._model = model;
 		this._logSeparator = logSeparator;
 		this.setStringifiedDeltas( stringifiedDeltas );
 	}
 
 	/**
-	 * Parses given string containing stringified deltas and sets parsed deltas as deltas to replay.
+	 * Parses the given string containing stringified deltas and sets parsed deltas as deltas to replay.
 	 *
 	 * @param {String} stringifiedDeltas Stringified deltas to replay.
 	 */
@@ -53,7 +53,7 @@ export default class DeltaReplayer {
 	}
 
 	/**
-	 * Applies all deltas with delay between actions.
+	 * Applies all deltas with a delay between actions.
 	 *
 	 * @param {Number} timeInterval Time between applying deltas.
 	 * @returns {Promise}
@@ -81,7 +81,7 @@ export default class DeltaReplayer {
 	/**
 	 * Applies `numberOfDeltas` deltas, beginning after the last applied delta (or first delta, if no deltas were applied).
 	 *
-	 * @param {Number} numberOfDeltas Number of deltas to apply.
+	 * @param {Number} numberOfDeltas The number of deltas to apply.
 	 * @returns {Promise}
 	 */
 	applyDeltas( numberOfDeltas ) {
@@ -112,29 +112,28 @@ export default class DeltaReplayer {
 	}
 
 	/**
-	 * Applies the next delta to replay. Returns promise with `isFinished` parameter that is `true` if the last
-	 * delta in replayer has been applied, `false` otherwise.
+	 * Applies the next delta to replay. Returns a promise with the `isFinished` parameter that is `true` if the last
+	 * delta in the replayer has been applied, `false` otherwise.
 	 *
 	 * @returns {Promise.<Boolean>}
 	 */
 	applyNextDelta() {
-		const document = this._document;
+		const model = this._model;
 
 		return new Promise( res => {
-			document.enqueueChanges( () => {
+			model.enqueueChange( writer => {
 				const jsonDelta = this._deltasToReplay.shift();
 
 				if ( !jsonDelta ) {
 					return res( true );
 				}
 
-				const delta = DeltaFactory.fromJSON( jsonDelta, this._document );
+				const delta = DeltaFactory.fromJSON( jsonDelta, model.document );
 
-				const batch = document.batch();
-				batch.addDelta( delta );
+				writer.batch.addDelta( delta );
 
 				for ( const operation of delta.operations ) {
-					document.applyOperation( operation );
+					model.applyOperation( operation );
 				}
 
 				res( false );

+ 216 - 152
packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -18,6 +18,7 @@ import ModelTextProxy from '../model/textproxy';
 import ModelElement from '../model/element';
 import Operation from '../model/operation/operation';
 import AttributeOperation from '../model/operation/attributeoperation';
+import DetachOperation from '../model/operation/detachoperation';
 import InsertOperation from '../model/operation/insertoperation';
 import MarkerOperation from '../model/operation/markeroperation';
 import MoveOperation from '../model/operation/moveoperation';
@@ -25,16 +26,18 @@ import NoOperation from '../model/operation/nooperation';
 import RenameOperation from '../model/operation/renameoperation';
 import RootAttributeOperation from '../model/operation/rootattributeoperation';
 import Delta from '../model/delta/delta';
-import { default as AttributeDelta, RootAttributeDelta } from '../model/delta/attributedelta';
+import AttributeDelta from '../model/delta/attributedelta';
 import InsertDelta from '../model/delta/insertdelta';
 import MarkerDelta from '../model/delta/markerdelta';
 import MergeDelta from '../model/delta/mergedelta';
 import MoveDelta from '../model/delta/movedelta';
 import RenameDelta from '../model/delta/renamedelta';
+import RootAttributeDelta from '../model/delta/rootattributedelta';
 import SplitDelta from '../model/delta/splitdelta';
 import UnwrapDelta from '../model/delta/unwrapdelta';
 import WrapDelta from '../model/delta/wrapdelta';
 import deltaTransform from '../model/delta/transform';
+import Model from '../model/model';
 import ModelDocument from '../model/document';
 import ModelDocumentFragment from '../model/documentfragment';
 import ModelRootElement from '../model/rootelement';
@@ -50,6 +53,46 @@ import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
 
 import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
 
+// Sandbox class allows creating mocks of the functions and restoring these mocks to the original values.
+class Sandbox {
+	constructor() {
+		// An array that contains functions which restore the original values of mocked objects.
+		// @private
+		// @type {Array.<Function>}
+		this._restores = [];
+	}
+
+	// Creates a new mock.
+	//
+	// @param {Object} object Object to mock.
+	// @param {String} methodName Function to mock.
+	// @param {Function} fakeMethod Function that will be executed.
+	mock( object, methodName, fakeMethod ) {
+		const originalMethod = object[ methodName ];
+
+		object[ methodName ] = fakeMethod;
+
+		this._restores.unshift( () => {
+			if ( originalMethod ) {
+				object[ methodName ] = originalMethod;
+			} else {
+				delete object[ methodName ];
+			}
+		} );
+	}
+
+	// Restores all mocked functions.
+	restore() {
+		for ( const restore of this._restores ) {
+			restore();
+		}
+
+		this._restores = [];
+	}
+}
+
+const sandbox = new Sandbox();
+
 const treeDump = Symbol( '_treeDump' );
 
 // Maximum number of stored states of model and view document.
@@ -70,7 +113,7 @@ let logger = console;
  *
  * Every operation applied on {@link module:engine/model/document~Document model.Document} is logged.
  *
- * Following classes are expanded with `log` and meaningful `toString` methods:
+ * The following classes are expanded with `log` and meaningful `toString` methods:
  * * {@link module:engine/model/position~Position model.Position},
  * * {@link module:engine/model/range~Range model.Range},
  * * {@link module:engine/model/text~Text model.Text},
@@ -84,25 +127,25 @@ let logger = console;
  * * {@link module:engine/view/documentfragment~DocumentFragment view.DocumentFragment},
  * * {@link module:engine/view/document~Document view.Document}.
  *
- * Additionally, following logging utility methods are added:
+ * Additionally, the following logging utility methods are added:
  * * {@link module:engine/model/text~Text model.Text} `logExtended`,
  * * {@link module:engine/model/element~Element model.Element} `logExtended`,
  * * {@link module:engine/model/element~Element model.Element} `logAll`,
  * * {@link module:engine/model/delta/delta~Delta model.Delta} `logAll`.
  *
- * Additionally, following classes are expanded with `logTree` and `printTree` methods:
+ * Additionally, the following classes are expanded with `logTree` and `printTree` methods:
  * * {@link module:engine/model/element~Element model.Element},
  * * {@link module:engine/model/documentfragment~DocumentFragment model.DocumentFragment},
  * * {@link module:engine/view/element~Element view.Element},
  * * {@link module:engine/view/documentfragment~DocumentFragment view.DocumentFragment}.
  *
- * Finally, following methods are added to {@link module:core/editor/editor~Editor}: `logModel`, `logView`, `logDocuments`.
- * All those methods take one parameter, which is a version of {@link module:engine/model/document~Document model document}
- * for which model or view document state should be logged.
+ * Finally, the following methods are added to {@link module:core/editor/editor~Editor}: `logModel`, `logView`, `logDocuments`.
+ * All those methods take one parameter, which is the version of the {@link module:engine/model/document~Document model document}
+ * for which the model or view document state should be logged.
  *
- * @param {Object} [_logger] Object with functions used to log messages and errors. By default messages are logged to console.
+ * @param {Object} [_logger] An object with functions used to log messages and errors. By default messages are logged to the console.
  * If specified, it is expected to have `log()` and `error()` methods.
- * @returns {module:engine/dev-utils/enableenginedebug~DebugPlugin} Plugin to be loaded in the editor.
+ * @returns {module:engine/dev-utils/enableenginedebug~DebugPlugin} The plugin to be loaded into the editor.
  */
 export default function enableEngineDebug( _logger = console ) {
 	logger = _logger;
@@ -118,60 +161,68 @@ export default function enableEngineDebug( _logger = console ) {
 	return DebugPlugin;
 }
 
+/**
+ * Restores all methods that have been overwritten.
+ */
+export function disableEngineDebug() {
+	sandbox.restore();
+	enabled = false;
+}
+
 function enableLoggingTools() {
-	ModelPosition.prototype.toString = function() {
+	sandbox.mock( ModelPosition.prototype, 'toString', function() {
 		return `${ this.root } [ ${ this.path.join( ', ' ) } ]`;
-	};
+	} );
 
-	ModelPosition.prototype.log = function() {
+	sandbox.mock( ModelPosition.prototype, 'log', function() {
 		logger.log( 'ModelPosition: ' + this );
-	};
+	} );
 
-	ModelRange.prototype.toString = function() {
+	sandbox.mock( ModelRange.prototype, 'toString', function() {
 		return `${ this.root } [ ${ this.start.path.join( ', ' ) } ] - [ ${ this.end.path.join( ', ' ) } ]`;
-	};
+	} );
 
-	ModelRange.prototype.log = function() {
+	sandbox.mock( ModelRange.prototype, 'log', function() {
 		logger.log( 'ModelRange: ' + this );
-	};
+	} );
 
-	ModelText.prototype.toString = function() {
+	sandbox.mock( ModelText.prototype, 'toString', function() {
 		return `#${ this.data }`;
-	};
+	} );
 
-	ModelText.prototype.logExtended = function() {
+	sandbox.mock( ModelText.prototype, 'logExtended', function() {
 		logger.log( `ModelText: ${ this }, attrs: ${ mapString( this.getAttributes() ) }` );
-	};
+	} );
 
-	ModelText.prototype.log = function() {
+	sandbox.mock( ModelText.prototype, 'log', function() {
 		logger.log( 'ModelText: ' + this );
-	};
+	} );
 
-	ModelTextProxy.prototype.toString = function() {
+	sandbox.mock( ModelTextProxy.prototype, 'toString', function() {
 		return `#${ this.data }`;
-	};
+	} );
 
-	ModelTextProxy.prototype.logExtended = function() {
+	sandbox.mock( ModelTextProxy.prototype, 'logExtended', function() {
 		logger.log( `ModelTextProxy: ${ this }, attrs: ${ mapString( this.getAttributes() ) }` );
-	};
+	} );
 
-	ModelTextProxy.prototype.log = function() {
+	sandbox.mock( ModelTextProxy.prototype, 'log', function() {
 		logger.log( 'ModelTextProxy: ' + this );
-	};
+	} );
 
-	ModelElement.prototype.toString = function() {
+	sandbox.mock( ModelElement.prototype, 'toString', function() {
 		return `<${ this.rootName || this.name }>`;
-	};
+	} );
 
-	ModelElement.prototype.log = function() {
+	sandbox.mock( ModelElement.prototype, 'log', function() {
 		logger.log( 'ModelElement: ' + this );
-	};
+	} );
 
-	ModelElement.prototype.logExtended = function() {
+	sandbox.mock( ModelElement.prototype, 'logExtended', function() {
 		logger.log( `ModelElement: ${ this }, ${ this.childCount } children, attrs: ${ mapString( this.getAttributes() ) }` );
-	};
+	} );
 
-	ModelElement.prototype.logAll = function() {
+	sandbox.mock( ModelElement.prototype, 'logAll', function() {
 		logger.log( '--------------------' );
 
 		this.logExtended();
@@ -180,9 +231,9 @@ function enableLoggingTools() {
 		for ( const child of this.getChildren() ) {
 			child.log();
 		}
-	};
+	} );
 
-	ModelElement.prototype.printTree = function( level = 0 ) {
+	sandbox.mock( ModelElement.prototype, 'printTree', function( level = 0 ) {
 		let string = '';
 
 		string += '\t'.repeat( level ) + `<${ this.rootName || this.name }${ mapToTags( this.getAttributes() ) }>`;
@@ -212,29 +263,29 @@ function enableLoggingTools() {
 		string += `</${ this.rootName || this.name }>`;
 
 		return string;
-	};
+	} );
 
-	ModelElement.prototype.logTree = function() {
+	sandbox.mock( ModelElement.prototype, 'logTree', function() {
 		logger.log( this.printTree() );
-	};
+	} );
 
-	ModelRootElement.prototype.toString = function() {
+	sandbox.mock( ModelRootElement.prototype, 'toString', function() {
 		return this.rootName;
-	};
+	} );
 
-	ModelRootElement.prototype.log = function() {
+	sandbox.mock( ModelRootElement.prototype, 'log', function() {
 		logger.log( 'ModelRootElement: ' + this );
-	};
+	} );
 
-	ModelDocumentFragment.prototype.toString = function() {
+	sandbox.mock( ModelDocumentFragment.prototype, 'toString', function() {
 		return 'documentFragment';
-	};
+	} );
 
-	ModelDocumentFragment.prototype.log = function() {
+	sandbox.mock( ModelDocumentFragment.prototype, 'log', function() {
 		logger.log( 'ModelDocumentFragment: ' + this );
-	};
+	} );
 
-	ModelDocumentFragment.prototype.printTree = function() {
+	sandbox.mock( ModelDocumentFragment.prototype, 'printTree', function() {
 		let string = 'ModelDocumentFragment: [';
 
 		for ( const child of this.getChildren() ) {
@@ -258,58 +309,66 @@ function enableLoggingTools() {
 		string += '\n]';
 
 		return string;
-	};
+	} );
 
-	ModelDocumentFragment.prototype.logTree = function() {
+	sandbox.mock( ModelDocumentFragment.prototype, 'logTree', function() {
 		logger.log( this.printTree() );
-	};
+	} );
 
-	Operation.prototype.log = function() {
+	sandbox.mock( Operation.prototype, 'log', function() {
 		logger.log( this.toString() );
-	};
+	} );
 
-	AttributeOperation.prototype.toString = function() {
+	sandbox.mock( AttributeOperation.prototype, 'toString', function() {
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.range }`;
-	};
+	} );
 
-	InsertOperation.prototype.toString = function() {
+	sandbox.mock( DetachOperation.prototype, 'toString', function() {
+		const range = ModelRange.createFromPositionAndShift( this.sourcePosition, this.howMany );
+		const nodes = Array.from( range.getItems() );
+		const nodeString = nodes.length > 1 ? `[ ${ nodes.length } ]` : nodes[ 0 ];
+
+		return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodeString } -> ${ range }`;
+	} );
+
+	sandbox.mock( InsertOperation.prototype, 'toString', function() {
 		const nodeString = this.nodes.length > 1 ? `[ ${ this.nodes.length } ]` : this.nodes.getNode( 0 );
 
 		return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodeString } -> ${ this.position }`;
-	};
+	} );
 
-	MarkerOperation.prototype.toString = function() {
+	sandbox.mock( MarkerOperation.prototype, 'toString', function() {
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`"${ this.name }": ${ this.oldRange } -> ${ this.newRange }`;
-	};
+	} );
 
-	MoveOperation.prototype.toString = function() {
+	sandbox.mock( MoveOperation.prototype, 'toString', function() {
 		const range = ModelRange.createFromPositionAndShift( this.sourcePosition, this.howMany );
 
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`${ range } -> ${ this.targetPosition }${ this.isSticky ? ' (sticky)' : '' }`;
-	};
+	} );
 
-	NoOperation.prototype.toString = function() {
+	sandbox.mock( NoOperation.prototype, 'toString', function() {
 		return `NoOperation( ${ this.baseVersion } )`;
-	};
+	} );
 
-	RenameOperation.prototype.toString = function() {
+	sandbox.mock( RenameOperation.prototype, 'toString', function() {
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`${ this.position }: "${ this.oldName }" -> "${ this.newName }"`;
-	};
+	} );
 
-	RootAttributeOperation.prototype.toString = function() {
+	sandbox.mock( RootAttributeOperation.prototype, 'toString', function() {
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.root.rootName }`;
-	};
+	} );
 
-	Delta.prototype.log = function() {
+	sandbox.mock( Delta.prototype, 'log', function() {
 		logger.log( this.toString() );
-	};
+	} );
 
-	Delta.prototype.logAll = function() {
+	sandbox.mock( Delta.prototype, 'logAll', function() {
 		logger.log( '--------------------' );
 
 		this.log();
@@ -317,9 +376,9 @@ function enableLoggingTools() {
 		for ( const op of this.operations ) {
 			op.log();
 		}
-	};
+	} );
 
-	Delta.prototype._saveHistory = function( itemToSave ) {
+	sandbox.mock( Delta.prototype, '_saveHistory', function( itemToSave ) {
 		const history = itemToSave.before.history ? itemToSave.before.history : [];
 
 		itemToSave.before = clone( itemToSave.before );
@@ -331,11 +390,11 @@ function enableLoggingTools() {
 		itemToSave.transformedBy = JSON.stringify( itemToSave.transformedBy );
 
 		this.history = history.concat( itemToSave );
-	};
+	} );
 
 	const _deltaTransformTransform = deltaTransform.transform;
 
-	deltaTransform.transform = function( a, b, context ) {
+	sandbox.mock( deltaTransform, 'transform', function( a, b, context ) {
 		let results;
 
 		try {
@@ -359,36 +418,36 @@ function enableLoggingTools() {
 		}
 
 		return results;
-	};
+	} );
 
-	AttributeDelta.prototype.toString = function() {
+	sandbox.mock( AttributeDelta.prototype, 'toString', function() {
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`"${ this.key }": -> ${ JSON.stringify( this.value ) }, ${ this.range }, ${ this.operations.length } ops`;
-	};
+	} );
 
-	InsertDelta.prototype.toString = function() {
+	sandbox.mock( InsertDelta.prototype, 'toString', function() {
 		const op = this._insertOperation;
 		const nodeString = op.nodes.length > 1 ? `[ ${ op.nodes.length } ]` : op.nodes.getNode( 0 );
 
 		return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodeString } -> ${ op.position }`;
-	};
+	} );
 
-	MarkerDelta.prototype.toString = function() {
+	sandbox.mock( MarkerDelta.prototype, 'toString', function() {
 		const op = this.operations[ 0 ];
 
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`"${ op.name }": ${ op.oldRange } -> ${ op.newRange }`;
-	};
+	} );
 
-	MergeDelta.prototype.toString = function() {
+	sandbox.mock( MergeDelta.prototype, 'toString', function() {
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			( this.position ?
 				this.position.toString() :
 				`(move from ${ this.operations[ 0 ].sourcePosition })`
 			);
-	};
+	} );
 
-	MoveDelta.prototype.toString = function() {
+	sandbox.mock( MoveDelta.prototype, 'toString', function() {
 		const opStrings = [];
 
 		for ( const op of this.operations ) {
@@ -399,67 +458,67 @@ function enableLoggingTools() {
 
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			opStrings.join( '; ' );
-	};
+	} );
 
-	RenameDelta.prototype.toString = function() {
+	sandbox.mock( RenameDelta.prototype, 'toString', function() {
 		const op = this.operations[ 0 ];
 
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`${ op.position }: "${ op.oldName }" -> "${ op.newName }"`;
-	};
+	} );
 
-	RootAttributeDelta.prototype.toString = function() {
+	sandbox.mock( RootAttributeDelta.prototype, 'toString', function() {
 		const op = this.operations[ 0 ];
 
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`"${ op.key }": ${ JSON.stringify( op.oldValue ) } -> ${ JSON.stringify( op.newValue ) }, ${ op.root.rootName }`;
-	};
+	} );
 
-	SplitDelta.prototype.toString = function() {
+	sandbox.mock( SplitDelta.prototype, 'toString', function() {
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			( this.position ?
 				this.position.toString() :
 				`(clone to ${ this._cloneOperation.position || this._cloneOperation.targetPosition })`
 			);
-	};
+	} );
 
-	UnwrapDelta.prototype.toString = function() {
+	sandbox.mock( UnwrapDelta.prototype, 'toString', function() {
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			this.position.toString();
-	};
+	} );
 
-	WrapDelta.prototype.toString = function() {
+	sandbox.mock( WrapDelta.prototype, 'toString', function() {
 		const wrapElement = this._insertOperation.nodes.getNode( 0 );
 
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`${ this.range } -> ${ wrapElement }`;
-	};
+	} );
 
-	ViewText.prototype.toString = function() {
+	sandbox.mock( ViewText.prototype, 'toString', function() {
 		return `#${ this.data }`;
-	};
+	} );
 
-	ViewText.prototype.logExtended = function() {
+	sandbox.mock( ViewText.prototype, 'logExtended', function() {
 		logger.log( 'ViewText: ' + this );
-	};
+	} );
 
-	ViewText.prototype.log = function() {
+	sandbox.mock( ViewText.prototype, 'log', function() {
 		logger.log( 'ViewText: ' + this );
-	};
+	} );
 
-	ViewTextProxy.prototype.toString = function() {
+	sandbox.mock( ViewTextProxy.prototype, 'toString', function() {
 		return `#${ this.data }`;
-	};
+	} );
 
-	ViewTextProxy.prototype.logExtended = function() {
+	sandbox.mock( ViewTextProxy.prototype, 'logExtended', function() {
 		logger.log( 'ViewTextProxy: ' + this );
-	};
+	} );
 
-	ViewTextProxy.prototype.log = function() {
+	sandbox.mock( ViewTextProxy.prototype, 'log', function() {
 		logger.log( 'ViewTextProxy: ' + this );
-	};
+	} );
 
-	ViewElement.prototype.printTree = function( level = 0 ) {
+	sandbox.mock( ViewElement.prototype, 'printTree', function( level = 0 ) {
 		let string = '';
 
 		string += '\t'.repeat( level ) + `<${ this.name }${ mapToTags( this.getAttributes() ) }>`;
@@ -479,13 +538,13 @@ function enableLoggingTools() {
 		string += `</${ this.name }>`;
 
 		return string;
-	};
+	} );
 
-	ViewElement.prototype.logTree = function() {
+	sandbox.mock( ViewElement.prototype, 'logTree', function() {
 		logger.log( this.printTree() );
-	};
+	} );
 
-	ViewDocumentFragment.prototype.printTree = function() {
+	sandbox.mock( ViewDocumentFragment.prototype, 'printTree', function() {
 		let string = 'ViewDocumentFragment: [';
 
 		for ( const child of this.getChildren() ) {
@@ -499,17 +558,17 @@ function enableLoggingTools() {
 		string += '\n]';
 
 		return string;
-	};
+	} );
 
-	ViewDocumentFragment.prototype.logTree = function() {
+	sandbox.mock( ViewDocumentFragment.prototype, 'logTree', function() {
 		logger.log( this.printTree() );
-	};
+	} );
 }
 
 function enableReplayerTools() {
-	const _modelDocumentApplyOperation = ModelDocument.prototype.applyOperation;
+	const _modelApplyOperation = Model.prototype.applyOperation;
 
-	ModelDocument.prototype.applyOperation = function( operation ) {
+	sandbox.mock( Model.prototype, 'applyOperation', function( operation ) {
 		if ( !this._lastDelta ) {
 			this._appliedDeltas = [];
 		} else if ( this._lastDelta !== operation.delta ) {
@@ -518,10 +577,10 @@ function enableReplayerTools() {
 
 		this._lastDelta = operation.delta;
 
-		_modelDocumentApplyOperation.call( this, operation );
-	};
+		return _modelApplyOperation.call( this, operation );
+	} );
 
-	ModelDocument.prototype.getAppliedDeltas = function() {
+	sandbox.mock( Model.prototype, 'getAppliedDeltas', function() {
 		// No deltas has been applied yet, return empty string.
 		if ( !this._lastDelta ) {
 			return '';
@@ -530,17 +589,17 @@ function enableReplayerTools() {
 		const appliedDeltas = this._appliedDeltas.concat( this._lastDelta );
 
 		return appliedDeltas.map( JSON.stringify ).join( LOG_SEPARATOR );
-	};
+	} );
 
-	ModelDocument.prototype.createReplayer = function( stringifiedDeltas ) {
+	sandbox.mock( Model.prototype, 'createReplayer', function( stringifiedDeltas ) {
 		return new DeltaReplayer( this, LOG_SEPARATOR, stringifiedDeltas );
-	};
+	} );
 }
 
 function enableDocumentTools() {
-	const _modelDocumentApplyOperation = ModelDocument.prototype.applyOperation;
+	const _modelApplyOperation = Model.prototype.applyOperation;
 
-	ModelDocument.prototype.applyOperation = function( operation ) {
+	sandbox.mock( Model.prototype, 'applyOperation', function( operation ) {
 		logger.log( 'Applying ' + operation );
 
 		if ( !this._operationLogs ) {
@@ -549,35 +608,35 @@ function enableDocumentTools() {
 
 		this._operationLogs.push( JSON.stringify( operation.toJSON() ) );
 
-		_modelDocumentApplyOperation.call( this, operation );
-	};
+		return _modelApplyOperation.call( this, operation );
+	} );
 
-	ModelDocument.prototype.log = function( version = null ) {
+	sandbox.mock( ModelDocument.prototype, 'log', function( version = null ) {
 		version = version === null ? this.version : version;
 
 		logDocument( this, version );
-	};
+	} );
 
-	ViewDocument.prototype.log = function( version ) {
+	sandbox.mock( ViewDocument.prototype, 'log', function( version ) {
 		logDocument( this, version );
-	};
+	} );
 
-	Editor.prototype.logModel = function( version = null ) {
-		version = version === null ? this.document.version : version;
+	sandbox.mock( Editor.prototype, 'logModel', function( version = null ) {
+		version = version === null ? this.model.document.version : version;
 
-		this.document.log( version );
-	};
+		this.model.document.log( version );
+	} );
 
-	Editor.prototype.logView = function( version ) {
-		this.editing.view.log( version );
-	};
+	sandbox.mock( Editor.prototype, 'logView', function( version ) {
+		this.editing.view.document.log( version );
+	} );
 
-	Editor.prototype.logDocuments = function( version = null ) {
-		version = version === null ? this.document.version : version;
+	sandbox.mock( Editor.prototype, 'logDocuments', function( version = null ) {
+		version = version === null ? this.model.document.version : version;
 
 		this.logModel( version );
 		this.logView( version );
-	};
+	} );
 
 	function logDocument( document, version ) {
 		logger.log( '--------------------' );
@@ -597,8 +656,10 @@ class DebugPlugin extends Plugin {
 	constructor( editor ) {
 		super( editor );
 
-		const modelDocument = this.editor.document;
-		const viewDocument = this.editor.editing.view;
+		const model = this.editor.model;
+		const modelDocument = model.document;
+		const view = this.editor.editing.view;
+		const viewDocument = view.document;
 
 		modelDocument[ treeDump ] = [];
 		viewDocument[ treeDump ] = [];
@@ -606,18 +667,21 @@ class DebugPlugin extends Plugin {
 		dumpTrees( modelDocument, modelDocument.version );
 		dumpTrees( viewDocument, modelDocument.version );
 
-		modelDocument.on( 'change', () => {
+		model.on( 'applyOperation', () => {
 			dumpTrees( modelDocument, modelDocument.version );
+		}, { priority: 'lowest' } );
+
+		model.document.on( 'change', () => {
 			dumpTrees( viewDocument, modelDocument.version );
 		}, { priority: 'lowest' } );
 	}
 }
 
-// Helper function, stores `document` state for given `version` as a string in private property.
+// Helper function, stores the `document` state for a given `version` as a string in a private property.
 function dumpTrees( document, version ) {
 	let string = '';
 
-	for ( const root of document.roots.values() ) {
+	for ( const root of document.roots ) {
 		string += root.printTree() + '\n';
 	}
 
@@ -630,7 +694,7 @@ function dumpTrees( document, version ) {
 	}
 }
 
-// Helper function, returns class name of given `Delta` or `Operation`.
+// Helper function, returns the class name of a given `Delta` or `Operation`.
 // @param {module:engine/model/delta/delta~Delta|module:engine/model/operation/operation~Operation}
 // @returns {String} Class name.
 function getClassName( obj ) {
@@ -639,7 +703,7 @@ function getClassName( obj ) {
 	return path[ path.length - 1 ];
 }
 
-// Helper function, converts map to {"key1":"value1","key2":"value2"} format.
+// Helper function, converts a map to the {"key1":"value1","key2":"value2"} format.
 // @param {Map} map Map to convert.
 // @returns {String} Converted map.
 function mapString( map ) {
@@ -652,7 +716,7 @@ function mapString( map ) {
 	return JSON.stringify( obj );
 }
 
-// Helper function, converts map to key1="value1" key2="value1" format.
+// Helper function, converts a map to the key1="value1" key2="value1" format.
 // @param {Map} map Map to convert.
 // @returns {String} Converted map.
 function mapToTags( map ) {

+ 159 - 136
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,96 +8,96 @@
  */
 
 /**
- * Collection of methods for manipulating {@link module:engine/model/model model} for testing purposes.
+ * Collection of methods for manipulating the {@link module:engine/model/model model} for testing purposes.
  */
 
 import RootElement from '../model/rootelement';
-import ModelDocument from '../model/document';
+import Model from '../model/model';
+import Batch from '../model/batch';
 import ModelRange from '../model/range';
 import ModelPosition from '../model/position';
-import ModelConversionDispatcher from '../conversion/modelconversiondispatcher';
 import ModelSelection from '../model/selection';
 import ModelDocumentFragment from '../model/documentfragment';
-import ModelElement from '../model/element';
-import ModelText from '../model/text';
-import modelWriter from '../model/writer';
+import DocumentSelection from '../model/documentselection';
 
-import ViewConversionDispatcher from '../conversion/viewconversiondispatcher';
-import ViewSelection from '../view/selection';
-import ViewDocumentFragment from '../view/documentfragment';
+import View from '../view/view';
 import ViewContainerElement from '../view/containerelement';
-import ViewAttributeElement from '../view/attributeelement';
+import ViewRootEditableElement from '../view/rooteditableelement';
 
-import Mapper from '../conversion/mapper';
 import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view';
+
+import DowncastDispatcher from '../conversion/downcastdispatcher';
+import UpcastDispatcher from '../conversion/upcastdispatcher';
+import Mapper from '../conversion/mapper';
 import {
 	convertRangeSelection,
 	convertCollapsedSelection,
-	convertSelectionAttribute
-} from '../conversion/model-selection-to-view-converters';
-import { insertText, insertElement, wrapItem } from '../conversion/model-to-view-converters';
+} from '../conversion/downcast-selection-converters';
+import { insertText, insertElement, wrap } from '../conversion/downcast-converters';
+
 import isPlainObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObject';
+import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 
 /**
- * Writes the contents of the {@link module:engine/model/document~Document Document} to an HTML-like string.
+ * Writes the content of the {@link module:engine/model/document~Document document} to an HTML-like string.
  *
- * **Note:** {@link module:engine/model/text~Text text} node contains attributes will be represented as:
+ * **Note:** A {@link module:engine/model/text~Text text} node that contains attributes will be represented as:
  *
  *		<$text attribute="value">Text data</$text>
  *
- * @param {module:engine/model/document~Document} document
+ * @param {module:engine/model/model~Model} model
  * @param {Object} [options]
- * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true` selection will
- * be not included in returned string.
- * @param {Boolean} [options.rootName='main'] Name of the root from which data should be stringified. If not provided
- * default `main` name will be used.
+ * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true`, the selection will
+ * not be included in the returned string.
+ * @param {String} [options.rootName='main'] The name of the root from which the data should be stringified. If not provided,
+ * the default `main` name will be used.
  * @returns {String} The stringified data.
  */
-export function getData( document, options = {} ) {
-	if ( !( document instanceof ModelDocument ) ) {
-		throw new TypeError( 'Document needs to be an instance of module:engine/model/document~Document.' );
+export function getData( model, options = {} ) {
+	if ( !( model instanceof Model ) ) {
+		throw new TypeError( 'Model needs to be an instance of module:engine/model/model~Model.' );
 	}
 
-	const withoutSelection = !!options.withoutSelection;
+	const withoutSelection = options.withoutSelection;
 	const rootName = options.rootName || 'main';
-	const root = document.getRoot( rootName );
+	const root = model.document.getRoot( rootName );
 
-	return withoutSelection ? getData._stringify( root ) : getData._stringify( root, document.selection );
+	return withoutSelection ? getData._stringify( root ) : getData._stringify( root, model.document.selection );
 }
 
 // Set stringify as getData private method - needed for testing/spying.
 getData._stringify = stringify;
 
 /**
- * Sets the contents of the {@link module:engine/model/document~Document Document} provided as HTML-like string.
- * It uses {@link module:engine/model/document~Document#enqueueChanges enqueueChanges} method.
+ * Sets the content of the {@link module:engine/model/document~Document document} provided as an HTML-like string.
  *
- * **Note:** Remember to register elements in {@link module:engine/model/document~Document#schema document's schema} before inserting them.
+ * **Note:** Remember to register elements in the {@link module:engine/model/model~Model#schema model's schema} before inserting them.
  *
- * **Note:** To create {@link module:engine/model/text~Text text} node witch containing attributes use:
+ * **Note:** To create a {@link module:engine/model/text~Text text} node that contains attributes use:
  *
  *		<$text attribute="value">Text data</$text>
  *
- * @param {module:engine/model/document~Document} document
- * @param {String} data HTML-like string to write into Document.
+ * @param {module:engine/model/model~Model} model
+ * @param {String} data HTML-like string to write into the document.
  * @param {Object} options
- * @param {String} [options.rootName='main'] Root name where parsed data will be stored. If not provided, default `main`
+ * @param {String} [options.rootName='main'] Root name where parsed data will be stored. If not provided, the default `main`
  * name will be used.
- * @param {Array<Object>} [options.selectionAttributes] List of attributes which will be passed to the selection.
- * @param {Boolean} [options.lastRangeBackward=false] If set to true last range will be added as backward.
+ * @param {Array<Object>} [options.selectionAttributes] A list of attributes which will be passed to the selection.
+ * @param {Boolean} [options.lastRangeBackward=false] If set to `true`, the last range will be added as backward.
  * @param {String} [options.batchType='transparent'] Batch type used for inserting elements.
  * See {@link module:engine/model/batch~Batch#type}.
  */
-export function setData( document, data, options = {} ) {
-	if ( !( document instanceof ModelDocument ) ) {
-		throw new TypeError( 'Document needs to be an instance of module:engine/model/document~Document.' );
+export function setData( model, data, options = {} ) {
+	if ( !( model instanceof Model ) ) {
+		throw new TypeError( 'Model needs to be an instance of module:engine/model/model~Model.' );
 	}
 
 	let modelDocumentFragment, selection;
-	const modelRoot = document.getRoot( options.rootName || 'main' );
+	const modelRoot = model.document.getRoot( options.rootName || 'main' );
+	const batch = new Batch( options.batchType || 'transparent' );
 
 	// Parse data string to model.
-	const parsedResult = setData._parse( data, document.schema, {
+	const parsedResult = setData._parse( data, model.schema, {
 		lastRangeBackward: options.lastRangeBackward,
 		selectionAttributes: options.selectionAttributes,
 		context: [ modelRoot.name ]
@@ -111,15 +111,14 @@ export function setData( document, data, options = {} ) {
 		modelDocumentFragment = parsedResult;
 	}
 
-	document.enqueueChanges( () => {
+	model.enqueueChange( batch, writer => {
 		// Replace existing model in document by new one.
-		document.batch( options.batchType || 'transparent' )
-			.remove( ModelRange.createIn( modelRoot ) )
-			.insert( ModelPosition.createAt( modelRoot, 0 ), modelDocumentFragment );
+		writer.remove( ModelRange.createIn( modelRoot ) );
+		writer.insert( modelDocumentFragment, modelRoot );
 
 		// Clean up previous document selection.
-		document.selection.clearAttributes();
-		document.selection.removeAllRanges();
+		writer.setSelection( null );
+		writer.removeSelectionAttribute( model.document.selection.getAttributeKeys() );
 
 		// Update document selection if specified.
 		if ( selection ) {
@@ -132,10 +131,10 @@ export function setData( document, data, options = {} ) {
 				ranges.push( new ModelRange( start, end ) );
 			}
 
-			document.selection.setRanges( ranges, selection.isBackward );
+			writer.setSelection( ranges, { backward: selection.isBackward } );
 
 			if ( options.selectionAttributes ) {
-				document.selection.setAttributesTo( selection.getAttributes() );
+				writer.setSelectionAttribute( selection.getAttributes() );
 			}
 		}
 	} );
@@ -147,21 +146,21 @@ setData._parse = parse;
 /**
  * Converts model nodes to HTML-like string representation.
  *
- * **Note:** {@link module:engine/model/text~Text text} node contains attributes will be represented as:
+ * **Note:** A {@link module:engine/model/text~Text text} node that contains attributes will be represented as:
  *
  *		<$text attribute="value">Text data</$text>
  *
  * @param {module:engine/model/rootelement~RootElement|module:engine/model/element~Element|module:engine/model/text~Text|
- * module:engine/model/documentfragment~DocumentFragment} node Node to stringify.
+ * module:engine/model/documentfragment~DocumentFragment} node A node to stringify.
  * @param {module:engine/model/selection~Selection|module:engine/model/position~Position|
  * module:engine/model/range~Range} [selectionOrPositionOrRange=null]
- * Selection instance which ranges will be included in returned string data. If Range instance is provided - it will be
- * converted to selection containing this range. If Position instance is provided - it will be converted to selection
+ * A selection instance whose ranges will be included in the returned string data. If a range instance is provided, it will be
+ * converted to a selection containing this range. If a position instance is provided, it will be converted to a selection
  * containing one range collapsed at this position.
- * @returns {String} HTML-like string representing the model.
+ * @returns {String} An HTML-like string representing the model.
  */
 export function stringify( node, selectionOrPositionOrRange = null ) {
-	const modelDoc = new ModelDocument();
+	const model = new Model();
 	const mapper = new Mapper();
 	let selection, range;
 
@@ -184,72 +183,92 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 	// Get selection from passed selection or position or range if at least one is specified.
 	if ( selectionOrPositionOrRange instanceof ModelSelection ) {
 		selection = selectionOrPositionOrRange;
+	} else if ( selectionOrPositionOrRange instanceof DocumentSelection ) {
+		selection = selectionOrPositionOrRange;
 	} else if ( selectionOrPositionOrRange instanceof ModelRange ) {
-		selection = new ModelSelection();
-		selection.addRange( selectionOrPositionOrRange );
+		selection = new ModelSelection( selectionOrPositionOrRange );
 	} else if ( selectionOrPositionOrRange instanceof ModelPosition ) {
-		selection = new ModelSelection();
-		selection.addRange( new ModelRange( selectionOrPositionOrRange, selectionOrPositionOrRange ) );
+		selection = new ModelSelection( selectionOrPositionOrRange );
 	}
 
-	// Setup model to view converter.
-	const viewDocumentFragment = new ViewDocumentFragment();
-	const viewSelection = new ViewSelection();
-	const modelToView = new ModelConversionDispatcher( modelDoc, { mapper, viewSelection } );
+	// Set up conversion.
+	// Create a temporary view controller.
+	const view = new View();
+	const viewDocument = view.document;
+	const viewRoot = new ViewRootEditableElement( 'div' );
 
-	// Bind root elements.
-	mapper.bindElements( node.root, viewDocumentFragment );
+	// Create a temporary root element in view document.
+	viewRoot._document = view.document;
+	viewRoot.rootName = 'main';
+	viewDocument.roots.add( viewRoot );
+
+	// Create and setup downcast dispatcher.
+	const downcastDispatcher = new DowncastDispatcher( { mapper } );
 
-	modelToView.on( 'insert:$text', insertText() );
-	modelToView.on( 'addAttribute', wrapItem( ( value, data ) => {
-		if ( data.item.is( 'textProxy' ) ) {
-			return new ViewAttributeElement( 'model-text-with-attributes', { [ data.attributeKey ]: stringifyAttributeValue( value ) } );
+	// Bind root elements.
+	mapper.bindElements( node.root, viewRoot );
+
+	downcastDispatcher.on( 'insert:$text', insertText() );
+	downcastDispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
+		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection || data.item.is( 'textProxy' ) ) {
+			const converter = wrap( ( modelAttributeValue, viewWriter ) => {
+				return viewWriter.createAttributeElement(
+					'model-text-with-attributes',
+					{ [ data.attributeKey ]: stringifyAttributeValue( modelAttributeValue ) }
+				);
+			} );
+
+			converter( evt, data, conversionApi );
 		}
-	} ) );
-	modelToView.on( 'insert', insertElement( data => {
+	} );
+	downcastDispatcher.on( 'insert', insertElement( modelItem => {
 		// Stringify object types values for properly display as an output string.
-		const attributes = convertAttributes( data.item.getAttributes(), stringifyAttributeValue );
+		const attributes = convertAttributes( modelItem.getAttributes(), stringifyAttributeValue );
 
-		return new ViewContainerElement( data.item.name, attributes );
-	} ) );
-	modelToView.on( 'selection', convertRangeSelection() );
-	modelToView.on( 'selection', convertCollapsedSelection() );
-	modelToView.on( 'selectionAttribute', convertSelectionAttribute( ( value, data ) => {
-		return new ViewAttributeElement( 'model-text-with-attributes', { [ data.key ]: value } );
+		return new ViewContainerElement( modelItem.name, attributes );
 	} ) );
+	downcastDispatcher.on( 'selection', convertRangeSelection() );
+	downcastDispatcher.on( 'selection', convertCollapsedSelection() );
 
 	// Convert model to view.
-	modelToView.convertInsertion( range );
+	const writer = view._writer;
+	downcastDispatcher.convertInsert( range, writer );
 
 	// Convert model selection to view selection.
 	if ( selection ) {
-		modelToView.convertSelection( selection, [] );
+		downcastDispatcher.convertSelection( selection, model.markers, writer );
 	}
 
 	// Parse view to data string.
-	const data = viewStringify( viewDocumentFragment, viewSelection, { sameSelectionCharacters: true } );
+	let data = viewStringify( viewRoot, viewDocument.selection, { sameSelectionCharacters: true } );
+
+	// Removing unneccessary <div> and </div> added because `viewRoot` was also stringified alongside input data.
+	data = data.substr( 5, data.length - 11 );
+
+	view.destroy();
 
 	// Replace valid XML `model-text-with-attributes` element name to `$text`.
 	return data.replace( new RegExp( 'model-text-with-attributes', 'g' ), '$text' );
 }
 
 /**
- * Parses HTML-like string and returns model {@link module:engine/model/rootelement~RootElement rootElement}.
+ * Parses an HTML-like string and returns the model {@link module:engine/model/rootelement~RootElement rootElement}.
  *
- * **Note:** To create {@link module:engine/model/text~Text text} node witch containing attributes use:
+ * **Note:** To create a {@link module:engine/model/text~Text text} node that contains attributes use:
  *
  *		<$text attribute="value">Text data</$text>
  *
  * @param {String} data HTML-like string to be parsed.
- * @param {module:engine/model/schema~Schema} schema Schema instance uses by converters for element validation.
- * @param {Object} options Additional configuration.
- * @param {Array<Object>} [options.selectionAttributes] List of attributes which will be passed to the selection.
- * @param {Boolean} [options.lastRangeBackward=false] If set to true last range will be added as backward.
- * @param {module:engine/model/schema~SchemaPath} [options.context=[ '$root' ]] The conversion context.
- * If not provided default `[ '$root' ]` will be used.
+ * @param {module:engine/model/schema~Schema} schema A schema instance used by converters for element validation.
+ * @param {module:engine/model/batch~Batch} batch A batch used for conversion.
+ * @param {Object} [options={}] Additional configuration.
+ * @param {Array<Object>} [options.selectionAttributes] A list of attributes which will be passed to the selection.
+ * @param {Boolean} [options.lastRangeBackward=false] If set to `true`, the last range will be added as backward.
+ * @param {module:engine/model/schema~SchemaContextDefinition} [options.context='$root'] The conversion context.
+ * If not provided, the default `'$root'` will be used.
  * @returns {module:engine/model/element~Element|module:engine/model/text~Text|
- * module:engine/model/documentfragment~DocumentFragment|Object} Returns parsed model node or
- * object with two fields `model` and `selection` when selection ranges were included in data to parse.
+ * module:engine/model/documentfragment~DocumentFragment|Object} Returns the parsed model node or
+ * an object with two fields: `model` and `selection`, when selection ranges were included in the data to parse.
  */
 export function parse( data, schema, options = {} ) {
 	const mapper = new Mapper();
@@ -273,19 +292,26 @@ export function parse( data, schema, options = {} ) {
 		viewDocumentFragment = parsedResult;
 	}
 
-	// Setup view to model converter.
-	const viewToModel = new ViewConversionDispatcher( { schema, mapper } );
+	// Set up upcast dispatcher.
+	const modelController = new Model();
+	const upcastDispatcher = new UpcastDispatcher( { schema, mapper } );
 
-	viewToModel.on( 'documentFragment', convertToModelFragment() );
-	viewToModel.on( 'element:model-text-with-attributes', convertToModelText( true ) );
-	viewToModel.on( 'element', convertToModelElement() );
-	viewToModel.on( 'text', convertToModelText() );
+	upcastDispatcher.on( 'documentFragment', convertToModelFragment() );
+	upcastDispatcher.on( 'element:model-text-with-attributes', convertToModelText( true ) );
+	upcastDispatcher.on( 'element', convertToModelElement() );
+	upcastDispatcher.on( 'text', convertToModelText() );
+
+	upcastDispatcher.isDebug = true;
 
 	// Convert view to model.
-	let model = viewToModel.convert( viewDocumentFragment.root, { context: options.context || [ '$root' ] } );
+	let model = modelController.change(
+		writer => upcastDispatcher.convert( viewDocumentFragment.root, writer, options.context || '$root' )
+	);
+
+	mapper.bindElements( model, viewDocumentFragment.root );
 
 	// If root DocumentFragment contains only one element - return that element.
-	if ( model.is( 'documentFragment' ) && model.childCount == 1 ) {
+	if ( model.childCount == 1 ) {
 		model = model.getChild( 0 );
 	}
 
@@ -296,16 +322,15 @@ export function parse( data, schema, options = {} ) {
 
 		// Convert ranges.
 		for ( const viewRange of viewSelection.getRanges() ) {
-			ranges.push( ( mapper.toModelRange( viewRange ) ) );
+			ranges.push( mapper.toModelRange( viewRange ) );
 		}
 
 		// Create new selection.
-		selection = new ModelSelection();
-		selection.setRanges( ranges, viewSelection.isBackward );
+		selection = new ModelSelection( ranges, { backward: viewSelection.isBackward } );
 
 		// Set attributes to selection if specified.
-		if ( options.selectionAttributes ) {
-			selection.setAttributesTo( options.selectionAttributes );
+		for ( const [ key, value ] of toMap( options.selectionAttributes || [] ) ) {
+			selection.setAttribute( key, value );
 		}
 	}
 
@@ -321,52 +346,47 @@ export function parse( data, schema, options = {} ) {
 // -- Converters view -> model -----------------------------------------------------
 
 function convertToModelFragment() {
-	return ( evt, data, consumable, conversionApi ) => {
-		const convertedChildren = conversionApi.convertChildren( data.input, consumable, data );
+	return ( evt, data, conversionApi ) => {
+		const childrenResult = conversionApi.convertChildren( data.viewItem, data.modelCursor );
 
-		data.output = new ModelDocumentFragment( modelWriter.normalizeNodes( convertedChildren ) );
-		conversionApi.mapper.bindElements( data.output, data.input );
+		conversionApi.mapper.bindElements( data.modelCursor.parent, data.viewItem );
+
+		data = Object.assign( data, childrenResult );
 
 		evt.stop();
 	};
 }
 
 function convertToModelElement() {
-	return ( evt, data, consumable, conversionApi ) => {
-		const schemaQuery = {
-			name: data.input.name,
-			attributes: Array.from( data.input.getAttributeKeys() ),
-			inside: data.context
-		};
-
-		if ( !conversionApi.schema.check( schemaQuery ) ) {
-			throw new Error( `Element '${ schemaQuery.name }' not allowed in context ${ JSON.stringify( data.context ) }.` );
+	return ( evt, data, conversionApi ) => {
+		const elementName = data.viewItem.name;
+
+		if ( !conversionApi.schema.checkChild( data.modelCursor, elementName ) ) {
+			throw new Error( `Element '${ elementName }' was not allowed in given position.` );
 		}
 
 		// View attribute value is a string so we want to typecast it to the original type.
 		// E.g. `bold="true"` - value will be parsed from string `"true"` to boolean `true`.
-		const attributes = convertAttributes( data.input.getAttributes(), parseAttributeValue );
+		const attributes = convertAttributes( data.viewItem.getAttributes(), parseAttributeValue );
+		const element = conversionApi.writer.createElement( data.viewItem.name, attributes );
+
+		conversionApi.writer.insert( element, data.modelCursor );
 
-		data.output = new ModelElement( data.input.name, attributes );
-		conversionApi.mapper.bindElements( data.output, data.input );
+		conversionApi.mapper.bindElements( element, data.viewItem );
 
-		data.context.push( data.output );
-		data.output.appendChildren( conversionApi.convertChildren( data.input, consumable, data ) );
-		data.context.pop();
+		conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( element ) );
+
+		data.modelRange = ModelRange.createOn( element );
+		data.modelCursor = data.modelRange.end;
 
 		evt.stop();
 	};
 }
 
 function convertToModelText( withAttributes = false ) {
-	return ( evt, data, consumable, conversionApi ) => {
-		const schemaQuery = {
-			name: '$text',
-			inside: data.context
-		};
-
-		if ( !conversionApi.schema.check( schemaQuery ) ) {
-			throw new Error( `Element '${ schemaQuery.name }' not allowed in context ${ JSON.stringify( data.context ) }.` );
+	return ( evt, data, conversionApi ) => {
+		if ( !conversionApi.schema.checkChild( data.modelCursor, '$text' ) ) {
+			throw new Error( 'Text was not allowed in given position.' );
 		}
 
 		let node;
@@ -374,14 +394,17 @@ function convertToModelText( withAttributes = false ) {
 		if ( withAttributes ) {
 			// View attribute value is a string so we want to typecast it to the original type.
 			// E.g. `bold="true"` - value will be parsed from string `"true"` to boolean `true`.
-			const attributes = convertAttributes( data.input.getAttributes(), parseAttributeValue );
+			const attributes = convertAttributes( data.viewItem.getAttributes(), parseAttributeValue );
 
-			node = new ModelText( data.input.getChild( 0 ).data, attributes );
+			node = conversionApi.writer.createText( data.viewItem.getChild( 0 ).data, attributes );
 		} else {
-			node = new ModelText( data.input.data );
+			node = conversionApi.writer.createText( data.viewItem.data );
 		}
 
-		data.output = node;
+		conversionApi.writer.insert( node, data.modelCursor );
+
+		data.modelRange = ModelRange.createFromPositionAndShift( data.modelCursor, node.offsetSize );
+		data.modelCursor = data.modelRange.end;
 
 		evt.stop();
 	};

+ 233 - 209
packages/ckeditor5-engine/src/dev-utils/view.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,10 +8,10 @@
  */
 
 /**
- * Collection of methods for manipulating {@link module:engine/view/view view} for testing purposes.
+ * Collection of methods for manipulating the {@link module:engine/view/view view} for testing purposes.
  */
 
-import Document from '../view/document';
+import View from '../view/view';
 import ViewDocumentFragment from '../view/documentfragment';
 import XmlDataProcessor from '../dataprocessor/xmldataprocessor';
 import ViewElement from '../view/element';
@@ -35,25 +35,26 @@ const allowedTypes = {
 };
 
 /**
- * Writes the contents of the {@link module:engine/view/document~Document Document} to an HTML-like string.
+ * Writes the content of the {@link module:engine/view/document~Document document} to an HTML-like string.
  *
- * @param {module:engine/view/document~Document} document
+ * @param {module:engine/view/view~View} view
  * @param {Object} [options]
- * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true` selection will
- * be not included in returned string.
- * @param {Boolean} [options.rootName='main'] Name of the root from which data should be stringified. If not provided
- * default `main` name will be used.
- * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed (`<container:p>`
+ * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true`, the selection will
+ * not be included in the returned string.
+ * @param {Boolean} [options.rootName='main'] The name of the root from which the data should be stringified. If not provided,
+ * the default `main` name will be used.
+ * @param {Boolean} [options.showType=false] When set to `true`, the type of elements will be printed (`<container:p>`
  * instead of `<p>`, `<attribute:b>` instead of `<b>` and `<empty:img>` instead of `<img>`).
- * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed
+ * @param {Boolean} [options.showPriority=false] When set to `true`, attribute element's priority will be printed
  * (`<span view-priority="12">`, `<b view-priority="10">`).
  * @returns {String} The stringified data.
  */
-export function getData( document, options = {} ) {
-	if ( !( document instanceof Document ) ) {
-		throw new TypeError( 'Document needs to be an instance of module:engine/view/document~Document.' );
+export function getData( view, options = {} ) {
+	if ( !( view instanceof View ) ) {
+		throw new TypeError( 'View needs to be an instance of module:engine/view/view~View.' );
 	}
 
+	const document = view.document;
 	const withoutSelection = !!options.withoutSelection;
 	const rootName = options.rootName || 'main';
 	const root = document.getRoot( rootName );
@@ -72,26 +73,30 @@ export function getData( document, options = {} ) {
 getData._stringify = stringify;
 
 /**
- * Sets the contents of the {@link module:engine/view/document~Document Document} provided as HTML-like string.
+ * Sets the content of the {@link module:engine/view/document~Document document} provided as an HTML-like string.
  *
- * @param {module:engine/view/document~Document} document
- * @param {String} data HTML-like string to write into Document.
+ * @param {module:engine/view/view~View} view
+ * @param {String} data An HTML-like string to write into the document.
  * @param {Object} options
- * @param {String} [options.rootName='main'] Root name where parsed data will be stored. If not provided,
- * default `main` name will be used.
+ * @param {String} [options.rootName='main'] The root name where parsed data will be stored. If not provided,
+ * the default `main` name will be used.
  */
-export function setData( document, data, options = {} ) {
-	if ( !( document instanceof Document ) ) {
-		throw new TypeError( 'Document needs to be an instance of module:engine/view/document~Document.' );
+export function setData( view, data, options = {} ) {
+	if ( !( view instanceof View ) ) {
+		throw new TypeError( 'View needs to be an instance of module:engine/view/view~View.' );
 	}
 
+	const document = view.document;
 	const rootName = options.rootName || 'main';
 	const root = document.getRoot( rootName );
-	const result = setData._parse( data, { rootElement: root } );
 
-	if ( result.view && result.selection ) {
-		document.selection.setTo( result.selection );
-	}
+	view.change( writer => {
+		const result = setData._parse( data, { rootElement: root } );
+
+		if ( result.view && result.selection ) {
+			writer.setSelection( result.selection );
+		}
+	} );
 }
 
 // Set parse as setData private method - needed for testing/spying.
@@ -99,17 +104,17 @@ setData._parse = parse;
 
 /**
  * Converts view elements to HTML-like string representation.
- * Root element can be provided as {@link module:engine/view/text~Text Text}:
+ * A root element can be provided as {@link module:engine/view/text~Text text}:
  *
  *		const text = new Text( 'foobar' );
  *		stringify( text ); // 'foobar'
  *
- * or as {@link module:engine/view/element~Element Element}:
+ * or as an {@link module:engine/view/element~Element element}:
  *
  *		const element = new Element( 'p', null, new Text( 'foobar' ) );
  *		stringify( element ); // '<p>foobar</p>'
  *
- * or as {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment}:
+ * or as a {@link module:engine/view/documentfragment~DocumentFragment document fragment}:
  *
  *		const text = new Text( 'foobar' );
  *		const b = new Element( 'b', { name: 'test' }, text );
@@ -118,44 +123,46 @@ setData._parse = parse;
  *
  *		stringify( fragment ); // '<p style="color:red;"></p><b name="test">foobar</b>'
  *
- * Additionally {@link module:engine/view/selection~Selection Selection} instance can be provided, then ranges from that selection
- * will be included in output data.
- * If range position is placed inside element node, it will be represented with `[` and `]`:
+ * Additionally, a {@link module:engine/view/selection~Selection selection} instance can be provided. Ranges from the selection
+ * will then be included in output data.
+ * If a range position is placed inside the element node, it will be represented with `[` and `]`:
  *
  *		const text = new Text( 'foobar' );
  *		const b = new Element( 'b', null, text );
  *		const p = new Element( 'p', null, b );
- *		const selection = new Selection();
- *		selection.addRange( Range.createFromParentsAndOffsets( p, 0, p, 1 ) );
+ *		const selection = new Selection(
+ *			Range.createFromParentsAndOffsets( p, 0, p, 1 )
+ *		);
  *
  *		stringify( p, selection ); // '<p>[<b>foobar</b>]</p>'
  *
- * If range is placed inside text node, it will be represented with `{` and `}`:
+ * If a range is placed inside the text node, it will be represented with `{` and `}`:
  *
  *		const text = new Text( 'foobar' );
  *		const b = new Element( 'b', null, text );
  *		const p = new Element( 'p', null, b );
- *		const selection = new Selection();
- *		selection.addRange( Range.createFromParentsAndOffsets( text, 1, text, 5 ) );
+ *		const selection = new Selection( Range.createFromParentsAndOffsets( text, 1, text, 5 ) );
  *
  *		stringify( p, selection ); // '<p><b>f{ooba}r</b></p>'
  *
  * ** Note: **
  * It is possible to unify selection markers to `[` and `]` for both (inside and outside text)
- * by setting `sameSelectionCharacters=true` option. It is mainly used when view stringify option is used by model utils.
+ * by setting the `sameSelectionCharacters=true` option. It is mainly used when the view stringify option is used by
+ * model utilities.
  *
  * Multiple ranges are supported:
  *
  *		const text = new Text( 'foobar' );
- *		const selection = new Selection();
- *		selection.addRange( Range.createFromParentsAndOffsets( text, 0, text, 1 ) );
- *		selection.addRange( Range.createFromParentsAndOffsets( text, 3, text, 5 ) );
+ *		const selection = new Selection( [
+ *			Range.createFromParentsAndOffsets( text, 0, text, 1 ) ),
+ *			Range.createFromParentsAndOffsets( text, 3, text, 5 ) )
+ *		] );
  *
  *		stringify( text, selection ); // '{f}oo{ba}r'
  *
- * Instead of {@link module:engine/view/selection~Selection Selection} instance {@link module:engine/view/range~Range Range} or
- * {@link module:engine/view/position~Position Position} instance can be provided. If Range instance is provided - it will be
- * converted to selection containing this range. If Position instance is provided - it will be converted to selection
+ * A {@link module:engine/view/range~Range range} or {@link module:engine/view/position~Position position} instance can be provided
+ * instead of the {@link module:engine/view/selection~Selection selection} instance. If a range instance is provided, it will be
+ * converted to a selection containing this range. If a position instance is provided, it will be converted to a selection
  * containing one range collapsed at this position.
  *
  *		const text = new Text( 'foobar' );
@@ -165,12 +172,12 @@ setData._parse = parse;
  *		stringify( text, range ); // '{f}oobar'
  *		stringify( text, position ); // 'foo{}bar'
  *
- * Additional options object can be provided.
+ * An additional `options` object can be provided.
  * If `options.showType` is set to `true`, element's types will be
- * presented for {@link module:engine/view/attributeelement~AttributeElement AttributeElements},
- * {@link module:engine/view/containerelement~ContainerElement ContainerElements}
- * {@link module:engine/view/emptyelement~EmptyElement EmptyElements}
- * and {@link module:engine/view/uielement~UIElement UIElements}:
+ * presented for {@link module:engine/view/attributeelement~AttributeElement attribute elements},
+ * {@link module:engine/view/containerelement~ContainerElement container elements}
+ * {@link module:engine/view/emptyelement~EmptyElement empty elements}
+ * and {@link module:engine/view/uielement~UIElement UI elements}:
  *
  *		const attribute = new AttributeElement( 'b' );
  *		const container = new ContainerElement( 'p' );
@@ -181,40 +188,39 @@ setData._parse = parse;
  *		getData( empty, null, { showType: true } ); // '<empty:img></empty:img>'
  *		getData( ui, null, { showType: true } ); // '<ui:span></ui:span>'
  *
- * If `options.showPriority` is set to `true`, priority will be displayed for all
- * {@link module:engine/view/attributeelement~AttributeElement AttributeElements}.
+ * If `options.showPriority` is set to `true`, a priority will be displayed for all
+ * {@link module:engine/view/attributeelement~AttributeElement attribute elements}.
  *
  *		const attribute = new AttributeElement( 'b' );
- *		attribute.priority = 20;
+ *		attribute._priority = 20;
  *		getData( attribute, null, { showPriority: true } ); // <b view-priority="20"></b>
  *
  * @param {module:engine/view/text~Text|module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment}
- * node Node to stringify.
+ * node The node to stringify.
  * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|module:engine/view/range~Range}
  * [selectionOrPositionOrRange = null ]
- * Selection instance which ranges will be included in returned string data. If Range instance is provided - it will be
- * converted to selection containing this range. If Position instance is provided - it will be converted to selection
+ * A selection instance whose ranges will be included in the returned string data. If a range instance is provided, it will be
+ * converted to a selection containing this range. If a position instance is provided, it will be converted to a selection
  * containing one range collapsed at this position.
- * @param {Object} [options] Object with additional options.
- * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed (`<container:p>`
+ * @param {Object} [options] An object with additional options.
+ * @param {Boolean} [options.showType=false] When set to `true`, the type of elements will be printed (`<container:p>`
  * instead of `<p>`, `<attribute:b>` instead of `<b>` and `<empty:img>` instead of `<img>`).
- * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed
+ * @param {Boolean} [options.showPriority=false] When set to `true`,  the attribute element's priority will be printed
  * (`<span view-priority="12">`, `<b view-priority="10">`).
- * @param {Boolean} [options.ignoreRoot=false] When set to `true` root's element opening and closing will not be printed.
- * Mainly used by `getData` function to ignore {@link module:engine/view/document~Document Document's} root element.
- * @param {Boolean} [options.sameSelectionCharacters=false] When set to `true` then selection inside text will be marked as `{` and `}`
- * and selection outside text as `[` and `]`. When set to `false` then both will be marked as `[` and `]` only.
- * @returns {String} HTML-like string representing the view.
+ * @param {Boolean} [options.ignoreRoot=false] When set to `true`, the root's element opening and closing will not be printed.
+ * Mainly used by the `getData` function to ignore the {@link module:engine/view/document~Document document's} root element.
+ * @param {Boolean} [options.sameSelectionCharacters=false] When set to `true`, the selection inside the text will be marked as
+ *  `{` and `}` and the selection outside the text as `[` and `]`. When set to `false`, both will be marked as `[` and `]` only.
+ * @returns {String} An HTML-like string representing the view.
  */
 export function stringify( node, selectionOrPositionOrRange = null, options = {} ) {
 	let selection;
 
-	if ( selectionOrPositionOrRange instanceof Position ) {
-		selection = new Selection();
-		selection.addRange( new Range( selectionOrPositionOrRange, selectionOrPositionOrRange ) );
-	} else if ( selectionOrPositionOrRange instanceof Range ) {
-		selection = new Selection();
-		selection.addRange( selectionOrPositionOrRange );
+	if (
+		selectionOrPositionOrRange instanceof Position ||
+		selectionOrPositionOrRange instanceof Range
+	) {
+		selection = new Selection( selectionOrPositionOrRange );
 	} else {
 		selection = selectionOrPositionOrRange;
 	}
@@ -225,23 +231,23 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
 }
 
 /**
- * Parses HTML-like string and returns view tree nodes.
- * Simple string will be converted to {@link module:engine/view/text~Text Text} node:
+ * Parses an HTML-like string and returns view tree nodes.
+ * A simple string will be converted to a {@link module:engine/view/text~Text text} node:
  *
- *		parse( 'foobar' ); // Returns instance of Text.
+ *		parse( 'foobar' ); // Returns an instance of text.
  *
- * {@link module:engine/view/element~Element Elements} will be parsed with attributes an children:
+ * {@link module:engine/view/element~Element Elements} will be parsed with attributes as children:
  *
- *		parse( '<b name="baz">foobar</b>' ); // Returns instance of Element with `baz` attribute and text child node.
+ *		parse( '<b name="baz">foobar</b>' ); // Returns an instance of element with the `baz` attribute and a text child node.
  *
- * Multiple nodes provided on root level will be converted to
- * {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment}:
+ * Multiple nodes provided on root level will be converted to a
+ * {@link module:engine/view/documentfragment~DocumentFragment document fragment}:
  *
- *		parse( '<b>foo</b><i>bar</i>' ); // Returns DocumentFragment with two child elements.
+ *		parse( '<b>foo</b><i>bar</i>' ); // Returns a document fragment with two child elements.
  *
- * Method can parse multiple {@link module:engine/view/range~Range ranges} provided in string data and return
- * {@link module:engine/view/selection~Selection Selection} instance containing these ranges. Ranges placed inside
- * {@link module:engine/view/text~Text Text} nodes should be marked using `{` and `}` brackets:
+ * The method can parse multiple {@link module:engine/view/range~Range ranges} provided in string data and return a
+ * {@link module:engine/view/selection~Selection selection} instance containing these ranges. Ranges placed inside
+ * {@link module:engine/view/text~Text text} nodes should be marked using `{` and `}` brackets:
  *
  *		const { text, selection } = parse( 'f{ooba}r' );
  *
@@ -251,49 +257,51 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
  *
  * ** Note: **
  * It is possible to unify selection markers to `[` and `]` for both (inside and outside text)
- * by setting `sameSelectionCharacters=true` option. It is mainly used when view parse option is used by model utils.
+ * by setting `sameSelectionCharacters=true` option. It is mainly used when the view parse option is used by model utilities.
  *
- * Sometimes there is a need for defining order of ranges inside created selection. This can be achieved by providing
- * ranges order array as additional parameter:
+ * Sometimes there is a need for defining the order of ranges inside the created selection. This can be achieved by providing
+ * the range order array as an additional parameter:
  *
  *		const { root, selection } = parse( '{fo}ob{ar}{ba}z', { order: [ 2, 3, 1 ] } );
  *
- * In above example first range (`{fo}`) will be added to selection as second one, second range (`{ar}`) will be added
- * as third and third range (`{ba}`) will be added as first one.
+ * In the example above, the first range (`{fo}`) will be added to the selection as the second one, the second range (`{ar}`) will be
+ * added as the third and the third range (`{ba}`) will be added as the first one.
  *
- * If selection's last range should be added as backward one (so the {@link module:engine/view/selection~Selection#anchor selection
- * anchor} is represented by `end` position and {@link module:engine/view/selection~Selection#focus selection focus} is
- * represented by `start` position) use `lastRangeBackward` flag:
+ * If the selection's last range should be added as a backward one (so the {@link module:engine/view/selection~Selection#anchor selection
+ * anchor} is represented by the `end` position and {@link module:engine/view/selection~Selection#focus selection focus} is
+ * represented by the `start` position), use the `lastRangeBackward` flag:
  *
  *		const { root, selection } = parse( `{foo}bar{baz}`, { lastRangeBackward: true } );
  *
- * Other examples and edge cases:
+ * Some more examples and edge cases:
  *
- *		// Returns empty DocumentFragment.
+ *		// Returns an empty document fragment.
  *		parse( '' );
  *
- *		// Returns empty DocumentFragment and collapsed selection.
+ *		// Returns an empty document fragment and a collapsed selection.
  *		const { root, selection } = parse( '[]' );
  *
- *		// Returns Element and selection that is placed inside of DocumentFragment containing that element.
+ *		// Returns an element and a selection that is placed inside the document fragment containing that element.
  *		const { root, selection } = parse( '[<a></a>]' );
  *
- * @param {String} data HTML-like string to be parsed.
+ * @param {String} data An HTML-like string to be parsed.
  * @param {Object} options
- * @param {Array.<Number>} [options.order] Array with order of parsed ranges added to returned
- * {@link module:engine/view/selection~Selection Selection} instance. Each element should represent desired position of each range in
- * selection instance. For example: `[2, 3, 1]` means that first range will be placed as second, second as third and third as first.
- * @param {Boolean} [options.lastRangeBackward=false] If set to true last range will be added as backward to the returned
- * {@link module:engine/view/selection~Selection Selection} instance.
+ * @param {Array.<Number>} [options.order] An array with the order of parsed ranges added to the returned
+ * {@link module:engine/view/selection~Selection Selection} instance. Each element should represent the desired position of each range in
+ * the selection instance. For example: `[2, 3, 1]` means that the first range will be placed as the second, the second as the third and
+ * the third as the first.
+ * @param {Boolean} [options.lastRangeBackward=false] If set to `true`, the last range will be added as backward to the returned
+ * {@link module:engine/view/selection~Selection selection} instance.
  * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment}
- * [options.rootElement=null] Default root to use when parsing elements.
- * When set to `null` root element will be created automatically. If set to
- * {@link module:engine/view/element~Element Element} or {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment}
- * - this node will be used as root for all parsed nodes.
- * @param {Boolean} [options.sameSelectionCharacters=false] When set to `false` then selection inside text should be marked using
- * `{` and `}` and selection outside text using `[` and `]`. When set to `true` then both should be marked with `[` and `]` only.
+ * [options.rootElement=null] The default root to use when parsing elements.
+ * When set to `null`, the root element will be created automatically. If set to
+ * {@link module:engine/view/element~Element Element} or {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment},
+ * this node will be used as the root for all parsed nodes.
+ * @param {Boolean} [options.sameSelectionCharacters=false] When set to `false`, the selection inside the text should be marked using
+ * `{` and `}` and the selection outside the ext using `[` and `]`. When set to `true`, both should be marked with `[` and `]` only.
  * @returns {module:engine/view/text~Text|module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|Object}
- * Returns parsed view node or object with two fields `view` and `selection` when selection ranges were included in data to parse.
+ * Returns the parsed view node or an object with two fields: `view` and `selection` when selection ranges were included in the data
+ * to parse.
  */
 export function parse( data, options = {} ) {
 	options.order = options.order || [];
@@ -314,10 +322,10 @@ export function parse( data, options = {} ) {
 	// If custom root is provided - move all nodes there.
 	if ( options.rootElement ) {
 		const root = options.rootElement;
-		const nodes = view.removeChildren( 0, view.childCount );
+		const nodes = view._removeChildren( 0, view.childCount );
 
-		root.removeChildren( 0, root.childCount );
-		root.appendChildren( nodes );
+		root._removeChildren( 0, root.childCount );
+		root._appendChildren( nodes );
 
 		view = root;
 	}
@@ -332,8 +340,7 @@ export function parse( data, options = {} ) {
 
 	// When ranges are present - return object containing view, and selection.
 	if ( ranges.length ) {
-		const selection = new Selection();
-		selection.setRanges( ranges, !!options.lastRangeBackward );
+		const selection = new Selection( ranges, { backward: !!options.lastRangeBackward } );
 
 		return {
 			view,
@@ -343,38 +350,39 @@ export function parse( data, options = {} ) {
 
 	// If single element is returned without selection - remove it from parent and return detached element.
 	if ( view.parent ) {
-		view.remove();
+		view._remove();
 	}
 
 	return view;
 }
 
 /**
- * Private helper class used for converting ranges represented as text inside view {@link module:engine/view/text~Text Text nodes}.
+ * Private helper class used for converting ranges represented as text inside view {@link module:engine/view/text~Text text nodes}.
  *
  * @private
  */
 class RangeParser {
 	/**
-	 * Create RangeParser instance.
+	 * Creates a range parser instance.
 	 *
-	 * @param {Object} options RangeParser configuration.
-	 * @param {Boolean} [options.sameSelectionCharacters=false] When set to `true` it means that selection inside text is marked as
-	 * `{` and `}` and selection outside text as `[` and `]`. When set to `false` then both are marked as `[` and `]`.
+	 * @param {Object} options The range parser configuration.
+	 * @param {Boolean} [options.sameSelectionCharacters=false] When set to `true`, the selection inside the text is marked as
+	 * `{` and `}` and the selection outside the text as `[` and `]`. When set to `false`, both are marked as `[` and `]`.
 	 */
 	constructor( options ) {
 		this.sameSelectionCharacters = !!options.sameSelectionCharacters;
 	}
 
 	/**
-	 * Parses the view, and returns ranges represented inside {@link module:engine/view/text~Text Text nodes}.
-	 * Method will remove all occurrences of `{`, `}`, `[` and `]` from found text nodes. If text node is empty after
-	 * the process - it will be removed too.
+	 * Parses the view and returns ranges represented inside {@link module:engine/view/text~Text text nodes}.
+	 * The method will remove all occurrences of `{`, `}`, `[` and `]` from found text nodes. If a text node is empty after
+	 * the process, it will be removed, too.
 	 *
-	 * @param {module:engine/view/node~Node} node Starting node.
-	 * @param {Array.<Number>} order Ranges order. Each element should represent desired position of the range after
-	 * sorting. For example: `[2, 3, 1]` means that first range will be placed as second, second as third and third as first.
-	 * @returns {Array.<module:engine/view/range~Range>} Array with ranges found.
+	 * @param {module:engine/view/node~Node} node The starting node.
+	 * @param {Array.<Number>} order The order of ranges. Each element should represent the desired position of the range after
+	 * sorting. For example: `[2, 3, 1]` means that the first range will be placed as the second, the second as the third and the third
+	 * as the first.
+	 * @returns {Array.<module:engine/view/range~Range>} An array with ranges found.
 	 */
 	parse( node, order ) {
 		this._positions = [];
@@ -400,9 +408,8 @@ class RangeParser {
 	}
 
 	/**
-	 * Gathers positions of brackets inside view tree starting from provided node. Method will remove all occurrences of
-	 * `{`, `}`, `[` and `]` from found text nodes. If text node is empty after the process - it will be removed
-	 * too.
+	 * Gathers positions of brackets inside the view tree starting from the provided node. The method will remove all occurrences of
+	 * `{`, `}`, `[` and `]` from found text nodes. If a text node is empty after the process, it will be removed, too.
 	 *
 	 * @private
 	 * @param {module:engine/view/node~Node} node Staring node.
@@ -442,13 +449,13 @@ class RangeParser {
 			}
 
 			text = text.replace( regexp, '' );
-			node.data = text;
+			node._data = text;
 			const index = node.index;
 			const parent = node.parent;
 
 			// Remove empty text nodes.
 			if ( !text ) {
-				node.remove();
+				node._remove();
 			}
 
 			for ( const item of brackets ) {
@@ -500,13 +507,14 @@ class RangeParser {
 	}
 
 	/**
-	 * Sort ranges in given order. Ranges order should be an array, each element should represent desired position
+	 * Sorts ranges in a given order. Range order should be an array and each element should represent the desired position
 	 * of the range after sorting.
-	 * For example: `[2, 3, 1]` means that first range will be placed as second, second as third and third as first.
+	 * For example: `[2, 3, 1]` means that the first range will be placed as the second, the second as the third and the third
+	 * as the first.
 	 *
 	 * @private
 	 * @param {Array.<module:engine/view/range~Range>} ranges Ranges to sort.
-	 * @param {Array.<Number>} rangesOrder Array with new ranges order.
+	 * @param {Array.<Number>} rangesOrder An array with new range order.
 	 * @returns {Array} Sorted ranges array.
 	 */
 	_sortRanges( ranges, rangesOrder ) {
@@ -566,24 +574,24 @@ class RangeParser {
 }
 
 /**
- * Private helper class used for converting view tree to string.
+ * Private helper class used for converting the view tree to a string.
  *
  * @private
  */
 class ViewStringify {
 	/**
-	 * Creates ViewStringify instance.
+	 * Creates a view stringify instance.
 	 *
 	 * @param root
-	 * @param {module:engine/view/selection~Selection} selection Selection which ranges should be also converted to string.
-	 * @param {Object} options Options object.
-	 * @param {Boolean} [options.showType=false] When set to `true` type of elements will be printed (`<container:p>`
+	 * @param {module:engine/view/selection~Selection} selection A selection whose ranges should also be converted to a string.
+	 * @param {Object} options An options object.
+	 * @param {Boolean} [options.showType=false] When set to `true`, the type of elements will be printed (`<container:p>`
 	 * instead of `<p>`, `<attribute:b>` instead of `<b>` and `<empty:img>` instead of `<img>`).
-	 * @param {Boolean} [options.showPriority=false] When set to `true` AttributeElement's priority will be printed.
-	 * @param {Boolean} [options.ignoreRoot=false] When set to `true` root's element opening and closing tag will not
+	 * @param {Boolean} [options.showPriority=false] When set to `true`, the attribute element's priority will be printed.
+	 * @param {Boolean} [options.ignoreRoot=false] When set to `true`, the root's element opening and closing tag will not
 	 * be outputted.
-	 * @param {Boolean} [options.sameSelectionCharacters=false] When set to `true` it means that selection inside text is marked as
-	 * `{` and `}` and selection outside text as `[` and `]`. When set to `false` then both are marked as `[` and `]`.
+	 * @param {Boolean} [options.sameSelectionCharacters=false] When set to `true`, the selection inside the text is marked as
+	 * `{` and `}` and the selection outside the text as `[` and `]`. When set to `false`, both are marked as `[` and `]`.
 	 */
 	constructor( root, selection, options ) {
 		this.root = root;
@@ -601,7 +609,7 @@ class ViewStringify {
 	}
 
 	/**
-	 * Converts view to string.
+	 * Converts the view to a string.
 	 *
 	 * @returns {String} String representation of the view elements.
 	 */
@@ -615,8 +623,8 @@ class ViewStringify {
 	}
 
 	/**
-	 * Executes simple walker that iterates over all elements in the view tree starting from root element.
-	 * Calls `callback` with parsed chunks of string data.
+	 * Executes a simple walker that iterates over all elements in the view tree starting from the root element.
+	 * Calls the `callback` with parsed chunks of string data.
 	 *
 	 * @private
 	 * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element|module:engine/view/text~Text} root
@@ -650,8 +658,8 @@ class ViewStringify {
 	}
 
 	/**
-	 * Checks if given {@link module:engine/view/element~Element Element} has {@link module:engine/view/range~Range#start range start} or
-	 * {@link module:engine/view/range~Range#start range end} placed at given offset and returns its string representation.
+	 * Checks if a given {@link module:engine/view/element~Element element} has a {@link module:engine/view/range~Range#start range start}
+	 * or a {@link module:engine/view/range~Range#start range end} placed at a given offset and returns its string representation.
 	 *
 	 * @private
 	 * @param {module:engine/view/element~Element} element
@@ -680,8 +688,9 @@ class ViewStringify {
 	}
 
 	/**
-	 * Checks if given {@link module:engine/view/element~Element Text node} has {@link module:engine/view/range~Range#start range start} or
-	 * {@link module:engine/view/range~Range#start range end} placed somewhere inside. Returns string representation of text
+	 * Checks if a given {@link module:engine/view/element~Element Text node} has a
+	 * {@link module:engine/view/range~Range#start range start} or a
+	 * {@link module:engine/view/range~Range#start range end} placed somewhere inside. Returns a string representation of text
 	 * with range delimiters placed inside.
 	 *
 	 * @private
@@ -734,10 +743,10 @@ class ViewStringify {
 	}
 
 	/**
-	 * Converts passed {@link module:engine/view/element~Element Element} to opening tag.
-	 * Depending on current configuration opening tag can be simple (`<a>`), contain type prefix (`<container:p>`,
-	 * `<attribute:a>` or `<empty:img>`), contain priority information ( `<attribute:a view-priority="20">` ).
-	 * Element's attributes also will be included (`<a href="http://ckeditor.com" name="foobar">`).
+	 * Converts the passed {@link module:engine/view/element~Element element} to an opening tag.
+	 * Depending on the current configuration, the opening tag can be simple (`<a>`), contain a type prefix (`<container:p>`,
+	 * `<attribute:a>` or `<empty:img>`) or contain priority information ( `<attribute:a view-priority="20">` ).
+	 * Element attributes will also be included (`<a href="https://ckeditor.com" name="foobar">`).
 	 *
 	 * @private
 	 * @param {module:engine/view/element~Element} element
@@ -755,8 +764,8 @@ class ViewStringify {
 	}
 
 	/**
-	 * Converts passed {@link module:engine/view/element~Element Element} to closing tag.
-	 * Depending on current configuration closing tag can be simple (`</a>`) or contain type prefix (`</container:p>`,
+	 * Converts the passed {@link module:engine/view/element~Element element} to a closing tag.
+	 * Depending on the current configuration, the closing tag can be simple (`</a>`) or contain a type prefix (`</container:p>`,
 	 * `</attribute:a>` or `</empty:img>`).
 	 *
 	 * @private
@@ -771,14 +780,14 @@ class ViewStringify {
 	}
 
 	/**
-	 * Converts passed {@link module:engine/view/element~Element Element's} type to its string representation
+	 * Converts the passed {@link module:engine/view/element~Element element's} type to its string representation
 	 *
 	 * Returns:
-	 * * 'attribute' for {@link module:engine/view/attributeelement~AttributeElement AttributeElements},
-	 * * 'container' for {@link module:engine/view/containerelement~ContainerElement ContainerElements},
-	 * * 'empty' for {@link module:engine/view/emptyelement~EmptyElement EmptyElements}.
-	 * * 'ui' for {@link module:engine/view/uielement~UIElement UIElements}.
-	 * * empty string when current configuration is preventing showing elements' types.
+	 * * 'attribute' for {@link module:engine/view/attributeelement~AttributeElement attribute elements},
+	 * * 'container' for {@link module:engine/view/containerelement~ContainerElement container elements},
+	 * * 'empty' for {@link module:engine/view/emptyelement~EmptyElement empty elements}.
+	 * * 'ui' for {@link module:engine/view/uielement~UIElement UI elements}.
+	 * * an empty string when the current configuration is preventing showing elements' types.
 	 *
 	 * @private
 	 * @param {module:engine/view/element~Element} element
@@ -797,10 +806,10 @@ class ViewStringify {
 	}
 
 	/**
-	 * Converts passed {@link module:engine/view/element~Element Element} to its priority representation.
-	 * Priority string representation will be returned when passed element is an instance of
-	 * {@link module:engine/view/attributeelement~AttributeElement AttributeElement} and current configuration allow to show priority.
-	 * Otherwise returns empty string.
+	 * Converts the passed {@link module:engine/view/element~Element element} to its priority representation.
+	 * The priority string representation will be returned when the passed element is an instance of
+	 * {@link module:engine/view/attributeelement~AttributeElement attribute element} and the current configuration allows to show the
+	 * priority. Otherwise returns an empty string.
 	 *
 	 * @private
 	 * @param {module:engine/view/element~Element} element
@@ -815,8 +824,8 @@ class ViewStringify {
 	}
 
 	/**
-	 * Converts passed {@link module:engine/view/element~Element Element} attributes to their string representation.
-	 * If element has no attributes - empty string is returned.
+	 * Converts the passed {@link module:engine/view/element~Element element} attributes to their string representation.
+	 * If an element has no attributes, an empty string is returned.
 	 *
 	 * @private
 	 * @param {module:engine/view/element~Element} element
@@ -827,25 +836,40 @@ class ViewStringify {
 		const keys = [ ...element.getAttributeKeys() ].sort();
 
 		for ( const attribute of keys ) {
-			attributes.push( `${ attribute }="${ element.getAttribute( attribute ) }"` );
+			let attributeValue;
+
+			if ( attribute === 'class' ) {
+				attributeValue = [ ...element.getClassNames() ]
+					.sort()
+					.join( ' ' );
+			} else if ( attribute === 'style' ) {
+				attributeValue = [ ...element.getStyleNames() ]
+					.sort()
+					.map( style => `${ style }:${ element.getStyle( style ) }` )
+					.join( ';' );
+			} else {
+				attributeValue = element.getAttribute( attribute );
+			}
+
+			attributes.push( `${ attribute }="${ attributeValue }"` );
 		}
 
 		return attributes.join( ' ' );
 	}
 }
 
-// Converts {@link module:engine/view/element~Element Elements} to
-// {@link module:engine/view/attributeelement~AttributeElement AttributeElements},
-// {@link module:engine/view/containerelement~ContainerElement ContainerElements},
-// {@link module:engine/view/emptyelement~EmptyElement EmptyElements} or
-// {@link module:engine/view/uielement~UIElement UIElements}.
-// It converts whole tree starting from the `rootNode`. Conversion is based on element names.
-// See `_convertElement` method for more details.
+// Converts {@link module:engine/view/element~Element elements} to
+// {@link module:engine/view/attributeelement~AttributeElement attribute elements},
+// {@link module:engine/view/containerelement~ContainerElement container elements},
+// {@link module:engine/view/emptyelement~EmptyElement empty elements} or
+// {@link module:engine/view/uielement~UIElement UI elements}.
+// It converts the whole tree starting from the `rootNode`. The conversion is based on element names.
+// See the `_convertElement` method for more details.
 //
 // @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|module:engine/view/text~Text}
-//  rootNode Root node to convert.
+//  rootNode The root node to convert.
 // @returns {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|
-// module:engine/view/text~Text} Root node of converted elements.
+// module:engine/view/text~Text} The root node of converted elements.
 function _convertViewElements( rootNode ) {
 	if ( rootNode.is( 'element' ) || rootNode.is( 'documentFragment' ) ) {
 		// Convert element or leave document fragment.
@@ -863,7 +887,7 @@ function _convertViewElements( rootNode ) {
 				throw new Error( 'Parse error - cannot parse inside UIElement.' );
 			}
 
-			convertedElement.appendChildren( _convertViewElements( child ) );
+			convertedElement._appendChildren( _convertViewElements( child ) );
 		}
 
 		return convertedElement;
@@ -872,27 +896,27 @@ function _convertViewElements( rootNode ) {
 	return rootNode;
 }
 
-// Converts {@link module:engine/view/element~Element Element} to
-// {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
-// {@link module:engine/view/containerelement~ContainerElement ContainerElement},
-// {@link module:engine/view/emptyelement~EmptyElement EmptyElement} or
-// {@link module:engine/view/uielement~UIElement UIElement}.
-// If element's name is in format `attribute:b` with `view-priority="11"` attribute it will be converted to
-// {@link module:engine/view/attributeelement~AttributeElement AttributeElement} with priority 11.
-// If element's name is in format `container:p` - it will be converted to
-// {@link module:engine/view/containerelement~ContainerElement ContainerElement}.
-// If element's name is in format `empty:img` - it will be converted to
-// {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
-// If element's name is in format `ui:span` - it will be converted to
-// {@link module:engine/view/uielement~UIElement UIElement}.
-// If element's name will not contain any additional information - {@link module:engine/view/element~Element view Element} will be
+// Converts an {@link module:engine/view/element~Element element} to
+// {@link module:engine/view/attributeelement~AttributeElement attribute element},
+// {@link module:engine/view/containerelement~ContainerElement container element},
+// {@link module:engine/view/emptyelement~EmptyElement empty element} or
+// {@link module:engine/view/uielement~UIElement UI element}.
+// If the element's name is in the format of `attribute:b` with `view-priority="11"` attribute, it will be converted to
+// an {@link module:engine/view/attributeelement~AttributeElement attribute element} with a priority of 11.
+// If the element's name is in the format of `container:p`, it will be converted to
+// a {@link module:engine/view/containerelement~ContainerElement container element}.
+// If the element's name is in the format of `empty:img`, it will be converted to
+// an {@link module:engine/view/emptyelement~EmptyElement empty element}.
+// If the element's name is in the format of `ui:span`, it will be converted to
+// a {@link module:engine/view/uielement~UIElement UI element}.
+// If the element's name does not contain any additional information, a {@link module:engine/view/element~Element view Element} will be
 // returned.
 //
-// @param {module:engine/view/element~Element} viewElement View element to convert.
+// @param {module:engine/view/element~Element} viewElement A view element to convert.
 // @returns {module:engine/view/element~Element|module:engine/view/attributeelement~AttributeElement|
 // module:engine/view/emptyelement~EmptyElement|module:engine/view/uielement~UIElement|
-// module:engine/view/containerelement~ContainerElement} Tree view
-// element converted according to it's name.
+// module:engine/view/containerelement~ContainerElement} A tree view
+// element converted according to its name.
 function _convertElement( viewElement ) {
 	const info = _convertElementNameAndPriority( viewElement );
 	const ElementConstructor = allowedTypes[ info.type ];
@@ -900,35 +924,35 @@ function _convertElement( viewElement ) {
 
 	if ( newElement.is( 'attributeElement' ) ) {
 		if ( info.priority !== null ) {
-			newElement.priority = info.priority;
+			newElement._priority = info.priority;
 		}
 	}
 
 	// Move attributes.
 	for ( const attributeKey of viewElement.getAttributeKeys() ) {
-		newElement.setAttribute( attributeKey, viewElement.getAttribute( attributeKey ) );
+		newElement._setAttribute( attributeKey, viewElement.getAttribute( attributeKey ) );
 	}
 
 	return newElement;
 }
 
-// Converts `view-priority` attribute and {@link module:engine/view/element~Element#name Element's name} information needed for creating
-// {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
-// {@link module:engine/view/containerelement~ContainerElement ContainerElement},
-// {@link module:engine/view/emptyelement~EmptyElement EmptyElement} or,
-// {@link module:engine/view/uielement~UIElement UIElement}.
-// Name can be provided in two formats: as a simple element's name (`div`), or as a type and name (`container:div`,
+// Converts the `view-priority` attribute and the {@link module:engine/view/element~Element#name element's name} information needed for
+// creating {@link module:engine/view/attributeelement~AttributeElement attribute element},
+// {@link module:engine/view/containerelement~ContainerElement container element},
+// {@link module:engine/view/emptyelement~EmptyElement empty element} or
+// {@link module:engine/view/uielement~UIElement UI element}.
+// The name can be provided in two formats: as a simple element's name (`div`), or as a type and name (`container:div`,
 // `attribute:span`, `empty:img`, `ui:span`);
 //
-// @param {module:engine/view/element~Element} element Element which name should be converted.
-// @returns {Object} info Object with parsed information.
-// @returns {String} info.name Parsed name of the element.
-// @returns {String|null} info.type Parsed type of the element, can be `attribute`, `container` or `empty`.
-// returns {Number|null} info.priority Parsed priority of the element.
+// @param {module:engine/view/element~Element} element The element whose name should be converted.
+// @returns {Object} info An object with parsed information.
+// @returns {String} info.name The parsed name of the element.
+// @returns {String|null} info.type The parsed type of the element. It can be `attribute`, `container` or `empty`.
+// returns {Number|null} info.priority The parsed priority of the element.
 function _convertElementNameAndPriority( viewElement ) {
 	const parts = viewElement.name.split( ':' );
 	const priority = _convertPriority( viewElement.getAttribute( 'view-priority' ) );
-	viewElement.removeAttribute( 'view-priority' );
+	viewElement._removeAttribute( 'view-priority' );
 
 	if ( parts.length == 1 ) {
 		return {
@@ -952,7 +976,7 @@ function _convertElementNameAndPriority( viewElement ) {
 	throw new Error( `Parse error - cannot parse element's name: ${ viewElement.name }.` );
 }
 
-// Checks if element's type is allowed. Returns `attribute`, `container`, `empty` or `null`.
+// Checks if the element's type is allowed. Returns `attribute`, `container`, `empty` or `null`.
 //
 // @param {String} type
 // @returns {String|null}
@@ -960,7 +984,7 @@ function _convertType( type ) {
 	return allowedTypes[ type ] ? type : null;
 }
 
-// Checks if given priority is allowed. Returns null if priority cannot be converted.
+// Checks if a given priority is allowed. Returns null if the priority cannot be converted.
 //
 // @param {String} priorityString
 // returns {Number|Null}

+ 34 - 95
packages/ckeditor5-engine/src/model/batch.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -7,82 +7,72 @@
  * @module engine/model/batch
  */
 
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
 /**
- * `Batch` instance groups document changes ({@link module:engine/model/delta/delta~Delta deltas}). All deltas grouped in a single `Batch`
- * can be reverted together, so you can think about `Batch` as of a single undo step. If you want to extend given undo step you
- * can call another method on the same `Batch` object. If you want to create a separate undo step you can create a new `Batch`.
- *
- * For example to create two separate undo steps you can call:
- *
- *		doc.batch().insert( firstPosition, 'foo' );
- *		doc.batch().insert( secondPosition, 'bar' );
- *
- * To create a single undo step:
- *
- *		const batch = doc.batch();
- *		batch.insert( firstPosition, 'foo' );
- *		batch.insert( secondPosition, 'bar' );
+ * A batch instance groups model changes ({@link module:engine/model/delta/delta~Delta deltas}). All deltas grouped in a single batch
+ * can be reverted together, so you can think about a batch as of a single undo step. If you want to extend a given undo step, you
+ * can add more changes to the batch using {@link module:engine/model/model~Model#enqueueChange}:
  *
- * Note that all document modification methods (insert, remove, split, etc.) are chainable so you can shorten code to:
+ *		model.enqueueChange( batch, writer => {
+ *			writer.insertText( 'foo', paragraph, 'end' );
+ *		} );
  *
- *		doc.batch().insert( firstPosition, 'foo' ).insert( secondPosition, 'bar' );
+ * @see module:engine/model/model~Model#enqueueChange
+ * @see module:engine/model/model~Model#change
  */
 export default class Batch {
 	/**
-	 * Creates `Batch` instance. Not recommended to use directly, use {@link module:engine/model/document~Document#batch} instead.
+	 * Creates a batch instance.
 	 *
-	 * @param {module:engine/model/document~Document} document Document which this Batch changes.
-	 * @param {'transparent'|'default'} [type='default'] Type of the batch.
+	 * @see module:engine/model/model~Model#enqueueChange
+	 * @see module:engine/model/model~Model#change
+	 * @param {'transparent'|'default'} [type='default'] The type of the batch.
 	 */
-	constructor( document, type = 'default' ) {
+	constructor( type = 'default' ) {
 		/**
-		 * Document which this batch changes.
+		 * An array of deltas that compose this batch.
 		 *
 		 * @readonly
-		 * @member {module:engine/model/document~Document} module:engine/model/batch~Batch#document
-		 */
-		this.document = document;
-
-		/**
-		 * Array of deltas which compose this batch.
-		 *
-		 * @readonly
-		 * @member {Array.<module:engine/model/delta/delta~Delta>} module:engine/model/batch~Batch#deltas
+		 * @type {Array.<module:engine/model/delta/delta~Delta>}
 		 */
 		this.deltas = [];
 
 		/**
-		 * Type of the batch.
+		 * The type of the batch.
 		 *
-		 * Can be one of the following values:
-		 * * `'default'` - all "normal" batches, most commonly used type.
-		 * * `'transparent'` - batch that should be ignored by other features, i.e. initial batch or collaborative editing changes.
+		 * It can be one of the following values:
+		 * * `'default'` &ndash; All "normal" batches. This is the most commonly used type.
+		 * * `'transparent'` &ndash; A batch that should be ignored by other features, i.e. an initial batch or collaborative editing
+		 * changes.
 		 *
 		 * @readonly
-		 * @member {'transparent'|'default'} module:engine/model/batch~Batch#type
+		 * @type {'transparent'|'default'}
 		 */
 		this.type = type;
 	}
 
 	/**
-	 * Returns this batch base version, which is equal to the base version of first delta in the batch.
-	 * If there are no deltas in the batch, it returns `null`.
+	 * Returns the base version of this batch, which is equal to the base version of the first delta (which has the base version set)
+	 * in the batch. If there are no deltas in the batch or neither delta has the base version set, it returns `null`.
 	 *
 	 * @readonly
 	 * @type {Number|null}
 	 */
 	get baseVersion() {
-		return this.deltas.length > 0 ? this.deltas[ 0 ].baseVersion : null;
+		for ( const delta of this.deltas ) {
+			if ( delta.baseVersion !== null ) {
+				return delta.baseVersion;
+			}
+		}
+
+		return null;
 	}
 
 	/**
-	 * Adds delta to the batch instance. All modification methods (insert, remove, split, etc.) use this method
+	 * Adds a delta to the batch instance. All modification methods (insert, remove, split, etc.) use this method
 	 * to add created deltas.
 	 *
-	 * @param {module:engine/model/delta/delta~Delta} delta Delta to add.
-	 * @return {module:engine/model/delta/delta~Delta} Added delta.
+	 * @param {module:engine/model/delta/delta~Delta} delta A delta to add.
+	 * @return {module:engine/model/delta/delta~Delta} An added delta.
 	 */
 	addDelta( delta ) {
 		delta.batch = this;
@@ -102,54 +92,3 @@ export default class Batch {
 		}
 	}
 }
-
-/**
- * Function to register batch methods. To make code scalable `Batch` do not have modification
- * methods built in. They can be registered using this method.
- *
- * This method checks if there is no naming collision and throws `batch-register-taken` if the method name
- * is already taken.
- *
- * Besides that no magic happens here, the method is added to the `Batch` class prototype.
- *
- * For example:
- *
- *		Batch.register( 'insert', function( position, nodes ) {
- *			// You can use a class inheriting from `Delta` if that class should handle OT in a special way.
- *			const delta = new Delta();
- *
- *			// Add delta to the Batch instance. It is important to add a delta to the batch before applying any operation.
- *			this.addDelta( delta );
- *
- *			// Create operations which should be components of this delta.
- *			const operation = new InsertOperation( position, nodes, this.document.version );
- *
- *			// Add operation to the delta. It is important to add operation before applying it.
- *			delta.addOperation( operation );
- *
- *			// Remember to apply every operation, no magic, you need to do it manually.
- *			this.document.applyOperation( operation );
- *
- *			// Make this method chainable.
- *			return this;
- *		} );
- *
- * @method module:engine/model/batch~Batch.register
- * @param {String} name Method name.
- * @param {Function} creator Method body.
- */
-export function register( name, creator ) {
-	if ( Batch.prototype[ name ] ) {
-		/**
-		 * This batch method name is already taken.
-		 *
-		 * @error batch-register-taken
-		 * @param {String} name
-		 */
-		throw new CKEditorError(
-			'model-batch-register-taken: This batch method name is already taken.',
-			{ name } );
-	}
-
-	Batch.prototype[ name ] = creator;
-}

+ 2 - 151
packages/ckeditor5-engine/src/model/delta/attributedelta.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -9,16 +9,12 @@
 
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
-import { register } from '../batch';
-import AttributeOperation from '../operation/attributeoperation';
-import RootAttributeOperation from '../operation/rootattributeoperation';
 import NoOperation from '../operation/nooperation';
-import Position from '../position';
 import Range from '../range';
 
 /**
  * To provide specific OT behavior and better collisions solving, methods to change attributes
- * ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute})
+ * ({@link module:engine/model/writer~Writer#setAttribute} and {@link module:engine/model/writer~Writer#removeAttribute})
  * use `AttributeDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta
@@ -112,149 +108,4 @@ export default class AttributeDelta extends Delta {
 	}
 }
 
-/**
- * To provide specific OT behavior and better collisions solving, methods to change attributes
- * ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute})
- * use `RootAttributeDelta` class which inherits from the `Delta` class and may
- * overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export class RootAttributeDelta extends Delta {
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.RootAttributeDelta';
-	}
-}
-
-/**
- * Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
- * or on a {@link module:engine/model/range~Range range}.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#setAttribute
- * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
- * Model item or range on which the attribute will be set.
- * @param {String} key Attribute key.
- * @param {*} value Attribute new value.
- */
-register( 'setAttribute', function( itemOrRange, key, value ) {
-	attribute( this, key, value, itemOrRange );
-
-	return this;
-} );
-
-/**
- * Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
- * or from a {@link module:engine/model/range~Range range}.
- *
- * @chainable
- * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
- * Model item or range from which the attribute will be removed.
- * @method module:engine/model/batch~Batch#removeAttribute
- * @param {String} key Attribute key.
- */
-register( 'removeAttribute', function( itemOrRange, key ) {
-	attribute( this, key, null, itemOrRange );
-
-	return this;
-} );
-
-function attribute( batch, key, value, itemOrRange ) {
-	if ( itemOrRange instanceof Range ) {
-		changeRange( batch, batch.document, key, value, itemOrRange );
-	} else {
-		changeItem( batch, batch.document, key, value, itemOrRange );
-	}
-}
-
-function changeItem( batch, doc, key, value, item ) {
-	const previousValue = item.getAttribute( key );
-	let range, operation;
-
-	const delta = item.is( 'rootElement' ) ? new RootAttributeDelta() : new AttributeDelta();
-
-	if ( previousValue != value ) {
-		batch.addDelta( delta );
-
-		if ( item.is( 'rootElement' ) ) {
-			// If we change attributes of root element, we have to use `RootAttributeOperation`.
-			operation = new RootAttributeOperation( item, key, previousValue, value, doc.version );
-		} else {
-			if ( item.is( 'element' ) ) {
-				// If we change the attribute of the element, we do not want to change attributes of its children, so
-				// the end of the range cannot be after the closing tag, it should be inside that element, before any of
-				// it's children, so the range will contain only the opening tag.
-				range = new Range( Position.createBefore( item ), Position.createFromParentAndOffset( item, 0 ) );
-			} else {
-				// If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change
-				// all characters represented by it.
-				range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
-			}
-
-			operation = new AttributeOperation( range, key, previousValue, value, doc.version );
-		}
-
-		delta.addOperation( operation );
-		doc.applyOperation( operation );
-	}
-}
-
-// Because attribute operation needs to have the same attribute value on the whole range, this function splits the range
-// into smaller parts.
-function changeRange( batch, doc, attributeKey, attributeValue, range ) {
-	const delta = new AttributeDelta();
-
-	// Position of the last split, the beginning of the new range.
-	let lastSplitPosition = range.start;
-
-	// Currently position in the scanning range. Because we need value after the position, it is not a current
-	// position of the iterator but the previous one (we need to iterate one more time to get the value after).
-	let position,
-		// Value before the currently position.
-		attributeValueBefore,
-		// Value after the currently position.
-		attributeValueAfter;
-
-	for ( const value of range ) {
-		attributeValueAfter = value.item.getAttribute( attributeKey );
-
-		// At the first run of the iterator the position in undefined. We also do not have a attributeValueBefore, but
-		// because attributeValueAfter may be null, attributeValueBefore may be equal attributeValueAfter ( undefined == null ).
-		if ( position && attributeValueBefore != attributeValueAfter ) {
-			// if attributeValueBefore == attributeValue there is nothing to change, so we add operation only if these values are different.
-			if ( attributeValueBefore != attributeValue ) {
-				addOperation();
-			}
-
-			lastSplitPosition = position;
-		}
-
-		position = value.nextPosition;
-		attributeValueBefore = attributeValueAfter;
-	}
-
-	// Because position in the loop is not the iterator position (see let position comment), the last position in
-	// the while loop will be last but one position in the range. We need to check the last position manually.
-	if ( position instanceof Position && position != lastSplitPosition && attributeValueBefore != attributeValue ) {
-		addOperation();
-	}
-
-	function addOperation() {
-		// Add delta to the batch only if there is at least operation in the delta. Add delta only once.
-		if ( delta.operations.length === 0 ) {
-			batch.addDelta( delta );
-		}
-
-		const range = new Range( lastSplitPosition, position );
-		const operation = new AttributeOperation( range, attributeKey, attributeValueBefore, attributeValue, doc.version );
-
-		delta.addOperation( operation );
-		doc.applyOperation( operation );
-	}
-}
-
 DeltaFactory.register( AttributeDelta );
-DeltaFactory.register( RootAttributeDelta );

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -13,7 +13,6 @@
 
 // Import default suite of deltas so a feature have to include only Batch class file.
 import './attributedelta';
-import './insertdelta';
 import './mergedelta';
 import './movedelta';
 import './removedelta';

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -369,9 +369,9 @@ addTransformationCase( SplitDelta, AttributeDelta, ( a, b, context ) => {
 	for ( const operation of b.operations ) {
 		if ( operation.range.containsPosition( splitPosition ) || operation.range.start.isEqual( splitPosition ) ) {
 			if ( operation.newValue !== null ) {
-				a._cloneOperation.nodes.getNode( 0 ).setAttribute( operation.key, operation.newValue );
+				a._cloneOperation.nodes.getNode( 0 )._setAttribute( operation.key, operation.newValue );
 			} else {
-				a._cloneOperation.nodes.getNode( 0 ).removeAttribute( operation.key );
+				a._cloneOperation.nodes.getNode( 0 )._removeAttribute( operation.key );
 			}
 
 			break;

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 2 - 52
packages/ckeditor5-engine/src/model/delta/insertdelta.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -10,16 +10,9 @@
 import Delta from './delta';
 import RemoveDelta from './removedelta';
 import DeltaFactory from './deltafactory';
-import InsertOperation from '../operation/insertoperation';
-import { register } from '../batch';
-import { normalizeNodes } from './../writer';
-
-import DocumentFragment from '../documentfragment';
-import Range from '../../model/range.js';
-import Position from '../../model/position.js';
 
 /**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#insert Batch#insert} method
+ * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#insert Batch#insert} method
  * uses the `InsertDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta
@@ -78,47 +71,4 @@ export default class InsertDelta extends Delta {
 	}
 }
 
-/**
- * Inserts a node or nodes at the given position.
- *
- * When inserted element is a {@link module:engine/model/documentfragment~DocumentFragment} and has markers its markers will
- * be set to {@link module:engine/model/document~Document#markers}.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#insert
- * @param {module:engine/model/position~Position} position Position of insertion.
- * @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted.
- */
-register( 'insert', function( position, nodes ) {
-	const normalizedNodes = normalizeNodes( nodes );
-
-	// If nothing is inserted do not create delta and operation.
-	if ( normalizedNodes.length === 0 ) {
-		return this;
-	}
-
-	const delta = new InsertDelta();
-	const insert = new InsertOperation( position, normalizedNodes, this.document.version );
-
-	this.addDelta( delta );
-	delta.addOperation( insert );
-	this.document.applyOperation( insert );
-
-	// When element is a DocumentFragment we need to move its markers to Document#markers.
-	if ( nodes instanceof DocumentFragment ) {
-		for ( const [ markerName, markerRange ] of nodes.markers ) {
-			// We need to migrate marker range from DocumentFragment to Document.
-			const rangeRootPosition = Position.createAt( markerRange.root );
-			const range = new Range(
-				markerRange.start._getCombined( rangeRootPosition, position ),
-				markerRange.end._getCombined( rangeRootPosition, position )
-			);
-
-			this.setMarker( markerName, range );
-		}
-	}
-
-	return this;
-} );
-
 DeltaFactory.register( InsertDelta );

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -9,13 +9,10 @@
 
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
-import { register } from '../batch';
-import MarkerOperation from '../operation/markeroperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#setMarker Batch#setMarker}
- * and {@link module:engine/model/batch~Batch#removeMarker Batch#removeMarker} methods use the `MarkerDelta` class which inherits
+ * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#setMarker Batch#setMarker}
+ * and {@link module:engine/model/writer~Writer#removeMarker Batch#removeMarker} methods use the `MarkerDelta` class which inherits
  * from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta
@@ -46,86 +43,4 @@ export default class MarkerDelta extends Delta {
 	}
 }
 
-/**
- * Adds or updates {@link module:engine/model/markercollection~Marker marker} with given name to given `range`.
- *
- * If passed name is a name of already existing marker (or {@link module:engine/model/markercollection~Marker Marker} instance
- * is passed), `range` parameter may be omitted. In this case marker will not be updated in
- * {@link module:engine/model/document~Document#markers document marker collection}. However the marker will be added to
- * the document history. This may be important for other features, like undo. From document history point of view, it will
- * look like the marker was created and added to the document at the moment when it is set using this method.
- *
- * This is useful if the marker is created before it can be added to document history (e.g. a feature creating the marker
- * is waiting for additional data, etc.). In this case, the marker may be first created directly through
- * {@link module:engine/model/markercollection~MarkerCollection MarkerCollection API} and only later added using `Batch` API.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#setMarker
- * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to add or update.
- * @param {module:engine/model/range~Range} [newRange] Marker range.
- */
-register( 'setMarker', function( markerOrName, newRange ) {
-	const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
-	const currentMarker = this.document.markers.get( name );
-
-	if ( !newRange && !currentMarker ) {
-		/**
-		 * Range parameter is required when adding a new marker.
-		 *
-		 * @error batch-setMarker-no-range
-		 */
-		throw new CKEditorError( 'batch-setMarker-no-range: Range parameter is required when adding a new marker.' );
-	}
-
-	const currentRange = currentMarker ? currentMarker.getRange() : null;
-
-	if ( !newRange ) {
-		// If `newRange` is not given, treat this as synchronizing existing marker.
-		// Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
-		addOperation( this, name, null, currentRange );
-	} else {
-		// Just change marker range.
-		addOperation( this, name, currentRange, newRange );
-	}
-
-	return this;
-} );
-
-/**
- * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#removeMarker
- * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove.
- */
-register( 'removeMarker', function( markerOrName ) {
-	const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
-
-	if ( !this.document.markers.has( name ) ) {
-		/**
-		 * Trying to remove marker which does not exist.
-		 *
-		 * @error batch-removeMarker-no-marker
-		 */
-		throw new CKEditorError( 'batch-removeMarker-no-marker: Trying to remove marker which does not exist.' );
-	}
-
-	const oldRange = this.document.markers.get( name ).getRange();
-
-	addOperation( this, name, oldRange, null );
-
-	return this;
-} );
-
-function addOperation( batch, name, oldRange, newRange ) {
-	const doc = batch.document;
-	const delta = new MarkerDelta();
-
-	const operation = new MarkerOperation( name, oldRange, newRange, doc.markers, doc.version );
-
-	batch.addDelta( delta );
-	delta.addOperation( operation );
-	doc.applyOperation( operation );
-}
-
 DeltaFactory.register( MarkerDelta );

+ 2 - 67
packages/ckeditor5-engine/src/model/delta/mergedelta.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -10,15 +10,9 @@
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
 import SplitDelta from './splitdelta';
-import { register } from '../batch';
-import Position from '../position';
-import Element from '../element';
-import RemoveOperation from '../operation/removeoperation';
-import MoveOperation from '../operation/moveoperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method
+ * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#merge} method
  * uses the `MergeDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta
@@ -70,63 +64,4 @@ export default class MergeDelta extends Delta {
 	}
 }
 
-/**
- * Merges two siblings at the given position.
- *
- * Node before and after the position have to be an element. Otherwise `batch-merge-no-element-before` or
- * `batch-merge-no-element-after` error will be thrown.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#merge
- * @param {module:engine/model/position~Position} position Position of merge.
- */
-register( 'merge', function( position ) {
-	const delta = new MergeDelta();
-	this.addDelta( delta );
-
-	const nodeBefore = position.nodeBefore;
-	const nodeAfter = position.nodeAfter;
-
-	if ( !( nodeBefore instanceof Element ) ) {
-		/**
-		 * Node before merge position must be an element.
-		 *
-		 * @error batch-merge-no-element-before
-		 */
-		throw new CKEditorError( 'batch-merge-no-element-before: Node before merge position must be an element.' );
-	}
-
-	if ( !( nodeAfter instanceof Element ) ) {
-		/**
-		 * Node after merge position must be an element.
-		 *
-		 * @error batch-merge-no-element-after
-		 */
-		throw new CKEditorError( 'batch-merge-no-element-after: Node after merge position must be an element.' );
-	}
-
-	const positionAfter = Position.createFromParentAndOffset( nodeAfter, 0 );
-	const positionBefore = Position.createFromParentAndOffset( nodeBefore, nodeBefore.maxOffset );
-
-	const move = new MoveOperation(
-		positionAfter,
-		nodeAfter.maxOffset,
-		positionBefore,
-		this.document.version
-	);
-
-	move.isSticky = true;
-	delta.addOperation( move );
-	this.document.applyOperation( move );
-
-	const graveyard = this.document.graveyard;
-	const gyPosition = new Position( graveyard, [ 0 ] );
-
-	const remove = new RemoveOperation( position, 1, gyPosition, this.document.version );
-	delta.addOperation( remove );
-	this.document.applyOperation( remove );
-
-	return this;
-} );
-
 DeltaFactory.register( MergeDelta );

+ 2 - 43
packages/ckeditor5-engine/src/model/delta/movedelta.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -9,14 +9,9 @@
 
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
-import { register } from '../batch';
-import MoveOperation from '../operation/moveoperation';
-import Position from '../position';
-import Range from '../range';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#move} method
+ * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#move} method
  * uses the `MoveDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta
@@ -86,40 +81,4 @@ export default class MoveDelta extends Delta {
 	}
 }
 
-function addMoveOperation( batch, delta, sourcePosition, howMany, targetPosition ) {
-	const operation = new MoveOperation( sourcePosition, howMany, targetPosition, batch.document.version );
-	delta.addOperation( operation );
-	batch.document.applyOperation( operation );
-}
-
-/**
- * Moves given {@link module:engine/model/item~Item model item} or given range to target position.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#move
- * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range of nodes to move.
- * @param {module:engine/model/position~Position} targetPosition Position where moved nodes will be inserted.
- */
-register( 'move', function( itemOrRange, targetPosition ) {
-	const delta = new MoveDelta();
-	this.addDelta( delta );
-
-	if ( itemOrRange instanceof Range ) {
-		if ( !itemOrRange.isFlat ) {
-			/**
-			 * Range to move is not flat.
-			 *
-			 * @error batch-move-range-not-flat
-			 */
-			throw new CKEditorError( 'batch-move-range-not-flat: Range to move is not flat.' );
-		}
-
-		addMoveOperation( this, delta, itemOrRange.start, itemOrRange.end.offset - itemOrRange.start.offset, targetPosition );
-	} else {
-		addMoveOperation( this, delta, Position.createBefore( itemOrRange ), 1, targetPosition );
-	}
-
-	return this;
-} );
-
 DeltaFactory.register( MoveDelta );

+ 2 - 40
packages/ckeditor5-engine/src/model/delta/removedelta.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,14 +8,10 @@
  */
 
 import MoveDelta from './movedelta';
-import { register } from '../batch';
 import DeltaFactory from './deltafactory';
-import RemoveOperation from '../operation/removeoperation';
-import Position from '../position';
-import Range from '../range';
 
 /**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#remove} method
+ * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#remove} method
  * uses the `RemoveDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta
@@ -29,38 +25,4 @@ export default class RemoveDelta extends MoveDelta {
 	}
 }
 
-function addRemoveDelta( batch, position, howMany ) {
-	const delta = new RemoveDelta();
-	batch.addDelta( delta );
-
-	const graveyard = batch.document.graveyard;
-	const gyPosition = new Position( graveyard, [ 0 ] );
-
-	const operation = new RemoveOperation( position, howMany, gyPosition, batch.document.version );
-	delta.addOperation( operation );
-	batch.document.applyOperation( operation );
-}
-
-/**
- * Removes given {@link module:engine/model/item~Item model item} or given range.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#remove
- * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove.
- */
-register( 'remove', function( itemOrRange ) {
-	if ( itemOrRange instanceof Range ) {
-		// The array is reversed, so the ranges to remove are in correct order and do not have to be updated.
-		const ranges = itemOrRange.getMinimalFlatRanges().reverse();
-
-		for ( const flat of ranges ) {
-			addRemoveDelta( this, flat.start, flat.end.offset - flat.start.offset );
-		}
-	} else {
-		addRemoveDelta( this, Position.createBefore( itemOrRange ), 1 );
-	}
-
-	return this;
-} );
-
 DeltaFactory.register( RemoveDelta );

+ 2 - 39
packages/ckeditor5-engine/src/model/delta/renamedelta.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -9,14 +9,9 @@
 
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
-import { register } from '../batch';
-import RenameOperation from '../operation/renameoperation';
-import Element from '../element';
-import Position from '../position';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#rename Batch#rename} method
+ * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#rename Batch#rename} method
  * uses the `RenameDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta
@@ -44,36 +39,4 @@ export default class RenameDelta extends Delta {
 	}
 }
 
-function apply( batch, delta, operation ) {
-	delta.addOperation( operation );
-	batch.document.applyOperation( operation );
-}
-
-/**
- * Renames given element.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#rename
- * @param {module:engine/model/element~Element} element The element to rename.
- * @param {String} newName New element name.
- */
-register( 'rename', function( element, newName ) {
-	if ( !( element instanceof Element ) ) {
-		/**
-		 * Trying to rename an object which is not an instance of Element.
-		 *
-		 * @error batch-rename-not-element-instance
-		 */
-		throw new CKEditorError( 'batch-rename-not-element-instance: Trying to rename an object which is not an instance of Element.' );
-	}
-
-	const delta = new RenameDelta();
-	this.addDelta( delta );
-
-	const renameOperation = new RenameOperation( Position.createBefore( element ), element.name, newName, this.document.version );
-	apply( this, delta, renameOperation );
-
-	return this;
-} );
-
 DeltaFactory.register( RenameDelta );

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

@@ -0,0 +1,30 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/delta/rootattributedelta
+ */
+
+import Delta from './delta';
+import DeltaFactory from './deltafactory';
+
+/**
+ * To provide specific OT behavior and better collisions solving, methods to change attributes
+ * ({@link module:engine/model/writer~Writer#setAttribute} and {@link module:engine/model/writer~Writer#removeAttribute})
+ * use `RootAttributeDelta` class which inherits from the `Delta` class and may
+ * overwrite some methods.
+ *
+ * @extends module:engine/model/delta/delta~Delta
+ */
+export default class RootAttributeDelta extends Delta {
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.model.delta.RootAttributeDelta';
+	}
+}
+
+DeltaFactory.register( RootAttributeDelta );

+ 2 - 57
packages/ckeditor5-engine/src/model/delta/splitdelta.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -9,16 +9,11 @@
 
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
-import { register } from '../batch';
-import Position from '../position';
-import Element from '../element';
-import InsertOperation from '../operation/insertoperation';
 import MoveOperation from '../operation/moveoperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import MergeDelta from '../delta/mergedelta';
 
 /**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#split} method
+ * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#split} method
  * uses `SplitDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta
@@ -85,54 +80,4 @@ export default class SplitDelta extends Delta {
 	}
 }
 
-/**
- * Splits an element at the given position.
- *
- * The element cannot be a root element, as root element cannot be split. The `batch-split-root` error will be thrown if
- * you try to split the root element.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#split
- * @param {module:engine/model/position~Position} position Position of split.
- */
-register( 'split', function( position ) {
-	const delta = new SplitDelta();
-	this.addDelta( delta );
-
-	const splitElement = position.parent;
-
-	if ( !splitElement.parent ) {
-		/**
-		 * Root element can not be split.
-		 *
-		 * @error batch-split-root
-		 */
-		throw new CKEditorError( 'batch-split-root: Root element can not be split.' );
-	}
-
-	const copy = new Element( splitElement.name, splitElement.getAttributes() );
-
-	const insert = new InsertOperation(
-		Position.createAfter( splitElement ),
-		copy,
-		this.document.version
-	);
-
-	delta.addOperation( insert );
-	this.document.applyOperation( insert );
-
-	const move = new MoveOperation(
-		position,
-		splitElement.maxOffset - position.offset,
-		Position.createFromParentAndOffset( copy, 0 ),
-		this.document.version
-	);
-	move.isSticky = true;
-
-	delta.addOperation( move );
-	this.document.applyOperation( move );
-
-	return this;
-} );
-
 DeltaFactory.register( SplitDelta );

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 2 - 53
packages/ckeditor5-engine/src/model/delta/unwrapdelta.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -10,14 +10,9 @@
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
 import WrapDelta from './wrapdelta';
-import { register } from '../batch';
-import Position from '../position';
-import RemoveOperation from '../operation/removeoperation';
-import MoveOperation from '../operation/moveoperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method
+ * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#merge} method
  * uses the `UnwrapDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta
@@ -64,50 +59,4 @@ export default class UnwrapDelta extends Delta {
 	}
 }
 
-/**
- * Unwraps children of the given element – all its children are moved before it and then the element is removed.
- * Throws error if you try to unwrap an element which does not have a parent.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#unwrap
- * @param {module:engine/model/element~Element} position Element to unwrap.
- */
-register( 'unwrap', function( element ) {
-	if ( element.parent === null ) {
-		/**
-		 * Trying to unwrap an element which has no parent.
-		 *
-		 * @error batch-unwrap-element-no-parent
-		 */
-		throw new CKEditorError( 'batch-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
-	}
-
-	const delta = new UnwrapDelta();
-	this.addDelta( delta );
-
-	const sourcePosition = Position.createFromParentAndOffset( element, 0 );
-
-	const move = new MoveOperation(
-		sourcePosition,
-		element.maxOffset,
-		Position.createBefore( element ),
-		this.document.version
-	);
-
-	move.isSticky = true;
-	delta.addOperation( move );
-	this.document.applyOperation( move );
-
-	// Computing new position because we moved some nodes before `element`.
-	// If we would cache `Position.createBefore( element )` we remove wrong node.
-	const graveyard = this.document.graveyard;
-	const gyPosition = new Position( graveyard, [ 0 ] );
-
-	const remove = new RemoveOperation( Position.createBefore( element ), 1, gyPosition, this.document.version );
-	delta.addOperation( remove );
-	this.document.applyOperation( remove );
-
-	return this;
-} );
-
 DeltaFactory.register( UnwrapDelta );

+ 2 - 42
packages/ckeditor5-engine/src/model/delta/weakinsertdelta.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,13 +8,10 @@
  */
 
 import InsertDelta from './insertdelta';
-import { register } from '../batch';
 import DeltaFactory from './deltafactory';
-import InsertOperation from '../operation/insertoperation';
-import { normalizeNodes } from './../writer';
 
 /**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#insert} method
+ * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#insert} method
  * uses the `WeakInsertDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta
@@ -28,41 +25,4 @@ export default class WeakInsertDelta extends InsertDelta {
 	}
 }
 
-/**
- * Inserts a node or nodes at given position. {@link module:engine/model/batch~Batch#weakInsert weakInsert} is commonly used for actions
- * like typing or plain-text paste (without formatting). There are two differences between
- * {@link module:engine/model/batch~Batch#insert insert} and {@link module:engine/model/batch~Batch#weakInsert weakInsert}:
- *
- * * When using `weakInsert`, inserted nodes will have same attributes as the current attributes of
- * {@link module:engine/model/document~Document#selection document selection}.
- * * If {@link module:engine/model/operation/insertoperation~InsertOperation insert operation} position is inside a range changed by
- * {@link module:engine/model/operation/attributeoperation~AttributeOperation attribute operation},
- * the attribute operation is split into two operations.
- * Thanks to this, attribute change "omits" the inserted nodes. The correct behavior for `WeakInsertDelta` is that
- * {@link module:engine/model/operation/attributeoperation~AttributeOperation AttributeOperation} does not "break" and also
- * applies attributes for inserted nodes. This behavior has to be reflected during
- * {@link module:engine/model/delta/transform~transform delta transformation}.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#weakInsert
- * @param {module:engine/model/position~Position} position Position of insertion.
- * @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted.
- */
-register( 'weakInsert', function( position, nodes ) {
-	const delta = new WeakInsertDelta();
-	this.addDelta( delta );
-
-	nodes = normalizeNodes( nodes );
-
-	for ( const node of nodes ) {
-		node.setAttributesTo( this.document.selection.getAttributes() );
-	}
-
-	const operation = new InsertOperation( position, nodes, this.document.version );
-	delta.addOperation( operation );
-	this.document.applyOperation( operation );
-
-	return this;
-} );
-
 DeltaFactory.register( WeakInsertDelta );

+ 2 - 67
packages/ckeditor5-engine/src/model/delta/wrapdelta.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -10,16 +10,10 @@
 import Delta from './delta';
 import DeltaFactory from './deltafactory';
 import UnwrapDelta from './unwrapdelta';
-import { register } from '../batch';
-import Position from '../position';
 import Range from '../range';
-import Element from '../element';
-import InsertOperation from '../operation/insertoperation';
-import MoveOperation from '../operation/moveoperation';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method
+ * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#merge} method
  * uses the `WrapDelta` class which inherits from the `Delta` class and may overwrite some methods.
  *
  * @extends module:engine/model/delta/delta~Delta
@@ -91,63 +85,4 @@ export default class WrapDelta extends Delta {
 	}
 }
 
-/**
- * Wraps given range with given element or with a new element with specified name, if string has been passed.
- * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat}). If not, error will be thrown.
- *
- * @chainable
- * @method module:engine/model/batch~Batch#wrap
- * @param {module:engine/model/range~Range} range Range to wrap.
- * @param {module:engine/model/element~Element|String} elementOrString Element or name of element to wrap the range with.
- */
-register( 'wrap', function( range, elementOrString ) {
-	if ( !range.isFlat ) {
-		/**
-		 * Range to wrap is not flat.
-		 *
-		 * @error batch-wrap-range-not-flat
-		 */
-		throw new CKEditorError( 'batch-wrap-range-not-flat: Range to wrap is not flat.' );
-	}
-
-	const element = elementOrString instanceof Element ? elementOrString : new Element( elementOrString );
-
-	if ( element.childCount > 0 ) {
-		/**
-		 * Element to wrap with is not empty.
-		 *
-		 * @error batch-wrap-element-not-empty
-		 */
-		throw new CKEditorError( 'batch-wrap-element-not-empty: Element to wrap with is not empty.' );
-	}
-
-	if ( element.parent !== null ) {
-		/**
-		 * Element to wrap with is already attached to a tree model.
-		 *
-		 * @error batch-wrap-element-attached
-		 */
-		throw new CKEditorError( 'batch-wrap-element-attached: Element to wrap with is already attached to tree model.' );
-	}
-
-	const delta = new WrapDelta();
-	this.addDelta( delta );
-
-	const insert = new InsertOperation( range.end, element, this.document.version );
-	delta.addOperation( insert );
-	this.document.applyOperation( insert );
-
-	const targetPosition = Position.createFromParentAndOffset( element, 0 );
-	const move = new MoveOperation(
-		range.start,
-		range.end.offset - range.start.offset,
-		targetPosition,
-		this.document.version
-	);
-	delta.addOperation( move );
-	this.document.applyOperation( move );
-
-	return this;
-} );
-
 DeltaFactory.register( WrapDelta );

+ 987 - 0
packages/ckeditor5-engine/src/model/differ.js

@@ -0,0 +1,987 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/differ
+ */
+
+import Position from './position';
+import Range from './range';
+
+/**
+ * Calculates the difference between two model states.
+ *
+ * Receives operations that are to be applied on the model document. Marks parts of the model document tree which
+ * are changed and saves the state of these elements before the change. Then, it compares saved elements with the
+ * changed elements, after all changes are applied on the model document. Calculates the diff between saved
+ * elements and new ones and returns a change set.
+ */
+export default class Differ {
+	constructor() {
+		/**
+		 * A map that stores changes that happened in a given element.
+		 *
+		 * The keys of the map are references to the model elements.
+		 * The values of the map are arrays with changes that were done on this element.
+		 *
+		 * @private
+		 * @type {Map}
+		 */
+		this._changesInElement = new Map();
+
+		/**
+		 * A map that stores "element's children snapshots". A snapshot is representing children of a given element before
+		 * the first change was applied on that element. Snapshot items are objects with two properties: `name`,
+		 * containing the element name (or `'$text'` for a text node) and `attributes` which is a map of the node's attributes.
+		 *
+		 * @private
+		 * @type {Map}
+		 */
+		this._elementSnapshots = new Map();
+
+		/**
+		 * A map that stores all changed markers.
+		 *
+		 * The keys of the map are marker names.
+		 * The values of the map are objects with the `oldRange` and `newRange` properties. They store the marker range
+		 * state before and after the change.
+		 *
+		 * @private
+		 * @type {Map}
+		 */
+		this._changedMarkers = new Map();
+
+		/**
+		 * Stores the number of changes that were processed. Used to order the changes chronologically. It is important
+		 * when changes are sorted.
+		 *
+		 * @private
+		 * @type {Number}
+		 */
+		this._changeCount = 0;
+
+		/**
+		 * For efficiency purposes, `Differ` stores the change set returned by the differ after {@link #getChanges} call.
+		 * Cache is reset each time a new operation is buffered. If the cache has not been reset, {@link #getChanges} will
+		 * return the cached value instead of calculating it again.
+		 *
+		 * This property stores those changes that did not take place in graveyard root.
+		 *
+		 * @private
+		 * @type {Array.<Object>|null}
+		 */
+		this._cachedChanges = null;
+
+		/**
+		 * For efficiency purposes, `Differ` stores the change set returned by the differ after the {@link #getChanges} call.
+		 * The cache is reset each time a new operation is buffered. If the cache has not been reset, {@link #getChanges} will
+		 * return the cached value instead of calculating it again.
+		 *
+		 * This property stores all changes evaluated by `Differ`, including those that took place in the graveyard.
+		 *
+		 * @private
+		 * @type {Array.<Object>|null}
+		 */
+		this._cachedChangesWithGraveyard = null;
+	}
+
+	/**
+	 * Informs whether there are any changes buffered in `Differ`.
+	 *
+	 * @readonly
+	 * @type {Boolean}
+	 */
+	get isEmpty() {
+		return this._changesInElement.size == 0 && this._changedMarkers.size == 0;
+	}
+
+	/**
+	 * Buffers a given operation. An operation has to be buffered before it is executed.
+	 *
+	 * Operation type is checked and it is checked which nodes it will affect. These nodes are then stored in `Differ`
+	 * in the state before the operation is executed.
+	 *
+	 * @param {module:engine/model/operation/operation~Operation} operation An operation to buffer.
+	 */
+	bufferOperation( operation ) {
+		switch ( operation.type ) {
+			case 'insert': {
+				if ( this._isInInsertedElement( operation.position.parent ) ) {
+					return;
+				}
+
+				this._markInsert( operation.position.parent, operation.position.offset, operation.nodes.maxOffset );
+
+				break;
+			}
+			case 'addAttribute':
+			case 'removeAttribute':
+			case 'changeAttribute': {
+				for ( const item of operation.range.getItems() ) {
+					if ( this._isInInsertedElement( item.parent ) ) {
+						continue;
+					}
+
+					this._markAttribute( item );
+				}
+
+				break;
+			}
+			case 'remove':
+			case 'move':
+			case 'reinsert': {
+				const sourceParentInserted = this._isInInsertedElement( operation.sourcePosition.parent );
+				const targetParentInserted = this._isInInsertedElement( operation.targetPosition.parent );
+
+				if ( !sourceParentInserted ) {
+					this._markRemove( operation.sourcePosition.parent, operation.sourcePosition.offset, operation.howMany );
+				}
+
+				if ( !targetParentInserted ) {
+					this._markInsert( operation.targetPosition.parent, operation.getMovedRangeStart().offset, operation.howMany );
+				}
+
+				break;
+			}
+			case 'rename': {
+				if ( this._isInInsertedElement( operation.position.parent ) ) {
+					return;
+				}
+
+				this._markRemove( operation.position.parent, operation.position.offset, 1 );
+				this._markInsert( operation.position.parent, operation.position.offset, 1 );
+
+				break;
+			}
+		}
+
+		// Clear cache after each buffered operation as it is no longer valid.
+		this._cachedChanges = null;
+	}
+
+	/**
+	 * Buffers marker change.
+	 *
+	 * @param {String} markerName The name of the marker that changed.
+	 * @param {module:engine/model/range~Range|null} oldRange Marker range before the change or `null` if the marker has just
+	 * been created.
+	 * @param {module:engine/model/range~Range|null} newRange Marker range after the change or `null` if the marker was removed.
+	 */
+	bufferMarkerChange( markerName, oldRange, newRange ) {
+		const buffered = this._changedMarkers.get( markerName );
+
+		if ( !buffered ) {
+			this._changedMarkers.set( markerName, {
+				oldRange,
+				newRange
+			} );
+		} else {
+			buffered.newRange = newRange;
+
+			if ( buffered.oldRange == null && buffered.newRange == null ) {
+				// The marker is going to be removed (`newRange == null`) but it did not exist before the change set
+				// (`buffered.oldRange == null`). In this case, do not keep the marker in buffer at all.
+				this._changedMarkers.delete( markerName );
+			}
+		}
+	}
+
+	/**
+	 * Returns all markers that should be removed as a result of buffered changes.
+	 *
+	 * @returns {Array.<Object>} Markers to remove. Each array item is an object containing the `name` and `range` properties.
+	 */
+	getMarkersToRemove() {
+		const result = [];
+
+		for ( const [ name, change ] of this._changedMarkers ) {
+			if ( change.oldRange != null ) {
+				result.push( { name, range: change.oldRange } );
+			}
+		}
+
+		return result;
+	}
+
+	/**
+	 * Returns all markers which should be added as a result of buffered changes.
+	 *
+	 * @returns {Array.<Object>} Markers to add. Each array item is an object containing the `name` and `range` properties.
+	 */
+	getMarkersToAdd() {
+		const result = [];
+
+		for ( const [ name, change ] of this._changedMarkers ) {
+			if ( change.newRange != null ) {
+				result.push( { name, range: change.newRange } );
+			}
+		}
+
+		return result;
+	}
+
+	/**
+	 * Calculates the diff between the old model tree state (the state before the first buffered operations since the last {@link #reset}
+	 * call) and the new model tree state (actual one). It should be called after all buffered operations are executed.
+	 *
+	 * The diff set is returned as an array of diff items, each describing a change done on the model. The items are sorted by
+	 * the position on which the change happened. If a position {@link module:engine/model/position~Position#isBefore is before}
+	 * another one, it will be on an earlier index in the diff set.
+	 *
+	 * Because calculating the diff is a costly operation, the result is cached. If no new operation was buffered since the
+	 * previous {@link #getChanges} call, the next call will return the cached value.
+	 *
+	 * @param {Object} options Additional options.
+	 * @param {Boolean} [options.includeChangesInGraveyard=false] If set to `true`, also changes that happened
+	 * in the graveyard root will be returned. By default, changes in the graveyard root are not returned.
+	 * @returns {Array.<Object>} Diff between the old and the new model tree state.
+	 */
+	getChanges( options = { includeChangesInGraveyard: false } ) {
+		// If there are cached changes, just return them instead of calculating changes again.
+		if ( this._cachedChanges ) {
+			if ( options.includeChangesInGraveyard ) {
+				return this._cachedChangesWithGraveyard.slice();
+			} else {
+				return this._cachedChanges.slice();
+			}
+		}
+
+		// Will contain returned results.
+		const diffSet = [];
+
+		// Check all changed elements.
+		for ( const element of this._changesInElement.keys() ) {
+			// Get changes for this element and sort them.
+			const changes = this._changesInElement.get( element ).sort( ( a, b ) => {
+				if ( a.offset === b.offset ) {
+					if ( a.type != b.type ) {
+						// If there are multiple changes at the same position, "remove" change should be first.
+						// If the order is different, for example, we would first add some nodes and then removed them
+						// (instead of the nodes that we should remove).
+						return a.type == 'remove' ? -1 : 1;
+					}
+
+					return 0;
+				}
+
+				return a.offset < b.offset ? -1 : 1;
+			} );
+
+			// Get children of this element before any change was applied on it.
+			const snapshotChildren = this._elementSnapshots.get( element );
+			// Get snapshot of current element's children.
+			const elementChildren = _getChildrenSnapshot( element.getChildren() );
+
+			// Generate actions basing on changes done on element.
+			const actions = _generateActionsFromChanges( snapshotChildren.length, changes );
+
+			let i = 0; // Iterator in `elementChildren` array -- iterates through current children of element.
+			let j = 0; // Iterator in `snapshotChildren` array -- iterates through old children of element.
+
+			// Process every action.
+			for ( const action of actions ) {
+				if ( action === 'i' ) {
+					// Generate diff item for this element and insert it into the diff set.
+					diffSet.push( this._getInsertDiff( element, i, elementChildren[ i ].name ) );
+
+					i++;
+				} else if ( action === 'r' ) {
+					// Generate diff item for this element and insert it into the diff set.
+					diffSet.push( this._getRemoveDiff( element, i, snapshotChildren[ j ].name ) );
+
+					j++;
+				} else if ( action === 'a' ) {
+					// Take attributes from saved and current children.
+					const elementAttributes = elementChildren[ i ].attributes;
+					const snapshotAttributes = snapshotChildren[ j ].attributes;
+					let range;
+
+					if ( elementChildren[ i ].name == '$text' ) {
+						range = Range.createFromParentsAndOffsets( element, i, element, i + 1 );
+					} else {
+						const index = element.offsetToIndex( i );
+						range = Range.createFromParentsAndOffsets( element, i, element.getChild( index ), 0 );
+					}
+
+					// Generate diff items for this change (there might be multiple attributes changed and
+					// there is a single diff for each of them) and insert them into the diff set.
+					diffSet.push( ...this._getAttributesDiff( range, snapshotAttributes, elementAttributes ) );
+
+					i++;
+					j++;
+				} else {
+					// `action` is 'equal'. Child not changed.
+					i++;
+					j++;
+				}
+			}
+		}
+
+		// Then, sort the changes by the position (change at position before other changes is first).
+		diffSet.sort( ( a, b ) => {
+			// If the change is in different root, we don't care much, but we'd like to have all changes in given
+			// root "together" in the array. So let's just sort them by the root name. It does not matter which root
+			// will be processed first.
+			if ( a.position.root != b.position.root ) {
+				return a.position.root.rootName < b.position.root.rootName ? -1 : 1;
+			}
+
+			// If change happens at the same position...
+			if ( a.position.isEqual( b.position ) ) {
+				// Keep chronological order of operations.
+				return a.changeCount < b.changeCount ? -1 : 1;
+			}
+
+			// If positions differ, position "on the left" should be earlier in the result.
+			return a.position.isBefore( b.position ) ? -1 : 1;
+		} );
+
+		// Glue together multiple changes (mostly on text nodes).
+		for ( let i = 1; i < diffSet.length; i++ ) {
+			const prevDiff = diffSet[ i - 1 ];
+			const thisDiff = diffSet[ i ];
+
+			// Glue remove changes if they happen on text on same position.
+			const isConsecutiveTextRemove =
+				prevDiff.type == 'remove' && thisDiff.type == 'remove' &&
+				prevDiff.name == '$text' && thisDiff.name == '$text' &&
+				prevDiff.position.isEqual( thisDiff.position );
+
+			// Glue insert changes if they happen on text on consecutive fragments.
+			const isConsecutiveTextAdd =
+				prevDiff.type == 'insert' && thisDiff.type == 'insert' &&
+				prevDiff.name == '$text' && thisDiff.name == '$text' &&
+				prevDiff.position.parent == thisDiff.position.parent &&
+				prevDiff.position.offset + prevDiff.length == thisDiff.position.offset;
+
+			// Glue attribute changes if they happen on consecutive fragments and have same key, old value and new value.
+			const isConsecutiveAttributeChange =
+				prevDiff.type == 'attribute' && thisDiff.type == 'attribute' &&
+				prevDiff.position.parent == thisDiff.position.parent &&
+				prevDiff.range.isFlat && thisDiff.range.isFlat &&
+				prevDiff.position.offset + prevDiff.length == thisDiff.position.offset &&
+				prevDiff.attributeKey == thisDiff.attributeKey &&
+				prevDiff.attributeOldValue == thisDiff.attributeOldValue &&
+				prevDiff.attributeNewValue == thisDiff.attributeNewValue;
+
+			if ( isConsecutiveTextRemove || isConsecutiveTextAdd || isConsecutiveAttributeChange ) {
+				diffSet[ i - 1 ].length++;
+
+				if ( isConsecutiveAttributeChange ) {
+					diffSet[ i - 1 ].range.end = diffSet[ i - 1 ].range.end.getShiftedBy( 1 );
+				}
+
+				diffSet.splice( i, 1 );
+				i--;
+			}
+		}
+
+		// Remove `changeCount` property from diff items. It is used only for sorting and is internal thing.
+		for ( const item of diffSet ) {
+			delete item.changeCount;
+
+			if ( item.type == 'attribute' ) {
+				delete item.position;
+				delete item.length;
+			}
+		}
+
+		this._changeCount = 0;
+
+		// Cache changes.
+		this._cachedChangesWithGraveyard = diffSet.slice();
+		this._cachedChanges = diffSet.slice().filter( _changesInGraveyardFilter );
+
+		if ( options.includeChangesInGraveyard ) {
+			return this._cachedChangesWithGraveyard;
+		} else {
+			return this._cachedChanges;
+		}
+	}
+
+	/**
+	 * Resets `Differ`. Removes all buffered changes.
+	 */
+	reset() {
+		this._changesInElement.clear();
+		this._elementSnapshots.clear();
+		this._changedMarkers.clear();
+		this._cachedChanges = null;
+	}
+
+	/**
+	 * Saves and handles an insert change.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} parent
+	 * @param {Number} offset
+	 * @param {Number} howMany
+	 */
+	_markInsert( parent, offset, howMany ) {
+		const changeItem = { type: 'insert', offset, howMany, count: this._changeCount++ };
+
+		this._markChange( parent, changeItem );
+	}
+
+	/**
+	 * Saves and handles a remove change.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} parent
+	 * @param {Number} offset
+	 * @param {Number} howMany
+	 */
+	_markRemove( parent, offset, howMany ) {
+		const changeItem = { type: 'remove', offset, howMany, count: this._changeCount++ };
+
+		this._markChange( parent, changeItem );
+
+		this._removeAllNestedChanges( parent, offset, howMany );
+	}
+
+	/**
+	 * Saves and handles an attribute change.
+	 *
+	 * @private
+	 * @param {module:engine/model/item~Item} item
+	 */
+	_markAttribute( item ) {
+		const changeItem = { type: 'attribute', offset: item.startOffset, howMany: item.offsetSize, count: this._changeCount++ };
+
+		this._markChange( item.parent, changeItem );
+	}
+
+	/**
+	 * Saves and handles a model change.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} parent
+	 * @param {Object} changeItem
+	 */
+	_markChange( parent, changeItem ) {
+		// First, make a snapshot of this parent's children (it will be made only if it was not made before).
+		this._makeSnapshot( parent );
+
+		// Then, get all changes that already were done on the element (empty array if this is the first change).
+		const changes = this._getChangesForElement( parent );
+
+		// Then, look through all the changes, and transform them or the new change.
+		this._handleChange( changeItem, changes );
+
+		// Add the new change.
+		changes.push( changeItem );
+
+		// Remove incorrect changes. During transformation some change might be, for example, included in another.
+		// In that case, the change will have `howMany` property set to `0` or less. We need to remove those changes.
+		for ( let i = 0; i < changes.length; i++ ) {
+			if ( changes[ i ].howMany < 1 ) {
+				changes.splice( i, 1 );
+
+				i--;
+			}
+		}
+	}
+
+	/**
+	 * Gets an array of changes that have already been saved for a given element.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} element
+	 * @returns {Array.<Object>}
+	 */
+	_getChangesForElement( element ) {
+		let changes;
+
+		if ( this._changesInElement.has( element ) ) {
+			changes = this._changesInElement.get( element );
+		} else {
+			changes = [];
+
+			this._changesInElement.set( element, changes );
+		}
+
+		return changes;
+	}
+
+	/**
+	 * Saves a children snapshot for a given element.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} element
+	 */
+	_makeSnapshot( element ) {
+		if ( !this._elementSnapshots.has( element ) ) {
+			this._elementSnapshots.set( element, _getChildrenSnapshot( element.getChildren() ) );
+		}
+	}
+
+	/**
+	 * For a given newly saved change, compares it with a change already done on the element and modifies the incoming
+	 * change and/or the old change.
+	 *
+	 * @private
+	 * @param {Object} inc Incoming (new) change.
+	 * @param {Array.<Object>} changes An array containing all the changes done on that element.
+	 */
+	_handleChange( inc, changes ) {
+		// We need a helper variable that will store how many nodes are to be still handled for this change item.
+		// `nodesToHandle` (how many nodes still need to be handled) and `howMany` (how many nodes were affected)
+		// needs to be differentiated.
+		//
+		// This comes up when there are multiple changes that are affected by `inc` change item.
+		//
+		// For example: assume two insert changes: `{ offset: 2, howMany: 1 }` and `{ offset: 5, howMany: 1 }`.
+		// Assume that `inc` change is remove `{ offset: 2, howMany: 2, nodesToHandle: 2 }`.
+		//
+		// Then, we:
+		// - "forget" about first insert change (it is "eaten" by remove),
+		// - because of that, at the end we will want to remove only one node (`nodesToHandle = 1`),
+		// - but still we have to change offset of the second insert change from `5` to `3`!
+		//
+		// So, `howMany` does not change throughout items transformation and keeps information about how many nodes were affected,
+		// while `nodesToHandle` means how many nodes need to be handled after the change item is transformed by other changes.
+		inc.nodesToHandle = inc.howMany;
+
+		for ( const old of changes ) {
+			const incEnd = inc.offset + inc.howMany;
+			const oldEnd = old.offset + old.howMany;
+
+			if ( inc.type == 'insert' ) {
+				if ( old.type == 'insert' ) {
+					if ( inc.offset <= old.offset ) {
+						old.offset += inc.howMany;
+					} else if ( inc.offset < oldEnd ) {
+						old.howMany += inc.nodesToHandle;
+						inc.nodesToHandle = 0;
+					}
+				}
+
+				if ( old.type == 'remove' ) {
+					if ( inc.offset < old.offset ) {
+						old.offset += inc.howMany;
+					}
+				}
+
+				if ( old.type == 'attribute' ) {
+					if ( inc.offset <= old.offset ) {
+						old.offset += inc.howMany;
+					} else if ( inc.offset < oldEnd ) {
+						// This case is more complicated, because attribute change has to be split into two.
+						// Example (assume that uppercase and lowercase letters mean different attributes):
+						//
+						// initial state:		abcxyz
+						// attribute change:	aBCXYz
+						// incoming insert:		aBCfooXYz
+						//
+						// Change ranges cannot intersect because each item has to be described exactly (it was either
+						// not changed, inserted, removed, or its attribute was changed). That's why old attribute
+						// change has to be split and both parts has to be handled separately from now on.
+						const howMany = old.howMany;
+
+						old.howMany = inc.offset - old.offset;
+
+						// Add the second part of attribute change to the beginning of processed array so it won't
+						// be processed again in this loop.
+						changes.unshift( {
+							type: 'attribute',
+							offset: incEnd,
+							howMany: howMany - old.howMany,
+							count: this._changeCount++
+						} );
+					}
+				}
+			}
+
+			if ( inc.type == 'remove' ) {
+				if ( old.type == 'insert' ) {
+					if ( incEnd <= old.offset ) {
+						old.offset -= inc.howMany;
+					} else if ( incEnd <= oldEnd ) {
+						if ( inc.offset < old.offset ) {
+							const intersectionLength = incEnd - old.offset;
+
+							old.offset = inc.offset;
+
+							old.howMany -= intersectionLength;
+							inc.nodesToHandle -= intersectionLength;
+						} else {
+							old.howMany -= inc.nodesToHandle;
+							inc.nodesToHandle = 0;
+						}
+					} else {
+						if ( inc.offset <= old.offset ) {
+							inc.nodesToHandle -= old.howMany;
+							old.howMany = 0;
+						} else if ( inc.offset < oldEnd ) {
+							const intersectionLength = oldEnd - inc.offset;
+
+							old.howMany -= intersectionLength;
+							inc.nodesToHandle -= intersectionLength;
+						}
+					}
+				}
+
+				if ( old.type == 'remove' ) {
+					if ( inc.offset + inc.howMany <= old.offset ) {
+						old.offset -= inc.howMany;
+					} else if ( inc.offset < old.offset ) {
+						old.offset = inc.offset;
+						old.howMany += inc.nodesToHandle;
+
+						inc.nodesToHandle = 0;
+					}
+				}
+
+				if ( old.type == 'attribute' ) {
+					if ( incEnd <= old.offset ) {
+						old.offset -= inc.howMany;
+					} else if ( inc.offset < old.offset ) {
+						const intersectionLength = incEnd - old.offset;
+
+						old.offset = inc.offset;
+						old.howMany -= intersectionLength;
+					} else if ( inc.offset < oldEnd ) {
+						if ( incEnd <= oldEnd ) {
+							// On first sight in this case we don't need to split attribute operation into two.
+							// However the changes set is later converted to actions (see `_generateActionsFromChanges`).
+							// For that reason, no two changes may intersect.
+							// So we cannot have an attribute change that "contains" remove change.
+							// Attribute change needs to be split.
+							const howMany = old.howMany;
+
+							old.howMany = inc.offset - old.offset;
+
+							const howManyAfter = howMany - old.howMany - inc.nodesToHandle;
+
+							// Add the second part of attribute change to the beginning of processed array so it won't
+							// be processed again in this loop.
+							changes.unshift( {
+								type: 'attribute',
+								offset: inc.offset,
+								howMany: howManyAfter,
+								count: this._changeCount++
+							} );
+						} else {
+							old.howMany -= oldEnd - inc.offset;
+						}
+					}
+				}
+			}
+
+			if ( inc.type == 'attribute' ) {
+				if ( old.type == 'insert' ) {
+					if ( inc.offset < old.offset && incEnd > old.offset ) {
+						if ( incEnd > oldEnd ) {
+							// This case is similar to a case described when incoming change was insert and old change was attribute.
+							// See comment above.
+							//
+							// This time incoming change is attribute. We need to split incoming change in this case too.
+							// However this time, the second part of the attribute change needs to be processed further
+							// because there might be other changes that it collides with.
+							const attributePart = {
+								type: 'attribute',
+								offset: oldEnd,
+								howMany: incEnd - oldEnd,
+								count: this._changeCount++
+							};
+
+							this._handleChange( attributePart, changes );
+
+							changes.push( attributePart );
+						}
+
+						inc.nodesToHandle = old.offset - inc.offset;
+					} else if ( inc.offset >= old.offset && inc.offset < oldEnd ) {
+						if ( incEnd > oldEnd ) {
+							inc.nodesToHandle = incEnd - oldEnd;
+							inc.offset = oldEnd;
+						} else {
+							inc.nodesToHandle = 0;
+						}
+					}
+				}
+
+				if ( old.type == 'attribute' ) {
+					if ( inc.offset >= old.offset && incEnd <= oldEnd ) {
+						inc.nodesToHandle = 0;
+					}
+				}
+			}
+		}
+
+		inc.howMany = inc.nodesToHandle;
+		delete inc.nodesToHandle;
+	}
+
+	/**
+	 * Returns an object with a single insert change description.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} parent The element in which the change happened.
+	 * @param {Number} offset The offset at which change happened.
+	 * @param {String} name The name of the removed element or `'$text'` for a character.
+	 * @returns {Object} The diff item.
+	 */
+	_getInsertDiff( parent, offset, name ) {
+		return {
+			type: 'insert',
+			position: Position.createFromParentAndOffset( parent, offset ),
+			name,
+			length: 1,
+			changeCount: this._changeCount++
+		};
+	}
+
+	/**
+	 * Returns an object with a single remove change description.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} parent The element in which change happened.
+	 * @param {Number} offset The offset at which change happened.
+	 * @param {String} name The name of the removed element or `'$text'` for a character.
+	 * @returns {Object} The diff item.
+	 */
+	_getRemoveDiff( parent, offset, name ) {
+		return {
+			type: 'remove',
+			position: Position.createFromParentAndOffset( parent, offset ),
+			name,
+			length: 1,
+			changeCount: this._changeCount++
+		};
+	}
+
+	/**
+	 * Returns an array of objects where each one is a single attribute change description.
+	 *
+	 * @private
+	 * @param {module:engine/model/range~Range} range The range where the change happened.
+	 * @param {Map} oldAttributes A map, map iterator or compatible object that contains attributes before the change.
+	 * @param {Map} newAttributes A map, map iterator or compatible object that contains attributes after the change.
+	 * @returns {Array.<Object>} An array containing one or more diff items.
+	 */
+	_getAttributesDiff( range, oldAttributes, newAttributes ) {
+		// Results holder.
+		const diffs = [];
+
+		// Clone new attributes as we will be performing changes on this object.
+		newAttributes = new Map( newAttributes );
+
+		// Look through old attributes.
+		for ( const [ key, oldValue ] of oldAttributes ) {
+			// Check what is the new value of the attribute (or if it was removed).
+			const newValue = newAttributes.has( key ) ? newAttributes.get( key ) : null;
+
+			// If values are different (or attribute was removed)...
+			if ( newValue !== oldValue ) {
+				// Add diff item.
+				diffs.push( {
+					type: 'attribute',
+					position: range.start,
+					range: Range.createFromRange( range ),
+					length: 1,
+					attributeKey: key,
+					attributeOldValue: oldValue,
+					attributeNewValue: newValue,
+					changeCount: this._changeCount++
+				} );
+			}
+
+			// Prevent returning two diff items for the same change.
+			newAttributes.delete( key );
+		}
+
+		// Look through new attributes that weren't handled above.
+		for ( const [ key, newValue ] of newAttributes ) {
+			// Each of them is a new attribute. Add diff item.
+			diffs.push( {
+				type: 'attribute',
+				position: range.start,
+				range: Range.createFromRange( range ),
+				length: 1,
+				attributeKey: key,
+				attributeOldValue: null,
+				attributeNewValue: newValue,
+				changeCount: this._changeCount++
+			} );
+		}
+
+		return diffs;
+	}
+
+	/**
+	 * Checks whether given element or any of its parents is an element that is buffered as an inserted element.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} element Element to check.
+	 * @returns {Boolean}
+	 */
+	_isInInsertedElement( element ) {
+		const parent = element.parent;
+
+		if ( !parent ) {
+			return false;
+		}
+
+		const changes = this._changesInElement.get( parent );
+		const offset = element.startOffset;
+
+		if ( changes ) {
+			for ( const change of changes ) {
+				if ( change.type == 'insert' && offset >= change.offset && offset < change.offset + change.howMany ) {
+					return true;
+				}
+			}
+		}
+
+		return this._isInInsertedElement( parent );
+	}
+
+	/**
+	 * Removes deeply all buffered changes that are registered in elements from range specified by `parent`, `offset`
+	 * and `howMany`.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} parent
+	 * @param {Number} offset
+	 * @param {Number} howMany
+	 */
+	_removeAllNestedChanges( parent, offset, howMany ) {
+		const range = Range.createFromParentsAndOffsets( parent, offset, parent, offset + howMany );
+
+		for ( const item of range.getItems( { shallow: true } ) ) {
+			if ( item.is( 'element' ) ) {
+				this._elementSnapshots.delete( item );
+				this._changesInElement.delete( item );
+
+				this._removeAllNestedChanges( item, 0, item.maxOffset );
+			}
+		}
+	}
+}
+
+// Returns an array that is a copy of passed child list with the exception that text nodes are split to one or more
+// objects, each representing one character and attributes set on that character.
+function _getChildrenSnapshot( children ) {
+	const snapshot = [];
+
+	for ( const child of children ) {
+		if ( child.is( 'text' ) ) {
+			for ( let i = 0; i < child.data.length; i++ ) {
+				snapshot.push( {
+					name: '$text',
+					attributes: new Map( child.getAttributes() )
+				} );
+			}
+		} else {
+			snapshot.push( {
+				name: child.name,
+				attributes: new Map( child.getAttributes() )
+			} );
+		}
+	}
+
+	return snapshot;
+}
+
+// Generates array of actions for given changes set.
+// It simulates what `diff` function does.
+// Generated actions are:
+// - 'e' for 'equal' - when item at that position did not change,
+// - 'i' for 'insert' - when item at that position was inserted,
+// - 'r' for 'remove' - when item at that position was removed,
+// - 'a' for 'attribute' - when item at that position has it attributes changed.
+//
+// Example (assume that uppercase letters have bold attribute, compare with function code):
+//
+// children before:	fooBAR
+// children after:	foxybAR
+//
+// changes: type: remove, offset: 1, howMany: 1
+//			type: insert, offset: 2, howMany: 2
+//			type: attribute, offset: 4, howMany: 1
+//
+// expected actions: equal (f), remove (o), equal (o), insert (x), insert (y), attribute (b), equal (A), equal (R)
+//
+// steps taken by th script:
+//
+// 1. change = "type: remove, offset: 1, howMany: 1"; offset = 0; oldChildrenHandled = 0
+//    1.1 between this change and the beginning is one not-changed node, fill with one equal action, one old child has been handled
+//    1.2 this change removes one node, add one remove action
+//    1.3 change last visited `offset` to 1
+//    1.4 since an old child has been removed, one more old child has been handled
+//    1.5 actions at this point are: equal, remove
+//
+// 2. change = "type: insert, offset: 2, howMany: 2"; offset = 1; oldChildrenHandled = 2
+//    2.1 between this change and previous change is one not-changed node, add equal action, another one old children has been handled
+//    2.2 this change inserts two nodes, add two insert actions
+//    2.3 change last visited offset to the end of the inserted range, that is 4
+//    2.4 actions at this point are: equal, remove, equal, insert, insert
+//
+// 3. change = "type: attribute, offset: 4, howMany: 1"; offset = 4, oldChildrenHandled = 3
+//    3.1 between this change and previous change are no not-changed nodes
+//    3.2 this change changes one node, add one attribute action
+//    3.3 change last visited `offset` to the end of change range, that is 5
+//    3.4 since an old child has been changed, one more old child has been handled
+//    3.5 actions at this point are: equal, remove, equal, insert, insert, attribute
+//
+// 4. after loop oldChildrenHandled = 4, oldChildrenLength = 6 (fooBAR is 6 characters)
+//    4.1 fill up with two equal actions
+//
+// The result actions are: equal, remove, equal, insert, insert, attribute, equal, equal.
+function _generateActionsFromChanges( oldChildrenLength, changes ) {
+	const actions = [];
+
+	let offset = 0;
+	let oldChildrenHandled = 0;
+
+	// Go through all buffered 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( '' ) );
+
+			oldChildrenHandled += change.offset - offset;
+		}
+
+		// Then, fill up actions accordingly to change type.
+		if ( change.type == 'insert' ) {
+			actions.push( ...'i'.repeat( change.howMany ).split( '' ) );
+
+			// 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( '' ) );
+
+			// The last handled offset is at the position where the nodes were removed.
+			offset = change.offset;
+			// We removed `howMany` old nodes, update `oldChildrenHandled`.
+			oldChildrenHandled += change.howMany;
+		} else {
+			actions.push( ...'a'.repeat( change.howMany ).split( '' ) );
+
+			// The last handled offset isa at the position after the changed range.
+			offset = change.offset + change.howMany;
+			// We changed `howMany` old nodes, update `oldChildrenHandled`.
+			oldChildrenHandled += change.howMany;
+		}
+	}
+
+	// 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( '' ) );
+	}
+
+	return actions;
+}
+
+// Filter callback for Array.filter that filters out change entries that are in graveyard.
+function _changesInGraveyardFilter( entry ) {
+	const posInGy = entry.position && entry.position.root.rootName == '$graveyard';
+	const rangeInGy = entry.range && entry.range.root.rootName == '$graveyard';
+
+	return !posInGy && !rangeInGy;
+}

+ 180 - 309
packages/ckeditor5-engine/src/model/document.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -7,20 +7,13 @@
  * @module engine/model/document
  */
 
-// Load all basic deltas and transformations, they register themselves.
-import './delta/basic-deltas';
-import './delta/basic-transformations';
-
+import Differ from './differ';
 import Range from './range';
 import Position from './position';
 import RootElement from './rootelement';
-import Batch from './batch';
 import History from './history';
 import DocumentSelection from './documentselection';
-import Schema from './schema';
-import TreeWalker from './treewalker';
-import MarkerCollection from './markercollection';
-import deltaTransform from './delta/transform';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
@@ -31,15 +24,9 @@ const graveyardName = '$graveyard';
 
 /**
  * Document tree model describes all editable data in the editor. It may contain multiple
- * {@link module:engine/model/document~Document#roots root elements}, for example if the editor have multiple editable areas,
- * each area will be represented by the separate root.
- *
- * All changes in the document are done by {@link module:engine/model/operation/operation~Operation operations}. To create operations in
- * a simple way, use the {@link module:engine/model/batch~Batch} API, for example:
+ * {@link module:engine/model/document~Document#roots root elements}. For example, if the editor has multiple editable areas,
+ * each area will be represented by a separate root.
  *
- *		doc.batch().insert( position, nodes ).split( otherPosition );
- *
- * @see module:engine/model/document~Document#batch
  * @mixes module:utils/emittermixin~EmitterMixin
  */
 export default class Document {
@@ -47,27 +34,28 @@ export default class Document {
 	 * Creates an empty document instance with no {@link #roots} (other than
 	 * the {@link #graveyard graveyard root}).
 	 */
-	constructor() {
+	constructor( model ) {
 		/**
-		 * Document version. It starts from `0` and every operation increases the version number. It is used to ensure that
-		 * operations are applied on the proper document version.
-		 * If the {@link module:engine/model/operation/operation~Operation#baseVersion} will not match document version the
-		 * {@link module:utils/ckeditorerror~CKEditorError model-document-applyOperation-wrong-version} error is thrown.
+		 * The {@link module:engine/model/model~Model model} that the document is a part of.
 		 *
 		 * @readonly
-		 * @member {Number}
+		 * @member {module:engine/model/model~Model}
 		 */
-		this.version = 0;
+		this.model = model;
 
 		/**
-		 * Schema for this document.
+		 * The document version. It starts from `0` and every operation increases the version number. It is used to ensure that
+		 * operations are applied on a proper document version.
+		 * If the {@link module:engine/model/operation/operation~Operation#baseVersion base version} does not match the document version,
+		 * a {@link module:utils/ckeditorerror~CKEditorError model-document-applyOperation-wrong-version} error is thrown.
 		 *
-		 * @member {module:engine/model/schema~Schema}
+		 * @readonly
+		 * @member {Number}
 		 */
-		this.schema = new Schema();
+		this.version = 0;
 
 		/**
-		 * Document's history.
+		 * The document's history.
 		 *
 		 * **Note:** Be aware that deltas applied to the document might get removed or changed.
 		 *
@@ -77,122 +65,137 @@ export default class Document {
 		this.history = new History( this );
 
 		/**
-		 * Document's markers' collection.
+		 * The selection done on this document.
 		 *
 		 * @readonly
-		 * @member {module:engine/model/markercollection~MarkerCollection}
+		 * @member {module:engine/model/documentselection~DocumentSelection}
 		 */
-		this.markers = new MarkerCollection();
+		this.selection = new DocumentSelection( this );
 
 		/**
-		 * Selection done on this document.
+		 * A list of roots that are owned and managed by this document. Use {@link #createRoot} and
+		 * {@link #getRoot} to manipulate it.
 		 *
 		 * @readonly
-		 * @member {module:engine/model/documentselection~DocumentSelection}
+		 * @member {module:utils/collection~Collection}
 		 */
-		this.selection = new DocumentSelection( this );
+		this.roots = new Collection( { idProperty: 'rootName' } );
 
 		/**
-		 * Array of pending changes. See: {@link #enqueueChanges}.
+		 * The model differ object. Its role is to buffer changes done on the model document and then calculate a diff of those changes.
 		 *
-		 * @private
-		 * @member {Array.<Function>}
+		 * @readonly
+		 * @member {module:engine/model/differ~Differ}
 		 */
-		this._pendingChanges = [];
+		this.differ = new Differ();
 
 		/**
-		 * List of roots that are owned and managed by this document. Use {@link #createRoot} and
-		 * {@link #getRoot} to manipulate it.
+		 * Post-fixer callbacks registered to the model document.
 		 *
-		 * @readonly
-		 * @member {Map}
+		 * @private
+		 * @member {Set}
 		 */
-		this.roots = new Map();
-
-		// Add events that will ensure selection correctness.
-		this.selection.on( 'change:range', () => {
-			for ( const range of this.selection.getRanges() ) {
-				if ( !this._validateSelectionRange( range ) ) {
-					/**
-					 * Range from {@link module:engine/model/documentselection~DocumentSelection document selection}
-					 * starts or ends at incorrect position.
-					 *
-					 * @error document-selection-wrong-position
-					 * @param {module:engine/model/range~Range} range
-					 */
-					throw new CKEditorError( 'document-selection-wrong-position: ' +
-						'Range from document selection starts or ends at incorrect position.', { range } );
-				}
-			}
-		} );
+		this._postFixers = new Set();
 
 		// Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
 		this.createRoot( '$root', graveyardName );
-	}
 
-	/**
-	 * Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
-	 *
-	 * @readonly
-	 * @member {module:engine/model/rootelement~RootElement}
-	 */
-	get graveyard() {
-		return this.getRoot( graveyardName );
-	}
+		// First, if the operation is a document operation check if it's base version is correct.
+		this.listenTo( model, 'applyOperation', ( evt, args ) => {
+			const operation = args[ 0 ];
+
+			if ( operation.isDocumentOperation && operation.baseVersion !== this.version ) {
+				/**
+				 * Only operations with matching versions can be applied.
+				 *
+				 * @error document-applyOperation-wrong-version
+				 * @param {module:engine/model/operation/operation~Operation} operation
+				 */
+				throw new CKEditorError(
+					'model-document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
+					{ operation }
+				);
+			}
+		}, { priority: 'highest' } );
 
-	/**
-	 * This is the entry point for all document changes. All changes on the document are done using
-	 * {@link module:engine/model/operation/operation~Operation operations}. To create operations in the simple way use the
-	 * {@link module:engine/model/batch~Batch} API available via {@link #batch} method.
-	 *
-	 * @fires event:change
-	 * @param {module:engine/model/operation/operation~Operation} operation Operation to be applied.
-	 */
-	applyOperation( operation ) {
-		if ( operation.baseVersion !== this.version ) {
-			/**
-			 * Only operations with matching versions can be applied.
-			 *
-			 * @error document-applyOperation-wrong-version
-			 * @param {module:engine/model/operation/operation~Operation} operation
-			 */
-			throw new CKEditorError(
-				'model-document-applyOperation-wrong-version: Only operations with matching versions can be applied.',
-				{ operation } );
-		}
+		// Then, still before an operation is applied on model, buffer the change in differ.
+		this.listenTo( model, 'applyOperation', ( evt, args ) => {
+			const operation = args[ 0 ];
 
-		const changes = operation._execute();
+			if ( operation.isDocumentOperation ) {
+				this.differ.bufferOperation( operation );
+			}
+		}, { priority: 'high' } );
 
-		this.version++;
+		// After the operation is applied, bump document's version and add the operation to the history.
+		this.listenTo( model, 'applyOperation', ( evt, args ) => {
+			const operation = args[ 0 ];
 
-		this.history.addDelta( operation.delta );
+			if ( operation.isDocumentOperation ) {
+				this.version++;
+				this.history.addDelta( operation.delta );
+			}
+		}, { priority: 'low' } );
+
+		// Listen to selection changes. If selection changed, mark it.
+		let hasSelectionChanged = false;
+
+		this.listenTo( this.selection, 'change', () => {
+			hasSelectionChanged = true;
+		} );
 
-		this.fire( 'change', operation.type, changes, operation.delta.batch, operation.delta.type );
+		// Wait for `_change` event from model, which signalizes that outermost change block has finished.
+		// When this happens, check if there were any changes done on document, and if so, call post fixers,
+		// fire `change` event for features and conversion and then reset the differ.
+		this.listenTo( model, '_change', ( evt, writer ) => {
+			if ( !this.differ.isEmpty || hasSelectionChanged ) {
+				this._callPostFixers( writer );
+
+				this.fire( 'change', writer.batch );
+
+				this.differ.reset();
+				hasSelectionChanged = false;
+			}
+		} );
+
+		// Buffer marker changes.
+		// This is not covered in buffering operations because markers may change outside of them (when they
+		// are modified using `model.markers` collection, not through `MarkerOperation`).
+		this.listenTo( model.markers, 'update', ( evt, marker, oldRange, newRange ) => {
+			// Whenever marker is updated, buffer that change.
+			this.differ.bufferMarkerChange( marker.name, oldRange, newRange );
+
+			if ( !oldRange ) {
+				// Whenever marker changes, buffer that.
+				marker.on( 'change', ( evt, oldRange ) => {
+					this.differ.bufferMarkerChange( marker.name, oldRange, marker.getRange() );
+				} );
+			}
+		} );
 	}
 
 	/**
-	 * Creates a {@link module:engine/model/batch~Batch} instance which allows to change the document.
+	 * The graveyard tree root. A document always has a graveyard root that stores removed nodes.
 	 *
-	 * @param {String} [type] Batch type. See {@link module:engine/model/batch~Batch#type}.
-	 * @returns {module:engine/model/batch~Batch} Batch instance.
+	 * @readonly
+	 * @member {module:engine/model/rootelement~RootElement}
 	 */
-	batch( type ) {
-		return new Batch( this, type );
+	get graveyard() {
+		return this.getRoot( graveyardName );
 	}
 
 	/**
 	 * Creates a new top-level root.
 	 *
-	 * @param {String} [elementName='$root'] Element name. Defaults to `'$root'` which also have
-	 * some basic schema defined (`$block`s are allowed inside the `$root`). Make sure to define a proper
-	 * schema if you use a different name.
-	 * @param {String} [rootName='main'] Unique root name.
-	 * @returns {module:engine/model/rootelement~RootElement} Created root.
+	 * @param {String} [elementName='$root'] The element name. Defaults to `'$root'` which also has some basic schema defined
+	 * (`$block`s are allowed inside the `$root`). Make sure to define a proper schema if you use a different name.
+	 * @param {String} [rootName='main'] A unique root name.
+	 * @returns {module:engine/model/rootelement~RootElement} The created root.
 	 */
 	createRoot( elementName = '$root', rootName = 'main' ) {
-		if ( this.roots.has( rootName ) ) {
+		if ( this.roots.get( rootName ) ) {
 			/**
-			 * Root with specified name already exists.
+			 * A root with the specified name already exists.
 			 *
 			 * @error model-document-createRoot-name-exists
 			 * @param {module:engine/model/document~Document} doc
@@ -205,13 +208,13 @@ export default class Document {
 		}
 
 		const root = new RootElement( this, elementName, rootName );
-		this.roots.set( rootName, root );
+		this.roots.add( root );
 
 		return root;
 	}
 
 	/**
-	 * Removes all events listeners set by document instance.
+	 * Removes all event listeners set by the document instance.
 	 */
 	destroy() {
 		this.selection.destroy();
@@ -219,165 +222,88 @@ export default class Document {
 	}
 
 	/**
-	 * Enqueues document changes. Any changes to be done on document (mostly using {@link #batch}
-	 * should be placed in the queued callback. If no other plugin is changing document at the moment, the callback will be
-	 * called immediately. Otherwise it will wait for all previously queued changes to finish happening. This way
-	 * queued callback will not interrupt other callbacks.
-	 *
-	 * When all queued changes are done {@link #event:changesDone} event is fired.
-	 *
-	 * @fires changesDone
-	 * @param {Function} callback Callback to enqueue.
-	 */
-	enqueueChanges( callback ) {
-		this._pendingChanges.push( callback );
-
-		if ( this._pendingChanges.length == 1 ) {
-			while ( this._pendingChanges.length ) {
-				this._pendingChanges[ 0 ]();
-				this._pendingChanges.shift();
-			}
-
-			this.fire( 'changesDone' );
-		}
-	}
-
-	/**
-	 * Returns top-level root by its name.
+	 * Returns the top-level root by its name.
 	 *
-	 * @param {String} [name='main'] Unique root name.
-	 * @returns {module:engine/model/rootelement~RootElement} Root registered under given name.
+	 * @param {String} [name='main'] A unique root name.
+	 * @returns {module:engine/model/rootelement~RootElement|null} The root registered under a given name or null when
+	 * there is no root with the given name.
 	 */
 	getRoot( name = 'main' ) {
-		if ( !this.roots.has( name ) ) {
-			/**
-			 * Root with specified name does not exist.
-			 *
-			 * @error model-document-getRoot-root-not-exist
-			 * @param {String} name
-			 */
-			throw new CKEditorError(
-				'model-document-getRoot-root-not-exist: Root with specified name does not exist.',
-				{ name }
-			);
-		}
-
 		return this.roots.get( name );
 	}
 
 	/**
-	 * Checks if root with given name is defined.
-	 *
-	 * @param {String} name Name of root to check.
-	 * @returns {Boolean}
-	 */
-	hasRoot( name ) {
-		return this.roots.has( name );
-	}
-
-	/**
-	 * Returns array with names of all roots (without the {@link #graveyard}) added to the document.
+	 * Returns an array with names of all roots (without the {@link #graveyard}) added to the document.
 	 *
 	 * @returns {Array.<String>} Roots names.
 	 */
 	getRootNames() {
-		return Array.from( this.roots.keys() ).filter( name => name != graveyardName );
+		return Array.from( this.roots, root => root.rootName ).filter( name => name != graveyardName );
 	}
 
 	/**
-	 * Basing on given `position`, finds and returns a {@link module:engine/model/range~Range Range} instance that is
-	 * nearest to that `position` and is a correct range for selection.
+	 * Used to register a post-fixer callback. A post-fixer mechanism guarantees that the features that listen to
+	 * the {@link module:engine/model/model~Model#event:_change model's change event} will operate on a correct model state.
 	 *
-	 * Correct selection range might be collapsed - when it's located in position where text node can be placed.
-	 * Non-collapsed range is returned when selection can be placed around element marked as "object" in
-	 * {@link module:engine/model/schema~Schema schema}.
+	 * An execution of a feature may lead to an incorrect document tree state. The callbacks are used to fix the document tree after
+	 * it has changed. Post-fixers are fired just after all changes from the outermost change block were applied but
+	 * before the {@link module:engine/model/document~Document#event:change change 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.
 	 *
-	 * Direction of searching for nearest correct selection range can be specified as:
-	 * * `both` - searching will be performed in both ways,
-	 * * `forward` - searching will be performed only forward,
-	 * * `backward` - searching will be performed only backward.
+	 * As a parameter, a post-fixer callback receives a {@link module:engine/model/writer~Writer writer} instance connected with the
+	 * executed changes block. Thanks to that, all changes done by the callback will be added to the same
+	 * {@link module:engine/model/batch~Batch batch} (and undo step) as the original changes. This makes post-fixer changes transparent
+	 * for the user.
 	 *
-	 * When valid selection range cannot be found, `null` is returned.
+	 * An example of a post-fixer is a callback that checks if all the data were removed from the editor. If so, the
+	 * callback should add an empty paragraph so that the editor is never empty:
 	 *
-	 * @param {module:engine/model/position~Position} position Reference position where new selection range should be looked for.
-	 * @param {'both'|'forward'|'backward'} [direction='both'] Search direction.
-	 * @returns {module:engine/model/range~Range|null} Nearest selection range or `null` if one cannot be found.
-	 */
-	getNearestSelectionRange( position, direction = 'both' ) {
-		// Return collapsed range if provided position is valid.
-		if ( this.schema.check( { name: '$text', inside: position } ) ) {
-			return new Range( position );
-		}
-
-		let backwardWalker, forwardWalker;
-
-		if ( direction == 'both' || direction == 'backward' ) {
-			backwardWalker = new TreeWalker( { startPosition: position, direction: 'backward' } );
-		}
-
-		if ( direction == 'both' || direction == 'forward' ) {
-			forwardWalker = new TreeWalker( { startPosition: position } );
-		}
-
-		for ( const data of combineWalkers( backwardWalker, forwardWalker ) ) {
-			const type = ( data.walker == backwardWalker ? 'elementEnd' : 'elementStart' );
-			const value = data.value;
-
-			if ( value.type == type && this.schema.objects.has( value.item.name ) ) {
-				return Range.createOn( value.item );
-			}
-
-			if ( this.schema.check( { name: '$text', inside: value.nextPosition } ) ) {
-				return new Range( value.nextPosition );
-			}
-		}
-
-		return null;
-	}
-
-	/**
-	 * Transforms two sets of deltas by themselves. Returns both transformed sets.
+	 *		document.registerPostFixer( writer => {
+	 *			const changes = document.differ.getChanges();
 	 *
-	 * @param {Array.<module:engine/model/delta/delta~Delta>} deltasA Array with the first set of deltas to transform. These
-	 * deltas are considered more important (than `deltasB`) when resolving conflicts.
-	 * @param {Array.<module:engine/model/delta/delta~Delta>} deltasB Array with the second set of deltas to transform. These
-	 * deltas are considered less important (than `deltasA`) when resolving conflicts.
-	 * @param {Boolean} [useContext=false] When set to `true`, transformation will store and use additional context
-	 * information to guarantee more expected results. Should be used whenever deltas related to already applied
-	 * deltas are transformed (for example when undoing changes).
-	 * @returns {Object}
-	 * @returns {Array.<module:engine/model/delta/delta~Delta>} return.deltasA The first set of deltas transformed
-	 * by the second set of deltas.
-	 * @returns {Array.<module:engine/model/delta/delta~Delta>} return.deltasB The second set of deltas transformed
-	 * by the first set of deltas.
+	 *			// Check if the changes lead to an empty root in the editor.
+	 *			for ( const entry of changes ) {
+	 *				if ( entry.type == 'remove' && entry.position.root.isEmpty ) {
+	 *					writer.insertElement( 'paragraph', entry.position.root, 0 );
+	 *
+	 *					// It is fine to return early, even if multiple roots would need to be fixed.
+	 *					// All post-fixers will be fired again, so if there are more empty roots, those will be fixed, too.
+	 *					return true;
+	 *				}
+	 *			}
+	 *		} );
+	 *
+	 * @param {Function} postFixer
 	 */
-	transformDeltas( deltasA, deltasB, useContext = false ) {
-		return deltaTransform.transformDeltaSets( deltasA, deltasB, useContext ? this : null );
+	registerPostFixer( postFixer ) {
+		this._postFixers.add( postFixer );
 	}
 
 	/**
-	 * Custom toJSON method to solve child-parent circular dependencies.
+	 * A custom `toJSON()` method to solve child-parent circular dependencies.
 	 *
-	 * @returns {Object} Clone of this object with the document property changed to string.
+	 * @returns {Object} A clone of this object with the document property changed to a string.
 	 */
 	toJSON() {
 		const json = clone( this );
 
 		// Due to circular references we need to remove parent reference.
 		json.selection = '[engine.model.DocumentSelection]';
+		json.model = '[engine.model.Model]';
 
 		return json;
 	}
 
 	/**
-	 * Returns default root for this document which is either the first root that was added to the the document using
+	 * Returns the default root for this document which is either the first root that was added to the document using
 	 * {@link #createRoot} or the {@link #graveyard graveyard root} if no other roots were created.
 	 *
 	 * @protected
 	 * @returns {module:engine/model/rootelement~RootElement} The default root for this document.
 	 */
 	_getDefaultRoot() {
-		for ( const root of this.roots.values() ) {
+		for ( const root of this.roots ) {
 			if ( root !== this.graveyard ) {
 				return root;
 			}
@@ -387,29 +313,30 @@ export default class Document {
 	}
 
 	/**
-	 * Returns a default range for this selection. The default range is a collapsed range that starts and ends
-	 * at the beginning of this selection's document's {@link #_getDefaultRoot default root}.
+	 * Returns the default range for this selection. The default range is a collapsed range that starts and ends
+	 * at the beginning of this selection's document {@link #_getDefaultRoot default root}.
 	 *
 	 * @protected
 	 * @returns {module:engine/model/range~Range}
 	 */
 	_getDefaultRange() {
 		const defaultRoot = this._getDefaultRoot();
+		const schema = this.model.schema;
 
 		// Find the first position where the selection can be put.
 		const position = new Position( defaultRoot, [ 0 ] );
-		const nearestRange = this.getNearestSelectionRange( position );
+		const nearestRange = schema.getNearestSelectionRange( position );
 
 		// If valid selection range is not found - return range collapsed at the beginning of the root.
 		return nearestRange || new Range( position );
 	}
 
 	/**
-	 * Checks whether given {@link module:engine/model/range~Range range} is a valid range for
-	 * {@link #selection document's selection}.
+	 * Checks whether a given {@link module:engine/model/range~Range range} is a valid range for
+	 * the {@link #selection document's selection}.
 	 *
 	 * @private
-	 * @param {module:engine/model/range~Range} range Range to check.
+	 * @param {module:engine/model/range~Range} range A range to check.
 	 * @returns {Boolean} `true` if `range` is valid, `false` otherwise.
 	 */
 	_validateSelectionRange( range ) {
@@ -417,48 +344,31 @@ export default class Document {
 	}
 
 	/**
-	 * Fired when document changes by applying an operation.
-	 *
-	 * There are 5 types of change:
-	 *
-	 * * 'insert' when nodes are inserted,
-	 * * 'remove' when nodes are removed,
-	 * * 'reinsert' when remove is undone,
-	 * * 'move' when nodes are moved,
-	 * * 'rename' when element is renamed,
-	 * * 'marker' when a marker changes (added, removed or its range is changed),
-	 * * 'addAttribute' when attributes are added,
-	 * * 'removeAttribute' when attributes are removed,
-	 * * 'changeAttribute' when attributes change,
-	 * * 'addRootAttribute' when attribute for root is added,
-	 * * 'removeRootAttribute' when attribute for root is removed,
-	 * * 'changeRootAttribute' when attribute for root changes.
+	 * Performs post-fixer loops. Executes post-fixer callbacks as long as none of them has done any changes to the model.
 	 *
-	 * @event change
-	 * @param {String} type Change type, possible option: 'insert', 'remove', 'reinsert', 'move', 'attribute'.
-	 * @param {Object} data Additional information about the change.
-	 * @param {module:engine/model/range~Range} [data.range] Range in model containing changed nodes. Note that the range state is
-	 * after changes has been done, i.e. for 'remove' the range will be in the {@link #graveyard graveyard root}.
-	 * The range is not defined for root, rename and marker types.
-	 * @param {module:engine/model/position~Position} [data.sourcePosition] Change source position.
-	 * Exists for 'remove', 'reinsert' and 'move'.
-	 * Note that this position state is before changes has been done, i.e. for 'reinsert' the source position will be in the
-	 * {@link #graveyard graveyard root}.
-	 * @param {String} [data.key] Only for attribute types. Key of changed / inserted / removed attribute.
-	 * @param {*} [data.oldValue] Only for 'removeAttribute', 'removeRootAttribute', 'changeAttribute' or
-	 * 'changeRootAttribute' type.
-	 * @param {*} [data.newValue] Only for 'addAttribute', 'addRootAttribute', 'changeAttribute' or
-	 * 'changeRootAttribute' type.
-	 * @param {module:engine/model/rootelement~RootElement} [data.root] Root element which attributes got changed. This is defined
-	 * only for root types.
-	 * @param {module:engine/model/batch~Batch} batch A {@link module:engine/model/batch~Batch batch}
-	 * of changes which this change is a part of.
+	 * @private
 	 */
+	_callPostFixers( writer ) {
+		let wasFixed = false;
+
+		do {
+			for ( const callback of this._postFixers ) {
+				wasFixed = callback( writer );
+
+				if ( wasFixed ) {
+					break;
+				}
+			}
+		} while ( wasFixed );
+	}
 
 	/**
-	 * Fired when all queued document changes are done. See {@link #enqueueChanges}.
+	 * Fired after an {@link module:engine/model/model~Model#enqueueChange enqueue change block} or the outermost
+	 * {@link module:engine/model/model~Model#change change block} was executed and the document model tree was changed
+	 * during that block execution.
 	 *
-	 * @event changesDone
+	 * @event change
+	 * @param {module:engine/model/batch~Batch} batch The batch that was used in the executed changes block.
 	 */
 }
 
@@ -478,42 +388,3 @@ function validateTextNodePosition( rangeBoundary ) {
 
 	return true;
 }
-
-// Generator function returning values from provided walkers, switching between them at each iteration. If only one walker
-// is provided it will return data only from that walker.
-//
-// @param {module:engine/module/treewalker~TreeWalker} [backward] Walker iterating in backward direction.
-// @param {module:engine/module/treewalker~TreeWalker} [forward] Walker iterating in forward direction.
-// @returns {Iterable.<Object>} Object returned at each iteration contains `value` and `walker` (informing which walker returned
-// given value) fields.
-function* combineWalkers( backward, forward ) {
-	let done = false;
-
-	while ( !done ) {
-		done = true;
-
-		if ( backward ) {
-			const step = backward.next();
-
-			if ( !step.done ) {
-				done = false;
-				yield{
-					walker: backward,
-					value: step.value
-				};
-			}
-		}
-
-		if ( forward ) {
-			const step = forward.next();
-
-			if ( !step.done ) {
-				done = false;
-				yield{
-					walker: forward,
-					value: step.value
-				};
-			}
-		}
-	}
-}

+ 75 - 58
packages/ckeditor5-engine/src/model/documentfragment.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -10,6 +10,7 @@
 import NodeList from './nodelist';
 import Element from './element';
 import Text from './text';
+import TextProxy from './textproxy';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 
 /**
@@ -17,23 +18,28 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  * can be seen as siblings. In other words, it is a detached part of model tree, without a root.
  *
  * DocumentFragment has own {@link module:engine/model/markercollection~MarkerCollection}. Markers from this collection
- * will be set to the {@link module:engine/model/document~Document#markers document markers} by a
- * {@link module:engine/model/writer~writer.insert} function.
+ * will be set to the {@link module:engine/model/model~Model#markers model markers} by a
+ * {@link module:engine/model/writer~Writer#insert} function.
  */
 export default class DocumentFragment {
 	/**
 	 * Creates an empty `DocumentFragment`.
 	 *
+	 * **Note:** Constructor of this class shouldn't be used directly in the code.
+	 * Use the {@link module:engine/model/writer~Writer#createDocumentFragment} method instead.
+	 *
+	 * @protected
 	 * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children]
 	 * Nodes to be contained inside the `DocumentFragment`.
 	 */
 	constructor( children ) {
 		/**
 		 * DocumentFragment static markers map. This is a list of names and {@link module:engine/model/range~Range ranges}
-		 * which will be set as Markers to {@link module:engine/model/document~Document#markers document markers collection}
+		 * which will be set as Markers to {@link module:engine/model/model~Model#markers model markers collection}
 		 * when DocumentFragment will be inserted to the document.
 		 *
-		 * @member {Map<String, {module:engine/model/range~Range}>} module:engine/model/documentfragment~DocumentFragment#markers
+		 * @readonly
+		 * @member {Map<String,module:engine/model/range~Range>} module:engine/model/documentfragment~DocumentFragment#markers
 		 */
 		this.markers = new Map();
 
@@ -46,14 +52,14 @@ export default class DocumentFragment {
 		this._children = new NodeList();
 
 		if ( children ) {
-			this.insertChildren( 0, children );
+			this._insertChildren( 0, children );
 		}
 	}
 
 	/**
 	 * Returns an iterator that iterates over all nodes contained inside this document fragment.
 	 *
-	 * @returns {Iterator.<module:engine/model/node~Node>}
+	 * @returns {Iterable.<module:engine/model/node~Node>}
 	 */
 	[ Symbol.iterator ]() {
 		return this.getChildren();
@@ -215,55 +221,6 @@ export default class DocumentFragment {
 		return this._children.offsetToIndex( offset );
 	}
 
-	/**
-	 * {@link #insertChildren Inserts} one or more nodes at the end of this document fragment.
-	 *
-	 * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
-	 */
-	appendChildren( nodes ) {
-		this.insertChildren( this.childCount, nodes );
-	}
-
-	/**
-	 * Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes
-	 * to this document fragment.
-	 *
-	 * @param {Number} index Index at which nodes should be inserted.
-	 * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
-	 */
-	insertChildren( index, nodes ) {
-		nodes = normalize( nodes );
-
-		for ( const node of nodes ) {
-			// If node that is being added to this element is already inside another element, first remove it from the old parent.
-			if ( node.parent !== null ) {
-				node.remove();
-			}
-
-			node.parent = this;
-		}
-
-		this._children.insertNodes( index, nodes );
-	}
-
-	/**
-	 * Removes one or more nodes starting at the given index
-	 * and sets {@link module:engine/model/node~Node#parent parent} of these nodes to `null`.
-	 *
-	 * @param {Number} index Index of the first node to remove.
-	 * @param {Number} [howMany=1] Number of nodes to remove.
-	 * @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
-	 */
-	removeChildren( index, howMany = 1 ) {
-		const nodes = this._children.removeNodes( index, howMany );
-
-		for ( const node of nodes ) {
-			node.parent = null;
-		}
-
-		return nodes;
-	}
-
 	/**
 	 * Converts `DocumentFragment` instance to plain object and returns it.
 	 * Takes care of converting all of this document fragment's children.
@@ -302,11 +259,63 @@ export default class DocumentFragment {
 
 		return new DocumentFragment( children );
 	}
+
+	/**
+	 * {@link #_insertChildren Inserts} one or more nodes at the end of this document fragment.
+	 *
+	 * @protected
+	 * @param {module:engine/model/item~Item|Iterable.<module:engine/model/item~Item>} items Items to be inserted.
+	 */
+	_appendChildren( items ) {
+		this._insertChildren( this.childCount, items );
+	}
+
+	/**
+	 * Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes
+	 * to this document fragment.
+	 *
+	 * @protected
+	 * @param {Number} index Index at which nodes should be inserted.
+	 * @param {module:engine/model/item~Item|Iterable.<module:engine/model/item~Item>} items Items to be inserted.
+	 */
+	_insertChildren( index, items ) {
+		const nodes = normalize( items );
+
+		for ( const node of nodes ) {
+			// If node that is being added to this element is already inside another element, first remove it from the old parent.
+			if ( node.parent !== null ) {
+				node._remove();
+			}
+
+			node.parent = this;
+		}
+
+		this._children._insertNodes( index, nodes );
+	}
+
+	/**
+	 * Removes one or more nodes starting at the given index
+	 * and sets {@link module:engine/model/node~Node#parent parent} of these nodes to `null`.
+	 *
+	 * @protected
+	 * @param {Number} index Index of the first node to remove.
+	 * @param {Number} [howMany=1] Number of nodes to remove.
+	 * @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
+	 */
+	_removeChildren( index, howMany = 1 ) {
+		const nodes = this._children._removeNodes( index, howMany );
+
+		for ( const node of nodes ) {
+			node.parent = null;
+		}
+
+		return nodes;
+	}
 }
 
 // Converts strings to Text and non-iterables to arrays.
 //
-// @param {String|module:engine/model/node~Node|Iterable.<String|module:engine/model/node~Node>}
+// @param {String|module:engine/model/item~Item|Iterable.<module:engine/model/item~Item>}
 // @return {Iterable.<module:engine/model/node~Node>}
 function normalize( nodes ) {
 	// Separate condition because string is iterable.
@@ -321,6 +330,14 @@ function normalize( nodes ) {
 	// Array.from to enable .map() on non-arrays.
 	return Array.from( nodes )
 		.map( node => {
-			return typeof node == 'string' ? new Text( node ) : node;
+			if ( typeof node == 'string' ) {
+				return new Text( node );
+			}
+
+			if ( node instanceof TextProxy ) {
+				return new Text( node.data, node.getAttributes() );
+			}
+
+			return node;
 		} );
 }

Fișier diff suprimat deoarece este prea mare
+ 582 - 230
packages/ckeditor5-engine/src/model/documentselection.js


+ 89 - 66
packages/ckeditor5-engine/src/model/element.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -10,6 +10,7 @@
 import Node from './node';
 import NodeList from './nodelist';
 import Text from './text';
+import TextProxy from './textproxy';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 
 /**
@@ -17,11 +18,17 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  * {@link module:engine/model/element~Element#getChildren child nodes}.
  *
  * **Important**: see {@link module:engine/model/node~Node} to read about restrictions using `Element` and `Node` API.
+ *
+ * @extends {module:engine/model/node~Node}
  */
 export default class Element extends Node {
 	/**
 	 * Creates a model element.
 	 *
+	 * **Note:** Constructor of this class shouldn't be used directly in the code.
+	 * Use the {@link module:engine/model/writer~Writer#createElement} method instead.
+	 *
+	 * @protected
 	 * @param {String} name Element's name.
 	 * @param {Object} [attrs] Element's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values.
 	 * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children]
@@ -33,6 +40,7 @@ export default class Element extends Node {
 		/**
 		 * Element name.
 		 *
+		 * @readonly
 		 * @member {String} module:engine/model/element~Element#name
 		 */
 		this.name = name;
@@ -46,7 +54,7 @@ export default class Element extends Node {
 		this._children = new NodeList();
 
 		if ( children ) {
-			this.insertChildren( 0, children );
+			this._insertChildren( 0, children );
 		}
 	}
 
@@ -148,19 +156,6 @@ export default class Element extends Node {
 		return this._children.getNodeStartOffset( node );
 	}
 
-	/**
-	 * Creates a copy of this element and returns it. Created element has the same name and attributes as the original element.
-	 * If clone is deep, the original element's children are also cloned. If not, then empty element is removed.
-	 *
-	 * @param {Boolean} [deep=false] If set to `true` clones element and all its children recursively. When set to `false`,
-	 * element will be cloned without any child.
-	 */
-	clone( deep = false ) {
-		const children = deep ? Array.from( this._children ).map( node => node.clone( true ) ) : null;
-
-		return new Element( this.name, this.getAttributes(), children );
-	}
-
 	/**
 	 * Returns index of a node that occupies given offset. If given offset is too low, returns `0`. If given offset is
 	 * too high, returns {@link module:engine/model/element~Element#getChildIndex index after last child}.
@@ -182,55 +177,6 @@ export default class Element extends Node {
 		return this._children.offsetToIndex( offset );
 	}
 
-	/**
-	 * {@link module:engine/model/element~Element#insertChildren Inserts} one or more nodes at the end of this element.
-	 *
-	 * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
-	 */
-	appendChildren( nodes ) {
-		this.insertChildren( this.childCount, nodes );
-	}
-
-	/**
-	 * Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes
-	 * to this element.
-	 *
-	 * @param {Number} index Index at which nodes should be inserted.
-	 * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
-	 */
-	insertChildren( index, nodes ) {
-		nodes = normalize( nodes );
-
-		for ( const node of nodes ) {
-			// If node that is being added to this element is already inside another element, first remove it from the old parent.
-			if ( node.parent !== null ) {
-				node.remove();
-			}
-
-			node.parent = this;
-		}
-
-		this._children.insertNodes( index, nodes );
-	}
-
-	/**
-	 * Removes one or more nodes starting at the given index and sets
-	 * {@link module:engine/model/node~Node#parent parent} of these nodes to `null`.
-	 *
-	 * @param {Number} index Index of the first node to remove.
-	 * @param {Number} [howMany=1] Number of nodes to remove.
-	 * @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
-	 */
-	removeChildren( index, howMany = 1 ) {
-		const nodes = this._children.removeNodes( index, howMany );
-
-		for ( const node of nodes ) {
-			node.parent = null;
-		}
-
-		return nodes;
-	}
-
 	/**
 	 * Returns a descendant node by its path relative to this element.
 	 *
@@ -273,6 +219,75 @@ export default class Element extends Node {
 		return json;
 	}
 
+	/**
+	 * Creates a copy of this element and returns it. Created element has the same name and attributes as the original element.
+	 * If clone is deep, the original element's children are also cloned. If not, then empty element is removed.
+	 *
+	 * @protected
+	 * @param {Boolean} [deep=false] If set to `true` clones element and all its children recursively. When set to `false`,
+	 * element will be cloned without any child.
+	 */
+	_clone( deep = false ) {
+		const children = deep ? Array.from( this._children ).map( node => node._clone( true ) ) : null;
+
+		return new Element( this.name, this.getAttributes(), children );
+	}
+
+	/**
+	 * {@link module:engine/model/element~Element#_insertChildren Inserts} one or more nodes at the end of this element.
+	 *
+	 * @see module:engine/model/writer~Writer#append
+	 * @protected
+	 * @param {module:engine/model/item~Item|Iterable.<module:engine/model/item~Item>} nodes Nodes to be inserted.
+	 */
+	_appendChildren( nodes ) {
+		this._insertChildren( this.childCount, nodes );
+	}
+
+	/**
+	 * Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes
+	 * to this element.
+	 *
+	 * @see module:engine/model/writer~Writer#insert
+	 * @protected
+	 * @param {Number} index Index at which nodes should be inserted.
+	 * @param {module:engine/model/item~Item|Iterable.<module:engine/model/item~Item>} items Items to be inserted.
+	 */
+	_insertChildren( index, items ) {
+		const nodes = normalize( items );
+
+		for ( const node of nodes ) {
+			// If node that is being added to this element is already inside another element, first remove it from the old parent.
+			if ( node.parent !== null ) {
+				node._remove();
+			}
+
+			node.parent = this;
+		}
+
+		this._children._insertNodes( index, nodes );
+	}
+
+	/**
+	 * Removes one or more nodes starting at the given index and sets
+	 * {@link module:engine/model/node~Node#parent parent} of these nodes to `null`.
+	 *
+	 * @see module:engine/model/writer~Writer#remove
+	 * @protected
+	 * @param {Number} index Index of the first node to remove.
+	 * @param {Number} [howMany=1] Number of nodes to remove.
+	 * @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
+	 */
+	_removeChildren( index, howMany = 1 ) {
+		const nodes = this._children._removeNodes( index, howMany );
+
+		for ( const node of nodes ) {
+			node.parent = null;
+		}
+
+		return nodes;
+	}
+
 	/**
 	 * Creates an `Element` instance from given plain object (i.e. parsed JSON string).
 	 * Converts `Element` children to proper nodes.
@@ -303,7 +318,7 @@ export default class Element extends Node {
 
 // Converts strings to Text and non-iterables to arrays.
 //
-// @param {String|module:engine/model/node~Node|Iterable.<String|module:engine/model/node~Node>}
+// @param {String|module:engine/model/item~Item|Iterable.<String|module:engine/model/item~Item>}
 // @return {Iterable.<module:engine/model/node~Node>}
 function normalize( nodes ) {
 	// Separate condition because string is iterable.
@@ -318,6 +333,14 @@ function normalize( nodes ) {
 	// Array.from to enable .map() on non-arrays.
 	return Array.from( nodes )
 		.map( node => {
-			return typeof node == 'string' ? new Text( node ) : node;
+			if ( typeof node == 'string' ) {
+				return new Text( node );
+			}
+
+			if ( node instanceof TextProxy ) {
+				return new Text( node.data, node.getAttributes() );
+			}
+
+			return node;
 		} );
 }

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -76,7 +76,7 @@ export default class History {
 	 * that deltas from the first one will be returned.
 	 * @param {Number} [to=Number.POSITIVE_INFINITY] Base version up to which deltas should be returned (exclusive).
 	 * Defaults to `Number.POSITIVE_INFINITY` which means that deltas up to the last one will be returned.
-	 * @returns {Iterator.<module:engine/model/delta/delta~Delta>} Deltas added to the history from given base versions range.
+	 * @returns {Iterable.<module:engine/model/delta/delta~Delta>} Deltas added to the history from given base versions range.
 	 */
 	* getDeltas( from = 0, to = Number.POSITIVE_INFINITY ) {
 		// No deltas added, nothing to yield.

+ 2 - 2
packages/ckeditor5-engine/src/model/item.jsdoc

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,7 +8,7 @@
  */
 
 /**
- * Item is a {@link module:engine/model/node~Node Node} or {module:engine/model/textproxy~TextProxy TextProxy}.
+ * Item is a {@link module:engine/model/node~Node} or {@link module:engine/model/textproxy~TextProxy}.
  *
  * @typedef {module:engine/model/node~Node|module:engine/model/textproxy~TextProxy} module:engine/model/item~Item
  */

+ 33 - 13
packages/ckeditor5-engine/src/model/liveposition.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -140,14 +140,20 @@ function bindWithDocument() {
 	const supportedTypes = new Set( [ 'insert', 'move', 'remove', 'reinsert' ] );
 
 	this.listenTo(
-		this.root.document,
-		'change',
-		( event, type, changes ) => {
-			if ( supportedTypes.has( type ) ) {
-				transform.call( this, type, changes.range, changes.sourcePosition );
+		this.root.document.model,
+		'applyOperation',
+		( event, args ) => {
+			const operation = args[ 0 ];
+
+			if ( !operation.isDocumentOperation ) {
+				return;
+			}
+
+			if ( supportedTypes.has( operation.type ) ) {
+				transform.call( this, operation );
 			}
 		},
-		{ priority: 'high' }
+		{ priority: 'low' }
 	);
 }
 
@@ -157,16 +163,24 @@ function bindWithDocument() {
  * @ignore
  * @private
  * @method transform
- * @param {String} type Type of changes applied to the Tree Model.
- * @param {module:engine/model/range~Range} range Range containing the result of applied change.
- * @param {module:engine/model/position~Position} [position] Additional position parameter provided by some change events.
+ * @param {module:engine/model/operation/operation~Operation} operation Executed operation.
  */
-function transform( type, range, position ) {
+function transform( operation ) {
 	/* eslint-disable no-case-declarations */
+	let range;
+	let position;
+
+	if ( operation.type == 'insert' ) {
+		range = Range.createFromPositionAndShift( operation.position, operation.nodes.maxOffset );
+	} else {
+		range = Range.createFromPositionAndShift( operation.getMovedRangeStart(), operation.howMany );
+		position = operation.sourcePosition;
+	}
+
 	const howMany = range.end.offset - range.start.offset;
 	let transformed;
 
-	switch ( type ) {
+	switch ( operation.type ) {
 		case 'insert':
 			const insertBefore = this.stickiness == 'sticksToNext';
 			transformed = this._getTransformedByInsertion( range.start, howMany, insertBefore );
@@ -186,7 +200,13 @@ function transform( type, range, position ) {
 				transformed = this._getCombined( position, range.start );
 			} else {
 				const insertBefore = this.stickiness == 'sticksToNext';
-				transformed = this._getTransformedByMove( position, range.start, howMany, insertBefore );
+
+				// `Position._getTransformedByMove` is expecting `targetPosition` to be "before" move
+				// (before transformation). `range.start` is already after the move happened.
+				// We have to revert `targetPosition` to the state before the move.
+				const targetPosition = range.start._getTransformedByInsertion( position, howMany );
+
+				transformed = this._getTransformedByMove( position, targetPosition, howMany, insertBefore );
 			}
 			break;
 	}

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -120,14 +120,20 @@ function bindWithDocument() {
 	const supportedTypes = new Set( [ 'insert', 'move', 'remove', 'reinsert' ] );
 
 	this.listenTo(
-		this.root.document,
-		'change',
-		( event, type, changes, batch, deltaType ) => {
-			if ( supportedTypes.has( type ) ) {
-				transform.call( this, type, deltaType, batch, changes.range, changes.sourcePosition );
+		this.root.document.model,
+		'applyOperation',
+		( event, args ) => {
+			const operation = args[ 0 ];
+
+			if ( !operation.isDocumentOperation ) {
+				return;
+			}
+
+			if ( supportedTypes.has( operation.type ) ) {
+				transform.call( this, operation );
 			}
 		},
-		{ priority: 'high' }
+		{ priority: 'low' }
 	);
 }
 
@@ -137,13 +143,22 @@ function bindWithDocument() {
  * @ignore
  * @private
  * @method transform
- * @param {String} [changeType] Type of change applied to the model document.
- * @param {String} [deltaType] Type of delta which introduced the change.
- * @param {module:engine/model/batch~Batch} batch Batch which changes the live range.
- * @param {module:engine/model/range~Range} targetRange Range containing the result of applied change.
- * @param {module:engine/model/position~Position} [sourcePosition] Source position for move, remove and reinsert change types.
+ * @param {module:engine/model/operation/operation~Operation} operation Executed operation.
  */
-function transform( changeType, deltaType, batch, targetRange, sourcePosition ) {
+function transform( operation ) {
+	const changeType = operation.type;
+	const batch = operation.delta.batch;
+
+	let targetRange;
+	let sourcePosition;
+
+	if ( changeType == 'insert' ) {
+		targetRange = Range.createFromPositionAndShift( operation.position, operation.nodes.maxOffset );
+	} else {
+		targetRange = Range.createFromPositionAndShift( operation.getMovedRangeStart(), operation.howMany );
+		sourcePosition = operation.sourcePosition;
+	}
+
 	const howMany = targetRange.end.offset - targetRange.start.offset;
 	let targetPosition = targetRange.start;
 
@@ -154,7 +169,7 @@ function transform( changeType, deltaType, batch, targetRange, sourcePosition )
 		targetPosition = targetPosition._getTransformedByInsertion( sourcePosition, howMany );
 	}
 
-	const result = this._getTransformedByDocumentChange( changeType, deltaType, targetPosition, howMany, sourcePosition );
+	const result = this._getTransformedByDocumentChange( changeType, operation.delta.type, targetPosition, howMany, sourcePosition );
 
 	// Decide whether moved part should be included in the range.
 	//

+ 169 - 56
packages/ckeditor5-engine/src/model/markercollection.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -15,14 +15,18 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 /**
- * Creates, stores and manages {@link ~Marker markers}.
+ * The collection of all {@link module:engine/model/markercollection~Marker markers} attached to the document.
+ * It lets you {@link module:engine/model/markercollection~MarkerCollection#get get} markers or track them using
+ * {@link module:engine/model/markercollection~MarkerCollection#event:update} event.
  *
- * Markers are created by {@link ~MarkerCollection#set setting} a name for a {@link module:engine/model/liverange~LiveRange live range}
- * in `MarkerCollection`. Name is used to group and identify markers. Names have to be unique, but markers can be grouped by
- * using common prefixes, separated with `:`, for example: `user:john` or `search:3`.
+ * To create, change or remove makers use {@link module:engine/model/writer~Writer model writers'} methods:
+ * {@link module:engine/model/writer~Writer#setMarker} or {@link module:engine/model/writer~Writer#removeMarker}. Since
+ * the writer is the only proper way to change the data model it is not possible to change markers directly using this
+ * collection. All markers created by the writer will be automatically added to this collection.
  *
- * Since markers are based on {@link module:engine/model/liverange~LiveRange live ranges}, for efficiency reasons, it's
- * best to create and keep at least markers as possible.
+ * By default there is one marker collection available as {@link module:engine/model/model~Model#markers model property}.
+ *
+ * @see module:engine/model/markercollection~Marker
  */
 export default class MarkerCollection {
 	/**
@@ -39,9 +43,11 @@ export default class MarkerCollection {
 	}
 
 	/**
-	 * Returns an iterator that iterates over all {@link ~Marker markers} added to the collection.
+	 * Iterable interface.
+	 *
+	 * Iterates over all {@link ~Marker markers} added to the collection.
 	 *
-	 * @returns {Iterator}
+	 * @returns {Iterable}
 	 */
 	[ Symbol.iterator ]() {
 		return this._markers.values();
@@ -72,35 +78,48 @@ export default class MarkerCollection {
 	 * Creates and adds a {@link ~Marker marker} to the `MarkerCollection` with given name on given
 	 * {@link module:engine/model/range~Range range}.
 	 *
-	 * If `MarkerCollection` already had a marker with given name (or {@link ~Marker marker} was passed) and the range to
-	 * set is different, the marker in collection is removed and then new marker is added. If the range was same, nothing
-	 * happens and `false` is returned.
+	 * If `MarkerCollection` already had a marker with given name (or {@link ~Marker marker} was passed), the marker in
+	 * collection is updated and {@link module:engine/model/markercollection~MarkerCollection#event:update} event is fired
+	 * but only if there was a change (marker range or {@link module:engine/model/markercollection~Marker#managedUsingOperations}
+	 * flag has changed.
 	 *
-	 * @fires module:engine/model/markercollection~MarkerCollection#event:add
-	 * @fires module:engine/model/markercollection~MarkerCollection#event:remove
-	 * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of marker to add or Marker instance to update.
+	 * @protected
+	 * @fires module:engine/model/markercollection~MarkerCollection#event:update
+	 * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of marker to set or marker instance to update.
 	 * @param {module:engine/model/range~Range} range Marker range.
-	 * @returns {module:engine/model/markercollection~Marker} `Marker` instance added to the collection.
+	 * @param {Boolean} [managedUsingOperations=false] Specifies whether the marker is managed using operations.
+	 * @returns {module:engine/model/markercollection~Marker} `Marker` instance which was added or updated.
 	 */
-	set( markerOrName, range ) {
+	_set( markerOrName, range, managedUsingOperations = false ) {
 		const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
 		const oldMarker = this._markers.get( markerName );
 
 		if ( oldMarker ) {
 			const oldRange = oldMarker.getRange();
+			let hasChanged = false;
+
+			if ( !oldRange.isEqual( range ) ) {
+				oldMarker._attachLiveRange( LiveRange.createFromRange( range ) );
+				hasChanged = true;
+			}
 
-			if ( oldRange.isEqual( range ) ) {
-				return oldMarker;
+			if ( managedUsingOperations != oldMarker.managedUsingOperations ) {
+				oldMarker._managedUsingOperations = managedUsingOperations;
+				hasChanged = true;
 			}
 
-			this.remove( markerName );
+			if ( hasChanged ) {
+				this.fire( 'update:' + markerName, oldMarker, oldRange, range );
+			}
+
+			return oldMarker;
 		}
 
 		const liveRange = LiveRange.createFromRange( range );
-		const marker = new Marker( markerName, liveRange );
+		const marker = new Marker( markerName, liveRange, managedUsingOperations );
 
 		this._markers.set( markerName, marker );
-		this.fire( 'add:' + markerName, marker );
+		this.fire( 'update:' + markerName, marker, null, range );
 
 		return marker;
 	}
@@ -108,16 +127,18 @@ export default class MarkerCollection {
 	/**
 	 * Removes given {@link ~Marker marker} or a marker with given name from the `MarkerCollection`.
 	 *
+	 * @protected
+	 * @fires module:engine/model/markercollection~MarkerCollection#event:update
 	 * @param {String} markerOrName Marker or name of a marker to remove.
 	 * @returns {Boolean} `true` if marker was found and removed, `false` otherwise.
 	 */
-	remove( markerOrName ) {
+	_remove( markerOrName ) {
 		const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
 		const oldMarker = this._markers.get( markerName );
 
 		if ( oldMarker ) {
 			this._markers.delete( markerName );
-			this.fire( 'remove:' + markerName, oldMarker );
+			this.fire( 'update:' + markerName, oldMarker, oldMarker.getRange(), null );
 
 			this._destroyMarker( oldMarker );
 
@@ -131,7 +152,7 @@ export default class MarkerCollection {
 	 * Returns iterator that iterates over all markers, which ranges contain given {@link module:engine/model/position~Position position}.
 	 *
 	 * @param {module:engine/model/position~Position} position
-	 * @returns {Iterator.<module:engine/model/markercollection~Marker>}
+	 * @returns {Iterable.<module:engine/model/markercollection~Marker>}
 	 */
 	* getMarkersAtPosition( position ) {
 		for ( const marker of this ) {
@@ -165,7 +186,7 @@ export default class MarkerCollection {
 	 *		Array.from( markersCollection.getMarkersGroup( 'a' ) ); // []
 	 *
 	 * @param prefix
-	 * @returns {Iterator.<module:engine/model/markercollection~Marker>}
+	 * @returns {Iterable.<module:engine/model/markercollection~Marker>}
 	 */
 	* getMarkersGroup( prefix ) {
 		for ( const marker of this._markers.values() ) {
@@ -183,22 +204,18 @@ export default class MarkerCollection {
 	 */
 	_destroyMarker( marker ) {
 		marker.stopListening();
-		marker._liveRange.detach();
-		marker._liveRange = null;
+		marker._detachLiveRange();
 	}
 
 	/**
-	 * Fired whenever marker is added to `MarkerCollection`.
+	 * Fired whenever marker is added, updated or removed from `MarkerCollection`.
 	 *
-	 * @event add
-	 * @param {module:engine/model/markercollection~Marker} The added marker.
-	 */
-
-	/**
-	 * Fired whenever marker is removed from `MarkerCollection`.
-	 *
-	 * @event remove
-	 * @param {module:engine/model/markercollection~Marker} marker The removed marker.
+	 * @event update
+	 * @param {module:engine/model/markercollection~Marker} Updated Marker.
+	 * @param {module:engine/model/range~Range|null} oldRange Marker range before the update. When is not defined it
+	 * means that marker is just added.
+	 * @param {module:engine/model/range~Range|null} newRange Marker range after update. When is not defined it
+	 * means that marker is just removed.
 	 */
 }
 
@@ -207,24 +224,67 @@ mix( MarkerCollection, EmitterMixin );
 /**
  * `Marker` is a continuous parts of model (like a range), is named and represent some kind of information about marked
  * part of model document. In contrary to {@link module:engine/model/node~Node nodes}, which are building blocks of
- * model document tree, markers are not stored directly in document tree. Still, they are document data, by giving
+ * model document tree, markers are not stored directly in document tree but in
+ * {@link module:engine/model/model~Model#markers model markers' collection}. Still, they are document data, by giving
  * additional meaning to the part of a model document between marker start and marker end.
  *
  * In this sense, markers are similar to adding and converting attributes on nodes. The difference is that attribute is
  * connected with a given node (e.g. a character is bold no matter if it gets moved or content around it changes).
- * Markers on the other hand are continuous ranges and are characterised by their start and end position. This means that
+ * Markers on the other hand are continuous ranges and are characterized by their start and end position. This means that
  * any character in the marker is marked by the marker. For example, if a character is moved outside of marker it stops being
  * "special" and the marker is shrunk. Similarly, when a character is moved into the marker from other place in document
  * model, it starts being "special" and the marker is enlarged.
  *
- * Since markers are based on {@link module:engine/model/liverange~LiveRange live ranges}, for efficiency reasons, it's
- * best to create and keep at least markers as possible.
+ * Another upside of markers is that finding marked part of document is fast and easy. Using attributes to mark some nodes
+ * and then trying to find that part of document would require traversing whole document tree. Marker gives instant access
+ * to the range which it is marking at the moment.
+ *
+ * Markers are built from a name and a range.
+ *
+ * Range of the marker is updated automatically when document changes, using
+ * {@link module:engine/model/liverange~LiveRange live range} mechanism.
  *
- * Markers can be converted to view by adding appropriate converters for
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addMarker} and
- * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:removeMarker}
- * events, or by building converters for {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}
- * using {@link module:engine/conversion/buildmodelconverter~buildModelConverter model converter builder}.
+ * Name is used to group and identify markers. Names have to be unique, but markers can be grouped by
+ * using common prefixes, separated with `:`, for example: `user:john` or `search:3`. That's useful in term of creating
+ * namespaces for custom elements (e.g. comments, highlights). You can use this prefixes in
+ * {@link module:engine/model/markercollection~MarkerCollection#event:update} listeners to listen on changes in a group of markers.
+ * For instance: `model.markers.on( 'set:user', callback );` will be called whenever any `user:*` markers changes.
+ *
+ * There are two types of markers.
+ *
+ * 1. Markers managed directly, without using operations. They are added directly by {@link module:engine/model/writer~Writer}
+ * to the {@link module:engine/model/markercollection~MarkerCollection} without any additional mechanism. They can be used
+ * as bookmarks or visual markers. They are great for showing results of the find, or select link when the focus is in the input.
+ *
+ * 1. Markers managed using operations. These markers are also stored in {@link module:engine/model/markercollection~MarkerCollection}
+ * but changes in these markers is managed the same way all other changes in the model structure - using operations.
+ * Therefore, they are handled in the undo stack and synchronized between clients if the collaboration plugin is enabled.
+ * This type of markers is useful for solutions like spell checking or comments.
+ *
+ * Both type of them should be added / updated by {@link module:engine/model/writer~Writer#setMarker}
+ * and removed by {@link module:engine/model/writer~Writer#removeMarker} methods.
+ *
+ *		model.change( ( writer ) => {
+ * 			const marker = writer.setMarker( name, range, { usingOperation: true } );
+ *
+ * 			// ...
+ *
+ * 			writer.removeMarker( marker );
+ *		} );
+ *
+ * See {@link module:engine/model/writer~Writer} to find more examples.
+ *
+ * Since markers need to track change in the document, for efficiency reasons, it is best to create and keep as little
+ * markers as possible and remove them as soon as they are not needed anymore.
+ *
+ * Markers can be downcasted and upcasted.
+ *
+ * Markers downcast happens on {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} and
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} events.
+ * Use {@link module:engine/conversion/downcast-converters downcast converters} or attach a custom converter to mentioned events.
+ * For {@link module:engine/controller/datacontroller~DataController data pipeline}, marker should be downcasted to an element.
+ * Then, it can be upcasted back to a marker. Again, use {@link module:engine/conversion/upcast-converters upcast converters} or
+ * attach a custom converter to {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element}.
  *
  * Another upside of markers is that finding marked part of document is fast and easy. Using attributes to mark some nodes
  * and then trying to find that part of document would require traversing whole document tree. Marker gives instant access
@@ -238,27 +298,47 @@ class Marker {
 	 *
 	 * @param {String} name Marker name.
 	 * @param {module:engine/model/liverange~LiveRange} liveRange Range marked by the marker.
+	 * @param {Boolean} managedUsingOperations Specifies whether the marker is managed using operations.
 	 */
-	constructor( name, liveRange ) {
+	constructor( name, liveRange, managedUsingOperations ) {
 		/**
 		 * Marker's name.
 		 *
 		 * @readonly
-		 * @member {String} #name
+		 * @type {String}
 		 */
 		this.name = name;
 
 		/**
-		 * Range marked by the marker.
+		 * Flag indicates if the marker is managed using operations or not.
 		 *
 		 * @protected
+		 * @member {Boolean}
+		 */
+		this._managedUsingOperations = managedUsingOperations;
+
+		/**
+		 * Range marked by the marker.
+		 *
+		 * @private
 		 * @member {module:engine/model/liverange~LiveRange} #_liveRange
 		 */
-		this._liveRange = liveRange;
+		this._liveRange = this._attachLiveRange( liveRange );
+	}
 
-		// Delegating does not work with namespaces. Alternatively, we could delegate all events (using `*`).
-		this._liveRange.delegate( 'change:range' ).to( this );
-		this._liveRange.delegate( 'change:content' ).to( this );
+	/**
+	 * Returns value of flag indicates if the marker is managed using operations or not.
+	 * See {@link ~Marker marker class description} to learn more about marker types.
+	 * See {@link module:engine/model/writer~Writer#setMarker}.
+	 *
+	 * @returns {Boolean}
+	 */
+	get managedUsingOperations() {
+		if ( !this._liveRange ) {
+			throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.' );
+		}
+
+		return this._managedUsingOperations;
 	}
 
 	/**
@@ -307,13 +387,46 @@ class Marker {
 		return Range.createFromRange( this._liveRange );
 	}
 
+	/**
+	 * Binds new live range to marker and detach the old one if is attached.
+	 *
+	 * @protected
+	 * @param {module:engine/model/liverange~LiveRange} liveRange Live range to attach
+	 * @return {module:engine/model/liverange~LiveRange} Attached live range.
+	 */
+	_attachLiveRange( liveRange ) {
+		if ( this._liveRange ) {
+			this._detachLiveRange();
+		}
+
+		// Delegating does not work with namespaces. Alternatively, we could delegate all events (using `*`).
+		liveRange.delegate( 'change:range' ).to( this );
+		liveRange.delegate( 'change:content' ).to( this );
+
+		this._liveRange = liveRange;
+
+		return liveRange;
+	}
+
+	/**
+	 * Unbinds and destroys currently attached live range.
+	 *
+	 * @protected
+	 */
+	_detachLiveRange() {
+		this._liveRange.stopDelegating( 'change:range', this );
+		this._liveRange.stopDelegating( 'change:content', this );
+		this._liveRange.detach();
+		this._liveRange = null;
+	}
+
 	/**
 	 * Fired whenever {@link ~Marker#_liveRange marker range} is changed due to changes on {@link module:engine/model/document~Document}.
 	 * This is a delegated {@link module:engine/model/liverange~LiveRange#event:change:range LiveRange change:range event}.
 	 *
 	 * When marker is removed from {@link module:engine/model/markercollection~MarkerCollection MarkerCollection},
 	 * all event listeners listening to it should be removed. It is best to do it on
-	 * {@link module:engine/model/markercollection~MarkerCollection#event:remove MarkerCollection remove event}.
+	 * {@link module:engine/model/markercollection~MarkerCollection#event:update MarkerCollection update event}.
 	 *
 	 * @see module:engine/model/liverange~LiveRange#event:change:range
 	 * @event change:range
@@ -327,7 +440,7 @@ class Marker {
 	 *
 	 * When marker is removed from {@link module:engine/model/markercollection~MarkerCollection MarkerCollection},
 	 * all event listeners listening to it should be removed. It is best to do it on
-	 * {@link module:engine/model/markercollection~MarkerCollection#event:remove MarkerCollection remove event}.
+	 * {@link module:engine/model/markercollection~MarkerCollection#event:update MarkerCollection update event}.
 	 *
 	 * @see module:engine/model/liverange~LiveRange#event:change:content
 	 * @event change:content

+ 435 - 0
packages/ckeditor5-engine/src/model/model.js

@@ -0,0 +1,435 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/model
+ */
+
+// Load all basic deltas and transformations, they register themselves.
+import './delta/basic-deltas';
+import './delta/basic-transformations';
+
+import Batch from './batch';
+import Writer from './writer';
+import Schema from './schema';
+import Document from './document';
+import MarkerCollection from './markercollection';
+import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import deltaTransform from './delta/transform';
+import ModelElement from './element';
+import ModelRange from './range';
+
+import insertContent from './utils/insertcontent';
+import deleteContent from './utils/deletecontent';
+import modifySelection from './utils/modifyselection';
+import getSelectedContent from './utils/getselectedcontent';
+
+/**
+ * Editor's data model class. Model defines all the data: both nodes that are attached to the roots of the
+ * {@link module:engine/model/model~Model#document model document}, and also all detached nodes which has not been yet
+ * added to the document.
+ *
+ * All those nodes are created and modified by the {@link module:engine/model/writer~Writer model writer}, which can be
+ * accessed by using {@link module:engine/model/model~Model#change} or {@link module:engine/model/model~Model#enqueueChange} methods.
+ *
+ * @mixes module:utils/observablemixin~ObservableMixin
+ */
+export default class Model {
+	constructor() {
+		/**
+		 * Models markers' collection.
+		 *
+		 * @readonly
+		 * @member {module:engine/model/markercollection~MarkerCollection}
+		 */
+		this.markers = new MarkerCollection();
+
+		/**
+		 * Editors document model.
+		 *
+		 * @readonly
+		 * @member {module:engine/model/document~Document}
+		 */
+		this.document = new Document( this );
+
+		/**
+		 * Schema for editors model.
+		 *
+		 * @readonly
+		 * @member {module:engine/model/schema~Schema}
+		 */
+		this.schema = new Schema();
+
+		/**
+		 * All callbacks added by {@link module:engine/model/model~Model#change} or
+		 * {@link module:engine/model/model~Model#enqueueChange} methods waiting to be executed.
+		 *
+		 * @private
+		 * @type {Array.<Function>}
+		 */
+		this._pendingChanges = [];
+
+		/**
+		 * The last created and currently used writer instance.
+		 *
+		 * @private
+		 * @member {module:engine/model/writer~Writer}
+		 */
+		this._currentWriter = null;
+
+		[ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation' ]
+			.forEach( methodName => this.decorate( methodName ) );
+
+		// Adding operation validation with `highest` priority, so it is called before any other feature would like
+		// to do anything with the operation. If the operation has incorrect parameters it should throw on the earliest occasion.
+		this.on( 'applyOperation', ( evt, args ) => {
+			const operation = args[ 0 ];
+
+			operation._validate();
+		}, { priority: 'highest' } );
+
+		// Register some default abstract entities.
+		this.schema.register( '$root', {
+			isLimit: true
+		} );
+		this.schema.register( '$block', {
+			allowIn: '$root',
+			isBlock: true
+		} );
+		this.schema.register( '$text', {
+			allowIn: '$block'
+		} );
+		this.schema.register( '$clipboardHolder', {
+			allowContentOf: '$root',
+			isLimit: true
+		} );
+		this.schema.extend( '$text', { allowIn: '$clipboardHolder' } );
+
+		// Element needed by `upcastElementToMarker` converter.
+		// This element temporarily represents marker bound during conversion process and is removed
+		// at the end of conversion. `UpcastDispatcher` or at least `Conversion` class looks like a better for this
+		// registration but both know nothing about Schema.
+		this.schema.register( '$marker', {
+			allowIn: [ '$root', '$block' ]
+		} );
+	}
+
+	/**
+	 * Change method is the primary way of changing the model. You should use it to modify any node, including detached
+	 * nodes (not added to the {@link module:engine/model/model~Model#document model document}).
+	 *
+	 *		model.change( writer => {
+	 *			writer.insertText( 'foo', paragraph, 'end' );
+	 *		} );
+	 *
+	 * All changes inside the change block use the same {@link module:engine/model/batch~Batch} so they share the same
+	 * undo step.
+	 *
+	 *		model.change( writer => {
+	 *			writer.insertText( 'foo', paragraph, 'end' ); // foo.
+	 *
+	 *			model.change( writer => {
+	 *				writer.insertText( 'bar', paragraph, 'end' ); // foobar.
+	 *			} );
+	 *
+	 * 			writer.insertText( 'bom', paragraph, 'end' ); // foobarbom.
+	 *		} );
+	 *
+	 * Change block is executed immediately.
+	 *
+	 * You can also return a value from the change block.
+	 *
+	 *		const img = model.change( writer => {
+	 *			return writer.createElement( 'img' );
+	 *		} );
+	 *
+	 * When the outermost block is done the {@link #event:_change} event is fired.
+	 *
+	 * @see #enqueueChange
+	 * @param {Function} callback Callback function which may modify the 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 );
+		}
+	}
+
+	/**
+	 * `enqueueChange` method performs similar task as the {@link #change change method}, with two major differences.
+	 *
+	 * First, the callback of the `enqueueChange` is executed when all other changes are done. It might be executed
+	 * immediately if it is not nested in any other change block, but if it is nested in another (enqueue)change block,
+	 * it will be delayed and executed after the outermost block.
+	 *
+	 *		model.change( writer => {
+	 *			console.log( 1 );
+	 *
+	 *			model.enqueueChange( writer => {
+	 *				console.log( 2 );
+	 *			} );
+	 *
+	 * 			console.log( 3 );
+	 *		} ); // Will log: 1, 3, 2.
+	 *
+	 * Second, it lets you define the {@link module:engine/model/batch~Batch} into which you want to add your changes.
+	 * By default, a new batch is created. In the sample above, `change` and `enqueueChange` blocks use a different
+	 * batch (and different {@link module:engine/model/writer~Writer} since each of them operates on the separate batch).
+	 *
+	 * Using `enqueueChange` block you can also add some changes to the batch you used before.
+	 *
+	 *		model.enqueueChange( batch, writer => {
+	 *			writer.insertText( 'foo', paragraph, 'end' );
+	 *		} );
+	 *
+	 * `Batch` instance can be obtained from {@link module:engine/model/writer~Writer#batch the writer}.
+	 *
+	 * @param {module:engine/model/batch~Batch|String} batchOrType Batch or batch type should be used in the callback.
+	 * If not defined, a new batch will be created.
+	 * @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();
+		}
+
+		this._pendingChanges.push( { batch: batchOrType, callback } );
+
+		if ( this._pendingChanges.length == 1 ) {
+			this._runPendingChanges();
+		}
+	}
+
+	/**
+	 * {@link module:utils/observablemixin~ObservableMixin#decorate Decorated} function to apply
+	 * {@link module:engine/model/operation/operation~Operation operations} on the model.
+	 *
+	 * @param {module:engine/model/operation/operation~Operation} operation Operation to apply
+	 */
+	applyOperation( operation ) {
+		operation._execute();
+	}
+
+	/**
+	 * Transforms two sets of deltas by themselves. Returns both transformed sets.
+	 *
+	 * @param {Array.<module:engine/model/delta/delta~Delta>} deltasA Array with the first set of deltas to transform. These
+	 * deltas are considered more important (than `deltasB`) when resolving conflicts.
+	 * @param {Array.<module:engine/model/delta/delta~Delta>} deltasB Array with the second set of deltas to transform. These
+	 * deltas are considered less important (than `deltasA`) when resolving conflicts.
+	 * @param {Boolean} [useContext=false] When set to `true`, transformation will store and use additional context
+	 * information to guarantee more expected results. Should be used whenever deltas related to already applied
+	 * deltas are transformed (for example when undoing changes).
+	 * @returns {Object}
+	 * @returns {Array.<module:engine/model/delta/delta~Delta>} return.deltasA The first set of deltas transformed
+	 * by the second set of deltas.
+	 * @returns {Array.<module:engine/model/delta/delta~Delta>} return.deltasB The second set of deltas transformed
+	 * by the first set of deltas.
+	 */
+	transformDeltas( deltasA, deltasB, useContext = false ) {
+		return deltaTransform.transformDeltaSets( deltasA, deltasB, useContext ? this.document : null );
+	}
+
+	/**
+	 * See {@link module:engine/model/utils/insertcontent~insertContent}.
+	 *
+	 * @fires insertContent
+	 * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
+	 * @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted.
+	 */
+	insertContent( content, selection ) {
+		insertContent( this, content, selection );
+	}
+
+	/**
+	 * See {@link module:engine/model/utils/deletecontent.deleteContent}.
+	 *
+	 * Note: For the sake of predictability, the resulting selection should always be collapsed.
+	 * In cases where a feature wants to modify deleting behavior so selection isn't collapsed
+	 * (e.g. a table feature may want to keep row selection after pressing <kbd>Backspace</kbd>),
+	 * then that behavior should be implemented in the view's listener. At the same time, the table feature
+	 * will need to modify this method's behavior too, e.g. to "delete contents and then collapse
+	 * the selection inside the last selected cell" or "delete the row and collapse selection somewhere near".
+	 * That needs to be done in order to ensure that other features which use `deleteContent()` will work well with tables.
+	 *
+	 * @fires deleteContent
+	 * @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted.
+	 * @param {Object} options See {@link module:engine/model/utils/deletecontent~deleteContent}'s options.
+	 */
+	deleteContent( selection, options ) {
+		deleteContent( this, selection, options );
+	}
+
+	/**
+	 * See {@link module:engine/model/utils/modifyselection~modifySelection}.
+	 *
+	 * @fires modifySelection
+	 * @param {module:engine/model/selection~Selection} selection The selection to modify.
+	 * @param {Object} options See {@link module:engine/model/utils/modifyselection.modifySelection}'s options.
+	 */
+	modifySelection( selection, options ) {
+		modifySelection( this, selection, options );
+	}
+
+	/**
+	 * See {@link module:engine/model/utils/getselectedcontent~getSelectedContent}.
+	 *
+	 * @fires getSelectedContent
+	 * @param {module:engine/model/selection~Selection} selection The selection of which content will be retrieved.
+	 * @returns {module:engine/model/documentfragment~DocumentFragment} Document fragment holding the clone of the selected content.
+	 */
+	getSelectedContent( selection ) {
+		return getSelectedContent( this, selection );
+	}
+
+	/**
+	 * Checks whether given {@link module:engine/model/range~Range range} or {@link module:engine/model/element~Element element}
+	 * has any content.
+	 *
+	 * Content is any text node or element which is registered in {@link module:engine/model/schema~Schema schema}.
+	 *
+	 * @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
+	 * @returns {Boolean}
+	 */
+	hasContent( rangeOrElement ) {
+		if ( rangeOrElement instanceof ModelElement ) {
+			rangeOrElement = ModelRange.createIn( rangeOrElement );
+		}
+
+		if ( rangeOrElement.isCollapsed ) {
+			return false;
+		}
+
+		for ( const item of rangeOrElement.getItems() ) {
+			// Remember, `TreeWalker` returns always `textProxy` nodes.
+			if ( item.is( 'textProxy' ) || this.schema.isObject( item ) ) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Removes all events listeners set by model instance and destroys {@link module:engine/model/document~Document}.
+	 */
+	destroy() {
+		this.document.destroy();
+		this.stopListening();
+	}
+
+	/**
+	 * Common part of {@link module:engine/model/model~Model#change} and {@link module:engine/model/model~Model#enqueueChange}
+	 * which calls callbacks and returns array of values returned by these callbacks.
+	 *
+	 * @private
+	 * @returns {Array.<*>} Array of values returned by callbacks.
+	 */
+	_runPendingChanges() {
+		const ret = [];
+
+		while ( this._pendingChanges.length ) {
+			// Create a new writer using batch instance created for this chain of changes.
+			const currentBatch = this._pendingChanges[ 0 ].batch;
+			this._currentWriter = new Writer( this, currentBatch );
+
+			// Execute changes callback and gather the returned value.
+			const callbackReturnValue = this._pendingChanges[ 0 ].callback( this._currentWriter );
+			ret.push( callbackReturnValue );
+
+			// Fire internal `_change` event.
+			this.fire( '_change', this._currentWriter );
+
+			this._pendingChanges.shift();
+			this._currentWriter = null;
+		}
+
+		return ret;
+	}
+
+	/**
+	 * Fired after leaving each {@link module:engine/model/model~Model#enqueueChange} block or outermost
+	 * {@link module:engine/model/model~Model#change} block.
+	 *
+	 * **Note:** This is an internal event! Use {@link module:engine/model/document~Document#event:change} instead.
+	 *
+	 * @protected
+	 * @event _change
+	 * @param {module:engine/model/writer~Writer} writer `Writer` instance that has been used in the change block.
+	 */
+
+	/**
+	 * Fired every time any {@link module:engine/model/operation/operation~Operation operation} is applied on the model
+	 * using {@link #applyOperation}.
+	 *
+	 * Note that this event is suitable only for very specific use-cases. Use it if you need to listen to every single operation
+	 * applied on the document. However, in most cases {@link module:engine/model/document~Document#event:change} should
+	 * be used.
+	 *
+	 * A few callbacks are already added to this event by engine internal classes:
+	 *
+	 * * with `highest` priority operation is validated,
+	 * * with `normal` priority operation is executed,
+	 * * with `low` priority the {@link module:engine/model/document~Document} updates its version,
+	 * * with `low` priority {@link module:engine/model/liveposition~LivePosition} and {@link module:engine/model/liverange~LiveRange}
+	 * update themselves.
+	 *
+	 * @event applyOperation
+	 * @param {Array} args Arguments of the `applyOperation` which is an array with a single element - applied
+	 * {@link module:engine/model/operation/operation~Operation operation}.
+	 */
+
+	/**
+	 * Event fired when {@link #insertContent} method is called.
+	 *
+	 * The {@link #insertContent default action of that method} is implemented as a
+	 * listener to this event so it can be fully customized by the features.
+	 *
+	 * @event insertContent
+	 * @param {Array} args The arguments passed to the original method.
+	 */
+
+	/**
+	 * Event fired when {@link #deleteContent} method is called.
+	 *
+	 * The {@link #deleteContent default action of that method} is implemented as a
+	 * listener to this event so it can be fully customized by the features.
+	 *
+	 * @event deleteContent
+	 * @param {Array} args The arguments passed to the original method.
+	 */
+
+	/**
+	 * Event fired when {@link #modifySelection} method is called.
+	 *
+	 * The {@link #modifySelection default action of that method} is implemented as a
+	 * listener to this event so it can be fully customized by the features.
+	 *
+	 * @event modifySelection
+	 * @param {Array} args The arguments passed to the original method.
+	 */
+
+	/**
+	 * Event fired when {@link #getSelectedContent} method is called.
+	 *
+	 * The {@link #getSelectedContent default action of that method} is implemented as a
+	 * listener to this event so it can be fully customized by the features.
+	 *
+	 * @event getSelectedContent
+	 * @param {Array} args The arguments passed to the original method.
+	 */
+}
+
+mix( Model, ObservableMixin );

+ 0 - 8
packages/ckeditor5-engine/src/model/model.jsdoc

@@ -1,8 +0,0 @@
-/**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/model
- */

+ 55 - 42
packages/ckeditor5-engine/src/model/node.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -17,10 +17,10 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  *
  * **Note:** If a node is detached from the model tree, you can manipulate it using it's API.
  * However, it is **very important** that nodes already attached to model tree should be only changed through
- * {@link module:engine/model/document~Document#batch Batch API}.
+ * {@link module:engine/model/writer~Writer Writer API}.
  *
- * Changes done by `Node` methods, like {@link module:engine/model/element~Element#insertChildren insertChildren} or
- * {@link module:engine/model/node~Node#setAttribute setAttribute}
+ * Changes done by `Node` methods, like {@link module:engine/model/element~Element#_insertChildren _insertChildren} or
+ * {@link module:engine/model/node~Node#_setAttribute _setAttribute}
  * do not generate {@link module:engine/model/operation/operation~Operation operations}
  * which are essential for correct editor work if you modify nodes in {@link module:engine/model/document~Document document} root.
  *
@@ -30,9 +30,9 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  * 3. Change `Node` that was already added to the model using `Batch` API.
  *
  * Similarly, you cannot use `Batch` API on a node that has not been added to the model tree, with the exception
- * of {@link module:engine/model/batch~Batch#insert inserting} that node to the model tree.
+ * of {@link module:engine/model/writer~Writer#insert inserting} that node to the model tree.
  *
- * Be aware that using {@link module:engine/model/batch~Batch#remove remove from Batch API} does not allow to use `Node` API because
+ * Be aware that using {@link module:engine/model/writer~Writer#remove remove from Batch API} does not allow to use `Node` API because
  * the information about `Node` is still kept in model document.
  *
  * In case of {@link module:engine/model/element~Element element node}, adding and removing children also counts as changing a node and
@@ -201,15 +201,6 @@ export default class Node {
 		return this.root.document || null;
 	}
 
-	/**
-	 * Creates a copy of this node, that is a node with exactly same attributes, and returns it.
-	 *
-	 * @returns {module:engine/model/node~Node} Node with same attributes as this node.
-	 */
-	clone() {
-		return new Node( this._attrs );
-	}
-
 	/**
 	 * Gets path to the node. The path is an array containing starting offsets of consecutive ancestors of this node,
 	 * beginning from {@link module:engine/model/node~Node#root root}, down to this node's starting offset. The path can be used to
@@ -282,13 +273,6 @@ export default class Node {
 		return i === 0 ? null : ancestorsA[ i - 1 ];
 	}
 
-	/**
-	 * Removes this node from it's parent.
-	 */
-	remove() {
-		this.parent.removeChildren( this.index );
-	}
-
 	/**
 	 * Checks if the node has an attribute with given key.
 	 *
@@ -324,61 +308,90 @@ export default class Node {
 	/**
 	 * Returns iterator that iterates over this node's attribute keys.
 	 *
-	 * @returns {Iterator.<String>}
+	 * @returns {Iterable.<String>}
 	 */
 	getAttributeKeys() {
 		return this._attrs.keys();
 	}
 
+	/**
+	 * Converts `Node` to plain object and returns it.
+	 *
+	 * @returns {Object} `Node` converted to plain object.
+	 */
+	toJSON() {
+		const json = {};
+
+		if ( this._attrs.size ) {
+			json.attributes = [ ...this._attrs ];
+		}
+
+		return json;
+	}
+
+	/**
+	 * Creates a copy of this node, that is a node with exactly same attributes, and returns it.
+	 *
+	 * @protected
+	 * @returns {module:engine/model/node~Node} Node with same attributes as this node.
+	 */
+	_clone() {
+		return new Node( this._attrs );
+	}
+
+	/**
+	 * Removes this node from it's parent.
+	 *
+	 * @see module:engine/model/writer~Writer#remove
+	 * @protected
+	 */
+	_remove() {
+		this.parent._removeChildren( this.index );
+	}
+
 	/**
 	 * Sets attribute on the node. If attribute with the same key already is set, it's value is overwritten.
 	 *
+	 * @see module:engine/model/writer~Writer#setAttribute
+	 * @protected
 	 * @param {String} key Key of attribute to set.
 	 * @param {*} value Attribute value.
 	 */
-	setAttribute( key, value ) {
+	_setAttribute( key, value ) {
 		this._attrs.set( key, value );
 	}
 
 	/**
 	 * Removes all attributes from the node and sets given attributes.
 	 *
+	 * @see module:engine/model/writer~Writer#setAttributes
+	 * @protected
 	 * @param {Object} [attrs] Attributes to set. See {@link module:utils/tomap~toMap} for a list of accepted values.
 	 */
-	setAttributesTo( attrs ) {
+	_setAttributesTo( attrs ) {
 		this._attrs = toMap( attrs );
 	}
 
 	/**
 	 * Removes an attribute with given key from the node.
 	 *
+	 * @see module:engine/model/writer~Writer#removeAttribute
+	 * @protected
 	 * @param {String} key Key of attribute to remove.
 	 * @returns {Boolean} `true` if the attribute was set on the element, `false` otherwise.
 	 */
-	removeAttribute( key ) {
+	_removeAttribute( key ) {
 		return this._attrs.delete( key );
 	}
 
 	/**
 	 * Removes all attributes from the node.
-	 */
-	clearAttributes() {
-		this._attrs.clear();
-	}
-
-	/**
-	 * Converts `Node` to plain object and returns it.
 	 *
-	 * @returns {Object} `Node` converted to plain object.
+	 * @see module:engine/model/writer~Writer#clearAttributes
+	 * @protected
 	 */
-	toJSON() {
-		const json = {};
-
-		if ( this._attrs.size ) {
-			json.attributes = [ ...this._attrs ];
-		}
-
-		return json;
+	_clearAttributes() {
+		this._attrs.clear();
 	}
 
 	/**

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -19,6 +19,7 @@ export default class NodeList {
 	/**
 	 * Creates an empty node list.
 	 *
+	 * @protected
 	 * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes contained in this node list.
 	 */
 	constructor( nodes ) {
@@ -31,14 +32,16 @@ export default class NodeList {
 		this._nodes = [];
 
 		if ( nodes ) {
-			this.insertNodes( 0, nodes );
+			this._insertNodes( 0, nodes );
 		}
 	}
 
 	/**
-	 * Returns an iterator that iterates over all nodes contained inside this node list.
+	 * Iterable interface.
 	 *
-	 * @returns {Iterator.<module:engine/model/node~Node>}
+	 * Iterates over all nodes contained inside this node list.
+	 *
+	 * @returns {Iterable.<module:engine/model/node~Node>}
 	 */
 	[ Symbol.iterator ]() {
 		return this._nodes[ Symbol.iterator ]();
@@ -162,10 +165,11 @@ export default class NodeList {
 	/**
 	 * Inserts given nodes at given index.
 	 *
+	 * @protected
 	 * @param {Number} index Index at which nodes should be inserted.
 	 * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
 	 */
-	insertNodes( index, nodes ) {
+	_insertNodes( index, nodes ) {
 		// Validation.
 		for ( const node of nodes ) {
 			if ( !( node instanceof Node ) ) {
@@ -184,11 +188,12 @@ export default class NodeList {
 	/**
 	 * Removes one or more nodes starting at the given index.
 	 *
+	 * @protected
 	 * @param {Number} indexStart Index of the first node to remove.
 	 * @param {Number} [howMany=1] Number of nodes to remove.
 	 * @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
 	 */
-	removeNodes( indexStart, howMany = 1 ) {
+	_removeNodes( indexStart, howMany = 1 ) {
 		return this._nodes.splice( indexStart, howMany );
 	}
 

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -10,7 +10,7 @@
 import Operation from './operation';
 import Range from '../range';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import writer from '../writer';
+import { _setAttribute } from './utils';
 import isEqual from '@ckeditor/ckeditor5-utils/src/lib/lodash/isEqual';
 
 /**
@@ -37,7 +37,8 @@ export default class AttributeOperation extends Operation {
 	 * @param {String} key Key of an attribute to change or remove.
 	 * @param {*} oldValue Old value of the attribute with given key or `null`, if attribute was not set before.
 	 * @param {*} newValue New value of the attribute with given key or `null`, if operation should remove attribute.
-	 * @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied.
+	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
+	 * can be applied or `null` if the operation operates on detached (non-document) tree.
 	 */
 	constructor( range, key, oldValue, newValue, baseVersion ) {
 		super( baseVersion );
@@ -109,14 +110,13 @@ export default class AttributeOperation extends Operation {
 	/**
 	 * @inheritDoc
 	 */
-	_execute() {
-		// Validation.
+	_validate() {
 		for ( const item of this.range.getItems() ) {
 			if ( this.oldValue !== null && !isEqual( item.getAttribute( this.key ), this.oldValue ) ) {
 				/**
 				 * Changed node has different attribute value than operation's old attribute value.
 				 *
-				 * @error operation-attribute-wrong-old-value
+				 * @error attribute-operation-wrong-old-value
 				 * @param {module:engine/model/item~Item} item
 				 * @param {String} key
 				 * @param {*} value
@@ -142,14 +142,17 @@ export default class AttributeOperation extends Operation {
 				);
 			}
 		}
+	}
 
+	/**
+	 * @inheritDoc
+	 */
+	_execute() {
 		// If value to set is same as old value, don't do anything.
 		if ( !isEqual( this.oldValue, this.newValue ) ) {
 			// Execution.
-			writer.setAttribute( this.range, this.key, this.newValue );
+			_setAttribute( this.range, this.key, this.newValue );
 		}
-
-		return { range: this.range, key: this.key, oldValue: this.oldValue, newValue: this.newValue };
 	}
 
 	/**

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

@@ -0,0 +1,84 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/operation/detachoperation
+ */
+
+import Operation from './operation';
+import Position from '../position';
+import Range from '../range';
+import { _remove } from './utils';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+/**
+ * Operation to permanently remove node from detached root.
+ * Note this operation is only a local operation and won't be send to the other clients.
+ *
+ * @extends module:engine/model/operation/operation~Operation
+ */
+export default class DetachOperation extends Operation {
+	/**
+	 * Creates an insert operation.
+	 *
+	 * @param {module:engine/model/position~Position} sourcePosition
+	 * Position before the first {@link module:engine/model/item~Item model item} to move.
+	 * @param {Number} howMany Offset size of moved range. Moved range will start from `sourcePosition` and end at
+	 * `sourcePosition` with offset shifted by `howMany`.
+	 */
+	constructor( sourcePosition, howMany ) {
+		super( null );
+
+		/**
+		 * Position before the first {@link module:engine/model/item~Item model item} to detach.
+		 *
+		 * @member {module:engine/model/position~Position} #sourcePosition
+		 */
+		this.sourcePosition = Position.createFromPosition( sourcePosition );
+
+		/**
+		 * Offset size of moved range.
+		 *
+		 * @member {Number} #howMany
+		 */
+		this.howMany = howMany;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	get type() {
+		return 'detach';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_validate() {
+		if ( this.sourcePosition.root.document ) {
+			/**
+			 * Cannot detach document node.
+			 * Use {@link module:engine/model/operation/removeoperation~RemoveOperation remove operation} instead.
+			 *
+			 * @error detach-operation-on-document-node
+			 */
+			throw new CKEditorError( 'detach-operation-on-document-node: Cannot detach document node.' );
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_execute() {
+		_remove( Range.createFromPositionAndShift( this.sourcePosition, this.howMany ) );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.model.operation.DetachOperation';
+	}
+}

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -11,9 +11,10 @@ import Operation from './operation';
 import Position from '../position';
 import NodeList from '../nodelist';
 import RemoveOperation from './removeoperation';
-import { insert, normalizeNodes } from '../writer';
+import { _insert, _normalizeNodes } from './utils';
 import Text from '../text';
 import Element from '../element';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * Operation to insert one or more nodes at given position in the model.
@@ -26,7 +27,8 @@ export default class InsertOperation extends Operation {
 	 *
 	 * @param {module:engine/model/position~Position} position Position of insertion.
 	 * @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted.
-	 * @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which operation can be applied.
+	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
+	 * can be applied or `null` if the operation operates on detached (non-document) tree.
 	 */
 	constructor( position, nodes, baseVersion ) {
 		super( baseVersion );
@@ -45,7 +47,7 @@ export default class InsertOperation extends Operation {
 		 * @readonly
 		 * @member {module:engine/model/nodelist~NodeList} module:engine/model/operation/insertoperation~InsertOperation#nodeList
 		 */
-		this.nodes = new NodeList( normalizeNodes( nodes ) );
+		this.nodes = new NodeList( _normalizeNodes( nodes ) );
 	}
 
 	/**
@@ -61,7 +63,7 @@ export default class InsertOperation extends Operation {
 	 * @returns {module:engine/model/operation/insertoperation~InsertOperation} Clone of this operation.
 	 */
 	clone() {
-		const nodes = new NodeList( [ ...this.nodes ].map( node => node.clone( true ) ) );
+		const nodes = new NodeList( [ ...this.nodes ].map( node => node._clone( true ) ) );
 
 		return new InsertOperation( this.position, nodes, this.baseVersion );
 	}
@@ -78,6 +80,24 @@ export default class InsertOperation extends Operation {
 		return new RemoveOperation( this.position, this.nodes.maxOffset, gyPosition, this.baseVersion + 1 );
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	_validate() {
+		const targetElement = this.position.parent;
+
+		if ( !targetElement || targetElement.maxOffset < this.position.offset ) {
+			/**
+			 * Insertion position is invalid.
+			 *
+			 * @error insert-operation-position-invalid
+			 */
+			throw new CKEditorError(
+				'insert-operation-position-invalid: Insertion position is invalid.'
+			);
+		}
+	}
+
 	/**
 	 * @inheritDoc
 	 */
@@ -87,11 +107,9 @@ export default class InsertOperation extends Operation {
 		// to the operation, not modified. For example, text nodes can get merged or cropped while Elements can
 		// get children. It is important that InsertOperation has the copy of original nodes in intact state.
 		const originalNodes = this.nodes;
-		this.nodes = new NodeList( [ ...originalNodes ].map( node => node.clone( true ) ) );
-
-		const range = insert( this.position, originalNodes );
+		this.nodes = new NodeList( [ ...originalNodes ].map( node => node._clone( true ) ) );
 
-		return { range };
+		_insert( this.position, originalNodes );
 	}
 
 	/**

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -19,7 +19,8 @@ export default class MarkerOperation extends Operation {
 	 * @param {module:engine/model/range~Range} oldRange Marker range before the change.
 	 * @param {module:engine/model/range~Range} newRange Marker range after the change.
 	 * @param {module:engine/model/markercollection~MarkerCollection} markers Marker collection on which change should be executed.
-	 * @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied.
+	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
+	 * can be applied or `null` if the operation operates on detached (non-document) tree.
 	 */
 	constructor( name, oldRange, newRange, markers, baseVersion ) {
 		super( baseVersion );
@@ -86,11 +87,9 @@ export default class MarkerOperation extends Operation {
 	 * @inheritDoc
 	 */
 	_execute() {
-		const type = this.newRange ? 'set' : 'remove';
+		const type = this.newRange ? '_set' : '_remove';
 
-		this._markers[ type ]( this.name, this.newRange );
-
-		return { name: this.name, type };
+		this._markers[ type ]( this.name, this.newRange, true );
 	}
 
 	/**
@@ -123,7 +122,7 @@ export default class MarkerOperation extends Operation {
 			json.name,
 			json.oldRange ? Range.fromJSON( json.oldRange, document ) : null,
 			json.newRange ? Range.fromJSON( json.newRange, document ) : null,
-			document.markers,
+			document.model.markers,
 			json.baseVersion
 		);
 	}

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -12,7 +12,7 @@ import Position from '../position';
 import Range from '../range';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
-import writer from './../writer';
+import { _move } from './utils';
 
 /**
  * Operation to move a range of {@link module:engine/model/item~Item model items}
@@ -29,7 +29,8 @@ export default class MoveOperation extends Operation {
 	 * @param {Number} howMany Offset size of moved range. Moved range will start from `sourcePosition` and end at
 	 * `sourcePosition` with offset shifted by `howMany`.
 	 * @param {module:engine/model/position~Position} targetPosition Position at which moved nodes will be inserted.
-	 * @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which operation can be applied.
+	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
+	 * can be applied or `null` if the operation operates on detached (non-document) tree.
 	 */
 	constructor( sourcePosition, howMany, targetPosition, baseVersion ) {
 		super( baseVersion );
@@ -120,7 +121,7 @@ export default class MoveOperation extends Operation {
 	/**
 	 * @inheritDoc
 	 */
-	_execute() {
+	_validate() {
 		const sourceElement = this.sourcePosition.parent;
 		const targetElement = this.targetPosition.parent;
 		const sourceOffset = this.sourcePosition.offset;
@@ -172,13 +173,13 @@ export default class MoveOperation extends Operation {
 				}
 			}
 		}
+	}
 
-		const range = writer.move( Range.createFromPositionAndShift( this.sourcePosition, this.howMany ), this.targetPosition );
-
-		return {
-			sourcePosition: this.sourcePosition,
-			range
-		};
+	/**
+	 * @inheritDoc
+	 */
+	_execute() {
+		_move( Range.createFromPositionAndShift( this.sourcePosition, this.howMany ), this.targetPosition );
 	}
 
 	/**

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -42,11 +42,7 @@ export default class NoOperation extends Operation {
 		return new NoOperation( this.baseVersion + 1 );
 	}
 
-	/**
-	 * @inheritDoc
-	 */
 	_execute() {
-		return {};
 	}
 
 	/**

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -17,12 +17,14 @@ import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
 export default class Operation {
 	/**
 	 * Base operation constructor.
-	 * @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied.
+	 *
+	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
+	 * can be applied or `null` if the operation operates on detached (non-document) tree.
 	 */
 	constructor( baseVersion ) {
 		/**
 		 * {@link module:engine/model/document~Document#version} on which operation can be applied. If you try to
-		 * {@link module:engine/model/document~Document#applyOperation apply} operation with different base version than the
+		 * {@link module:engine/model/model~Model#applyOperation apply} operation with different base version than the
 		 * {@link module:engine/model/document~Document#version document version} the
 		 * {@link module:utils/ckeditorerror~CKEditorError model-document-applyOperation-wrong-version} error is thrown.
 		 *
@@ -30,6 +32,14 @@ export default class Operation {
 		 */
 		this.baseVersion = baseVersion;
 
+		/**
+		 * Defines whether operation is executed on attached or detached {@link module:engine/model/item~Item items}.
+		 *
+		 * @readonly
+		 * @member {Boolean} #isDocumentOperation
+		 */
+		this.isDocumentOperation = this.baseVersion !== null;
+
 		/**
 		 * Operation type.
 		 *
@@ -66,16 +76,23 @@ export default class Operation {
 		 */
 
 		/**
-		 * Executes the operation - modifications described by the operation attributes
-		 * will be applied to the tree model.
+		 * Executes the operation - modifications described by the operation properties will be applied to the model tree.
 		 *
 		 * @protected
 		 * @method #_execute
-		 * @returns {Object} Object with additional information about the applied changes. It properties depends on the
-		 * operation type.
 		 */
 	}
 
+	/**
+	 * Checks whether the operation's parameters are correct and the operation can be correctly executed. Throws
+	 * an error if operation is not valid.
+	 *
+	 * @protected
+	 * @method #_validate
+	 */
+	_validate() {
+	}
+
 	/**
 	 * Custom toJSON method to solve child-parent circular dependencies.
 	 *
@@ -90,6 +107,9 @@ export default class Operation {
 		// Remove parent delta to avoid circular dependencies.
 		delete json.delta;
 
+		// Only document operations are shared with other clients so it is not necessary to keep this information.
+		delete json.isDocumentOperation;
+
 		return json;
 	}
 

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -9,6 +9,7 @@
 
 import MoveOperation from './moveoperation';
 import RemoveOperation from './removeoperation';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * Operation to reinsert previously removed nodes back to the non-graveyard root. This operation acts like
@@ -51,6 +52,21 @@ export default class ReinsertOperation extends MoveOperation {
 		return new RemoveOperation( this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1 );
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	_validate() {
+		super._validate();
+
+		if ( !this.sourcePosition.root.document ) {
+			throw new CKEditorError( 'reinsert-operation-on-detached-item: Cannot reinsert detached item.' );
+		}
+
+		if ( !this.targetPosition.root.document ) {
+			throw new CKEditorError( 'reinsert-operation-to-detached-parent: Cannot reinsert item to detached parent.' );
+		}
+	}
+
 	/**
 	 * @inheritDoc
 	 */

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -9,6 +9,7 @@
 
 import MoveOperation from './moveoperation';
 import ReinsertOperation from './reinsertoperation';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * Operation to remove a range of nodes.
@@ -32,6 +33,24 @@ export default class RemoveOperation extends MoveOperation {
 		return new ReinsertOperation( this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1 );
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	_validate() {
+		super._validate();
+
+		if ( !this.sourcePosition.root.document ) {
+			/**
+			 * Item that is going to be removed needs to be a {@link module:engine/model/document~Document document} child.
+			 * To remove Item from detached document fragment use
+			 * {@link module:engine/model/operation/detachoperation~DetachOperation DetachOperation}.
+			 *
+			 * @error remove-operation-on-detached-item
+			 */
+			throw new CKEditorError( 'remove-operation-on-detached-item: Cannot remove detached item.' );
+		}
+	}
+
 	/**
 	 * @inheritDoc
 	 */

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -26,7 +26,8 @@ export default class RenameOperation extends Operation {
 	 * @param {module:engine/model/position~Position} position Position before an element to change.
 	 * @param {String} oldName Current name of the element.
 	 * @param {String} newName New name for the element.
-	 * @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied.
+	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
+	 * can be applied or `null` if the operation operates on detached (non-document) tree.
 	 */
 	constructor( position, oldName, newName, baseVersion ) {
 		super( baseVersion );
@@ -81,8 +82,7 @@ export default class RenameOperation extends Operation {
 	/**
 	 * @inheritDoc
 	 */
-	_execute() {
-		// Validation.
+	_validate() {
 		const element = this.position.nodeAfter;
 
 		if ( !( element instanceof Element ) ) {
@@ -104,14 +104,15 @@ export default class RenameOperation extends Operation {
 				'rename-operation-wrong-name: Element to change has different name than operation\'s old name.'
 			);
 		}
+	}
 
-		// If value to set is same as old value, don't do anything.
-		if ( element.name != this.newName ) {
-			// Execution.
-			element.name = this.newName;
-		}
+	/**
+	 * @inheritDoc
+	 */
+	_execute() {
+		const element = this.position.nodeAfter;
 
-		return { element, oldName: this.oldName };
+		element.name = this.newName;
 	}
 
 	/**

+ 33 - 8
packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -31,7 +31,8 @@ export default class RootAttributeOperation extends Operation {
 	 * @param {String} key Key of an attribute to change or remove.
 	 * @param {*} oldValue Old value of the attribute with given key or `null` if adding a new attribute.
 	 * @param {*} newValue New value to set for the attribute. If `null`, then the operation just removes the attribute.
-	 * @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which the operation can be applied.
+	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
+	 * can be applied or `null` if the operation operates on detached (non-document) tree.
 	 */
 	constructor( root, key, oldValue, newValue, baseVersion ) {
 		super( baseVersion );
@@ -69,6 +70,9 @@ export default class RootAttributeOperation extends Operation {
 		this.newValue = newValue;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	get type() {
 		if ( this.oldValue === null ) {
 			return 'addRootAttribute';
@@ -97,7 +101,25 @@ export default class RootAttributeOperation extends Operation {
 		return new RootAttributeOperation( this.root, this.key, this.newValue, this.oldValue, this.baseVersion + 1 );
 	}
 
-	_execute() {
+	/**
+	 * @inheritDoc
+	 */
+	_validate() {
+		if ( this.root != this.root.root || this.root.is( 'documentFragment' ) ) {
+			/**
+			 * The element to change is not a root element.
+			 *
+			 * @error rootattribute-operation-not-a-root
+			 * @param {module:engine/model/rootelement~RootElement} root
+			 * @param {String} key
+			 * @param {*} value
+			 */
+			throw new CKEditorError(
+				'rootattribute-operation-not-a-root: The element to change is not a root element.',
+				{ root: this.root, key: this.key }
+			);
+		}
+
 		if ( this.oldValue !== null && this.root.getAttribute( this.key ) !== this.oldValue ) {
 			/**
 			 * The attribute which should be removed does not exists for the given node.
@@ -127,14 +149,17 @@ export default class RootAttributeOperation extends Operation {
 				{ root: this.root, key: this.key }
 			);
 		}
+	}
 
+	/**
+	 * @inheritDoc
+	 */
+	_execute() {
 		if ( this.newValue !== null ) {
-			this.root.setAttribute( this.key, this.newValue );
+			this.root._setAttribute( this.key, this.newValue );
 		} else {
-			this.root.removeAttribute( this.key );
+			this.root._removeAttribute( this.key );
 		}
-
-		return { root: this.root, key: this.key, oldValue: this.oldValue, newValue: this.newValue };
 	}
 
 	/**
@@ -152,7 +177,7 @@ export default class RootAttributeOperation extends Operation {
 	 * @returns {module:engine/model/operation/rootattributeoperation~RootAttributeOperation}
 	 */
 	static fromJSON( json, document ) {
-		if ( !document.hasRoot( json.root ) ) {
+		if ( !document.getRoot( json.root ) ) {
 			/**
 			 * Cannot create RootAttributeOperation for document. Root with specified name does not exist.
 			 *

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -34,7 +34,7 @@ import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
  *
  * Whenever the {@link module:engine/model/document~Document document}
  * has different {@link module:engine/model/document~Document#version}
- * than the operation you want to {@link module:engine/model/document~Document#applyOperation apply}, you need to transform that
+ * than the operation you want to {@link module:engine/model/model~Model#applyOperation apply}, you need to transform that
  * operation by all operations which were already applied to the {@link module:engine/model/document~Document document} and have greater
  * {@link module:engine/model/document~Document#version} than the operation being applied. Transform them in the same order as those
  * operations which were applied. This way all modifications done to the Tree Data Model will be reflected

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

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

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -104,6 +104,7 @@ export default class Position {
 		 *		     |- LI
 		 *		        |- b^a|r  ^ has path: [ 1, 1, 1 ]   | has path: [ 1, 1, 2 ]
 		 *
+		 * @readonly
 		 * @member {Array.<Number>} module:engine/model/position~Position#path
 		 */
 		this.path = path;
@@ -764,7 +765,7 @@ export default class Position {
 			return new Position( doc.graveyard, json.path );
 		}
 
-		if ( !doc.hasRoot( json.root ) ) {
+		if ( !doc.getRoot( json.root ) ) {
 			/**
 			 * Cannot create position for document. Root with specified name does not exist.
 			 *

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -42,7 +42,9 @@ export default class Range {
 	}
 
 	/**
-	 * Returns an iterator that iterates over all {@link module:engine/model/item~Item items} that are in this range and returns
+	 * Iterable interface.
+	 *
+	 * Iterates over all {@link module:engine/model/item~Item items} that are in this range and returns
 	 * them together with additional information like length or {@link module:engine/model/position~Position positions},
 	 * grouped as {@link module:engine/model/treewalker~TreeWalkerValue}.
 	 * It iterates over all {@link module:engine/model/textproxy~TextProxy text contents} that are inside the range

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 

+ 1296 - 502
packages/ckeditor5-engine/src/model/schema.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -7,236 +7,456 @@
  * @module engine/model/schema
  */
 
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+
+import Range from './range';
 import Position from './position';
 import Element from './element';
-import Range from './range';
-import DocumentSelection from './documentselection';
-import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
-import isArray from '@ckeditor/ckeditor5-utils/src/lib/lodash/isArray';
-import isString from '@ckeditor/ckeditor5-utils/src/lib/lodash/isString';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import TreeWalker from './treewalker';
 
 /**
- * Schema is a definition of the structure of the document. It allows to define which tree model items (element, text, etc.)
- * can be nested within which ones and which attributes can be applied to them. It's created during the run-time of the application,
- * typically by features. Also, the features can query the schema to learn what structure is allowed and act accordingly.
+ * The model's schema. It defines allowed and disallowed structures of nodes as well as nodes' attributes.
+ * The schema is usually defined by features and based on them the editing framework and features
+ * make decisions how to change and process the model.
+ *
+ * The instance of schema is available in {@link module:engine/model/model~Model#schema `editor.model.schema`}.
+ *
+ * # Schema definitions
+ *
+ * Schema defines allowed model structures and allowed attributes separately. They are also checked separately
+ * by using the {@link ~Schema#checkChild} and {@link ~Schema#checkAttribute} methods.
+ *
+ * ## Defining allowed structures
+ *
+ * When a feature introduces a model element it should register it in the schema. Besides
+ * defining that such an element may exist in the model, the feature also needs to define where
+ * this element may be placed:
+ *
+ *		schema.register( 'myElement', {
+ *			allowIn: '$root'
+ *		} );
+ *
+ * This lets the schema know that `<myElement>` may be a child of the `<$root>` element. `$root` is one of generic
+ * nodes defined by the editing framework. By default, the editor names the main root element a `<$root>`,
+ * so the above definition allows `<myElement>` in the main editor element.
+ *
+ * In other words, this would be correct:
+ *
+ *		<$root><myElement></myElement></$root>
+ *
+ * While this would not be correct:
+ *
+ *		<$root><foo><myElement></myElement></foo></$root>
+ *
+ * ## Generic items
+ *
+ * There are three basic generic items: `$root`, `$block` and `$text`.
+ * They are defined as follows:
+ *
+ *		this.schema.register( '$root', {
+ *			isLimit: true
+ *		} );
+ *		this.schema.register( '$block', {
+ *			allowIn: '$root',
+ *			isBlock: true
+ *		} );
+ *		this.schema.register( '$text', {
+ *			allowIn: '$block'
+ *		} );
+ *
+ * These definitions can then be reused by features to create their own definitions in a more extensible way.
+ * For example, the {@link module:paragraph/paragraph~Paragraph} feature will define its item as:
+ *
+ *		schema.register( 'paragraph', {
+ *			inheritAllFrom: '$block'
+ *		} );
+ *
+ * Which translates to:
+ *
+ *		schema.register( 'paragraph', {
+ *			allowWhere: '$block',
+ *			allowContentOf: '$block',
+ *			allowAttributesOf: '$block',
+ *			inheritTypesFrom: '$block'
+ *		} );
+ *
+ * Which can be read as:
+ *
+ * * The `<paragraph>` element will be allowed in elements in which `<$block>` is allowed (e.g. in `<$root>`).
+ * * The `<paragraph>` element will allow all nodes which are allowed in `<$block>` (e.g. `$text`).
+ * * The `<paragraph>` element will allow all attributes allowed on `<$block>`.
+ * * The `<paragraph>` element will inherit all `is*` properties of `<$block>` (e.g. `isBlock`).
+ *
+ * Thanks to the fact that `<paragraph>`'s definition is inherited from `<$block>` other features can use the `<$block>`
+ * type to indirectly extend `<paragraph>`'s definition. For example, the {@link module:block-quote/blockquote~BlockQuote}
+ * feature does this:
+ *
+ *		schema.register( 'blockQuote', {
+ *			allowWhere: '$block',
+ *			allowContentOf: '$root'
+ *		} );
+ *
+ * Thanks to that, despite the fact that block quote and paragraph features know nothing about themselves, paragraphs
+ * will be allowed in block quotes and block quotes will be allowed in all places where blocks are. So if anyone will
+ * register a `<section>` element (with `allowContentOf: '$root'` rule), that `<section>` elements will allow
+ * block quotes too.
  *
- * For instance, if a feature wants to define that an attribute bold is allowed on the text it needs to register this rule like this:
+ * The side effect of such a definition inheritance is that now `<blockQuote>` is allowed in `<blockQuote>` which needs to be
+ * resolved by a callback which will disallow this specific structure.
  *
- *		editor.document.schema.allow( '$text', 'bold' );
+ * You can read more about the format of an item definition in {@link module:engine/model/schema~SchemaItemDefinition}.
  *
- * Note: items prefixed with `$` are special group of items. By default, `Schema` defines three special items:
+ * ## Defining advanced rules in `checkChild()`'s callbacks
  *
- * * `$inline` represents all inline elements,
- * * `$text` is a sub-group of `$inline` and represents text nodes,
- * * `$block` represents block elements,
- * * `$root` represents default editing roots (those that allow only `$block`s inside them).
+ * The {@link ~Schema#checkChild} method which is the base method used to check whether some element is allowed in a given structure
+ * is {@link module:utils/observablemixin~ObservableMixin#decorate a decorated method}.
+ * It means that you can add listeners to implement your specific rules which are not limited by the declarative
+ * {@link module:engine/model/schema~SchemaItemDefinition API}.
  *
- * When registering an item it's possible to tell that this item should inherit from some other existing item.
- * E.g. `p` can inherit from `$block`, so whenever given attribute is allowed on the `$block` it will automatically be
- * also allowed on the `p` element. By default, `$text` item already inherits from `$inline`.
+ * Those listeners can be added either by listening directly to the {@link ~Schema#event:checkChild} event or
+ * by using the handy {@link ~Schema#addChildCheck} method.
+ *
+ * For instance, the block quote feature defines such a listener to disallow nested `<blockQuote>` structures:
+ *
+ *		schema.addChildCheck( context, childDefinition ) => {
+ *			// Note that context is automatically normalized to SchemaContext instance and
+ *			// child to its definition (SchemaCompiledItemDefinition).
+ *
+ *			// If checkChild() is called with a context that ends with blockQuote and blockQuote as a child
+ *			// to check, make the checkChild() method return false.
+ *			if ( context.endsWith( 'blockQuote' ) && childDefinition.name == 'blockQuote' ) {
+ *				return false;
+ *			}
+ *		} );
+ *
+ * ## Defining attributes
+ *
+ * TODO
+ *
+ * ## Implementing additional constraints
+ *
+ * Schema's capabilities were limited to simple (and atomic) {@link ~Schema#checkChild} and
+ * {@link ~Schema#checkAttribute} checks on purpose.
+ * One may imagine that schema should support defining more complex rules such as
+ * "element `<x>` must be always followed by `<y>`".
+ * While it is feasible to create an API which would enable feeding the schema with such definitions,
+ * it is unfortunately unrealistic to then expect that every editing feature will consider those rules when processing the model.
+ * It is also unrealistic to expect that it will be done automatically by the schema and the editing engine themselves.
+ *
+ * For instance, let's get back to the "element `<x>` must be always followed by `<y>`" rule and this initial content:
+ *
+ *		<$root>
+ *			<x>foo</x>
+ *			<y>bar[bom</y>
+ *			<z>bom]bar</z>
+ *		</$root>
+ *
+ * Now, imagine that the user presses the "block quote" button. Usually it would wrap the two selected blocks
+ * (`<y>` and `<z>`) with a `<blockQuote>` element:
+ *
+ *		<$root>
+ *			<x>foo</x>
+ *			<blockQuote>
+ *				<y>bar[bom</y>
+ *				<z>bom]bar</z>
+ *			</blockQuote>
+ *		</$root>
+ *
+ * But it turns out that this creates an incorrect structure – `<x>` is not followed by `<y>` anymore.
+ *
+ * What should happen instead? There are at least 4 possible solutions: the block quote feature should not be
+ * applicable in such a context, someone should create a new `<y>` right after `<x>`, `<x>` should be moved
+ * inside `<blockQuote>` together with `<y>` or vice versa.
+ *
+ * While this is a relatively simple scenario (unlike most real-time collaboration scenarios),
+ * it turns out that it's already hard to say what should happen and who should react to fix this content.
+ *
+ * Therefore, if your editor needs to implement such rules, you should do that through model's post-fixers
+ * fixing incorrect content or actively prevent such situations (e.g. by disabling certain features).
+ * It means that those constraints will be defined specifically for your scenario by your code which
+ * makes their implementation much easier.
+ *
+ * So the answer for who and how should implement additional constraints is your features or your editor
+ * through CKEditor 5's rich and open API.
+ *
+ * @mixes module:utils/observablemixin~ObservableMixin
  */
 export default class Schema {
 	/**
-	 * Creates Schema instance.
+	 * Creates schema instance.
 	 */
 	constructor() {
-		/**
-		 * Names of elements which have "object" nature. This means that these
-		 * elements should be treated as whole, never merged, can be selected from outside, etc.
-		 * Just like images, placeholder widgets, etc.
-		 *
-		 * @member {Set.<String>} module:engine/model/schema~Schema#objects
-		 */
-		this.objects = new Set();
-
-		/**
-		 * Names of elements to which editing operations should be limited.
-		 * For example, the <kbd>Enter</kbd> should not split such elements and
-		 * <kbd>Backspace</kbd> should not be able to leave or modify such elements.
-		 *
-		 * @member {Set.<String>} module:engine/model/schema~Schema#limits
-		 */
-		this.limits = new Set();
-
-		/**
-		 * Schema items registered in the schema.
-		 *
-		 * @private
-		 * @member {Map} module:engine/model/schema~Schema#_items
-		 */
-		this._items = new Map();
-
-		/**
-		 * Description of what entities are a base for given entity.
-		 *
-		 * @private
-		 * @member {Map} module:engine/model/schema~Schema#_extensionChains
-		 */
-		this._extensionChains = new Map();
-
-		// Register some default abstract entities.
-		this.registerItem( '$root' );
-		this.registerItem( '$block' );
-		this.registerItem( '$inline' );
-		this.registerItem( '$text', '$inline' );
-
-		this.allow( { name: '$block', inside: '$root' } );
-		this.allow( { name: '$inline', inside: '$block' } );
-
-		this.limits.add( '$root' );
-
-		// TMP!
-		// Create an "all allowed" context in the schema for processing the pasted content.
-		// Read: https://github.com/ckeditor/ckeditor5-engine/issues/638#issuecomment-255086588
-
-		this.registerItem( '$clipboardHolder', '$root' );
-		this.allow( { name: '$inline', inside: '$clipboardHolder' } );
+		this._sourceDefinitions = {};
+
+		this.decorate( 'checkChild' );
+		this.decorate( 'checkAttribute' );
+
+		this.on( 'checkAttribute', ( evt, args ) => {
+			args[ 0 ] = new SchemaContext( args[ 0 ] );
+		}, { priority: 'highest' } );
+
+		this.on( 'checkChild', ( evt, args ) => {
+			args[ 0 ] = new SchemaContext( args[ 0 ] );
+			args[ 1 ] = this.getDefinition( args[ 1 ] );
+		}, { priority: 'highest' } );
 	}
 
 	/**
-	 * Allows given query in the schema.
-	 *
-	 *		// Allow text with bold attribute in all P elements.
-	 *		schema.registerItem( 'p', '$block' );
-	 *		schema.allow( { name: '$text', attributes: 'bold', inside: 'p' } );
+	 * Registers schema item. Can only be called once for every item name.
 	 *
-	 *		// Allow header in Ps that are in DIVs
-	 *		schema.registerItem( 'header', '$block' );
-	 *		schema.registerItem( 'div', '$block' );
-	 *		schema.allow( { name: 'header', inside: 'div p' } ); // inside: [ 'div', 'p' ] would also work.
+	 *		schema.register( 'paragraph', {
+	 *			inheritAllFrom: '$block'
+	 *		} );
 	 *
-	 * @param {module:engine/model/schema~SchemaQuery} query Allowed query.
+	 * @param {String} itemName
+	 * @param {module:engine/model/schema~SchemaItemDefinition} definition
 	 */
-	allow( query ) {
-		this._getItem( query.name ).allow( Schema._normalizeQueryPath( query.inside ), query.attributes );
+	register( itemName, definition ) {
+		if ( this._sourceDefinitions[ itemName ] ) {
+			// TODO docs
+			throw new CKEditorError( 'schema-cannot-register-item-twice: A single item cannot be registered twice in the schema.', {
+				itemName
+			} );
+		}
+
+		this._sourceDefinitions[ itemName ] = [
+			Object.assign( {}, definition )
+		];
+
+		this._clearCache();
 	}
 
 	/**
-	 * Disallows given query in the schema.
+	 * Extends a {@link #register registered} item's definition.
+	 *
+	 * Extending properties such as `allowIn` will add more items to the existing properties,
+	 * while redefining properties such as `isBlock` will override the previously defined ones.
 	 *
-	 * @see #allow
-	 * @param {module:engine/model/schema~SchemaQuery} query Disallowed query.
+	 *		schema.register( 'foo', {
+	 *			allowIn: '$root',
+	 *			isBlock: true;
+	 *		} );
+	 *		schema.extend( 'foo', {
+	 *			allowIn: 'blockQuote',
+	 *			isBlock: false
+	 *		} );
+	 *
+	 *		schema.getDefinition( 'foo' );
+	 *		//	{
+	 *		//		allowIn: [ '$root', 'blockQuote' ],
+	 *		// 		isBlock: false
+	 *		//	}
+	 *
+	 * @param {String} itemName
+	 * @param {module:engine/model/schema~SchemaItemDefinition} definition
 	 */
-	disallow( query ) {
-		this._getItem( query.name ).disallow( Schema._normalizeQueryPath( query.inside ), query.attributes );
+	extend( itemName, definition ) {
+		if ( !this._sourceDefinitions[ itemName ] ) {
+			// TODO docs
+			throw new CKEditorError( 'schema-cannot-extend-missing-item: Cannot extend an item which was not registered yet.', {
+				itemName
+			} );
+		}
+
+		this._sourceDefinitions[ itemName ].push( Object.assign( {}, definition ) );
+
+		this._clearCache();
 	}
 
 	/**
-	 * Makes a requirement in schema that entity represented by given item has to have given set of attributes. Some
-	 * elements in the model might require some attributes to be set. If multiple sets of attributes are required it
-	 * is enough that the entity fulfills only one set.
-	 *
-	 *		// "a" element must either have "href" attribute or "name" attribute
-	 *		schema.requireAttributes( 'a', [ 'href' ] );
-	 *		schema.requireAttributes( 'a', [ 'name' ] );
-	 *		// "img" element must have both "src" and "alt" attributes
-	 *		schema.requireAttributes( 'img', [ 'src', 'alt' ] );
-	 *
-	 * @param {String} name Entity name.
-	 * @param {Array.<String>} attributes Attributes that has to be set on the entity to make it valid.
+	 * Returns all registered items.
+	 *
+	 * @returns {Object.<String,module:engine/model/schema~SchemaCompiledItemDefinition>}
 	 */
-	requireAttributes( name, attributes ) {
-		this._getItem( name ).requireAttributes( attributes );
+	getDefinitions() {
+		if ( !this._compiledDefinitions ) {
+			this._compile();
+		}
+
+		return this._compiledDefinitions;
 	}
 
 	/**
-	 * Checks whether given query is allowed in schema.
-	 *
-	 *		// Check whether bold text is allowed in header element.
-	 *		let query = {
-	 *			name: '$text',
-	 *			attributes: 'bold',
-	 *			inside: 'header'
-	 *		};
-	 *		if ( schema.check( query ) ) { ... }
-	 *
-	 *		// Check whether bold and italic text can be placed at caret position.
-	 *		let caretPos = editor.document.selection.getFirstPosition();
-	 *		let query = {
-	 *			name: '$text',
-	 *			attributes: [ 'bold', 'italic' ],
-	 *			inside: caretPos
-	 *		};
-	 *		if ( schema.check( query ) ) { ... }
-	 *
-	 *		// Check whether image with alt, src and title is allowed in given elements path.
-	 *		let quoteElement = new Element( 'quote' );
-	 *		let query = {
-	 *			name: 'img',
-	 *			attributes: [ 'alt', 'src', 'title' ],
-	 *			// It is possible to mix strings with elements.
-	 *			// Query will check whether "img" can be inside "quoteElement" that is inside a block element.
-	 *			inside: [ '$block', quoteElement ]
-	 *		};
-	 *		if ( schema.check( query ) ) { ... }
-	 *
-	 * @param {module:engine/model/schema~SchemaQuery} query Query to check.
-	 * @returns {Boolean} `true` if given query is allowed in schema, `false` otherwise.
+	 * Returns a definition of the given item or `undefined` if item is not registered.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
+	 * @returns {module:engine/model/schema~SchemaCompiledItemDefinition}
 	 */
-	check( query ) {
-		if ( !this.hasItem( query.name ) ) {
-			return false;
-		}
+	getDefinition( item ) {
+		let itemName;
 
-		// If attributes property is a string or undefined, wrap it in an array for easier processing.
-		if ( !isArray( query.attributes ) ) {
-			query.attributes = [ query.attributes ];
-		} else if ( query.attributes.length === 0 ) {
-			// To simplify algorithms, when a SchemaItem path is added "without" attribute, it is added with
-			// attribute equal to undefined. This means that algorithms can work the same way for specified attributes
-			// and no-atrtibutes, but we have to fill empty array with "fake" undefined value for algorithms reasons.
-			query.attributes.push( undefined );
+		if ( typeof item == 'string' ) {
+			itemName = item;
+		} else if ( item.is && ( item.is( 'text' ) || item.is( 'textProxy' ) ) ) {
+			itemName = '$text';
+		}
+		// Element or module:engine/model/schema~SchemaContextItem.
+		else {
+			itemName = item.name;
 		}
 
-		// Normalize the path to an array of strings.
-		const path = Schema._normalizeQueryPath( query.inside );
+		return this.getDefinitions()[ itemName ];
+	}
+
+	/**
+	 * Returns `true` if the given item is registered in the schema.
+	 *
+	 *		schema.isRegistered( 'paragraph' ); // -> true
+	 *		schema.isRegistered( editor.model.document.getRoot() ); // -> true
+	 *		schema.isRegistered( 'foo' ); // -> false
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
+	 */
+	isRegistered( item ) {
+		return !!this.getDefinition( item );
+	}
+
+	/**
+	 * Returns `true` if the given item is defined to be
+	 * a block by {@link module:engine/model/schema~SchemaItemDefinition}'s `isBlock` property.
+	 *
+	 *		schema.isBlock( 'paragraph' ); // -> true
+	 *		schema.isBlock( '$root' ); // -> false
+	 *
+	 *		const paragraphElement = writer.createElement( 'paragraph' );
+	 *		schema.isBlock( paragraphElement ); // -> true
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
+	 */
+	isBlock( item ) {
+		const def = this.getDefinition( item );
+
+		return !!( def && def.isBlock );
+	}
 
-		// Get extension chain of given item and retrieve all schema items that are extended by given item.
-		const schemaItems = this._extensionChains.get( query.name ).map( name => {
-			return this._getItem( name );
-		} );
+	/**
+	 * Returns `true` if the given item is defined to be
+	 * a limit element by {@link module:engine/model/schema~SchemaItemDefinition}'s `isLimit` property.
+	 *
+	 *		schema.isLimit( 'paragraph' ); // -> false
+	 *		schema.isLimit( '$root' ); // -> true
+	 *		schema.isLimit( editor.model.document.getRoot() ); // -> true
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
+	 */
+	isLimit( item ) {
+		const def = this.getDefinition( item );
 
-		// First check if the query meets at required attributes for this item.
-		if ( !this._getItem( query.name )._checkRequiredAttributes( query.attributes ) ) {
+		return !!( def && def.isLimit );
+	}
+
+	/**
+	 * Returns `true` if the given item is defined to be
+	 * a object element by {@link module:engine/model/schema~SchemaItemDefinition}'s `isObject` property.
+	 *
+	 *		schema.isObject( 'paragraph' ); // -> false
+	 *		schema.isObject( 'image' ); // -> true
+	 *
+	 *		const imageElement = writer.createElement( 'image' );
+	 *		schema.isObject( imageElement ); // -> true
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
+	 */
+	isObject( item ) {
+		const def = this.getDefinition( item );
+
+		return !!( def && def.isObject );
+	}
+
+	/**
+	 * Checks whether the given node (`child`) can be a child of the given context.
+	 *
+	 *		schema.checkChild( model.document.getRoot(), paragraph ); // -> false
+	 *
+	 *		schema.register( 'paragraph', {
+	 *			allowIn: '$root'
+	 *		} );
+	 *		schema.checkChild( model.document.getRoot(), paragraph ); // -> true
+	 *
+	 * @fires checkChild
+	 * @param {module:engine/model/schema~SchemaContextDefinition} context Context in which the child will be checked.
+	 * @param {module:engine/model/node~Node|String} def The child to check.
+	 */
+	checkChild( context, def ) {
+		// Note: context and child are already normalized here to a SchemaContext and SchemaCompiledItemDefinition.
+		if ( !def ) {
 			return false;
 		}
 
-		// If there is matching disallow path, this query is not valid with schema.
-		for ( const attribute of query.attributes ) {
-			for ( const schemaItem of schemaItems ) {
-				if ( schemaItem._hasMatchingPath( 'disallow', path, attribute ) ) {
-					return false;
-				}
-			}
+		return this._checkContextMatch( def, context );
+	}
+
+	/**
+	 * Checks whether the given attribute can be applied in the given context (on the last
+	 * item of the context).
+	 *
+	 *		schema.checkAttribute( textNode, 'bold' ); // -> false
+	 *
+	 *		schema.extend( '$text', {
+	 *			allowAttributes: 'bold'
+	 *		} );
+	 *		schema.checkAttribute( textNode, 'bold' ); // -> true
+	 *
+	 * @fires checkAttribute
+	 * @param {module:engine/model/schema~SchemaContextDefinition} context Context in which the attribute will be checked.
+	 * @param {String} attributeName
+	 */
+	checkAttribute( context, attributeName ) {
+		const def = this.getDefinition( context.last );
+
+		if ( !def ) {
+			return false;
 		}
 
-		// At this point, the query is not disallowed.
-		// If there are correct allow paths that match the query, this query is valid with schema.
-		// Since we are supporting multiple attributes, we have to make sure that if attributes are set,
-		// we have allowed paths for all of them.
-		// Keep in mind that if the query has no attributes, query.attribute was converted to an array
-		// with a single `undefined` value. This fits the algorithm well.
-		for ( const attribute of query.attributes ) {
-			// Skip all attributes that are stored in elements.
-			// This isn't perfect solution but we have to deal with it for now.
-			// `attribute` may have `undefined` value.
-			if ( attribute && DocumentSelection._isStoreAttributeKey( attribute ) ) {
-				continue;
-			}
+		return def.allowAttributes.includes( attributeName );
+	}
 
-			let matched = false;
+	/**
+	 * Checks whether the given element (`elementToMerge`) can be merged with the specified base element (`positionOrBaseElement`).
+	 *
+	 * In other words – whether `elementToMerge`'s children {@link #checkChild are allowed} in the `positionOrBaseElement`.
+	 *
+	 * This check ensures that elements merged with {@link module:engine/model/writer~Writer#merge `Writer#merge()`}
+	 * will be valid.
+	 *
+	 * Instead of elements, you can pass the instance of {@link module:engine/model/position~Position} class as the `positionOrBaseElement`.
+	 * It means that the elements before and after the position will be checked whether they can be merged.
+	 *
+	 * @param {module:engine/model/position~Position|module:engine/model/element~Element} positionOrBaseElement The position or base
+	 * element to which the `elementToMerge` will be merged.
+	 * @param {module:engine/model/element~Element} elementToMerge The element to merge. Required if `positionOrBaseElement` is a element.
+	 * @returns {Boolean}
+	 */
+	checkMerge( positionOrBaseElement, elementToMerge = null ) {
+		if ( positionOrBaseElement instanceof Position ) {
+			const nodeBefore = positionOrBaseElement.nodeBefore;
+			const nodeAfter = positionOrBaseElement.nodeAfter;
+
+			if ( !( nodeBefore instanceof Element ) ) {
+				/**
+				 * The node before the merge position must be an element.
+				 *
+				 * @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.' );
+			}
 
-			for ( const schemaItem of schemaItems ) {
-				if ( schemaItem._hasMatchingPath( 'allow', path, attribute ) ) {
-					matched = true;
-					break;
-				}
+			if ( !( nodeAfter instanceof Element ) ) {
+				/**
+				 * The node after the merge position must be an element.
+				 *
+				 * @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.' );
 			}
 
-			// The attribute has not been matched, so it is not allowed by any schema item.
-			// The query is disallowed.
-			if ( !matched ) {
+			return this.checkMerge( nodeBefore, nodeAfter );
+		}
+
+		for ( const child of elementToMerge.getChildren() ) {
+			if ( !this.checkChild( positionOrBaseElement, child ) ) {
 				return false;
 			}
 		}
@@ -245,65 +465,141 @@ export default class Schema {
 	}
 
 	/**
-	 * Checks whether there is an item registered under given name in schema.
+	 * Allows registering a callback to the {@link #checkChild} method calls.
 	 *
-	 * @param itemName
-	 * @returns {Boolean}
+	 * Callbacks allow you to implement rules which are not otherwise possible to achieve
+	 * by using the declarative API of {@link module:engine/model/schema~SchemaItemDefinition}.
+	 * For example, by using this method you can disallow elements in specific contexts.
+	 *
+	 * This method is a shorthand for using the {@link #event:checkChild} event. For even better control,
+	 * you can use that event instead.
+	 *
+	 * Example:
+	 *
+	 *		// Disallow heading1 directly inside a blockQuote.
+	 *		schema.addChildCheck( ( context, childDefinition ) => {
+	 *			if ( context.endsWith( 'blockQuote' ) && childDefinition.name == 'heading1' ) {
+	 *				return false;
+	 *			}
+	 *		} );
+	 *
+	 * Which translates to:
+	 *
+	 *		schema.on( 'checkChild', ( evt, args ) => {
+	 *			const context = args[ 0 ];
+	 *			const childDefinition = args[ 1 ];
+	 *
+	 *			if ( context.endsWith( 'blockQuote' ) && childDefinition && childDefinition.name == 'heading1' ) {
+	 *				// Prevent next listeners from being called.
+	 *				evt.stop();
+	 *				// Set the checkChild()'s return value.
+	 *				evt.return = false;
+	 *			}
+	 *		}, { priority: 'high' } );
+	 *
+	 * @param {Function} callback The callback to be called. It is called with two parameters:
+	 * {@link module:engine/model/schema~SchemaContext} (context) instance and
+	 * {@link module:engine/model/schema~SchemaCompiledItemDefinition} (child-to-check definition).
+	 * The callback may return `true/false` to override `checkChild()`'s return value. If it does not return
+	 * a boolean value, the default algorithm (or other callbacks) will define `checkChild()`'s return value.
 	 */
-	hasItem( itemName ) {
-		return this._items.has( itemName );
+	addChildCheck( callback ) {
+		this.on( 'checkChild', ( evt, [ ctx, childDef ] ) => {
+			// checkChild() was called with a non-registered child.
+			// In 99% cases such check should return false, so not to overcomplicate all callbacks
+			// don't even execute them.
+			if ( !childDef ) {
+				return;
+			}
+
+			const retValue = callback( ctx, childDef );
+
+			if ( typeof retValue == 'boolean' ) {
+				evt.stop();
+				evt.return = retValue;
+			}
+		}, { priority: 'high' } );
 	}
 
 	/**
-	 * Registers given item name in schema.
+	 * Allows registering a callback to the {@link #checkAttribute} method calls.
+	 *
+	 * Callbacks allow you to implement rules which are not otherwise possible to achieve
+	 * by using the declarative API of {@link module:engine/model/schema~SchemaItemDefinition}.
+	 * For example, by using this method you can disallow attribute if node to which it is applied
+	 * is contained within some other element (e.g. you want to disallow `bold` on `$text` within `heading1`).
+	 *
+	 * This method is a shorthand for using the {@link #event:checkAttribute} event. For even better control,
+	 * you can use that event instead.
+	 *
+	 * Example:
+	 *
+	 *		// Disallow bold on $text inside heading1.
+	 *		schema.addChildCheck( ( context, attributeName ) => {
+	 *			if ( context.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
+	 *				return false;
+	 *			}
+	 *		} );
 	 *
-	 *		// Register P element that should be treated like all block elements.
-	 *		schema.registerItem( 'p', '$block' );
+	 * Which translates to:
 	 *
-	 * @param {String} itemName Name to register.
-	 * @param [isExtending] If set, new item will extend item with given name.
+	 *		schema.on( 'checkAttribute', ( evt, args ) => {
+	 *			const context = args[ 0 ];
+	 *			const attributeName = args[ 1 ];
+	 *
+	 *			if ( context.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
+	 *				// Prevent next listeners from being called.
+	 *				evt.stop();
+	 *				// Set the checkAttribute()'s return value.
+	 *				evt.return = false;
+	 *			}
+	 *		}, { priority: 'high' } );
+	 *
+	 * @param {Function} callback The callback to be called. It is called with two parameters:
+	 * {@link module:engine/model/schema~SchemaContext} (context) instance and attribute name.
+	 * The callback may return `true/false` to override `checkAttribute()`'s return value. If it does not return
+	 * a boolean value, the default algorithm (or other callbacks) will define `checkAttribute()`'s return value.
 	 */
-	registerItem( itemName, isExtending ) {
-		if ( this.hasItem( itemName ) ) {
-			/**
-			 * Item with specified name already exists in schema.
-			 *
-			 * @error model-schema-item-exists
-			 */
-			throw new CKEditorError( 'model-schema-item-exists: Item with specified name already exists in schema.' );
-		}
+	addAttributeCheck( callback ) {
+		this.on( 'checkAttribute', ( evt, [ ctx, attributeName ] ) => {
+			const retValue = callback( ctx, attributeName );
 
-		if ( !!isExtending && !this.hasItem( isExtending ) ) {
-			throw new CKEditorError( 'model-schema-no-item: Item with specified name does not exist in schema.' );
-		}
-
-		// Create new SchemaItem and add it to the items store.
-		this._items.set( itemName, new SchemaItem( this ) );
-
-		// Create an extension chain.
-		// Extension chain has all item names that should be checked when that item is on path to check.
-		// This simply means, that if item is not extending anything, it should have only itself in it's extension chain.
-		// Since extending is not dynamic, we can simply get extension chain of extended item and expand it with registered name,
-		// if the registered item is extending something.
-		const chain = this.hasItem( isExtending ) ? this._extensionChains.get( isExtending ).concat( itemName ) : [ itemName ];
-		this._extensionChains.set( itemName, chain );
+			if ( typeof retValue == 'boolean' ) {
+				evt.stop();
+				evt.return = retValue;
+			}
+		}, { priority: 'high' } );
 	}
 
 	/**
-	 * Checks whether item of given name is extending item of another given name.
+	 * Returns the lowest {@link module:engine/model/schema~Schema#isLimit limit element} containing the entire
+	 * selection or the root otherwise.
 	 *
-	 * @param {String} childItemName Name of the child item.
-	 * @param {String} parentItemName Name of the parent item.
-	 * @returns {Boolean} `true` if child item extends parent item, `false` otherwise.
+	 * @param {module:engine/model/selection~Selection} selection Selection which returns the common ancestor.
+	 * @returns {module:engine/model/element~Element}
 	 */
-	itemExtends( childItemName, parentItemName ) {
-		if ( !this.hasItem( childItemName ) || !this.hasItem( parentItemName ) ) {
-			throw new CKEditorError( 'model-schema-no-item: Item with specified name does not exist in schema.' );
-		}
+	getLimitElement( selection ) {
+		// Find the common ancestor for all selection's ranges.
+		let element = Array.from( selection.getRanges() )
+			.reduce( ( element, range ) => {
+				const rangeCommonAncestor = range.getCommonAncestor();
 
-		const chain = this._extensionChains.get( childItemName );
+				if ( !element ) {
+					return rangeCommonAncestor;
+				}
+
+				return element.getCommonAncestor( rangeCommonAncestor, { includeSelf: true } );
+			}, null );
 
-		return chain.some( itemName => itemName == parentItemName );
+		while ( !this.isLimit( element ) ) {
+			if ( element.parent ) {
+				element = element.parent;
+			} else {
+				break;
+			}
+		}
+
+		return element;
 	}
 
 	/**
@@ -320,21 +616,14 @@ export default class Schema {
 	checkAttributeInSelection( selection, attribute ) {
 		if ( selection.isCollapsed ) {
 			// Check whether schema allows for a text with the attribute in the selection.
-			return this.check( { name: '$text', inside: selection.getFirstPosition(), attributes: attribute } );
+			return this.checkAttribute( [ ...selection.getFirstPosition().getAncestors(), '$text' ], attribute );
 		} else {
 			const ranges = selection.getRanges();
 
 			// For all ranges, check nodes in them until you find a node that is allowed to have the attribute.
 			for ( const range of ranges ) {
 				for ( const value of range ) {
-					// If returned item does not have name property, it is a TextFragment.
-					const name = value.item.name || '$text';
-
-					// Attribute should be checked together with existing attributes.
-					// See https://github.com/ckeditor/ckeditor5-engine/issues/1110.
-					const attributes = Array.from( value.item.getAttributeKeys() ).concat( attribute );
-
-					if ( this.check( { name, inside: value.previousPosition, attributes } ) ) {
+					if ( this.checkAttribute( value.item, attribute ) ) {
 						// If we found a node that is allowed to have the attribute, return true.
 						return true;
 					}
@@ -347,7 +636,7 @@ export default class Schema {
 	}
 
 	/**
-	 * Transforms the given set ranges into a set of ranges where the given attribute is allowed (and can be applied).
+	 * Transforms the given set of ranges into a set of ranges where the given attribute is allowed (and can be applied).
 	 *
 	 * @param {Array.<module:engine/model/range~Range>} ranges Ranges to be validated.
 	 * @param {String} attribute The name of the attribute to check.
@@ -362,10 +651,7 @@ export default class Schema {
 			const to = range.end;
 
 			for ( const value of range.getWalker() ) {
-				const name = value.item.name || '$text';
-				const itemPosition = Position.createBefore( value.item );
-
-				if ( !this.check( { name, inside: itemPosition, attributes: attribute } ) ) {
+				if ( !this.checkAttribute( value.item, attribute ) ) {
 					if ( !from.isEqual( last ) ) {
 						validRanges.push( new Range( from, last ) );
 					}
@@ -385,368 +671,876 @@ export default class Schema {
 	}
 
 	/**
-	 * Returns the lowest {@link module:engine/model/schema~Schema#limits limit element} containing the entire
-	 * selection or the root otherwise.
+	 * Basing on given `position`, finds and returns a {@link module:engine/model/range~Range Range} instance that is
+	 * nearest to that `position` and is a correct range for selection.
 	 *
-	 * @param {module:engine/model/selection~Selection} selection Selection which returns the common ancestor.
-	 * @returns {module:engine/model/element~Element}
+	 * Correct selection range might be collapsed - when it's located in position where text node can be placed.
+	 * Non-collapsed range is returned when selection can be placed around element marked as "object" in
+	 * {@link module:engine/model/schema~Schema schema}.
+	 *
+	 * Direction of searching for nearest correct selection range can be specified as:
+	 * * `both` - searching will be performed in both ways,
+	 * * `forward` - searching will be performed only forward,
+	 * * `backward` - searching will be performed only backward.
+	 *
+	 * When valid selection range cannot be found, `null` is returned.
+	 *
+	 * @param {module:engine/model/position~Position} position Reference position where new selection range should be looked for.
+	 * @param {'both'|'forward'|'backward'} [direction='both'] Search direction.
+	 * @returns {module:engine/model/range~Range|null} Nearest selection range or `null` if one cannot be found.
 	 */
-	getLimitElement( selection ) {
-		// Find the common ancestor for all selection's ranges.
-		let element = Array.from( selection.getRanges() )
-			.reduce( ( node, range ) => {
-				if ( !node ) {
-					return range.getCommonAncestor();
-				}
+	getNearestSelectionRange( position, direction = 'both' ) {
+		// Return collapsed range if provided position is valid.
+		if ( this.checkChild( position, '$text' ) ) {
+			return new Range( position );
+		}
 
-				return node.getCommonAncestor( range.getCommonAncestor() );
-			}, null );
+		let backwardWalker, forwardWalker;
 
-		while ( !this.limits.has( element.name ) ) {
-			if ( element.parent ) {
-				element = element.parent;
-			} else {
-				break;
+		if ( direction == 'both' || direction == 'backward' ) {
+			backwardWalker = new TreeWalker( { startPosition: position, direction: 'backward' } );
+		}
+
+		if ( direction == 'both' || direction == 'forward' ) {
+			forwardWalker = new TreeWalker( { startPosition: position } );
+		}
+
+		for ( const data of combineWalkers( backwardWalker, forwardWalker ) ) {
+			const type = ( data.walker == backwardWalker ? 'elementEnd' : 'elementStart' );
+			const value = data.value;
+
+			if ( value.type == type && this.isObject( value.item ) ) {
+				return Range.createOn( value.item );
+			}
+
+			if ( this.checkChild( value.nextPosition, '$text' ) ) {
+				return new Range( value.nextPosition );
 			}
 		}
 
-		return element;
+		return null;
 	}
 
 	/**
-	 * Removes disallowed by {@link module:engine/model/schema~Schema schema} attributes from given nodes.
-	 * When {@link module:engine/model/batch~Batch batch} parameter is provided then attributes will be removed
-	 * using that batch, by creating {@link module:engine/model/delta/attributedelta~AttributeDelta attribute deltas}.
-	 * Otherwise, attributes will be removed directly from provided nodes using {@link module:engine/model/node~Node node} API.
+	 * Tries to find position ancestors that allows to insert given node.
+	 * It starts searching from the given position and goes node by node to the top of the model tree
+	 * as long as {@link module:engine/model/schema~Schema#isLimit limit element},
+	 * {@link module:engine/model/schema~Schema#isObject object element} or top-most ancestor won't be reached.
+	 *
+	 * @params {module:engine/model/node~Node} node Node for which allowed parent should be found.
+	 * @params {module:engine/model/position~Position} position Position from searching will start.
+	 * @returns {module:engine/model/element~Element|null} element Allowed parent or null if nothing was found.
+	 */
+	findAllowedParent( node, position ) {
+		let parent = position.parent;
+
+		while ( parent ) {
+			if ( this.checkChild( parent, node ) ) {
+				return parent;
+			}
+
+			// Do not split limit elements and objects.
+			if ( this.isLimit( parent ) || this.isObject( parent ) ) {
+				return null;
+			}
+
+			parent = parent.parent;
+		}
+
+		return null;
+	}
+
+	/**
+	 * Removes attributes disallowed by the schema.
 	 *
 	 * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes that will be filtered.
-	 * @param {module:engine/model/schema~SchemaPath} inside Path inside which schema will be checked.
-	 * @param {module:engine/model/batch~Batch} [batch] Batch to which the deltas will be added.
+	 * @param {module:engine/model/writer~Writer} writer
 	 */
-	removeDisallowedAttributes( nodes, inside, batch ) {
+	removeDisallowedAttributes( nodes, writer ) {
 		for ( const node of nodes ) {
-			const name = node.is( 'text' ) ? '$text' : node.name;
-			const attributes = Array.from( node.getAttributeKeys() );
-			const queryPath = Schema._normalizeQueryPath( inside );
-
-			// When node with attributes is not allowed in current position.
-			if ( !this.check( { name, attributes, inside: queryPath } ) ) {
-				// Let's remove attributes one by one.
-				// TODO: this should be improved to check all combination of attributes.
-				for ( const attribute of node.getAttributeKeys() ) {
-					if ( !this.check( { name, attributes: attribute, inside: queryPath } ) ) {
-						if ( batch ) {
-							batch.removeAttribute( node, attribute );
-						} else {
-							node.removeAttribute( attribute );
-						}
-					}
+			for ( const attribute of node.getAttributeKeys() ) {
+				if ( !this.checkAttribute( node, attribute ) ) {
+					writer.removeAttribute( attribute, node );
 				}
 			}
 
 			if ( node.is( 'element' ) ) {
-				this.removeDisallowedAttributes( node.getChildren(), queryPath.concat( node.name ), batch );
+				this.removeDisallowedAttributes( node.getChildren(), writer );
 			}
 		}
 	}
 
 	/**
-	 * Returns {@link module:engine/model/schema~SchemaItem schema item} that was registered in the schema under given name.
-	 * If item has not been found, throws error.
-	 *
 	 * @private
-	 * @param {String} itemName Name to look for in schema.
-	 * @returns {module:engine/model/schema~SchemaItem} Schema item registered under given name.
 	 */
-	_getItem( itemName ) {
-		if ( !this.hasItem( itemName ) ) {
-			throw new CKEditorError( 'model-schema-no-item: Item with specified name does not exist in schema.' );
+	_clearCache() {
+		this._compiledDefinitions = null;
+	}
+
+	/**
+	 * @private
+	 */
+	_compile() {
+		const compiledDefinitions = {};
+		const sourceRules = this._sourceDefinitions;
+		const itemNames = Object.keys( sourceRules );
+
+		for ( const itemName of itemNames ) {
+			compiledDefinitions[ itemName ] = compileBaseItemRule( sourceRules[ itemName ], itemName );
+		}
+
+		for ( const itemName of itemNames ) {
+			compileAllowContentOf( compiledDefinitions, itemName );
+		}
+
+		for ( const itemName of itemNames ) {
+			compileAllowWhere( compiledDefinitions, itemName );
 		}
 
-		return this._items.get( itemName );
+		for ( const itemName of itemNames ) {
+			compileAllowAttributesOf( compiledDefinitions, itemName );
+			compileInheritPropertiesFrom( compiledDefinitions, itemName );
+		}
+
+		for ( const itemName of itemNames ) {
+			cleanUpAllowIn( compiledDefinitions, itemName );
+			cleanUpAllowAttributes( compiledDefinitions, itemName );
+		}
+
+		this._compiledDefinitions = compiledDefinitions;
 	}
 
 	/**
-	 * Normalizes a path to an entity by converting it from {@link module:engine/model/schema~SchemaPath} to an array of strings.
-	 *
-	 * @protected
-	 * @param {module:engine/model/schema~SchemaPath} path Path to normalize.
-	 * @returns {Array.<String>} Normalized path.
+	 * @private
+	 * @param {module:engine/model/schema~SchemaCompiledItemDefinition} def
+	 * @param {module:engine/model/schema~SchemaContext} context
+	 * @param {Number} contextItemIndex
 	 */
-	static _normalizeQueryPath( path ) {
-		let normalized = [];
-
-		if ( isArray( path ) ) {
-			for ( const pathItem of path ) {
-				if ( pathItem instanceof Element ) {
-					normalized.push( pathItem.name );
-				} else if ( isString( pathItem ) ) {
-					normalized.push( pathItem );
-				}
-			}
-		} else if ( path instanceof Position ) {
-			let parent = path.parent;
+	_checkContextMatch( def, context, contextItemIndex = context.length - 1 ) {
+		const contextItem = context.getItem( contextItemIndex );
 
-			while ( parent !== null ) {
-				normalized.push( parent.name );
-				parent = parent.parent;
-			}
+		if ( def.allowIn.includes( contextItem.name ) ) {
+			if ( contextItemIndex == 0 ) {
+				return true;
+			} else {
+				const parentRule = this.getDefinition( contextItem );
 
-			normalized.reverse();
-		} else if ( isString( path ) ) {
-			normalized = path.split( ' ' );
+				return this._checkContextMatch( parentRule, context, contextItemIndex - 1 );
+			}
+		} else {
+			return false;
 		}
-
-		return normalized;
 	}
 }
 
+mix( Schema, ObservableMixin );
+
+/**
+ * Event fired when the {@link #checkChild} method is called. It allows plugging in
+ * additional behavior – e.g. implementing rules which cannot be defined using the declarative
+ * {@link module:engine/model/schema~SchemaItemDefinition} interface.
+ *
+ * **Note:** The {@link #addChildCheck} method is a more handy way to register callbacks. Internally,
+ * it registers a listener to this event but comes with a simpler API and it is the recommended choice
+ * in most of the cases.
+ *
+ * The {@link #checkChild} method fires an event because it is
+ * {@link module:utils/observablemixin~ObservableMixin#decorate decorated} with it. Thanks to that you can
+ * use this event in a various way, but the most important use case is overriding standard behaviour of the
+ * `checkChild()` method. Let's see a typical listener template:
+ *
+ *		schema.on( 'checkChild', ( evt, args ) => {
+ *			const context = args[ 0 ];
+ *			const childDefinition = args[ 1 ];
+ *		}, { priority: 'high' } );
+ *
+ * The listener is added with a `high` priority to be executed before the default method is really called. The `args` callback
+ * parameter contains arguments passed to `checkChild( context, child )`. However, the `context` parameter is already
+ * normalized to a {@link module:engine/model/schema~SchemaContext} instance and `child` to a
+ * {@link module:engine/model/schema~SchemaCompiledItemDefinition} instance, so you don't have to worry about
+ * the various ways how `context` and `child` may be passed to `checkChild()`.
+ *
+ * **Note:** `childDefinition` may be `undefined` if `checkChild()` was called with a non-registered element.
+ *
+ * So, in order to implement a rule "disallow `heading1` in `blockQuote`" you can add such a listener:
+ *
+ *		schema.on( 'checkChild', ( evt, args ) => {
+ *			const context = args[ 0 ];
+ *			const childDefinition = args[ 1 ];
+ *
+ *			if ( context.endsWith( 'blockQuote' ) && childDefinition && childDefinition.name == 'heading1' ) {
+ *				// Prevent next listeners from being called.
+ *				evt.stop();
+ *				// Set the checkChild()'s return value.
+ *				evt.return = false;
+ *			}
+ *		}, { priority: 'high' } );
+ *
+ * Allowing elements in specific contexts will be a far less common use case, because it's normally handled by
+ * `allowIn` rule from {@link module:engine/model/schema~SchemaItemDefinition} but if you have a complex scenario
+ * where `listItem` should be allowed only in element `foo` which must be in element `bar`, then this would be the way:
+ *
+ *		schema.on( 'checkChild', ( evt, args ) => {
+ *			const context = args[ 0 ];
+ *			const childDefinition = args[ 1 ];
+ *
+ *			if ( context.endsWith( 'bar foo' ) && childDefinition.name == 'listItem' ) {
+ *				// Prevent next listeners from being called.
+ *				evt.stop();
+ *				// Set the checkChild()'s return value.
+ *				evt.return = true;
+ *			}
+ *		}, { priority: 'high' } );
+ *
+ * @event checkChild
+ * @param {Array} args The `checkChild()`'s arguments.
+ */
+
 /**
- * SchemaItem is a singular registry item in {@link module:engine/model/schema~Schema} that groups and holds allow/disallow rules for
- * one entity. This class is used internally in {@link module:engine/model/schema~Schema} and should not be used outside it.
+ * Event fired when the {@link #checkAttribute} method is called. It allows plugging in
+ * additional behavior – e.g. implementing rules which cannot be defined using the declarative
+ * {@link module:engine/model/schema~SchemaItemDefinition} interface.
+ *
+ * **Note:** The {@link #addAttributeCheck} method is a more handy way to register callbacks. Internally,
+ * it registers a listener to this event but comes with a simpler API and it is the recommended choice
+ * in most of the cases.
+ *
+ * The {@link #checkAttribute} method fires an event because it's
+ * {@link module:utils/observablemixin~ObservableMixin#decorate decorated} with it. Thanks to that you can
+ * use this event in a various way, but the most important use case is overriding standard behaviour of the
+ * `checkAttribute()` method. Let's see a typical listener template:
+ *
+ *		schema.on( 'checkAttribute', ( evt, args ) => {
+ *			const context = args[ 0 ];
+ *			const attributeName = args[ 1 ];
+ *		}, { priority: 'high' } );
  *
- * @see module:engine/model/schema~Schema
- * @protected
+ * The listener is added with a `high` priority to be executed before the default method is really called. The `args` callback
+ * parameter contains arguments passed to `checkAttribute( context, attributeName )`. However, the `context` parameter is already
+ * normalized to a {@link module:engine/model/schema~SchemaContext} instance, so you don't have to worry about
+ * the various ways how `context` may be passed to `checkAttribute()`.
+ *
+ * So, in order to implement a rule "disallow `bold` in a text which is in a `heading1` you can add such a listener:
+ *
+ *		schema.on( 'checkAttribute', ( evt, args ) => {
+ *			const context = args[ 0 ];
+ *			const atributeName = args[ 1 ];
+ *
+ *			if ( context.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
+ *				// Prevent next listeners from being called.
+ *				evt.stop();
+ *				// Set the checkAttribute()'s return value.
+ *				evt.return = false;
+ *			}
+ *		}, { priority: 'high' } );
+ *
+ * Allowing attributes in specific contexts will be a far less common use case, because it's normally handled by
+ * `allowAttributes` rule from {@link module:engine/model/schema~SchemaItemDefinition} but if you have a complex scenario
+ * where `bold` should be allowed only in element `foo` which must be in element `bar`, then this would be the way:
+ *
+ *		schema.on( 'checkAttribute', ( evt, args ) => {
+ *			const context = args[ 0 ];
+ *			const atributeName = args[ 1 ];
+ *
+ *			if ( context.endsWith( 'bar foo $text' ) && attributeName == 'bold' ) {
+ *				// Prevent next listeners from being called.
+ *				evt.stop();
+ *				// Set the checkAttribute()'s return value.
+ *				evt.return = true;
+ *			}
+ *		}, { priority: 'high' } );
+ *
+ * @event checkAttribute
+ * @param {Array} args The `checkAttribute()`'s arguments.
  */
-export class SchemaItem {
+
+/**
+ * A definition of a {@link module:engine/model/schema~Schema schema} item.
+ *
+ * You can define the following rules:
+ *
+ * * `allowIn` – a string or an array of strings. Defines in which other items this item will be allowed.
+ * * `allowAttributes` – a string or an array of strings. Defines allowed attributes of the given item.
+ * * `allowContentOf` – a string or an array of strings. Inherit "allowed children" from other items.
+ * * `allowWhere` – a string or an array of strings. Inherit "allowed in" from other items.
+ * * `allowAttributesOf` – a string or an array of strings. Inherit attributes from other items.
+ * * `inheritTypesFrom` – a string or an array of strings. Inherit `is*` properties of other items.
+ * * `inheritAllFrom` – a string. A shorthand for `allowContentOf`, `allowWhere`, `allowAttributesOf`, `inheritTypesFrom`.
+ * * additionall, you can define the following `is*` properties: `isBlock`, `isLimit`, `isObject`. Read about them below.
+ *
+ * # The is* properties
+ *
+ * There are 3 commonly used `is*` properties. Their role is to assign additional semantics to schema items.
+ * You can define more properties but you will also need to implement support for them in the existing editor features.
+ *
+ * * `isBlock` – whether this item is paragraph-like. Generally speaking, a content is usually made out of blocks
+ * like paragraphs, list items, images, headings, etc. All these elements are marked as blocks. A block
+ * should not allow another block inside. Note: there's also the `$block` generic item which has `isBlock` set to `true`.
+ * Most block type items will inherit from `$block` (through `inheritAllFrom`).
+ * * `isLimit` – can be understood as whether this element should not be split by <kbd>Enter</kbd>.
+ * Examples of limit elements – `$root`, table cell, image caption, etc. In other words, all actions which happen inside
+ * a limit element are limitted to its content.
+ * * `isObject` – whether item is "self-contained" and should be treated as a whole. Examples of object elements –
+ * `image`, `table`, `video`, etc.
+ *
+ * # Generic items
+ *
+ * There are three basic generic items: `$root`, `$block` and `$text`.
+ * They are defined as follows:
+ *
+ *		this.schema.register( '$root', {
+ *			isLimit: true
+ *		} );
+ *		this.schema.register( '$block', {
+ *			allowIn: '$root',
+ *			isBlock: true
+ *		} );
+ *		this.schema.register( '$text', {
+ *			allowIn: '$block'
+ *		} );
+ *
+ * They reflect a typical editor content which is contained within one root, consists of several blocks
+ * (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.
+ *
+ * # Example definitions
+ *
+ * Allow `paragraph` in roots and block quotes:
+ *
+ *		schema.register( 'paragraph', {
+ *			allowIn: [ '$root', 'blockQuote' ],
+ *			isBlock: true
+ *		} );
+ *
+ * Allow `paragraph` everywhere where `$block` is allowed (i.e. in `$root`):
+ *
+ *		schema.register( 'paragraph', {
+ *			allowWhere: '$block',
+ *			isBlock: true
+ *		} );
+ *
+ * Make `image` a block object, which is allowed everywhere where `$block` is.
+ * Also, allow `src` and `alt` attributes on it:
+ *
+ *		schema.register( 'image', {
+ *			allowWhere: '$block',
+ *			allowAttributes: [ 'src', 'alt' ],
+ *			isBlock: true,
+ *			isObject: true
+ *		} );
+ *
+ * Make `caption` allowed in `image` and make it allow all the content of `$block`s (usually, `$text`).
+ * Also, mark it as a limit element so it can't be split:
+ *
+ *		schema.register( 'caption', {
+ *			allowIn: 'image',
+ *			allowContentOf: '$block',
+ *			isLimit: true
+ *		} );
+ *
+ * Make `listItem` inherit all from `$block` but also allow additional attributes:
+ *
+ *		schema.register( 'listItem', {
+ *			inheritAllFrom: '$block',
+ *			allowAttributes: [ 'type', 'indent' ]
+ *		} );
+ *
+ * Which translates to:
+ *
+ *		schema.register( 'listItem', {
+ *			allowWhere: '$block',
+ *			allowContentOf: '$block',
+ *			allowAttributesOf: '$block',
+ *			inheritTypesFrom: '$block',
+ *			allowAttributes: [ 'type', 'indent' ]
+ *		} );
+ *
+ * # Tips
+ *
+ * * Check schema definitions of existing features to see how they are defined.
+ * * If you want to publish your feature so other developers can use it, try to use
+ * generic items as much as possible.
+ * * Keep your model clean – limit it to the actual data and store information in an normalized way.
+ * * Remember about definining the `is*` properties. They don't affect the allowed structures, but they can
+ * affect how editor features treat your elements.
+ *
+ * @typedef {Object} module:engine/model/schema~SchemaItemDefinition
+ */
+
+/**
+ * A simplified version of {@link module:engine/model/schema~SchemaItemDefinition} after
+ * compilation by the {@link module:engine/model/schema~Schema schema}.
+ * Rules feed to the schema by {@link module:engine/model/schema~Schema#register}
+ * and {@link module:engine/model/schema~Schema#extend} are defined in the
+ * {@link module:engine/model/schema~SchemaItemDefinition} format.
+ * Later on, they are compiled to `SchemaCompiledItemDefition` so when you use e.g.
+ * the {@link module:engine/model/schema~Schema#getDefinition} method you get the compiled version.
+ *
+ * The compiled version contains only the following properties:
+ *
+ * * `name` property,
+ * * `is*` properties,
+ * * `allowIn` array,
+ * * `allowAttributes` array.
+ *
+ * @typedef {Object} module:engine/model/schema~SchemaCompiledItemDefinition
+ */
+
+/**
+ * A schema context – a list of ancestors of a given position in the document.
+ *
+ * Considering such a position:
+ *
+ *		<$root>
+ *			<blockQuote>
+ *				<paragraph>
+ *					^
+ *				</paragraph>
+ *			</blockQuote>
+ *		</$root>
+ *
+ * The context of this position is its {@link module:engine/model/position~Position#getAncestors lists of ancestors}:
+ *
+ *		[ rootElement, blockQuoteElement, paragraphElement ]
+ *
+ * Contexts are used in the {@link module:engine/model/schema~Schema#event:checkChild `Schema#checkChild`} and
+ * {@link module:engine/model/schema~Schema#event:checkAttribute `Schema#checkAttribute`} events as a definition
+ * of a place in the document where the check occurs. The context instances are created based on the first arguments
+ * of the {@link module:engine/model/schema~Schema#checkChild `Schema#checkChild()`} and
+ * {@link module:engine/model/schema~Schema#checkAttribute `Schema#checkAttribute()`} methods so when
+ * using these methods you need to use {@link module:engine/model/schema~SchemaContextDefinition}s.
+ */
+export class SchemaContext {
 	/**
-	 * Creates SchemaItem instance.
+	 * Creates an instance of the context.
 	 *
-	 * @param {module:engine/model/schema~Schema} schema Schema instance that owns this item.
+	 * @param {module:engine/model/schema~SchemaContextDefinition} context
 	 */
-	constructor( schema ) {
-		/**
-		 * Schema instance that owns this item.
-		 *
-		 * @private
-		 * @member {module:engine/model/schema~Schema} module:engine/model/schema~SchemaItem#_schema
-		 */
-		this._schema = schema;
-
-		/**
-		 * Paths in which the entity, represented by this item, is allowed.
-		 *
-		 * @private
-		 * @member {Array} module:engine/model/schema~SchemaItem#_allowed
-		 */
-		this._allowed = [];
-
-		/**
-		 * Paths in which the entity, represented by this item, is disallowed.
-		 *
-		 * @private
-		 * @member {Array} module:engine/model/schema~SchemaItem#_disallowed
-		 */
-		this._disallowed = [];
-
-		/**
-		 * Attributes that are required by the entity represented by this item.
-		 *
-		 * @protected
-		 * @member {Array} module:engine/model/schema~SchemaItem#_requiredAttributes
-		 */
-		this._requiredAttributes = [];
+	constructor( context ) {
+		if ( context instanceof SchemaContext ) {
+			return context;
+		}
+
+		if ( typeof context == 'string' ) {
+			context = [ context ];
+		} else if ( !Array.isArray( context ) ) {
+			// `context` is item or position.
+			// Position#getAncestors() doesn't accept any parameters but it works just fine here.
+			context = context.getAncestors( { includeSelf: true } );
+		}
+
+		if ( context[ 0 ] && typeof context[ 0 ] != 'string' && context[ 0 ].is( 'documentFragment' ) ) {
+			context.shift();
+		}
+
+		this._items = context.map( mapContextItem );
 	}
 
 	/**
-	 * Allows entity, represented by this item, to be in given path.
+	 * Number of items.
 	 *
-	 * @param {Array.<String>} path Path in which entity is allowed.
-	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have attribute(s) with this key.
+	 * @type {Number}
 	 */
-	allow( path, attributes ) {
-		this._addPath( '_allowed', path, attributes );
+	get length() {
+		return this._items.length;
 	}
 
 	/**
-	 * Disallows entity, represented by this item, to be in given path.
+	 * The last item (the lowest node).
 	 *
-	 * @param {Array.<String>} path Path in which entity is disallowed.
-	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have an attribute(s) with this key.
+	 * @type {module:engine/model/schema~SchemaContextItem}
 	 */
-	disallow( path, attributes ) {
-		this._addPath( '_disallowed', path, attributes );
+	get last() {
+		return this._items[ this._items.length - 1 ];
 	}
 
 	/**
-	 * Specifies that the entity, to be valid, requires given attributes set. It is possible to register multiple
-	 * different attributes set. If there are more than one attributes set required, the entity will be valid if
-	 * at least one of them is fulfilled.
+	 * Iterable interface.
 	 *
-	 * @param {Array.<String>} attributes Attributes that has to be set on the entity to make it valid.
+	 * Iterates over all context items.
+	 *
+	 * @returns {Iterable.<module:engine/model/schema~SchemaContextItem>}
 	 */
-	requireAttributes( attributes ) {
-		this._requiredAttributes.push( attributes );
+	[ Symbol.iterator ]() {
+		return this._items[ Symbol.iterator ]();
 	}
 
 	/**
-	 * Custom toJSON method to solve child-parent circular dependencies.
+	 * Returns new SchemaContext instance with additional item
+	 *
+	 * Item can be added as:
+	 *
+	 * 		const context = new SchemaContext( [ '$root' ] );
+	 *
+	 * 		// An element.
+	 * 		const fooElement = writer.createElement( 'fooElement' );
+	 * 		const newContext = context.push( fooElement ); // [ '$root', 'fooElement' ]
+	 *
+	 * 		// A text node.
+	 * 		const text = writer.createText( 'foobar' );
+	 * 		const newContext = context.push( text ); // [ '$root', '$text' ]
+	 *
+	 * 		// A string (element name).
+	 * 		const newContext = context.push( 'barElement' ); // [ '$root', 'barElement' ]
+	 *
+	 * **Note** {module:engine/model/node~Node} that is already in the model tree will be added as the only item (without ancestors).
 	 *
-	 * @returns {Object} Clone of this object with the parent property replaced with its name.
+	 * @param {String|module:engine/model/node~Node|Array<String|module:engine/model/node~Node>} item Item that will be added
+	 * to current context.
+	 * @returns {module:engine/model/schema~SchemaContext} New SchemaContext instance with additional item.
 	 */
-	toJSON() {
-		const json = clone( this );
+	push( item ) {
+		const ctx = new SchemaContext( [ item ] );
 
-		// Due to circular references we need to remove parent reference.
-		json._schema = '[model.Schema]';
+		ctx._items = [ ...this._items, ...ctx._items ];
 
-		return json;
+		return ctx;
 	}
 
 	/**
-	 * Adds path to the SchemaItem instance.
+	 * Gets an item on the given index.
 	 *
-	 * @private
-	 * @param {String} member Name of the array member into which the path will be added. Possible values are `_allowed` or `_disallowed`.
-	 * @param {Array.<String>} path Path to add.
-	 * @param {Array.<String>|String} [attributes] If set, this path will be used only for entities that have attribute(s) with this key.
+	 * @returns {module:engine/model/schema~SchemaContextItem}
 	 */
-	_addPath( member, path, attributes ) {
-		path = path.slice();
-
-		if ( !isArray( attributes ) ) {
-			attributes = [ attributes ];
-		}
+	getItem( index ) {
+		return this._items[ index ];
+	}
 
-		for ( const attribute of attributes ) {
-			this[ member ].push( { path, attribute } );
-		}
+	/**
+	 * Returns the names of items.
+	 *
+	 * @returns {Iterable.<String>}
+	 */
+	* getNames() {
+		yield* this._items.map( item => item.name );
 	}
 
 	/**
-	 * Returns all paths of given type that were previously registered in the item.
+	 * Checks whether the context ends with the given nodes.
 	 *
-	 * @private
-	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
-	 * @param {String} [attribute] If set, only paths registered for given attribute will be returned.
-	 * @returns {Array} Paths registered in the item.
+	 *		const ctx = new SchemaContext( [ rootElement, paragraphElement, textNode ] );
+	 *
+	 *		ctx.endsWith( '$text' ); // -> true
+	 *		ctx.endsWith( 'paragraph $text' ); // -> true
+	 *		ctx.endsWith( '$root' ); // -> false
+	 *		ctx.endsWith( 'paragraph' ); // -> false
+	 *
+	 * @param {String} query
+	 * @returns {Boolean}
 	 */
-	_getPaths( type, attribute ) {
-		const source = type === 'allow' ? this._allowed : this._disallowed;
-		const paths = [];
+	endsWith( query ) {
+		return Array.from( this.getNames() ).join( ' ' ).endsWith( query );
+	}
+}
 
-		for ( const item of source ) {
-			if ( item.attribute === attribute ) {
-				paths.push( item.path );
-			}
+/**
+ * The definition of a {@link module:engine/model/schema~SchemaContext schema context}.
+ *
+ * Contexts can be created in multiple ways:
+ *
+ * * By defining a **node** – in this cases this node and all its ancestors will be used.
+ * * By defining a **position** in the document – in this case all its ancestors will be used.
+ * * By defining an **array of nodes** – in this case this array defines the entire context.
+ * * By defining a **name of node** - in this case node will be "mocked". It is not recommended because context
+ * will be unrealistic (e.g. attributes of these nodes are not specified). However, at times this may be the only
+ * way to define the context (e.g. when checking some hypothetical situation).
+ * * By defining an **array of node names** (potentially, mixed with real nodes) – The same as **name of node**
+ * but it is possible to create a path.
+ * * By defining a {@link module:engine/model/schema~SchemaContext} instance - in this case the same instance as provided
+ * will be return.
+ *
+ * Examples of context definitions passed to the {@link module:engine/model/schema~Schema#checkChild `Schema#checkChild()`}
+ * method:
+ *
+ *		// Assuming that we have a $root > blockQuote > paragraph structure, the following code
+ *		// will check node 'foo' in the following context:
+ *		// [ rootElement, blockQuoteElement, paragraphElement ]
+ *		const contextDefinition = paragraphElement;
+ * 		const childToCheck = 'foo';
+ *		schema.checkChild( contextDefinition, childToCheck );
+ *
+ *		// Also check in [ rootElement, blockQuoteElement, paragraphElement ].
+ *		schema.checkChild( Position.createAt( paragraphElement ), 'foo' );
+ *
+ *		// Check in [ rootElement, paragraphElement ].
+ *		schema.checkChild( [ rootElement, paragraphElement ], 'foo' );
+ *
+ *		// Check only fakeParagraphElement.
+ *		schema.checkChild( 'paragraph', 'foo' );
+ *
+ *		// Check in [ fakeRootElement, fakeBarElement, paragraphElement ].
+ *		schema.checkChild( [ '$root', 'bar', paragraphElement ], 'foo' );
+ *
+ * All these `checkChild()` calls will fire {@link module:engine/model/schema~Schema#event:checkChild `Schema#checkChild`}
+ * events in which `args[ 0 ]` is an instance of the context. Therefore, you can write a listener like this:
+ *
+ *		schema.on( 'checkChild', ( evt, args ) => {
+ *			const ctx = args[ 0 ];
+ *
+ *			console.log( Array.from( ctx.getNames() ) );
+ *		} );
+ *
+ * Which will log the following:
+ *
+ *		[ '$root', 'blockQuote', 'paragraph' ]
+ *		[ '$root', 'paragraph' ]
+ *		[ '$root', 'bar', 'paragraph' ]
+ *
+ * Note: When using the {@link module:engine/model/schema~Schema#checkAttribute `Schema#checkAttribute()`} method
+ * you may want to check whether a text node may have an attribute. A {@link module:engine/model/text~Text} is a
+ * correct way to define a context so you can do this:
+ *
+ *		schema.checkAttribute( textNode, 'bold' );
+ *
+ * But sometimes you want to check whether a text at a given position might've had some attribute,
+ * in which case you can create a context by mising an array of elements with a `'$text'` string:
+ *
+ *		// Check in [ rootElement, paragraphElement, textNode ].
+ *		schema.checkChild( [ ...positionInParagraph.getAncestors(), '$text' ], 'bold' );
+ *
+ * @typedef {module:engine/model/node~Node|module:engine/model/position~Position|module:engine/model/schema~SchemaContext|
+ * String|Array.<String|module:engine/model/node~Node>} module:engine/model/schema~SchemaContextDefinition
+ */
+
+/**
+ * An item of the {@link module:engine/model/schema~SchemaContext schema context}.
+ *
+ * It contains 3 properties:
+ *
+ * * `name` – the name of this item,
+ * * `* getAttributeKeys()` – a generator of keys of item attributes,
+ * * `getAttribute( keyName )` – a method to get attribute values.
+ *
+ * The context item interface is a highly simplified version of {@link module:engine/model/node~Node} and its role
+ * is to expose only the information which schema checks are able to provide (which is the name of the node and
+ * node's attributes).
+ *
+ *		schema.on( 'checkChild', ( evt, args ) => {
+ *			const ctx = args[ 0 ];
+ *			const firstItem = ctx.getItem( 0 );
+ *
+ *			console.log( firstItem.name ); // -> '$root'
+ *			console.log( firstItem.getAttribute( 'foo' ) ); // -> 'bar'
+ *			console.log( Array.from( firstItem.getAttributeKeys() ) ); // -> [ 'foo', 'faa' ]
+ *		} );
+ *
+ * @typedef {Object} module:engine/model/schema~SchemaContextItem
+ */
+
+function compileBaseItemRule( sourceItemRules, itemName ) {
+	const itemRule = {
+		name: itemName,
+
+		allowIn: [],
+		allowContentOf: [],
+		allowWhere: [],
+
+		allowAttributes: [],
+		allowAttributesOf: [],
+
+		inheritTypesFrom: []
+	};
+
+	copyTypes( sourceItemRules, itemRule );
+
+	copyProperty( sourceItemRules, itemRule, 'allowIn' );
+	copyProperty( sourceItemRules, itemRule, 'allowContentOf' );
+	copyProperty( sourceItemRules, itemRule, 'allowWhere' );
+
+	copyProperty( sourceItemRules, itemRule, 'allowAttributes' );
+	copyProperty( sourceItemRules, itemRule, 'allowAttributesOf' );
+
+	copyProperty( sourceItemRules, itemRule, 'inheritTypesFrom' );
+
+	makeInheritAllWork( sourceItemRules, itemRule );
+
+	return itemRule;
+}
+
+function compileAllowContentOf( compiledDefinitions, itemName ) {
+	for ( const allowContentOfItemName of compiledDefinitions[ itemName ].allowContentOf ) {
+		// The allowContentOf property may point to an unregistered element.
+		if ( compiledDefinitions[ allowContentOfItemName ] ) {
+			const allowedChildren = getAllowedChildren( compiledDefinitions, allowContentOfItemName );
+
+			allowedChildren.forEach( allowedItem => {
+				allowedItem.allowIn.push( itemName );
+			} );
 		}
+	}
+
+	delete compiledDefinitions[ itemName ].allowContentOf;
+}
+
+function compileAllowWhere( compiledDefinitions, itemName ) {
+	for ( const allowWhereItemName of compiledDefinitions[ itemName ].allowWhere ) {
+		const inheritFrom = compiledDefinitions[ allowWhereItemName ];
+
+		// The allowWhere property may point to an unregistered element.
+		if ( inheritFrom ) {
+			const allowedIn = inheritFrom.allowIn;
 
-		return paths;
+			compiledDefinitions[ itemName ].allowIn.push( ...allowedIn );
+		}
 	}
 
-	/**
-	 * Checks whether given set of attributes fulfills required attributes of this item.
-	 *
-	 * @protected
-	 * @see module:engine/model/schema~SchemaItem#requireAttributes
-	 * @param {Array.<String>} attributesToCheck Attributes to check.
-	 * @returns {Boolean} `true` if given set or attributes fulfills required attributes, `false` otherwise.
-	 */
-	_checkRequiredAttributes( attributesToCheck ) {
-		let found = true;
+	delete compiledDefinitions[ itemName ].allowWhere;
+}
+
+function compileAllowAttributesOf( compiledDefinitions, itemName ) {
+	for ( const allowAttributeOfItem of compiledDefinitions[ itemName ].allowAttributesOf ) {
+		const inheritFrom = compiledDefinitions[ allowAttributeOfItem ];
+
+		if ( inheritFrom ) {
+			const inheritAttributes = inheritFrom.allowAttributes;
+
+			compiledDefinitions[ itemName ].allowAttributes.push( ...inheritAttributes );
+		}
+	}
+
+	delete compiledDefinitions[ itemName ].allowAttributesOf;
+}
+
+function compileInheritPropertiesFrom( compiledDefinitions, itemName ) {
+	const item = compiledDefinitions[ itemName ];
 
-		for ( const attributeSet of this._requiredAttributes ) {
-			found = true;
+	for ( const inheritPropertiesOfItem of item.inheritTypesFrom ) {
+		const inheritFrom = compiledDefinitions[ inheritPropertiesOfItem ];
 
-			for ( const attribute of attributeSet ) {
-				if ( attributesToCheck.indexOf( attribute ) == -1 ) {
-					found = false;
-					break;
+		if ( inheritFrom ) {
+			const typeNames = Object.keys( inheritFrom ).filter( name => name.startsWith( 'is' ) );
+
+			for ( const name of typeNames ) {
+				if ( !( name in item ) ) {
+					item[ name ] = inheritFrom[ name ];
 				}
 			}
+		}
+	}
 
-			if ( found ) {
-				break;
-			}
+	delete item.inheritTypesFrom;
+}
+
+// Remove items which weren't registered (because it may break some checks or we'd need to complicate them).
+// Make sure allowIn doesn't contain repeated values.
+function cleanUpAllowIn( compiledDefinitions, itemName ) {
+	const itemRule = compiledDefinitions[ itemName ];
+	const existingItems = itemRule.allowIn.filter( itemToCheck => compiledDefinitions[ itemToCheck ] );
+
+	itemRule.allowIn = Array.from( new Set( existingItems ) );
+}
+
+function cleanUpAllowAttributes( compiledDefinitions, itemName ) {
+	const itemRule = compiledDefinitions[ itemName ];
+
+	itemRule.allowAttributes = Array.from( new Set( itemRule.allowAttributes ) );
+}
+
+function copyTypes( sourceItemRules, itemRule ) {
+	for ( const sourceItemRule of sourceItemRules ) {
+		const typeNames = Object.keys( sourceItemRule ).filter( name => name.startsWith( 'is' ) );
+
+		for ( const name of typeNames ) {
+			itemRule[ name ] = sourceItemRule[ name ];
 		}
+	}
+}
 
-		return found;
+function copyProperty( sourceItemRules, itemRule, propertyName ) {
+	for ( const sourceItemRule of sourceItemRules ) {
+		if ( typeof sourceItemRule[ propertyName ] == 'string' ) {
+			itemRule[ propertyName ].push( sourceItemRule[ propertyName ] );
+		} else if ( Array.isArray( sourceItemRule[ propertyName ] ) ) {
+			itemRule[ propertyName ].push( ...sourceItemRule[ propertyName ] );
+		}
 	}
+}
 
-	/**
-	 * Checks whether this item has any registered path of given type that matches the provided path.
-	 *
-	 * @protected
-	 * @param {String} type Paths' type. Possible values are `allow` or `disallow`.
-	 * @param {Array.<String>} pathToCheck Path to check.
-	 * @param {String} [attribute] If set, only paths registered for given attribute will be checked.
-	 * @returns {Boolean} `true` if item has any registered matching path, `false` otherwise.
-	 */
-	_hasMatchingPath( type, pathToCheck, attribute ) {
-		const registeredPaths = this._getPaths( type, attribute );
+function makeInheritAllWork( sourceItemRules, itemRule ) {
+	for ( const sourceItemRule of sourceItemRules ) {
+		const inheritFrom = sourceItemRule.inheritAllFrom;
 
-		for ( const registeredPathPath of registeredPaths ) {
-			if ( matchPaths( this._schema, pathToCheck, registeredPathPath ) ) {
-				return true;
-			}
+		if ( inheritFrom ) {
+			itemRule.allowContentOf.push( inheritFrom );
+			itemRule.allowWhere.push( inheritFrom );
+			itemRule.allowAttributesOf.push( inheritFrom );
+			itemRule.inheritTypesFrom.push( inheritFrom );
 		}
-
-		return false;
 	}
 }
 
-/**
- * Object with query used by {@link module:engine/model/schema~Schema} to query schema or add allow/disallow rules to schema.
- *
- * @typedef {Object} module:engine/model/schema~SchemaQuery
- * @property {String} name Entity name.
- * @property {module:engine/model/schema~SchemaPath} inside Path inside which the entity is placed.
- * @property {Array.<String>|String} [attributes] If set, the query applies only to entities that has attribute(s) with given key.
- */
+function getAllowedChildren( compiledDefinitions, itemName ) {
+	const itemRule = compiledDefinitions[ itemName ];
 
-/**
- * Path to an entity, begins from the top-most ancestor. Can be passed in multiple formats. Internally, normalized to
- * an array of strings. If string is passed, entities from the path should be divided by ` ` (space character). If
- * an array is passed, unrecognized items are skipped. If position is passed, it is assumed that the entity is at given position.
- *
- * @typedef {String|Array.<String|module:engine/model/element~Element>|module:engine/model/position~Position}
- * module:engine/model/schema~SchemaPath
- */
+	return getValues( compiledDefinitions ).filter( def => def.allowIn.includes( itemRule.name ) );
+}
 
-// Checks whether the given pathToCheck and registeredPath right ends match.
-//
-// pathToCheck: C, D
-// registeredPath: A, B, C, D
-// result: OK
-//
-// pathToCheck: A, B, C
-// registeredPath: A, B, C, D
-// result: NOK
-//
-// Note – when matching paths, element extension chains (inheritance) are taken into consideration.
+function getValues( obj ) {
+	return Object.keys( obj ).map( key => obj[ key ] );
+}
+
+function mapContextItem( ctxItem ) {
+	if ( typeof ctxItem == 'string' ) {
+		return {
+			name: ctxItem,
+
+			* getAttributeKeys() {},
+
+			getAttribute() {}
+		};
+	} else {
+		return {
+			// '$text' means text nodes and text proxies.
+			name: ctxItem.is( 'element' ) ? ctxItem.name : '$text',
+
+			* getAttributeKeys() {
+				yield* ctxItem.getAttributeKeys();
+			},
+
+			getAttribute( key ) {
+				return ctxItem.getAttribute( key );
+			}
+		};
+	}
+}
+
+// Generator function returning values from provided walkers, switching between them at each iteration. If only one walker
+// is provided it will return data only from that walker.
 //
-// @param {Schema} schema
-// @param {Array.<String>} pathToCheck
-// @param {Array.<String>} registeredPath
-function matchPaths( schema, pathToCheck, registeredPath ) {
-	// Start checking from the right end of both tables.
-	let registeredPathIndex = registeredPath.length - 1;
-	let pathToCheckIndex = pathToCheck.length - 1;
-
-	// And finish once reaching an end of the shorter table.
-	while ( registeredPathIndex >= 0 && pathToCheckIndex >= 0 ) {
-		const checkName = pathToCheck[ pathToCheckIndex ];
-
-		// Fail when checking a path which contains element which aren't even registered to the schema.
-		if ( !schema.hasItem( checkName ) ) {
-			return false;
+// @param {module:engine/module/treewalker~TreeWalker} [backward] Walker iterating in backward direction.
+// @param {module:engine/module/treewalker~TreeWalker} [forward] Walker iterating in forward direction.
+// @returns {Iterable.<Object>} Object returned at each iteration contains `value` and `walker` (informing which walker returned
+// given value) fields.
+function* combineWalkers( backward, forward ) {
+	let done = false;
+
+	while ( !done ) {
+		done = true;
+
+		if ( backward ) {
+			const step = backward.next();
+
+			if ( !step.done ) {
+				done = false;
+				yield {
+					walker: backward,
+					value: step.value
+				};
+			}
 		}
 
-		const extChain = schema._extensionChains.get( checkName );
+		if ( forward ) {
+			const step = forward.next();
 
-		if ( extChain.includes( registeredPath[ registeredPathIndex ] ) ) {
-			registeredPathIndex--;
-			pathToCheckIndex--;
-		} else {
-			return false;
+			if ( !step.done ) {
+				done = false;
+				yield {
+					walker: forward,
+					value: step.value
+				};
+			}
 		}
 	}
-
-	return true;
 }
-
-/**
- * Item with specified name does not exist in schema.
- *
- * @error model-schema-no-item
- */

+ 189 - 218
packages/ckeditor5-engine/src/model/selection.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -9,33 +9,83 @@
 
 import Position from './position';
 import Element from './element';
+import Node from './node';
 import Range from './range';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
-import mapsEqual from '@ckeditor/ckeditor5-utils/src/mapsequal';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 
 /**
  * `Selection` is a group of {@link module:engine/model/range~Range ranges} which has a direction specified by
  * {@link module:engine/model/selection~Selection#anchor anchor} and {@link module:engine/model/selection~Selection#focus focus}.
  * Additionally, `Selection` may have it's own attributes.
+ *
+ * @mixes {module:utils/emittermixin~EmitterMixin}
  */
 export default class Selection {
 	/**
-	 * Creates new selection instance.
+	 * Creates new selection instance on the given
+	 * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges}
+	 * or creates an empty selection if no arguments passed.
+	 *
+	 * 		// Creates empty selection without ranges.
+	 *		const selection = new Selection();
+	 *
+	 *		// Creates selection at the given range.
+	 *		const range = new Range( start, end );
+	 *		const selection = new Selection( range );
+	 *
+	 *		// Creates selection at the given ranges
+	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		const selection = new Selection( ranges );
+	 *
+	 *		// Creates selection from the other selection.
+	 *		// Note: It doesn't copies selection attributes.
+	 *		const otherSelection = new Selection();
+	 *		const selection = new Selection( otherSelection );
+	 *
+	 * 		// Creates selection from the given document selection.
+	 *		// Note: It doesn't copies selection attributes.
+	 *		const documentSelection = new DocumentSelection( doc );
+	 *		const selection = new Selection( documentSelection );
+	 *
+	 * 		// Creates selection at the given position.
+	 *		const position = new Position( root, path );
+	 *		const selection = new Selection( position );
+	 *
+	 * 		// Creates selection at the start position of the given element.
+	 *		const paragraph = writer.createElement( 'paragraph' );
+	 *		const selection = new Selection( paragraph, offset );
+	 *
+	 * 		// Creates a range inside an {@link module:engine/model/element~Element element} which starts before the
+	 * 		// first child of that element and ends after the last child of that element.
+	 *		const selection = new Selection( paragraph, 'in' );
+	 *
+	 * 		// Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends
+	 * 		// just after the item.
+	 *		const selection = new Selection( paragraph, 'on' );
 	 *
-	 * @param {Iterable.<module:engine/view/range~Range>} [ranges] An optional iterable object of ranges to set.
-	 * @param {Boolean} [isLastBackward] An optional flag describing if last added range was selected forward - from start to end
-	 * (`false`) or backward - from end to start (`true`). Defaults to `false`.
+	 * `Selection`'s constructor allow passing additional options (`backward`) as the last argument.
+	 *
+	 * 		// Creates backward selection.
+	 *		const selection = new Selection( range, { backward: true } );
+	 *
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+	 * module:engine/model/position~Position|module:engine/model/element~Element|
+	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
+	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
+	 * @param {Object} [options]
+	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
 	 */
-	constructor( ranges, isLastBackward ) {
+	constructor( selectable, placeOrOffset, options ) {
 		/**
 		 * Specifies whether the last added range was added as a backward or forward range.
 		 *
 		 * @private
-		 * @member {Boolean}
+		 * @type {Boolean}
 		 */
 		this._lastRangeBackward = false;
 
@@ -43,7 +93,7 @@ export default class Selection {
 		 * Stores selection ranges.
 		 *
 		 * @protected
-		 * @member {Array.<module:engine/model/range~Range>}
+		 * @type {Array.<module:engine/model/range~Range>}
 		 */
 		this._ranges = [];
 
@@ -51,12 +101,12 @@ export default class Selection {
 		 * List of attributes set on current selection.
 		 *
 		 * @protected
-		 * @member {Map} module:engine/model/selection~Selection#_attrs
+		 * @type {Map.<String,*>}
 		 */
 		this._attrs = new Map();
 
-		if ( ranges ) {
-			this.setRanges( ranges, isLastBackward );
+		if ( selectable ) {
+			this.setTo( selectable, placeOrOffset, options );
 		}
 	}
 
@@ -121,6 +171,7 @@ export default class Selection {
 	/**
 	 * Returns number of ranges in selection.
 	 *
+	 * @readonly
 	 * @type {Number}
 	 */
 	get rangeCount() {
@@ -131,6 +182,7 @@ export default class Selection {
 	 * Specifies whether the {@link #focus}
 	 * precedes {@link #anchor}.
 	 *
+	 * @readonly
 	 * @type {Boolean}
 	 */
 	get isBackward() {
@@ -174,9 +226,9 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns an iterator that iterates over copies of selection ranges.
+	 * Returns an iterable that iterates over copies of selection ranges.
 	 *
-	 * @returns {Iterator.<module:engine/model/range~Range>}
+	 * @returns {Iterable.<module:engine/model/range~Range>}
 	 */
 	* getRanges() {
 		for ( const range of this._ranges ) {
@@ -259,52 +311,120 @@ export default class Selection {
 	}
 
 	/**
-	 * Adds a range to this selection. Added range is copied. This means that passed range is not saved in `Selection`
-	 * instance and operating on it will not change `Selection` state.
+	 * Sets this selection's ranges and direction to the specified location based on the given
+	 * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
 	 *
-	 * Accepts a flag describing in which way the selection is made - passed range might be selected from
-	 * {@link module:engine/model/range~Range#start start} to {@link module:engine/model/range~Range#end end}
-	 * or from {@link module:engine/model/range~Range#end end}
-	 * to {@link module:engine/model/range~Range#start start}.
-	 * The flag is used to set {@link #anchor} and
-	 * {@link #focus} properties.
+	 * 		// Removes all selection's ranges.
+	 *		selection.setTo( null );
 	 *
-	 * @fires change:range
-	 * @param {module:engine/model/range~Range} range Range to add.
-	 * @param {Boolean} [isBackward=false] Flag describing if added range was selected forward - from start to end (`false`)
-	 * or backward - from end to start (`true`).
-	 */
-	addRange( range, isBackward = false ) {
-		this._pushRange( range );
-		this._lastRangeBackward = !!isBackward;
-
-		this.fire( 'change:range', { directChange: true } );
-	}
-
-	/**
-	 * Removes all ranges that were added to the selection.
+	 *		// Sets selection to the given range.
+	 *		const range = new Range( start, end );
+	 *		selection.setTo( range );
 	 *
-	 * @fires change:range
+	 *		// Sets selection to given ranges.
+	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		selection.setTo( ranges );
+	 *
+	 *		// Sets selection to other selection.
+	 *		// Note: It doesn't copies selection attributes.
+	 *		const otherSelection = new Selection();
+	 *		selection.setTo( otherSelection );
+	 *
+	 * 		// Sets selection to the given document selection.
+	 *		// Note: It doesn't copies selection attributes.
+	 *		const documentSelection = new DocumentSelection( doc );
+	 *		selection.setTo( documentSelection );
+	 *
+	 * 		// Sets collapsed selection at the given position.
+	 *		const position = new Position( root, path );
+	 *		selection.setTo( position );
+	 *
+	 * 		// Sets collapsed selection at the position of the given node and an offset.
+	 *		selection.setTo( paragraph, offset );
+	 *
+	 * Creates a range inside an {@link module:engine/model/element~Element element} which starts before the first child of
+ 	 * that element and ends after the last child of that element.
+	 *
+	 *		selection.setTo( paragraph, 'in' );
+	 *
+	 * Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends just after the item.
+	 *
+	 *		selection.setTo( paragraph, 'on' );
+	 *
+	 * `Selection#setTo()`' method allow passing additional options (`backward`) as the last argument.
+	 *
+	 * 		// Sets backward selection.
+	 *		const selection = new Selection( range, { backward: true } );
+	 *
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+	 * module:engine/model/position~Position|module:engine/model/node~Node|
+	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
+	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
+	 * @param {Object} [options]
+	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
 	 */
-	removeAllRanges() {
-		if ( this._ranges.length > 0 ) {
-			this._removeAllRanges();
-			this.fire( 'change:range', { directChange: true } );
+	setTo( selectable, placeOrOffset, options ) {
+		if ( selectable === null ) {
+			this._setRanges( [] );
+		} else if ( selectable instanceof Selection ) {
+			this._setRanges( selectable.getRanges(), selectable.isBackward );
+		} else if ( selectable && typeof selectable.getRanges == 'function' ) {
+			// We assume that the selectable is a DocumentSelection.
+			// It can't be imported here, because it would lead to circular imports.
+			this._setRanges( selectable.getRanges(), selectable.isBackward );
+		} else if ( selectable instanceof Range ) {
+			this._setRanges( [ selectable ], !!placeOrOffset && !!placeOrOffset.backward );
+		} else if ( selectable instanceof Position ) {
+			this._setRanges( [ new Range( selectable ) ] );
+		} else if ( selectable instanceof Node ) {
+			const backward = !!options && !!options.backward;
+			let range;
+
+			if ( placeOrOffset == 'in' ) {
+				range = Range.createIn( selectable );
+			} else if ( placeOrOffset == 'on' ) {
+				range = Range.createOn( selectable );
+			} else if ( placeOrOffset !== undefined ) {
+				range = Range.createCollapsedAt( selectable, placeOrOffset );
+			} else {
+				/**
+				 * selection.setTo requires the second parameter when the first parameter is a node.
+				 *
+				 * @error model-selection-setTo-required-second-parameter
+				 */
+				throw new CKEditorError(
+					'model-selection-setTo-required-second-parameter: ' +
+					'selection.setTo requires the second parameter when the first parameter is a node.' );
+			}
+
+			this._setRanges( [ range ], backward );
+		} else if ( isIterable( selectable ) ) {
+			// We assume that the selectable is an iterable of ranges.
+			this._setRanges( selectable, placeOrOffset && !!placeOrOffset.backward );
+		} else {
+			/**
+			 * Cannot set selection to given place.
+			 *
+			 * @error model-selection-setTo-not-selectable
+			 */
+			throw new CKEditorError( 'model-selection-setTo-not-selectable: Cannot set selection to given place.' );
 		}
 	}
 
 	/**
 	 * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
 	 * is treated like the last added range and is used to set {@link module:engine/model/selection~Selection#anchor} and
-	 * {@link module:engine/model/selection~Selection#focus}. Accepts a flag describing in which direction the selection is made
-	 * (see {@link module:engine/model/selection~Selection#addRange}).
+	 * {@link module:engine/model/selection~Selection#focus}. Accepts a flag describing in which direction the selection is made.
 	 *
+	 * @protected
 	 * @fires change:range
 	 * @param {Iterable.<module:engine/model/range~Range>} newRanges Ranges to set.
 	 * @param {Boolean} [isLastBackward=false] Flag describing if last added range was selected forward - from start to end (`false`)
 	 * or backward - from end to start (`true`).
 	 */
-	setRanges( newRanges, isLastBackward = false ) {
+	_setRanges( newRanges, isLastBackward = false ) {
 		newRanges = Array.from( newRanges );
 
 		// Check whether there is any range in new ranges set that is different than all already added ranges.
@@ -334,93 +454,6 @@ export default class Selection {
 		this.fire( 'change:range', { directChange: true } );
 	}
 
-	/**
-	 * Sets this selection's ranges and direction to the specified location based on the given
-	 * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
-	 * {@link module:engine/model/range~Range range} or an iterable of {@link module:engine/model/range~Range ranges}.
-	 *
-	 * @param {module:engine/model/selection~Selection|module:engine/model/position~Position|
-	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range} selectable
-	 */
-	setTo( selectable ) {
-		if ( selectable instanceof Selection ) {
-			this.setRanges( selectable.getRanges(), selectable.isBackward );
-		} else if ( selectable instanceof Range ) {
-			this.setRanges( [ selectable ] );
-		} else if ( isIterable( selectable ) ) {
-			// We assume that the selectable is an iterable of ranges.
-			this.setRanges( selectable );
-		} else {
-			// We assume that the selectable is a position.
-			this.setRanges( [ new Range( selectable ) ] );
-		}
-	}
-
-	/**
-	 * Sets this selection in the provided element.
-	 *
-	 * @param {module:engine/model/element~Element} element
-	 */
-	setIn( element ) {
-		this.setRanges( [ Range.createIn( element ) ] );
-	}
-
-	/**
-	 * Sets this selection on the provided item.
-	 *
-	 * @param {module:engine/model/item~Item} item
-	 */
-	setOn( item ) {
-		this.setRanges( [ Range.createOn( item ) ] );
-	}
-
-	/**
-	 * Sets collapsed selection at the specified location.
-	 *
-	 * The location can be specified in the same form as {@link module:engine/model/position~Position.createAt} parameters.
-	 *
-	 * @fires change:range
-	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
-	 * first parameter is a {@link module:engine/model/item~Item model item}.
-	 */
-	setCollapsedAt( itemOrPosition, offset ) {
-		const pos = Position.createAt( itemOrPosition, offset );
-		const range = new Range( pos, pos );
-
-		this.setRanges( [ range ] );
-	}
-
-	/**
-	 * Collapses selection to the selection's {@link module:engine/model/selection~Selection#getFirstPosition first position}.
-	 * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
-	 * inside selection.
-	 *
-	 * @fires change
-	 */
-	collapseToStart() {
-		const startPosition = this.getFirstPosition();
-
-		if ( startPosition !== null ) {
-			this.setRanges( [ new Range( startPosition, startPosition ) ] );
-		}
-	}
-
-	/**
-	 * Collapses selection to the selection's {@link module:engine/model/selection~Selection#getLastPosition last position}.
-	 * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
-	 * inside selection.
-	 *
-	 * @fires change
-	 */
-	collapseToEnd() {
-		const endPosition = this.getLastPosition();
-
-		if ( endPosition !== null ) {
-			this.setRanges( [ new Range( endPosition, endPosition ) ] );
-		}
-	}
-
 	/**
 	 * Moves {@link module:engine/model/selection~Selection#focus} to the specified location.
 	 *
@@ -431,15 +464,15 @@ export default class Selection {
 	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/model/item~Item model item}.
 	 */
-	moveFocusTo( itemOrPosition, offset ) {
+	setFocus( itemOrPosition, offset ) {
 		if ( this.anchor === null ) {
 			/**
 			 * Cannot set selection focus if there are no ranges in selection.
 			 *
-			 * @error model-selection-moveFocusTo-no-ranges
+			 * @error model-selection-setFocus-no-ranges
 			 */
 			throw new CKEditorError(
-				'model-selection-moveFocusTo-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.'
 			);
 		}
 
@@ -456,10 +489,14 @@ export default class Selection {
 		}
 
 		if ( newFocus.compareWith( anchor ) == 'before' ) {
-			this.addRange( new Range( newFocus, anchor ), true );
+			this._pushRange( new Range( newFocus, anchor ) );
+			this._lastRangeBackward = true;
 		} else {
-			this.addRange( new Range( anchor, newFocus ) );
+			this._pushRange( new Range( anchor, newFocus ) );
+			this._lastRangeBackward = false;
 		}
+
+		this.fire( 'change:range', { directChange: true } );
 	}
 
 	/**
@@ -473,7 +510,7 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns iterator that iterates over this selection's attributes.
+	 * Returns iterable that iterates over this selection's attributes.
 	 *
 	 * Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
 	 * This format is accepted by native `Map` object and also can be passed in `Node` constructor.
@@ -485,9 +522,9 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns iterator that iterates over this selection's attribute keys.
+	 * Returns iterable that iterates over this selection's attribute keys.
 	 *
-	 * @returns {Iterator.<String>}
+	 * @returns {Iterable.<String>}
 	 */
 	getAttributeKeys() {
 		return this._attrs.keys();
@@ -503,23 +540,6 @@ export default class Selection {
 		return this._attrs.has( key );
 	}
 
-	/**
-	 * Removes all attributes from the selection.
-	 *
-	 * If there were any attributes in selection, fires the {@link #event:change} event with
-	 * removed attributes' keys.
-	 *
-	 * @fires change:attribute
-	 */
-	clearAttributes() {
-		if ( this._attrs.size > 0 ) {
-			const attributeKeys = Array.from( this._attrs.keys() );
-			this._attrs.clear();
-
-			this.fire( 'change:attribute', { attributeKeys, directChange: true } );
-		}
-	}
-
 	/**
 	 * Removes an attribute with given key from the selection.
 	 *
@@ -555,35 +575,6 @@ export default class Selection {
 		}
 	}
 
-	/**
-	 * Removes all attributes from the selection and sets given attributes.
-	 *
-	 * If given set of attributes is different than set of attributes already added to selection, fires
-	 * {@link #event:change change event} with keys of attributes that changed.
-	 *
-	 * @fires event:change:attribute
-	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
-	 */
-	setAttributesTo( attrs ) {
-		attrs = toMap( attrs );
-
-		if ( !mapsEqual( attrs, this._attrs ) ) {
-			// Create a set from keys of old and new attributes.
-			const changed = new Set( Array.from( attrs.keys() ).concat( Array.from( this._attrs.keys() ) ) );
-
-			for ( const [ key, value ] of attrs ) {
-				// If the attribute remains unchanged, remove it from changed set.
-				if ( this._attrs.get( key ) === value ) {
-					changed.delete( key );
-				}
-			}
-
-			this._attrs = attrs;
-
-			this.fire( 'change:attribute', { attributeKeys: Array.from( changed ), directChange: true } );
-		}
-	}
-
 	/**
 	 * Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
 	 * one range in the selection, and that range contains exactly one element.
@@ -629,7 +620,7 @@ export default class Selection {
 	 *		<paragraph>b</paragraph>
 	 *		<paragraph>]c</paragraph> // this block will not be returned
 	 *
-	 * @returns {Iterator.<module:engine/model/element~Element>}
+	 * @returns {Iterable.<module:engine/model/element~Element>}
 	 */
 	* getSelectedBlocks() {
 		const visited = new WeakSet();
@@ -675,20 +666,6 @@ export default class Selection {
 			limitEndPosition.isTouching( this.getLastPosition() );
 	}
 
-	/**
-	 * Creates and returns an instance of `Selection` that is a clone of given selection, meaning that it has same
-	 * ranges and same direction as this selection.
-	 *
-	 * @params {module:engine/model/selection~Selection} otherSelection Selection to be cloned.
-	 * @returns {module:engine/model/selection~Selection} `Selection` instance that is a clone of given selection.
-	 */
-	static createFromSelection( otherSelection ) {
-		const selection = new this();
-		selection.setTo( otherSelection );
-
-		return selection;
-	}
-
 	/**
 	 * Adds given range to internal {@link #_ranges ranges array}. Throws an error
 	 * if given range is intersecting with any range that is already stored in this selection.
@@ -697,10 +674,6 @@ export default class Selection {
 	 * @param {module:engine/model/range~Range} range Range to add.
 	 */
 	_pushRange( range ) {
-		if ( !( range instanceof Range ) ) {
-			throw new CKEditorError( 'model-selection-added-not-range: Trying to add an object that is not an instance of Range.' );
-		}
-
 		this._checkRange( range );
 		this._ranges.push( Range.createFromRange( range ) );
 	}
@@ -729,20 +702,11 @@ export default class Selection {
 		}
 	}
 
-	/**
-	 * Removes most recently added range from the selection.
-	 *
-	 * @protected
-	 */
-	_popRange() {
-		this._ranges.pop();
-	}
-
 	/**
 	 * Deletes ranges from internal range array. Uses {@link #_popRange _popRange} to
 	 * ensure proper ranges removal.
 	 *
-	 * @private
+	 * @protected
 	 */
 	_removeAllRanges() {
 		while ( this._ranges.length > 0 ) {
@@ -750,6 +714,15 @@ export default class Selection {
 		}
 	}
 
+	/**
+	 * Removes most recently added range from the selection.
+	 *
+	 * @protected
+	 */
+	_popRange() {
+		this._ranges.pop();
+	}
+
 	/**
 	 * @event change
 	 */
@@ -785,9 +758,7 @@ function isUnvisitedBlockContainer( element, visited ) {
 
 	visited.add( element );
 
-	// TODO https://github.com/ckeditor/ckeditor5-engine/issues/532#issuecomment-278900072.
-	// This should not be a `$block` check.
-	return element.document.schema.itemExtends( element.name, '$block' ) && element.parent;
+	return element.document.model.schema.isBlock( element ) && element.parent;
 }
 
 // Finds the lowest element in position's ancestors which is a block.

+ 28 - 9
packages/ckeditor5-engine/src/model/text.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -15,15 +15,21 @@ import Node from './node';
  * **Important:** see {@link module:engine/model/node~Node} to read about restrictions using `Text` and `Node` API.
  *
  * **Note:** keep in mind that `Text` instances might indirectly got removed from model tree when model is changed.
- * This happens when {@link module:engine/model/writer~writer model writer} is used to change model and the text node is merged with
+ * This happens when {@link module:engine/model/writer~Writer model writer} is used to change model and the text node is merged with
  * another text node. Then, both text nodes are removed and a new text node is inserted into the model. Because of
  * this behavior, keeping references to `Text` is not recommended. Instead, consider creating
  * {@link module:engine/model/liveposition~LivePosition live position} placed before the text node.
+ *
+ * @extends {module:engine/model/node~Node}
  */
 export default class Text extends Node {
 	/**
 	 * Creates a text node.
 	 *
+	 * **Note:** Constructor of this class shouldn't be used directly in the code.
+	 * Use the {@link module:engine/model/writer~Writer#createText} method instead.
+	 *
+	 * @protected
 	 * @param {String} data Node's text.
 	 * @param {Object} [attrs] Node's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values.
 	 */
@@ -33,9 +39,10 @@ export default class Text extends Node {
 		/**
 		 * Text data contained in this text node.
 		 *
+		 * @protected
 		 * @type {String}
 		 */
-		this.data = data || '';
+		this._data = data || '';
 	}
 
 	/**
@@ -46,17 +53,19 @@ export default class Text extends Node {
 	}
 
 	/**
-	 * @inheritDoc
+	 * Returns a text data contained in the node.
+	 *
+	 * @returns {String}
 	 */
-	is( type ) {
-		return type == 'text';
+	get data() {
+		return this._data;
 	}
 
 	/**
-	 * Creates a copy of this text node and returns it. Created text node has same text data and attributes as original text node.
+	 * @inheritDoc
 	 */
-	clone() {
-		return new Text( this.data, this.getAttributes() );
+	is( type ) {
+		return type == 'text';
 	}
 
 	/**
@@ -72,6 +81,16 @@ export default class Text extends Node {
 		return json;
 	}
 
+	/**
+	 * Creates a copy of this text node and returns it. Created text node has same text data and attributes as original text node.
+	 *
+	 * @protected
+	 * @returns {module:engine/model/text~Text} `Text` instance created using given plain object.
+	 */
+	_clone() {
+		return new Text( this.data, this.getAttributes() );
+	}
+
 	/**
 	 * Creates a `Text` instance from given plain object (i.e. parsed JSON string).
 	 *

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -28,7 +28,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  * parameter of methods.
  *
  * **Note:** `TextProxy` is a readonly interface. If you want to perform changes on model data represented by a `TextProxy`
- * use {@link module:engine/model/writer~writer model writer API}.
+ * use {@link module:engine/model/writer~Writer model writer API}.
  *
  * **Note:** `TextProxy` instances are created on the fly, basing on the current state of model. Because of this, it is
  * highly unrecommended to store references to `TextProxy` instances. `TextProxy` instances are not refreshed when
@@ -256,7 +256,7 @@ export default class TextProxy {
 	/**
 	 * Returns iterator that iterates over this node's attribute keys.
 	 *
-	 * @returns {Iterator.<String>}
+	 * @returns {Iterable.<String>}
 	 */
 	getAttributeKeys() {
 		return this.textNode.getAttributeKeys();

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -146,7 +146,9 @@ export default class TreeWalker {
 	}
 
 	/**
-	 * Iterator interface.
+	 * Iterable interface.
+	 *
+	 * @returns {Iterable.<module:engine/model/treewalker~TreeWalkerValue>}
 	 */
 	[ Symbol.iterator ]() {
 		return this;
@@ -181,10 +183,9 @@ export default class TreeWalker {
 	}
 
 	/**
-	 * Iterator interface method.
-	 * Detects walking direction and makes step forward or backward.
+	 * Gets the next tree walker's value.
 	 *
-	 * @returns {Object} Object implementing iterator interface, returning information about taken step.
+	 * @returns {module:engine/model/treewalker~TreeWalkerValue} Next tree walker's value.
 	 */
 	next() {
 		if ( this.direction == 'forward' ) {

+ 81 - 67
packages/ckeditor5-engine/src/controller/deletecontent.js → packages/ckeditor5-engine/src/model/utils/deletecontent.js

@@ -1,21 +1,24 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 /**
- * @module engine/controller/deletecontent
+ * @module engine/model/utils/deletecontent
  */
 
-import LivePosition from '../model/liveposition';
-import Position from '../model/position';
-import Range from '../model/range';
-import Element from '../model/element';
+import LivePosition from '../liveposition';
+import Position from '../position';
+import Range from '../range';
+import DocumentSelection from '../documentselection';
 
 /**
  * Deletes content of the selection and merge siblings. The resulting selection is always collapsed.
  *
- * @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted.
+ * @param {module:engine/model/model~Model} model The model in context of which the insertion
+ * should be performed.
+ * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
+ * Selection of which the content should be deleted.
  * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
  * @param {Object} [options]
  * @param {Boolean} [options.leaveUnmerged=false] Whether to merge elements after removing the content of the selection.
@@ -25,7 +28,7 @@ import Element from '../model/element';
  * * `<heading>x^y</heading>` with the option disabled (`leaveUnmerged == false`)
  * * `<heading>x^</heading><paragraph>y</paragraph>` with enabled (`leaveUnmerged == true`).
  *
- * Note: {@link module:engine/model/schema~Schema#objects object} and {@link module:engine/model/schema~Schema#limits limit}
+ * Note: {@link module:engine/model/schema~Schema#isObject object} and {@link module:engine/model/schema~Schema#isLimit limit}
  * elements will not be merged.
  *
  * @param {Boolean} [options.doNotResetEntireContent=false] Whether to skip replacing the entire content with a
@@ -36,63 +39,70 @@ import Element from '../model/element';
  * * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
  * * `<heading>^</heading>` with enabled (`doNotResetEntireContent == true`).
  */
-export default function deleteContent( selection, batch, options = {} ) {
+export default function deleteContent( model, selection, options = {} ) {
 	if ( selection.isCollapsed ) {
 		return;
 	}
 
-	const schema = batch.document.schema;
+	const schema = model.schema;
 
-	// 1. Replace the entire content with paragraph.
-	// See: https://github.com/ckeditor/ckeditor5-engine/issues/1012#issuecomment-315017594.
-	if ( !options.doNotResetEntireContent && shouldEntireContentBeReplacedWithParagraph( schema, selection ) ) {
-		replaceEntireContentWithParagraph( batch, selection );
+	model.change( writer => {
+		// 1. Replace the entire content with paragraph.
+		// See: https://github.com/ckeditor/ckeditor5-engine/issues/1012#issuecomment-315017594.
+		if ( !options.doNotResetEntireContent && shouldEntireContentBeReplacedWithParagraph( schema, selection ) ) {
+			replaceEntireContentWithParagraph( writer, selection, schema );
 
-		return;
-	}
+			return;
+		}
 
-	const selRange = selection.getFirstRange();
-	const startPos = selRange.start;
-	const endPos = LivePosition.createFromPosition( selRange.end );
+		const selRange = selection.getFirstRange();
+		const startPos = selRange.start;
+		const endPos = LivePosition.createFromPosition( selRange.end );
 
-	// 2. Remove the content if there is any.
-	if ( !selRange.start.isTouching( selRange.end ) ) {
-		batch.remove( selRange );
-	}
+		// 2. Remove the content if there is any.
+		if ( !selRange.start.isTouching( selRange.end ) ) {
+			writer.remove( selRange );
+		}
 
-	// 3. Merge elements in the right branch to the elements in the left branch.
-	// The only reasonable (in terms of data and selection correctness) case in which we need to do that is:
-	//
-	// <heading type=1>Fo[</heading><paragraph>]ar</paragraph> => <heading type=1>Fo^ar</heading>
-	//
-	// However, the algorithm supports also merging deeper structures (up to the depth of the shallower branch),
-	// as it's hard to imagine what should actually be the default behavior. Usually, specific features will
-	// want to override that behavior anyway.
-	if ( !options.leaveUnmerged ) {
-		mergeBranches( batch, startPos, endPos );
-
-		// We need to check and strip disallowed attributes in all nested nodes because after merge
-		// some attributes could end up in a path where are disallowed.
+		// 3. Merge elements in the right branch to the elements in the left branch.
+		// The only reasonable (in terms of data and selection correctness) case in which we need to do that is:
 		//
-		// e.g. bold is disallowed for <H1>
-		// <h1>Fo{o</h1><p>b}a<b>r</b><p> -> <h1>Fo{}a<b>r</b><h1> -> <h1>Fo{}ar<h1>.
-		schema.removeDisallowedAttributes( startPos.parent.getChildren(), startPos, batch );
-	}
+		// <heading type=1>Fo[</heading><paragraph>]ar</paragraph> => <heading type=1>Fo^ar</heading>
+		//
+		// However, the algorithm supports also merging deeper structures (up to the depth of the shallower branch),
+		// as it's hard to imagine what should actually be the default behavior. Usually, specific features will
+		// want to override that behavior anyway.
+		if ( !options.leaveUnmerged ) {
+			mergeBranches( writer, startPos, endPos );
+
+			// TMP this will be replaced with a postifxer.
+			// We need to check and strip disallowed attributes in all nested nodes because after merge
+			// some attributes could end up in a path where are disallowed.
+			//
+			// e.g. bold is disallowed for <H1>
+			// <h1>Fo{o</h1><p>b}a<b>r</b><p> -> <h1>Fo{}a<b>r</b><h1> -> <h1>Fo{}ar<h1>.
+			schema.removeDisallowedAttributes( startPos.parent.getChildren(), writer );
+		}
 
-	selection.setCollapsedAt( startPos );
+		if ( selection instanceof DocumentSelection ) {
+			writer.setSelection( startPos );
+		} else {
+			selection.setTo( startPos );
+		}
 
-	// 4. Autoparagraphing.
-	// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
-	if ( shouldAutoparagraph( schema, startPos ) ) {
-		insertParagraph( batch, startPos, selection );
-	}
+		// 4. Autoparagraphing.
+		// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
+		if ( shouldAutoparagraph( schema, startPos ) ) {
+			insertParagraph( writer, startPos, selection );
+		}
 
-	endPos.detach();
+		endPos.detach();
+	} );
 }
 
 // This function is a result of reaching the Ballmer's peak for just the right amount of time.
 // Even I had troubles documenting it after a while and after reading it again I couldn't believe that it really works.
-function mergeBranches( batch, startPos, endPos ) {
+function mergeBranches( writer, startPos, endPos ) {
 	const startParent = startPos.parent;
 	const endParent = endPos.parent;
 
@@ -112,7 +122,7 @@ function mergeBranches( batch, startPos, endPos ) {
 	// Check if operations we'll need to do won't need to cross object or limit boundaries.
 	// E.g., we can't merge endParent into startParent in this case:
 	// <limit><startParent>x[]</startParent></limit><endParent>{}</endParent>
-	if ( !checkCanBeMerged( startPos, endPos ) ) {
+	if ( !checkCanBeMerged( startPos, endPos, writer.model.schema ) ) {
 		return;
 	}
 
@@ -128,13 +138,13 @@ function mergeBranches( batch, startPos, endPos ) {
 		// <a><b>x[]</b></a><c><d>{}y</d></c>
 		// becomes:
 		// <a><b>x</b>[]<d>y</d></a><c>{}</c>
-		batch.move( endParent, startPos );
+		writer.insert( endParent, startPos );
 	}
 
 	// Merge two siblings:
 	// <a>x</a>[]<b>y</b> -> <a>xy</a> (the usual case)
 	// <a><b>x</b>[]<d>y</d></a><c></c> -> <a><b>xy</b>[]</a><c></c> (this is the "move parent" case shown above)
-	batch.merge( startPos );
+	writer.merge( startPos );
 
 	// Remove empty end ancestors:
 	// <a>fo[o</a><b><a><c>bar]</c></a></b>
@@ -146,16 +156,16 @@ function mergeBranches( batch, startPos, endPos ) {
 
 		endPos = Position.createBefore( parentToRemove );
 
-		batch.remove( parentToRemove );
+		writer.remove( parentToRemove );
 	}
 
 	// Continue merging next level.
-	mergeBranches( batch, startPos, endPos );
+	mergeBranches( writer, startPos, endPos );
 }
 
 function shouldAutoparagraph( schema, position ) {
-	const isTextAllowed = schema.check( { name: '$text', inside: position } );
-	const isParagraphAllowed = schema.check( { name: 'paragraph', inside: position } );
+	const isTextAllowed = schema.checkChild( position, '$text' );
+	const isParagraphAllowed = schema.checkChild( position, 'paragraph' );
 
 	return !isTextAllowed && isParagraphAllowed;
 }
@@ -166,12 +176,11 @@ function shouldAutoparagraph( schema, position ) {
 // E.g. in <bQ><p>x[]</p></bQ><widget><caption>{}</caption></widget>
 // we'll check <p>, <bQ>, <widget> and <caption>.
 // Usually, widget and caption are marked as objects/limits in the schema, so in this case merging will be blocked.
-function checkCanBeMerged( leftPos, rightPos ) {
-	const schema = leftPos.root.document.schema;
+function checkCanBeMerged( leftPos, rightPos, schema ) {
 	const rangeToCheck = new Range( leftPos, rightPos );
 
 	for ( const value of rangeToCheck.getWalker() ) {
-		if ( schema.objects.has( value.item.name ) || schema.limits.has( value.item.name ) ) {
+		if ( schema.isObject( value.item ) || schema.isLimit( value.item ) ) {
 			return false;
 		}
 	}
@@ -179,18 +188,23 @@ function checkCanBeMerged( leftPos, rightPos ) {
 	return true;
 }
 
-function insertParagraph( batch, position, selection ) {
-	const paragraph = new Element( 'paragraph' );
-	batch.insert( position, paragraph );
+function insertParagraph( writer, position, selection ) {
+	const paragraph = writer.createElement( 'paragraph' );
+
+	writer.insert( paragraph, position );
 
-	selection.setCollapsedAt( paragraph );
+	if ( selection instanceof DocumentSelection ) {
+		writer.setSelection( paragraph, 0 );
+	} else {
+		selection.setTo( paragraph, 0 );
+	}
 }
 
-function replaceEntireContentWithParagraph( batch, selection ) {
-	const limitElement = batch.document.schema.getLimitElement( selection );
+function replaceEntireContentWithParagraph( writer, selection ) {
+	const limitElement = writer.model.schema.getLimitElement( selection );
 
-	batch.remove( Range.createIn( limitElement ) );
-	insertParagraph( batch, Position.createAt( limitElement ), selection );
+	writer.remove( Range.createIn( limitElement ) );
+	insertParagraph( writer, Position.createAt( limitElement ), selection );
 }
 
 // We want to replace the entire content with a paragraph when:
@@ -210,5 +224,5 @@ function shouldEntireContentBeReplacedWithParagraph( schema, selection ) {
 		return false;
 	}
 
-	return schema.check( { name: 'paragraph', inside: limitElement.name } );
+	return schema.checkChild( limitElement, 'paragraph' );
 }

+ 148 - 0
packages/ckeditor5-engine/src/model/utils/getselectedcontent.js

@@ -0,0 +1,148 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/utils/getselectedcontent
+ */
+
+import Range from '../range';
+import Position from '../position';
+
+/**
+ * Gets a clone of the selected content.
+ *
+ * For example, for the following selection:
+ *
+ *		<p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p>
+ *
+ * It will return a document fragment with such a content:
+ *
+ *		<quote><h>st</h></quote><p>se</p>
+ *
+ * @param {module:engine/model/model~Model} model The model in context of which
+ * the selection modification should be performed.
+ * @param {module:engine/model/selection~Selection} selection The selection of which content will be returned.
+ * @returns {module:engine/model/documentfragment~DocumentFragment}
+ */
+export default function getSelectedContent( model, selection ) {
+	return model.change( writer => {
+		const frag = writer.createDocumentFragment();
+		const range = selection.getFirstRange();
+
+		if ( !range || range.isCollapsed ) {
+			return frag;
+		}
+
+		const root = range.start.root;
+		const commonPath = range.start.getCommonPath( range.end );
+		const commonParent = root.getNodeByPath( commonPath );
+
+		// ## 1st step
+		//
+		// First, we'll clone a fragment represented by a minimal flat range
+		// containing the original range to be cloned.
+		// E.g. let's consider such a range:
+		//
+		// <p>x</p><quote><p>y</p><h>fir[st</h></quote><p>se]cond</p><p>z</p>
+		//
+		// A minimal flat range containing this one is:
+		//
+		// <p>x</p>[<quote><p>y</p><h>first</h></quote><p>second</p>]<p>z</p>
+		//
+		// We can easily clone this structure, preserving e.g. the <quote> element.
+		let flatSubtreeRange;
+
+		if ( range.start.parent == range.end.parent ) {
+			// The original range is flat, so take it.
+			flatSubtreeRange = range;
+		} else {
+			flatSubtreeRange = Range.createFromParentsAndOffsets(
+				commonParent, range.start.path[ commonPath.length ],
+				commonParent, range.end.path[ commonPath.length ] + 1
+			);
+		}
+
+		const howMany = flatSubtreeRange.end.offset - flatSubtreeRange.start.offset;
+
+		// Clone the whole contents.
+		for ( const item of flatSubtreeRange.getItems( { shallow: true } ) ) {
+			if ( item.is( 'textProxy' ) ) {
+				writer.appendText( item.data, item.getAttributes(), frag );
+			} else {
+				writer.append( item._clone( true ), frag );
+			}
+		}
+
+		// ## 2nd step
+		//
+		// If the original range wasn't flat, then we need to remove the excess nodes from the both ends of the cloned fragment.
+		//
+		// For example, for the range shown in the 1st step comment, we need to remove these pieces:
+		//
+		// <quote>[<p>y</p>]<h>[fir]st</h></quote><p>se[cond]</p>
+		//
+		// So this will be the final copied content:
+		//
+		// <quote><h>st</h></quote><p>se</p>
+		//
+		// In order to do that, we remove content from these two ranges:
+		//
+		// [<quote><p>y</p><h>fir]st</h></quote><p>se[cond</p>]
+		if ( flatSubtreeRange != range ) {
+			// Find the position of the original range in the cloned fragment.
+			const newRange = range._getTransformedByMove( flatSubtreeRange.start, Position.createAt( frag, 0 ), howMany )[ 0 ];
+
+			const leftExcessRange = new Range( Position.createAt( frag ), newRange.start );
+			const rightExcessRange = new Range( newRange.end, Position.createAt( frag, 'end' ) );
+
+			removeRangeContent( rightExcessRange, writer );
+			removeRangeContent( leftExcessRange, writer );
+		}
+
+		return frag;
+	} );
+}
+
+// After https://github.com/ckeditor/ckeditor5-engine/issues/690 is fixed,
+// this function will, most likely, be able to rewritten using getMinimalFlatRanges().
+function removeRangeContent( range, writer ) {
+	const parentsToCheck = [];
+
+	Array.from( range.getItems( { direction: 'backward' } ) )
+		// We should better store ranges because text proxies will lose integrity
+		// with the text nodes when we'll start removing content.
+		.map( item => Range.createOn( item ) )
+		// Filter only these items which are fully contained in the passed range.
+		//
+		// E.g. for the following range: [<quote><p>y</p><h>fir]st</h>
+		// the walker will return the entire <h> element, when only the "fir" item inside it is fully contained.
+		.filter( itemRange => {
+			// We should be able to use Range.containsRange, but https://github.com/ckeditor/ckeditor5-engine/issues/691.
+			const contained =
+				( itemRange.start.isAfter( range.start ) || itemRange.start.isEqual( range.start ) ) &&
+				( itemRange.end.isBefore( range.end ) || itemRange.end.isEqual( range.end ) );
+
+			return contained;
+		} )
+		.forEach( itemRange => {
+			parentsToCheck.push( itemRange.start.parent );
+
+			writer.remove( itemRange );
+		} );
+
+	// Remove ancestors of the removed items if they turned to be empty now
+	// (their whole content was contained in the range).
+	parentsToCheck.forEach( parentToCheck => {
+		let parent = parentToCheck;
+
+		while ( parent.parent && parent.isEmpty ) {
+			const removeRange = Range.createOn( parent );
+
+			parent = parent.parent;
+
+			writer.remove( removeRange );
+		}
+	} );
+}

+ 130 - 132
packages/ckeditor5-engine/src/controller/insertcontent.js → packages/ckeditor5-engine/src/model/utils/insertcontent.js

@@ -1,76 +1,78 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 /**
- * @module engine/controller/insertcontent
+ * @module engine/model/utils/insertcontent
  */
 
-import Position from '../model/position';
-import LivePosition from '../model/liveposition';
-import Element from '../model/element';
-import Range from '../model/range';
+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';
 
 /**
  * Inserts content into the editor (specified selection) as one would expect the paste
  * functionality to work.
  *
- * **Note:** Use {@link module:engine/controller/datacontroller~DataController#insertContent} instead of this function.
+ * **Note:** Use {@link module:engine/model/model~Model#insertContent} instead of this function.
  * This function is only exposed to be reusable in algorithms
- * which change the {@link module:engine/controller/datacontroller~DataController#insertContent}
+ * which change the {@link module:engine/model/model~Model#insertContent}
  * method's behavior.
  *
- * @param {module:engine/controller/datacontroller~DataController} dataController The data controller in context of which the insertion
+ * @param {module:engine/model/model~Model} model The model in context of which the insertion
  * should be performed.
  * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
- * @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted.
- * @param {module:engine/model/batch~Batch} [batch] Batch to which deltas will be added. If not specified, then
- * changes will be added to a new batch.
+ * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
+ * Selection into which the content should be inserted.
  */
-export default function insertContent( dataController, content, selection, batch ) {
-	if ( !batch ) {
-		batch = dataController.model.batch();
-	}
-
-	if ( !selection.isCollapsed ) {
-		dataController.deleteContent( selection, batch );
-	}
-
-	const insertion = new Insertion( dataController, batch, selection.anchor );
+export default function insertContent( model, content, selection ) {
+	model.change( writer => {
+		if ( !selection.isCollapsed ) {
+			model.deleteContent( selection );
+		}
 
-	let nodesToInsert;
+		const insertion = new Insertion( model, writer, selection.anchor );
 
-	if ( content.is( 'documentFragment' ) ) {
-		nodesToInsert = content.getChildren();
-	} else {
-		nodesToInsert = [ content ];
-	}
+		let nodesToInsert;
 
-	insertion.handleNodes( nodesToInsert, {
-		// The set of children being inserted is the only set in this context
-		// so it's the first and last (it's a hack ;)).
-		isFirst: true,
-		isLast: true
-	} );
+		if ( content.is( 'documentFragment' ) ) {
+			nodesToInsert = content.getChildren();
+		} else {
+			nodesToInsert = [ content ];
+		}
 
-	const newRange = insertion.getSelectionRange();
+		insertion.handleNodes( nodesToInsert, {
+			// The set of children being inserted is the only set in this context
+			// so it's the first and last (it's a hack ;)).
+			isFirst: true,
+			isLast: true
+		} );
 
-	/* istanbul ignore else */
-	if ( newRange ) {
-		selection.setRanges( [ newRange ] );
-	} else {
-		// We are not testing else because it's a safe check for unpredictable edge cases:
-		// an insertion without proper range to select.
+		const newRange = insertion.getSelectionRange();
 
-		/**
-		 * 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.' );
-	}
+		/* istanbul ignore else */
+		if ( newRange ) {
+			if ( selection instanceof DocumentSelection ) {
+				writer.setSelection( newRange );
+			} else {
+				selection.setTo( newRange );
+			}
+		} 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.' );
+		}
+	} );
 }
 
 /**
@@ -79,20 +81,20 @@ export default function insertContent( dataController, content, selection, batch
  * @private
  */
 class Insertion {
-	constructor( dataController, batch, position ) {
+	constructor( model, writer, position ) {
 		/**
-		 * The data controller in context of which the insertion should be performed.
+		 * The model in context of which the insertion should be performed.
 		 *
-		 * @member {module:engine/controller/datacontroller~DataController} #dataController
+		 * @member {module:engine/model~Model} #model
 		 */
-		this.dataController = dataController;
+		this.model = model;
 
 		/**
 		 * Batch to which deltas will be added.
 		 *
-		 * @member {module:engine/controller/batch~Batch} #batch
+		 * @member {module:engine/controller/writer~Batch} #writer
 		 */
-		this.batch = batch;
+		this.writer = writer;
 
 		/**
 		 * The position at which (or near which) the next node will be inserted.
@@ -119,7 +121,9 @@ class Insertion {
 		 *
 		 * @member {module:engine/model/schema~Schema} #schema
 		 */
-		this.schema = dataController.model.schema;
+		this.schema = model.schema;
+
+		this._filterAttributesOf = [];
 	}
 
 	/**
@@ -140,6 +144,10 @@ class Insertion {
 				isLast: ( i === ( nodes.length - 1 ) ) && parentContext.isLast
 			} );
 		}
+
+		// TMP this will become a postfixer.
+		this.schema.removeDisallowedAttributes( this._filterAttributesOf, this.writer );
+		this._filterAttributesOf = [];
 	}
 
 	/**
@@ -153,12 +161,13 @@ class Insertion {
 			return Range.createOn( this.nodeToSelect );
 		}
 
-		return this.dataController.model.getNearestSelectionRange( this.position );
+		return this.model.schema.getNearestSelectionRange( this.position );
 	}
 
 	/**
 	 * Handles insertion of a single node.
 	 *
+	 * @private
 	 * @param {module:engine/model/node~Node} node
 	 * @param {Object} context
 	 * @param {Boolean} context.isFirst Whether the given node is the first one in the content to be inserted.
@@ -168,7 +177,7 @@ class Insertion {
 		// Let's handle object in a special way.
 		// * They should never be merged with other elements.
 		// * If they are not allowed in any of the selection ancestors, they could be either autoparagraphed or totally removed.
-		if ( this._checkIsObject( node ) ) {
+		if ( this.schema.isObject( node ) ) {
 			this._handleObject( node, context );
 
 			return;
@@ -203,6 +212,7 @@ class Insertion {
 	}
 
 	/**
+	 * @private
 	 * @param {module:engine/model/element~Element} node The object element.
 	 * @param {Object} context
 	 */
@@ -218,6 +228,7 @@ class Insertion {
 	}
 
 	/**
+	 * @private
 	 * @param {module:engine/model/node~Node} node The disallowed node which needs to be handled.
 	 * @param {Object} context
 	 */
@@ -226,24 +237,19 @@ class Insertion {
 		if ( node.is( 'element' ) ) {
 			this.handleNodes( node.getChildren(), context );
 		}
-		// If the node is a text and bare text is allowed in current position it means that the node
-		// contains disallowed attributes and we have to remove them.
-		else if ( this.schema.check( { name: '$text', inside: this.position } ) ) {
-			this.schema.removeDisallowedAttributes( [ node ], this.position );
-			this._handleNode( node, context );
-		}
-		// If text is not allowed, try autoparagraphing.
+		// If text is not allowed, try autoparagraphing it.
 		else {
 			this._tryAutoparagraphing( node, context );
 		}
 	}
 
 	/**
+	 * @private
 	 * @param {module:engine/model/node~Node} node The node to insert.
 	 */
 	_insert( node ) {
 		/* istanbul ignore if */
-		if ( !this._checkIsAllowed( node, this.position ) ) {
+		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(
@@ -256,20 +262,23 @@ class Insertion {
 
 		const livePos = LivePosition.createFromPosition( this.position );
 
-		this.batch.insert( this.position, node );
+		this.writer.insert( node, this.position );
 
 		this.position = Position.createFromPosition( livePos );
 		livePos.detach();
 
 		// The last inserted object should be selected because we can't put a collapsed selection after it.
-		if ( this._checkIsObject( node ) && !this.schema.check( { name: '$text', inside: this.position } ) ) {
+		if ( this.schema.isObject( node ) && !this.schema.checkChild( this.position, '$text' ) ) {
 			this.nodeToSelect = node;
 		} else {
 			this.nodeToSelect = null;
 		}
+
+		this._filterAttributesOf.push( node );
 	}
 
 	/**
+	 * @private
 	 * @param {module:engine/model/node~Node} node The node which could potentially be merged.
 	 * @param {Object} context
 	 */
@@ -278,20 +287,15 @@ class Insertion {
 			return;
 		}
 
-		const mergeLeft = context.isFirst && ( node.previousSibling instanceof Element ) && this.canMergeWith.has( node.previousSibling );
-		const mergeRight = context.isLast && ( node.nextSibling instanceof Element ) && this.canMergeWith.has( node.nextSibling );
+		const mergeLeft = this._canMergeLeft( node, context );
+		const mergeRight = this._canMergeRight( node, context );
 		const mergePosLeft = LivePosition.createBefore( node );
 		const mergePosRight = LivePosition.createAfter( node );
 
 		if ( mergeLeft ) {
 			const position = LivePosition.createFromPosition( this.position );
 
-			this.batch.merge( mergePosLeft );
-
-			// We need to check and strip disallowed attributes in all nested nodes because after merge
-			// some attributes could end up in a path where are disallowed.
-			const parent = position.nodeBefore;
-			this.schema.removeDisallowedAttributes( parent.getChildren(), Position.createAt( parent ), this.batch );
+			this.writer.merge( mergePosLeft );
 
 			this.position = Position.createFromPosition( position );
 			position.detach();
@@ -314,53 +318,77 @@ class Insertion {
 			// NOK: <p>xx[]</p> + <p>yy</p> => <p>xxyy[]</p> (when sticks to next)
 			const position = new LivePosition( this.position.root, this.position.path, 'sticksToPrevious' );
 
-			this.batch.merge( mergePosRight );
-
-			// We need to check and strip disallowed attributes in all nested nodes because after merge
-			// some attributes could end up in a place where are disallowed.
-			this.schema.removeDisallowedAttributes( position.parent.getChildren(), position, this.batch );
+			this.writer.merge( mergePosRight );
 
 			this.position = Position.createFromPosition( position );
 			position.detach();
 		}
 
+		if ( mergeLeft || mergeRight ) {
+			// After merge elements that were marked by _insert() to be filtered might be gone so
+			// we need to mark the new container.
+			this._filterAttributesOf.push( this.position.parent );
+		}
+
 		mergePosLeft.detach();
 		mergePosRight.detach();
+	}
 
-		// When there was no merge we need to check and strip disallowed attributes in all nested nodes of
-		// just inserted node because some attributes could end up in a place where are disallowed.
-		if ( !mergeLeft && !mergeRight ) {
-			this.schema.removeDisallowedAttributes( node.getChildren(), Position.createAt( node ), this.batch );
-		}
+	/**
+	 * Checks whether specified node can be merged with previous sibling element.
+	 *
+	 * @private
+	 * @param {module:engine/model/node~Node} node The node which could potentially be merged.
+	 * @param {Object} context
+	 * @returns {Boolean}
+	 */
+	_canMergeLeft( node, context ) {
+		const previousSibling = node.previousSibling;
+
+		return context.isFirst &&
+			( previousSibling instanceof Element ) &&
+			this.canMergeWith.has( previousSibling ) &&
+			this.model.schema.checkMerge( previousSibling, node );
+	}
+
+	/**
+	 * Checks whether specified node can be merged with next sibling element.
+	 *
+	 * @private
+	 * @param {module:engine/model/node~Node} node The node which could potentially be merged.
+	 * @param {Object} context
+	 * @returns {Boolean}
+	 */
+	_canMergeRight( node, context ) {
+		const nextSibling = node.nextSibling;
+
+		return context.isLast &&
+			( nextSibling instanceof Element ) &&
+			this.canMergeWith.has( nextSibling ) &&
+			this.model.schema.checkMerge( node, nextSibling );
 	}
 
 	/**
 	 * Tries wrapping the node in a new paragraph and inserting it this way.
 	 *
+	 * @private
 	 * @param {module:engine/model/node~Node} node The node which needs to be autoparagraphed.
 	 * @param {Object} context
 	 */
 	_tryAutoparagraphing( node, context ) {
-		const paragraph = new Element( 'paragraph' );
+		const paragraph = this.writer.createElement( 'paragraph' );
 
 		// Do not autoparagraph if the paragraph won't be allowed there,
 		// cause that would lead to an infinite loop. The paragraph would be rejected in
 		// the next _handleNode() call and we'd be here again.
-		if ( this._getAllowedIn( paragraph, this.position.parent ) ) {
-			// When node is a text and is disallowed by schema it means that contains disallowed attributes
-			// and we need to remove them.
-			if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) {
-				this.schema.removeDisallowedAttributes( [ node ], [ paragraph ] );
-			}
-
-			if ( this._checkIsAllowed( node, [ paragraph ] ) ) {
-				paragraph.appendChildren( node );
-				this._handleNode( paragraph, context );
-			}
+		if ( this._getAllowedIn( paragraph, this.position.parent ) && this.schema.checkChild( paragraph, node ) ) {
+			paragraph._appendChildren( node );
+			this._handleNode( paragraph, context );
 		}
 	}
 
 	/**
+	 * @private
 	 * @param {module:engine/model/node~Node} node
 	 * @returns {Boolean} Whether an allowed position was found.
 	 * `false` is returned if the node isn't allowed at any position up in the tree, `true` if was.
@@ -374,7 +402,7 @@ class Insertion {
 
 		while ( allowedIn != this.position.parent ) {
 			// If a parent which we'd need to leave is a limit element, break.
-			if ( this.schema.limits.has( this.position.parent.name ) ) {
+			if ( this.schema.isLimit( this.position.parent ) ) {
 				return false;
 			}
 
@@ -385,14 +413,14 @@ class Insertion {
 				// Special case – parent is empty (<p>^</p>) so isAtStart == isAtEnd == true.
 				// We can remove the element after moving selection out of it.
 				if ( parent.isEmpty ) {
-					this.batch.remove( parent );
+					this.writer.remove( parent );
 				}
 			} else if ( this.position.isAtEnd ) {
 				this.position = Position.createAfter( this.position.parent );
 			} else {
 				const tempPos = Position.createAfter( this.position.parent );
 
-				this.batch.split( this.position );
+				this.writer.split( this.position );
 
 				this.position = tempPos;
 
@@ -406,12 +434,13 @@ class Insertion {
 	/**
 	 * Gets the element in which the given node is allowed. It checks the passed element and all its ancestors.
 	 *
+	 * @private
 	 * @param {module:engine/model/node~Node} node The node to check.
 	 * @param {module:engine/model/element~Element} element The element in which the node's correctness should be checked.
 	 * @returns {module:engine/model/element~Element|null}
 	 */
 	_getAllowedIn( node, element ) {
-		if ( this._checkIsAllowed( node, [ element ] ) ) {
+		if ( this.schema.checkChild( element, node ) ) {
 			return element;
 		}
 
@@ -421,35 +450,4 @@ class Insertion {
 
 		return null;
 	}
-
-	/**
-	 * Check whether the given node is allowed in the specified schema path.
-	 *
-	 * @param {module:engine/model/node~Node} node
-	 * @param {module:engine/model/schema~SchemaPath} path
-	 */
-	_checkIsAllowed( node, path ) {
-		return this.schema.check( {
-			name: getNodeSchemaName( node ),
-			attributes: Array.from( node.getAttributeKeys() ),
-			inside: path
-		} );
-	}
-
-	/**
-	 * Checks whether according to the schema this is an object type element.
-	 *
-	 * @param {module:engine/model/node~Node} node The node to check.
-	 */
-	_checkIsObject( node ) {
-		return this.schema.objects.has( getNodeSchemaName( node ) );
-	}
-}
-
-// Gets a name under which we should check this node in the schema.
-//
-// @param {module:engine/model/node~Node} node The node.
-// @returns {String} Node name.
-function getNodeSchemaName( node ) {
-	return node.is( 'text' ) ? '$text' : node.name;
 }

+ 95 - 15
packages/ckeditor5-engine/src/controller/modifyselection.js → packages/ckeditor5-engine/src/model/utils/modifyselection.js

@@ -1,16 +1,19 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
 /**
- * @module engine/controller/modifyselection
+ * @module engine/model/utils/modifyselection
  */
 
-import Position from '../model/position';
-import TreeWalker from '../model/treewalker';
-import Range from '../model/range';
+import Position from '../position';
+import TreeWalker from '../treewalker';
+import Range from '../range';
 import { isInsideSurrogatePair, isInsideCombinedSymbol } from '@ckeditor/ckeditor5-utils/src/unicode';
+import DocumentSelection from '../documentselection';
+
+const wordBoundaryCharacters = ' ,.?!:;"-()';
 
 /**
  * Modifies the selection. Currently, the supported modifications are:
@@ -30,22 +33,24 @@ import { isInsideSurrogatePair, isInsideCombinedSymbol } from '@ckeditor/ckedito
  *  For example `𨭎` is represented in `String` by `\uD862\uDF4E`. Both `\uD862` and `\uDF4E` do not have any meaning
  *  outside the pair (are rendered as ? when alone). Position between them would be incorrect. In this case, selection
  *  extension will include whole "surrogate pair".
+ *  * `'word'` - moves selection by a whole word.
  *
  * **Note:** if you extend a forward selection in a backward direction you will in fact shrink it.
  *
- * @param {module:engine/controller/datacontroller~DataController} dataController The data controller in context of which
+ * @param {module:engine/model/model~Model} model The model in context of which
  * the selection modification should be performed.
  * @param {module:engine/model/selection~Selection} selection The selection to modify.
  * @param {Object} [options]
  * @param {'forward'|'backward'} [options.direction='forward'] The direction in which the selection should be modified.
- * @param {'character'|'codePoint'} [options.unit='character'] The unit by which selection should be modified.
+ * @param {'character'|'codePoint'|'word'} [options.unit='character'] The unit by which selection should be modified.
  */
-export default function modifySelection( dataController, selection, options = {} ) {
-	const schema = dataController.model.schema;
+export default function modifySelection( model, selection, options = {} ) {
+	const schema = model.schema;
 	const isForward = options.direction != 'backward';
 	const unit = options.unit ? options.unit : 'character';
 
 	const focus = selection.focus;
+
 	const walker = new TreeWalker( {
 		boundaries: getSearchRange( focus, isForward ),
 		singleCharacters: true,
@@ -64,7 +69,13 @@ export default function modifySelection( dataController, selection, options = {}
 		const position = tryExtendingTo( data, next.value );
 
 		if ( position ) {
-			selection.moveFocusTo( position );
+			if ( selection instanceof DocumentSelection ) {
+				model.change( writer => {
+					writer.setSelectionFocus( position );
+				} );
+			} else {
+				selection.setFocus( position );
+			}
 
 			return;
 		}
@@ -72,29 +83,35 @@ export default function modifySelection( dataController, selection, options = {}
 }
 
 // Checks whether the selection can be extended to the the walker's next value (next position).
+// @param {{ walker, unit, isForward, schema }} data
+// @param {module:engine/view/treewalker~TreeWalkerValue} value
 function tryExtendingTo( data, value ) {
 	// If found text, we can certainly put the focus in it. Let's just find a correct position
 	// based on the unit.
 	if ( value.type == 'text' ) {
-		return getCorrectPosition( data.walker, data.unit );
+		if ( data.unit === 'word' ) {
+			return getCorrectWordBreakPosition( data.walker, data.isForward );
+		}
+
+		return getCorrectPosition( data.walker, data.unit, data.isForward );
 	}
 
 	// Entering an element.
 	if ( value.type == ( data.isForward ? 'elementStart' : 'elementEnd' ) ) {
 		// If it's an object, we can select it now.
-		if ( data.schema.objects.has( value.item.name ) ) {
+		if ( data.schema.isObject( value.item ) ) {
 			return Position.createAt( value.item, data.isForward ? 'after' : 'before' );
 		}
 
 		// If text allowed on this position, extend to this place.
-		if ( data.schema.check( { name: '$text', inside: value.nextPosition } ) ) {
+		if ( data.schema.checkChild( value.nextPosition, '$text' ) ) {
 			return value.nextPosition;
 		}
 	}
 	// Leaving an element.
 	else {
 		// If leaving a limit element, stop.
-		if ( data.schema.limits.has( value.item.name ) ) {
+		if ( data.schema.isLimit( value.item ) ) {
 			// NOTE: Fast-forward the walker until the end.
 			data.walker.skip( () => true );
 
@@ -102,7 +119,7 @@ function tryExtendingTo( data, value ) {
 		}
 
 		// If text allowed on this position, extend to this place.
-		if ( data.schema.check( { name: '$text', inside: value.nextPosition } ) ) {
+		if ( data.schema.checkChild( value.nextPosition, '$text' ) ) {
 			return value.nextPosition;
 		}
 	}
@@ -110,6 +127,9 @@ function tryExtendingTo( data, value ) {
 
 // Finds a correct position by walking in a text node and checking whether selection can be extended to given position
 // or should be extended further.
+//
+// @param {module:engine/model/treewalker~TreeWalker} walker
+// @param {String} unit The unit by which selection should be modified.
 function getCorrectPosition( walker, unit ) {
 	const textNode = walker.position.textNode;
 
@@ -127,6 +147,45 @@ function getCorrectPosition( walker, unit ) {
 	return walker.position;
 }
 
+// Finds a correct position of a word break by walking in a text node and checking whether selection can be extended to given position
+// or should be extended further.
+//
+// @param {module:engine/model/treewalker~TreeWalker} walker
+// @param {Boolean} isForward Is the direction in which the selection should be modified is forward.
+function getCorrectWordBreakPosition( walker, isForward ) {
+	let textNode = walker.position.textNode;
+
+	if ( textNode ) {
+		let offset = walker.position.offset - textNode.startOffset;
+
+		while ( !isAtWordBoundary( textNode.data, offset, isForward ) && !isAtNodeBoundary( textNode, offset, isForward ) ) {
+			walker.next();
+
+			// Check of adjacent text nodes with different attributes (like BOLD).
+			// Example          : 'foofoo []bar<$text bold="true">bar</$text> bazbaz'
+			// should expand to : 'foofoo [bar<$text bold="true">bar</$text>] bazbaz'.
+			const nextNode = isForward ? walker.position.nodeAfter : walker.position.nodeBefore;
+
+			if ( nextNode ) {
+				// Check boundary char of an adjacent text node.
+				const boundaryChar = nextNode.data.charAt( isForward ? 0 : nextNode.data.length - 1 );
+
+				// Go to the next node if the character at the boundary of that node belongs to the same word.
+				if ( !wordBoundaryCharacters.includes( boundaryChar ) ) {
+					// If adjacent text node belongs to the same word go to it & reset values.
+					walker.next();
+
+					textNode = walker.position.textNode;
+				}
+			}
+
+			offset = walker.position.offset - textNode.startOffset;
+		}
+	}
+
+	return walker.position;
+}
+
 function getSearchRange( start, isForward ) {
 	const root = start.root;
 	const searchEnd = Position.createAt( root, isForward ? 'end' : 0 );
@@ -137,3 +196,24 @@ function getSearchRange( start, isForward ) {
 		return new Range( searchEnd, start );
 	}
 }
+
+// Checks if selection is on word boundary.
+//
+// @param {String} data The text node value to investigate.
+// @param {Number} offset Position offset.
+// @param {Boolean} isForward Is the direction in which the selection should be modified is forward.
+function isAtWordBoundary( data, offset, isForward ) {
+	// The offset to check depends on direction.
+	const offsetToCheck = offset + ( isForward ? 0 : -1 );
+
+	return wordBoundaryCharacters.includes( data.charAt( offsetToCheck ) );
+}
+
+// Checks if selection is on node boundary.
+//
+// @param {module:engine/model/text~Text} textNode The text node to investigate.
+// @param {Number} offset Position offset.
+// @param {Boolean} isForward Is the direction in which the selection should be modified is forward.
+function isAtNodeBoundary( textNode, offset, isForward ) {
+	return offset === ( isForward ? textNode.endOffset : 0 );
+}

+ 1253 - 247
packages/ckeditor5-engine/src/model/writer.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -7,312 +7,1318 @@
  * @module engine/model/writer
  */
 
-import Node from './node';
-import Text from './text';
-import TextProxy from './textproxy';
-import Range from './range';
+import AttributeDelta from './delta/attributedelta';
+import InsertDelta from './delta/insertdelta';
+import MarkerDelta from './delta/markerdelta';
+import MergeDelta from './delta/mergedelta';
+import MoveDelta from './delta/movedelta';
+import RemoveDelta from './delta/removedelta';
+import RenameDelta from './delta/renamedelta';
+import RootAttributeDelta from './delta/rootattributedelta';
+import SplitDelta from './delta/splitdelta';
+import UnwrapDelta from './delta/unwrapdelta';
+import WeakInsertDelta from './delta/weakinsertdelta';
+import WrapDelta from './delta/wrapdelta';
+
+import AttributeOperation from './operation/attributeoperation';
+import DetachOperation from './operation/detachoperation';
+import InsertOperation from './operation/insertoperation';
+import MarkerOperation from './operation/markeroperation';
+import MoveOperation from './operation/moveoperation';
+import RemoveOperation from './operation/removeoperation';
+import RenameOperation from './operation/renameoperation';
+import RootAttributeOperation from './operation/rootattributeoperation';
+
 import DocumentFragment from './documentfragment';
-import NodeList from './nodelist';
+import Text from './text';
+import Element from './element';
+import RootElement from './rootelement';
+import Position from './position';
+import Range from './range.js';
+import DocumentSelection from './documentselection';
+
+import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
+
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import uid from '@ckeditor/ckeditor5-utils/src/uid';
 
 /**
- * Contains functions used for composing model tree, grouped together under "model writer" name. Those functions
- * are built on top of {@link module:engine/model/node~Node node}, and it's child classes', APIs.
+ * Model writer it the proper way of modifying model. It should be used whenever you wants to create node, modify
+ * child nodes, attributes or text. To get writer use {@link module:engine/model/model~Model#change} or
+ * {@link module:engine/model/model~Model#enqueueChange}.
+ *
+ *		model.change( writer => {
+ *			writer.insertText( 'foo', paragraph, 'end' );
+ *		} );
  *
- * Model writer API has multiple advantages and it is highly recommended to use it when changing model tree and nodes:
- * * model writer API {@link module:engine/model/writer~writer.normalizeNodes normalizes inserted nodes}, which means that you can insert
- * not only {@link module:engine/model/node~Node nodes}, but also `String`s, {@link module:engine/model/textproxy~TextProxy text proxies}
- * and
- * {@link module:engine/model/documentfragment~DocumentFragment document fragments},
- * * model writer API operates on {@link module:engine/model/position~Position positions}, which means that you have
- * better control over manipulating model tree as positions operate on offsets rather than indexes,
- * * model writer API automatically merges {@link module:engine/model/text~Text text nodes} with same attributes, which means
- * lower memory usage and better efficiency.
+ * Note that writer can be passed to a nested function but you should never store and use it outside the `change` or
+ * `enqueueChange` block.
  *
- * @namespace writer
+ * @see module:engine/model/model~Model#change
+ * @see module:engine/model/model~Model#enqueueChange
  */
-const writer = {
-	insert,
-	remove,
-	move,
-	setAttribute,
-	removeAttribute,
-	normalizeNodes
-};
+export default class Writer {
+	/**
+	 * Writer class constructor.
+	 *
+	 * It is not recommended to use it directly, use {@link module:engine/model/model~Model#change} or
+	 * {@link module:engine/model/model~Model#enqueueChange} instead.
+	 *
+	 * @protected
+	 * @param {module:engine/model/model~Model} model
+	 * @param {module:engine/model/batch~Batch} batch
+	 */
+	constructor( model, batch ) {
+		/**
+		 * @readonly
+		 * @type {module:engine/model/model~Model}
+		 */
+		this.model = model;
 
-export default writer;
+		/**
+		 * @readonly
+		 * @type {module:engine/model/batch~Batch}
+		 */
+		this.batch = batch;
+	}
 
-/**
- * Inserts given nodes at given position.
- *
- * @function module:engine/model/writer~writer.insert
- * @param {module:engine/model/position~Position} position Position at which nodes should be inserted.
- * @param {module:engine/model/node~NodeSet} nodes Nodes to insert.
- * @returns {module:engine/model/range~Range} Range spanning over inserted elements.
- */
-export function insert( position, nodes ) {
-	nodes = normalizeNodes( nodes );
+	/**
+	 * Creates a new {@link module:engine/model/text~Text text node}.
+	 *
+	 *		writer.createText( 'foo' );
+	 *		writer.createText( 'foo', { 'bold': true } );
+	 *
+	 * @param {String} data Text data.
+	 * @param {Object} [attributes] Text attributes.
+	 * @returns {module:engine/model/text~Text} Created text node.
+	 */
+	createText( data, attributes ) {
+		return new Text( data, attributes );
+	}
 
-	// We have to count offset before inserting nodes because they can get merged and we would get wrong offsets.
-	const offset = nodes.reduce( ( sum, node ) => sum + node.offsetSize, 0 );
-	const parent = position.parent;
+	/**
+	 * Creates a new {@link module:engine/model/element~Element element}.
+	 *
+	 *		writer.createElement( 'paragraph' );
+	 *		writer.createElement( 'paragraph', { 'alignment': 'center' } );
+	 *
+	 * @param {String} name Name of the element.
+	 * @param {Object} [attributes] Elements attributes.
+	 * @returns {module:engine/model/element~Element} Created element.
+	 */
+	createElement( name, attributes ) {
+		return new Element( name, attributes );
+	}
 
-	// Insertion might be in a text node, we should split it if that's the case.
-	_splitNodeAtPosition( position );
-	const index = position.index;
+	/**
+	 * Creates a new {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
+	 *
+	 * @returns {module:engine/model/documentfragment~DocumentFragment} Created document fragment.
+	 */
+	createDocumentFragment() {
+		return new DocumentFragment();
+	}
 
-	// Insert nodes at given index. After splitting we have a proper index and insertion is between nodes,
-	// using basic `Element` API.
-	parent.insertChildren( index, nodes );
+	/**
+	 * Inserts item on given position.
+	 *
+	 *		const paragraph = writer.createElement( 'paragraph' );
+	 *		writer.insert( paragraph, position );
+	 *
+	 * Instead of using position you can use parent and offset:
+	 *
+	 * 		const text = writer.createText( 'foo' );
+	 *		writer.insert( text, paragraph, 5 );
+	 *
+	 * You can also use `end` instead of the offset to insert at the end:
+	 *
+	 * 		const text = writer.createText( 'foo' );
+	 *		writer.insert( text, paragraph, 'end' );
+	 *
+	 * Or insert before or after another element:
+	 *
+	 * 		const paragraph = writer.createElement( 'paragraph' );
+	 *		writer.insert( paragraph, anotherParagraph, 'after' );
+	 *
+	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
+	 *
+	 * Note that if the item already has parent it will be removed from the previous parent.
+	 *
+	 * Note that you cannot re-insert a node from a document to a different document or document fragment. In this case,
+	 * `model-writer-insert-forbidden-move` is thrown.
+	 *
+	 * If you want to move {@link module:engine/model/range~Range range} instead of an
+	 * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move move}.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment} item Item or document
+	 * fragment to insert.
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * second parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	insert( item, itemOrPosition, offset ) {
+		this._assertWriterUsedCorrectly();
 
-	// Merge text nodes, if possible. Merging is needed only at points where inserted nodes "touch" "old" nodes.
-	_mergeNodesAtIndex( parent, index + nodes.length );
-	_mergeNodesAtIndex( parent, index );
+		const position = Position.createAt( itemOrPosition, offset );
 
-	return new Range( position, position.getShiftedBy( offset ) );
-}
+		// For text that has no parent we need to make a WeakInsert.
+		const delta = item instanceof Text && !item.parent ? new WeakInsertDelta() : new InsertDelta();
 
-/**
- * Removed nodes in given range. Only {@link module:engine/model/range~Range#isFlat flat} ranges are accepted.
- *
- * @function module:engine/model/writer~writer.remove
- * @param {module:engine/model/range~Range} range Range containing nodes to remove.
- * @returns {Array.<module:engine/model/node~Node>}
- */
-export function remove( range ) {
-	if ( !range.isFlat ) {
-		/**
-		 * Trying to remove a range which starts and ends in different element.
-		 *
-		 * @error model-writer-remove-range-not-flat
-		 */
-		throw new CKEditorError( 'model-writer-remove-range-not-flat: ' +
-			'Trying to remove a range which starts and ends in different element.' );
+		// If item has a parent already.
+		if ( item.parent ) {
+			// We need to check if item is going to be inserted within the same document.
+			if ( isSameTree( item.root, position.root ) ) {
+				// If it's we just need to move it.
+				this.move( Range.createOn( item ), position );
+
+				return;
+			}
+			// 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.' );
+				} 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.
+					this.remove( item );
+				}
+			}
+		}
+
+		const version = position.root.document ? position.root.document.version : null;
+
+		const insert = new InsertOperation( position, item, version );
+
+		this.batch.addDelta( delta );
+		delta.addOperation( insert );
+		this.model.applyOperation( insert );
+
+		// When element is a DocumentFragment we need to move its markers to Document#markers.
+		if ( item instanceof DocumentFragment ) {
+			for ( const [ markerName, markerRange ] of item.markers ) {
+				// We need to migrate marker range from DocumentFragment to Document.
+				const rangeRootPosition = Position.createAt( markerRange.root );
+				const range = new Range(
+					markerRange.start._getCombined( rangeRootPosition, position ),
+					markerRange.end._getCombined( rangeRootPosition, position )
+				);
+
+				this.setMarker( markerName, range, { usingOperation: true } );
+			}
+		}
 	}
 
-	const parent = range.start.parent;
+	/**
+	 * Creates and inserts text on given position. You can optionally set text attributes:
+	 *
+	 *		writer.insertText( 'foo', position );
+	 *		writer.insertText( 'foo', { 'bold': true }, position );
+	 *
+	 * Instead of using position you can use parent and offset or define that text should be inserted at the end
+	 * or before or after other node:
+	 *
+	 * 		writer.insertText( 'foo', paragraph, 5 ); // inserts in paragraph, at offset 5
+	 *		writer.insertText( 'foo', paragraph, 'end' ); // inserts at the end of the paragraph
+	 *		writer.insertText( 'foo', image, 'after' ); // inserts after image
+	 *
+	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
+	 *
+	 * @param {String} data Text data.
+	 * @param {Object} [attributes] Text attributes.
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * third parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	insertText( text, attributes, itemOrPosition, offset ) {
+		if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
+			this.insert( this.createText( text ), attributes, itemOrPosition );
+		} else {
+			this.insert( this.createText( text, attributes ), itemOrPosition, offset );
+		}
+	}
 
-	// Range may be inside text nodes, we have to split them if that's the case.
-	_splitNodeAtPosition( range.start );
-	_splitNodeAtPosition( range.end );
+	/**
+	 * Creates and inserts element on given position. You can optionally set attributes:
+	 *
+	 *		writer.insertElement( 'paragraph', position );
+	 *		writer.insertElement( 'paragraph', { 'alignment': 'center' }, position );
+	 *
+	 * Instead of using position you can use parent and offset or define that text should be inserted at the end
+	 * or before or after other node:
+	 *
+	 * 		writer.insertElement( 'paragraph', paragraph, 5 ); // inserts in paragraph, at offset 5
+	 *		writer.insertElement( 'paragraph', blockquote, 'end' ); // insets at the end of the blockquote
+	 *		writer.insertElement( 'paragraph', image, 'after' ); // inserts after image
+	 *
+	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
+	 *
+	 * @param {String} name Name of the element.
+	 * @param {Object} [attributes] Elements attributes.
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * third parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	insertElement( name, attributes, itemOrPosition, offset ) {
+		if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
+			this.insert( this.createElement( name ), attributes, itemOrPosition );
+		} else {
+			this.insert( this.createElement( name, attributes ), itemOrPosition, offset );
+		}
+	}
 
-	// Remove the text nodes using basic `Element` API.
-	const removed = parent.removeChildren( range.start.index, range.end.index - range.start.index );
+	/**
+	 * Inserts item at the end of the given parent.
+	 *
+	 *		const paragraph = writer.createElement( 'paragraph' );
+	 *		writer.append( paragraph, root );
+	 *
+	 * Note that if the item already has parent it will be removed from the previous parent.
+	 *
+	 * If you want to move {@link module:engine/model/range~Range range} instead of an
+	 * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move move}.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment}
+	 * item Item or document fragment to insert.
+	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
+	 */
+	append( item, parent ) {
+		this.insert( item, parent, 'end' );
+	}
 
-	// Merge text nodes, if possible. After some nodes were removed, node before and after removed range will be
-	// touching at the position equal to the removed range beginning. We check merging possibility there.
-	_mergeNodesAtIndex( parent, range.start.index );
+	/**
+	 * Creates text node and inserts it at the end of the parent. You can optionally set text attributes:
+	 *
+	 *		writer.appendText( 'foo', paragraph );
+	 *		writer.appendText( 'foo', { 'bold': true }, paragraph );
+	 *
+	 * @param {String} text Text data.
+	 * @param {Object} [attributes] Text attributes.
+	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
+	 */
+	appendText( text, attributes, parent ) {
+		if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
+			this.insert( this.createText( text ), attributes, 'end' );
+		} else {
+			this.insert( this.createText( text, attributes ), parent, 'end' );
+		}
+	}
 
-	return removed;
-}
+	/**
+	 * Creates element and inserts it at the end of the parent. You can optionally set attributes:
+	 *
+	 *		writer.appendElement( 'paragraph', root );
+	 *		writer.appendElement( 'paragraph', { 'alignment': 'center' }, root );
+	 *
+	 * @param {String} name Name of the element.
+	 * @param {Object} [attributes] Elements attributes.
+	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
+	 */
+	appendElement( name, attributes, parent ) {
+		if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
+			this.insert( this.createElement( name ), attributes, 'end' );
+		} else {
+			this.insert( this.createElement( name, attributes ), parent, 'end' );
+		}
+	}
 
-/**
- * Moves nodes in given range to given target position. Only {@link module:engine/model/range~Range#isFlat flat} ranges are accepted.
- *
- * @param {module:engine/model/range~Range} sourceRange Range containing nodes to move.
- * @param {module:engine/model/position~Position} targetPosition Position to which nodes should be moved.
- * @returns {module:engine/model/range~Range} Range containing moved nodes.
- */
-export function move( sourceRange, targetPosition ) {
-	if ( !sourceRange.isFlat ) {
-		/**
-		 * Trying to move a range which starts and ends in different element.
-		 *
-		 * @error model-writer-move-range-not-flat
-		 */
-		throw new CKEditorError( 'model-writer-move-range-not-flat: ' +
-			'Trying to move a range which starts and ends in different element.' );
+	/**
+	 * Sets the text content for the specified `textNode`.
+	 *
+	 * @param {String} value New value.
+	 * @param {module:engine/model/text~Text} textNode Text node that will be updated.
+	 */
+	setTextData( value, textNode ) {
+		textNode._data = value;
 	}
 
-	const nodes = this.remove( sourceRange );
+	/**
+	 * Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
+	 * or on a {@link module:engine/model/range~Range range}.
+	 *
+	 * @param {String} key Attribute key.
+	 * @param {*} value Attribute new value.
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
+	 * Model item or range on which the attribute will be set.
+	 */
+	setAttribute( key, value, itemOrRange ) {
+		this._assertWriterUsedCorrectly();
 
-	// We have to fix `targetPosition` because model changed after nodes from `sourceRange` got removed and
-	// that change might have an impact on `targetPosition`.
-	targetPosition = targetPosition._getTransformedByDeletion( sourceRange.start, sourceRange.end.offset - sourceRange.start.offset );
+		if ( itemOrRange instanceof Range ) {
+			setAttributeOnRange( this, key, value, itemOrRange );
+		} else {
+			setAttributeOnItem( this, key, value, itemOrRange );
+		}
+	}
 
-	return this.insert( targetPosition, nodes );
-}
+	/**
+	 * Sets values of attributes on a {@link module:engine/model/item~Item model item}
+	 * or on a {@link module:engine/model/range~Range range}.
+	 *
+	 *		writer.setAttributes( {
+	 *			'bold': true,
+	 *			'italic': true
+	 *		}, range );
+	 *
+	 * @param {Object} attributes Attributes keys and values.
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
+	 * Model item or range on which the attributes will be set.
+	 */
+	setAttributes( attributes, itemOrRange ) {
+		for ( const [ key, val ] of toMap( attributes ) ) {
+			this.setAttribute( key, val, itemOrRange );
+		}
+	}
 
-/**
- * Sets given attribute on nodes in given range.
- *
- * @param {module:engine/model/range~Range} range Range containing nodes that should have the attribute set.
- * @param {String} key Key of attribute to set.
- * @param {*} value Attribute value.
- */
-export function setAttribute( range, key, value ) {
-	// Range might start or end in text nodes, so we have to split them.
-	_splitNodeAtPosition( range.start );
-	_splitNodeAtPosition( range.end );
-
-	// Iterate over all items in the range.
-	for ( const item of range.getItems() ) {
-		// Iterator will return `TextProxy` instances but we know that those text proxies will
-		// always represent full text nodes (this is guaranteed thanks to splitting we did before).
-		// So, we can operate on those text proxies' text nodes.
-		const node = item.is( 'textProxy' ) ? item.textNode : item;
-
-		if ( value !== null ) {
-			node.setAttribute( key, value );
+	/**
+	 * Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
+	 * or from a {@link module:engine/model/range~Range range}.
+	 *
+	 * @param {String} key Attribute key.
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
+	 * Model item or range from which the attribute will be removed.
+	 */
+	removeAttribute( key, itemOrRange ) {
+		this._assertWriterUsedCorrectly();
+
+		if ( itemOrRange instanceof Range ) {
+			setAttributeOnRange( this, key, null, itemOrRange );
+		} else {
+			setAttributeOnItem( this, key, null, itemOrRange );
+		}
+	}
+
+	/**
+	 * Removes all attributes from all elements in the range or from the given item.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
+	 * Model item or range from which all attributes will be removed.
+	 */
+	clearAttributes( itemOrRange ) {
+		this._assertWriterUsedCorrectly();
+
+		const removeAttributesFromItem = item => {
+			for ( const attribute of item.getAttributeKeys() ) {
+				this.removeAttribute( attribute, item );
+			}
+		};
+
+		if ( !( itemOrRange instanceof Range ) ) {
+			removeAttributesFromItem( itemOrRange );
 		} else {
-			node.removeAttribute( key );
+			for ( const item of itemOrRange.getItems() ) {
+				removeAttributesFromItem( item );
+			}
+		}
+	}
+
+	/**
+	 * Moves all items in the source range to the target position.
+	 *
+	 *		writer.move( sourceRange, targetPosition );
+	 *
+	 * Instead of the target position you can use parent and offset or define that range should be moved to the end
+	 * or before or after chosen item:
+	 *
+	 * 		writer.move( sourceRange, paragraph, 5 ); // moves all items in the range to the paragraph at offset 5
+	 *		writer.move( sourceRange, blockquote, 'end' ); // moves all items in the range at the end of the blockquote
+	 *		writer.move( sourceRange, image, 'after' ); // moves all items in the range after the image
+	 *
+	 * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
+	 *
+	 * Note that items can be moved only within the same tree. It means that you can move items within the same root
+	 * (element or document fragment) or between {@link module:engine/model/document~Document#roots documents roots},
+	 * but you can not move items from document fragment to the document or from one detached element to another. Use
+	 * {@link module:engine/model/writer~Writer#insert} in such cases.
+	 *
+	 * @param {module:engine/model/range~Range} range Source range.
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * second parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	move( range, itemOrPosition, offset ) {
+		this._assertWriterUsedCorrectly();
+
+		if ( !( range instanceof Range ) ) {
+			/**
+			 * Invalid range to move.
+			 *
+			 * @error writer-move-invalid-range
+			 */
+			throw new CKEditorError( 'writer-move-invalid-range: Invalid range to move.' );
 		}
 
-		// After attributes changing it may happen that some text nodes can be merged. Try to merge with previous node.
-		_mergeNodesAtIndex( node.parent, node.index );
+		if ( !range.isFlat ) {
+			/**
+			 * Range to move is not flat.
+			 *
+			 * @error writer-move-range-not-flat
+			 */
+			throw new CKEditorError( 'writer-move-range-not-flat: Range to move is not flat.' );
+		}
+
+		const position = Position.createAt( itemOrPosition, offset );
+
+		if ( !isSameTree( range.root, position.root ) ) {
+			/**
+			 * Range is going to be moved within not the same document. Please use
+			 * {@link module:engine/model/writer~Writer#insert insert} instead.
+			 *
+			 * @error writer-move-different-document
+			 */
+			throw new CKEditorError( 'writer-move-different-document: Range is going to be moved between different documents.' );
+		}
+
+		const delta = new MoveDelta();
+		this.batch.addDelta( delta );
+
+		const version = range.root.document ? range.root.document.version : null;
+
+		const operation = new MoveOperation( range.start, range.end.offset - range.start.offset, position, version );
+		delta.addOperation( operation );
+		this.model.applyOperation( operation );
 	}
 
-	// Try to merge last changed node with it's previous sibling (not covered by the loop above).
-	_mergeNodesAtIndex( range.end.parent, range.end.index );
-}
+	/**
+	 * Removes given model {@link module:engine/model/item~Item item} or {@link module:engine/model/range~Range range}.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove.
+	 */
+	remove( itemOrRange ) {
+		this._assertWriterUsedCorrectly();
 
-/**
- * Removes given attribute from nodes in given range.
- *
- * @param {module:engine/model/range~Range} range Range containing nodes that should have the attribute removed.
- * @param {String} key Key of attribute to remove.
- */
-export function removeAttribute( range, key ) {
-	this.setAttribute( range, key, null );
-}
+		const addRemoveDelta = ( position, howMany ) => {
+			const delta = new RemoveDelta();
+			this.batch.addDelta( delta );
 
-/**
- * Normalizes given object or an array of objects to an array of {@link module:engine/model/node~Node nodes}. See
- * {@link module:engine/model/node~NodeSet NodeSet} for details on how normalization is performed.
- *
- * @param {module:engine/model/node~NodeSet} nodes Objects to normalize.
- * @returns {Array.<module:engine/model/node~Node>} Normalized nodes.
- */
-export function normalizeNodes( nodes ) {
-	const normalized = [];
-
-	if ( !( nodes instanceof Array ) ) {
-		nodes = [ nodes ];
-	}
-
-	// Convert instances of classes other than Node.
-	for ( let i = 0; i < nodes.length; i++ ) {
-		if ( typeof nodes[ i ] == 'string' ) {
-			normalized.push( new Text( nodes[ i ] ) );
-		} else if ( nodes[ i ] instanceof TextProxy ) {
-			normalized.push( new Text( nodes[ i ].data, nodes[ i ].getAttributes() ) );
-		} else if ( nodes[ i ] instanceof DocumentFragment || nodes[ i ] instanceof NodeList ) {
-			for ( const child of nodes[ i ] ) {
-				normalized.push( child );
+			applyRemoveOperation( position, howMany, delta, this.model );
+		};
+
+		if ( itemOrRange instanceof Range ) {
+			// The array is reversed, so the ranges to remove are in correct order and do not have to be updated.
+			const ranges = itemOrRange.getMinimalFlatRanges().reverse();
+
+			for ( const flat of ranges ) {
+				addRemoveDelta( flat.start, flat.end.offset - flat.start.offset );
 			}
-		} else if ( nodes[ i ] instanceof Node ) {
-			normalized.push( nodes[ i ] );
+		} else {
+			const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1;
+
+			addRemoveDelta( Position.createBefore( itemOrRange ), howMany );
 		}
-		// Skip unrecognized type.
 	}
 
-	// Merge text nodes.
-	for ( let i = 1; i < normalized.length; i++ ) {
-		const node = normalized[ i ];
-		const prev = normalized[ i - 1 ];
+	/**
+	 * Merges two siblings at the given position.
+	 *
+	 * Node before and after the position have to be an element. Otherwise `writer-merge-no-element-before` or
+	 * `writer-merge-no-element-after` error will be thrown.
+	 *
+	 * @param {module:engine/model/position~Position} position Position of merge.
+	 */
+	merge( position ) {
+		this._assertWriterUsedCorrectly();
+
+		const delta = new MergeDelta();
+		this.batch.addDelta( delta );
+
+		const nodeBefore = position.nodeBefore;
+		const nodeAfter = position.nodeAfter;
+
+		if ( !( nodeBefore instanceof Element ) ) {
+			/**
+			 * Node before merge position must be an element.
+			 *
+			 * @error writer-merge-no-element-before
+			 */
+			throw new CKEditorError( 'writer-merge-no-element-before: Node before merge position must be an element.' );
+		}
 
-		if ( node instanceof Text && prev instanceof Text && _haveSameAttributes( node, prev ) ) {
-			// Doing this instead changing prev.data because .data is readonly.
-			normalized.splice( i - 1, 2, new Text( prev.data + node.data, prev.getAttributes() ) );
-			i--;
+		if ( !( nodeAfter instanceof Element ) ) {
+			/**
+			 * Node after merge position must be an element.
+			 *
+			 * @error writer-merge-no-element-after
+			 */
+			throw new CKEditorError( 'writer-merge-no-element-after: Node after merge position must be an element.' );
 		}
+
+		const positionAfter = Position.createFromParentAndOffset( nodeAfter, 0 );
+		const positionBefore = Position.createFromParentAndOffset( nodeBefore, nodeBefore.maxOffset );
+
+		const moveVersion = position.root.document ? position.root.document.version : null;
+
+		const move = new MoveOperation(
+			positionAfter,
+			nodeAfter.maxOffset,
+			positionBefore,
+			moveVersion
+		);
+
+		move.isSticky = true;
+		delta.addOperation( move );
+		this.model.applyOperation( move );
+
+		applyRemoveOperation( position, 1, delta, this.model );
 	}
 
-	return normalized;
-}
+	/**
+	 * Renames given element.
+	 *
+	 * @param {module:engine/model/element~Element} element The element to rename.
+	 * @param {String} newName New element name.
+	 */
+	rename( element, newName ) {
+		this._assertWriterUsedCorrectly();
 
-/**
- * Checks if nodes before and after given index in given element are {@link module:engine/model/text~Text text nodes} and
- * merges them into one node if they have same attributes.
- *
- * Merging is done by removing two text nodes and inserting a new text node containing data from both merged text nodes.
- *
- * @ignore
- * @private
- * @param {module:engine/model/element~Element} element Parent element of nodes to merge.
- * @param {Number} index Index between nodes to merge.
- */
-function _mergeNodesAtIndex( element, index ) {
-	const nodeBefore = element.getChild( index - 1 );
-	const nodeAfter = element.getChild( index );
+		if ( !( element instanceof Element ) ) {
+			/**
+			 * Trying to rename an object which is not an instance of Element.
+			 *
+			 * @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.'
+			);
+		}
+
+		const delta = new RenameDelta();
+		this.batch.addDelta( delta );
+
+		const version = element.root.document ? element.root.document.version : null;
+
+		const renameOperation = new RenameOperation( Position.createBefore( element ), element.name, newName, version );
+		delta.addOperation( renameOperation );
+		this.model.applyOperation( renameOperation );
+	}
+
+	/**
+	 * Splits elements start from the given position and goes to the top of the model tree as long as given
+	 * `limitElement` won't be reached. When limitElement is not defined then only a parent of given position will be split.
+	 *
+	 * The element needs to have a parent. It cannot be a root element nor document fragment.
+	 * The `writer-split-element-no-parent` error will be thrown if you try to split an element with no parent.
+	 *
+	 * @param {module:engine/model/position~Position} position Position of split.
+	 * @param {module:engine/model/node~Node} [limitElement] Stop splitting when this element will be reached.
+	 * @returns {Object} result Split result.
+	 * @returns {module:engine/model/position~Position} result.position between split elements.
+	 * @returns {module:engine/model/range~Range} result.range Range that stars from the end of the first split element and ands
+	 * at the beginning of the first copy element.
+	 */
+	split( position, limitElement ) {
+		this._assertWriterUsedCorrectly();
+
+		let splitElement = position.parent;
+
+		if ( !splitElement.parent ) {
+			/**
+			 * Element with no parent can not be split.
+			 *
+			 * @error writer-split-element-no-parent
+			 */
+			throw new CKEditorError( 'writer-split-element-no-parent: Element with no parent can not be split.' );
+		}
+
+		// When limit element is not defined lets set splitElement parent as limit.
+		if ( !limitElement ) {
+			limitElement = splitElement.parent;
+		}
+
+		if ( !position.parent.getAncestors( { includeSelf: true } ).includes( limitElement ) ) {
+			throw new CKEditorError( 'writer-split-invalid-limit-element: Limit element is not a position ancestor.' );
+		}
+
+		// We need to cache elements that will be created as a result of the first split because
+		// we need to create a range from the end of the first split element to the beginning of the
+		// first copy element. This should be handled by LiveRange but it doesn't work on detached nodes.
+		let firstSplitElement, firstCopyElement;
+
+		do {
+			const delta = new SplitDelta();
+			this.batch.addDelta( delta );
+
+			const copy = new Element( splitElement.name, splitElement.getAttributes() );
+			const insertVersion = splitElement.root.document ? splitElement.root.document.version : null;
+
+			const insert = new InsertOperation(
+				Position.createAfter( splitElement ),
+				copy,
+				insertVersion
+			);
+
+			delta.addOperation( insert );
+			this.model.applyOperation( insert );
+
+			const moveVersion = insertVersion !== null ? insertVersion + 1 : null;
 
-	// Check if both of those nodes are text objects with same attributes.
-	if ( nodeBefore && nodeAfter && nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) && _haveSameAttributes( nodeBefore, nodeAfter ) ) {
-		// Append text of text node after index to the before one.
-		const mergedNode = new Text( nodeBefore.data + nodeAfter.data, nodeBefore.getAttributes() );
+			const move = new MoveOperation(
+				position,
+				splitElement.maxOffset - position.offset,
+				Position.createFromParentAndOffset( copy, 0 ),
+				moveVersion
+			);
+			move.isSticky = true;
 
-		// Remove separate text nodes.
-		element.removeChildren( index - 1, 2 );
+			delta.addOperation( move );
+			this.model.applyOperation( move );
 
-		// Insert merged text node.
-		element.insertChildren( index - 1, mergedNode );
+			// Cache result of the first split.
+			if ( !firstSplitElement && !firstCopyElement ) {
+				firstSplitElement = splitElement;
+				firstCopyElement = copy;
+			}
+
+			position = Position.createBefore( copy );
+			splitElement = position.parent;
+		} while ( splitElement !== limitElement );
+
+		return {
+			position,
+			range: new Range( Position.createAt( firstSplitElement, 'end' ), Position.createAt( firstCopyElement ) )
+		};
+	}
+
+	/**
+	 * Wraps given range with given element or with a new element with specified name, if string has been passed.
+	 *
+	 * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat}). If not, error will be thrown.
+	 *
+	 * @param {module:engine/model/range~Range} range Range to wrap.
+	 * @param {module:engine/model/element~Element|String} elementOrString Element or name of element to wrap the range with.
+	 */
+	wrap( range, elementOrString ) {
+		this._assertWriterUsedCorrectly();
+
+		if ( !range.isFlat ) {
+			/**
+			 * Range to wrap is not flat.
+			 *
+			 * @error writer-wrap-range-not-flat
+			 */
+			throw new CKEditorError( 'writer-wrap-range-not-flat: Range to wrap is not flat.' );
+		}
+
+		const element = elementOrString instanceof Element ? elementOrString : new Element( elementOrString );
+
+		if ( element.childCount > 0 ) {
+			/**
+			 * Element to wrap with is not empty.
+			 *
+			 * @error writer-wrap-element-not-empty
+			 */
+			throw new CKEditorError( 'writer-wrap-element-not-empty: Element to wrap with is not empty.' );
+		}
+
+		if ( element.parent !== null ) {
+			/**
+			 * Element to wrap with is already attached to a tree model.
+			 *
+			 * @error writer-wrap-element-attached
+			 */
+			throw new CKEditorError( 'writer-wrap-element-attached: Element to wrap with is already attached to tree model.' );
+		}
+
+		const delta = new WrapDelta();
+		this.batch.addDelta( delta );
+
+		const insertVersion = range.root.document ? range.root.document.version : null;
+
+		const insert = new InsertOperation( range.end, element, insertVersion );
+		delta.addOperation( insert );
+		this.model.applyOperation( insert );
+
+		const moveVersion = insertVersion !== null ? insertVersion + 1 : null;
+
+		const targetPosition = Position.createFromParentAndOffset( element, 0 );
+		const move = new MoveOperation(
+			range.start,
+			range.end.offset - range.start.offset,
+			targetPosition,
+			moveVersion
+		);
+		delta.addOperation( move );
+		this.model.applyOperation( move );
+	}
+
+	/**
+	 * Unwraps children of the given element – all its children are moved before it and then the element is removed.
+	 * Throws error if you try to unwrap an element which does not have a parent.
+	 *
+	 * @param {module:engine/model/element~Element} element Element to unwrap.
+	 */
+	unwrap( element ) {
+		this._assertWriterUsedCorrectly();
+
+		if ( element.parent === null ) {
+			/**
+			 * Trying to unwrap an element which has no parent.
+			 *
+			 * @error writer-unwrap-element-no-parent
+			 */
+			throw new CKEditorError( 'writer-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
+		}
+
+		const delta = new UnwrapDelta();
+		this.batch.addDelta( delta );
+
+		const sourcePosition = Position.createFromParentAndOffset( element, 0 );
+		const moveVersion = sourcePosition.root.document ? sourcePosition.root.document.version : null;
+
+		const move = new MoveOperation(
+			sourcePosition,
+			element.maxOffset,
+			Position.createBefore( element ),
+			moveVersion
+		);
+
+		move.isSticky = true;
+		delta.addOperation( move );
+		this.model.applyOperation( move );
+
+		applyRemoveOperation( Position.createBefore( element ), 1, delta, this.model );
+	}
+
+	/**
+	 * Adds or updates a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
+	 * changes in the document and updates its range automatically, when model tree changes. Still, it is possible to change the
+	 * marker's range directly using this method.
+	 *
+	 * As the first parameter you can set marker name or instance. If none of them is provided, new marker, with a unique
+	 * name is created and returned.
+	 *
+	 * The `options.usingOperation` parameter lets you decide if the marker should be managed by operations or not. See
+	 * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
+	 * markers managed by operations and not-managed by operations. It is possible to change this option for an existing marker.
+	 * This is useful when a marker have been created earlier and then later, it needs to be added to the document history.
+	 *
+	 * Create/update marker directly base on marker's name:
+	 *
+	 * 		setMarker( markerName, range );
+	 *
+	 * Update marker using operation:
+	 *
+	 * 		setMarker( marker, range, { usingOperation: true } );
+	 * 		setMarker( markerName, range, { usingOperation: true } );
+	 *
+	 * Create marker with a unique id using operation:
+	 *
+	 * 		setMarker( range, { usingOperation: true } );
+	 *
+	 * Create marker directly without using operations:
+	 *
+	 * 		setMarker( range )
+	 *
+	 * Change marker's option (start using operations to manage it):
+	 *
+	 * 		setMarker( marker, { usingOperation: true } );
+	 *
+	 * Note: For efficiency reasons, it's best to create and keep as little markers as possible.
+	 *
+	 * @see module:engine/model/markercollection~Marker
+	 * @param {module:engine/model/markercollection~Marker|String} [markerOrName]
+	 * Name of a marker to create or update, or `Marker` instance to update, or range for the marker with a unique name.
+	 * @param {module:engine/model/range~Range} [range] Marker range.
+	 * @param {Object} [options]
+	 * @param {Boolean} [options.usingOperation=false] Flag indicated whether the marker should be added by MarkerOperation.
+	 * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
+	 * @returns {module:engine/model/markercollection~Marker} Marker that was set.
+	 */
+	setMarker( markerOrNameOrRange, rangeOrOptions, options ) {
+		this._assertWriterUsedCorrectly();
+
+		let markerName, newRange, usingOperation;
+
+		if ( markerOrNameOrRange instanceof Range ) {
+			markerName = uid();
+			newRange = markerOrNameOrRange;
+			usingOperation = !!rangeOrOptions && !!rangeOrOptions.usingOperation;
+		} else {
+			markerName = typeof markerOrNameOrRange === 'string' ? markerOrNameOrRange : markerOrNameOrRange.name;
+
+			if ( rangeOrOptions instanceof Range ) {
+				newRange = rangeOrOptions;
+				usingOperation = !!options && !!options.usingOperation;
+			} else {
+				newRange = null;
+				usingOperation = !!rangeOrOptions && !!rangeOrOptions.usingOperation;
+			}
+		}
+
+		const currentMarker = this.model.markers.get( markerName );
+
+		if ( !usingOperation ) {
+			if ( !newRange ) {
+				/**
+			 	 * Range parameter is required when adding a new marker.
+				 *
+				 * @error writer-setMarker-no-range
+				 */
+				throw new CKEditorError( 'writer-setMarker-no-range: Range parameter is required when adding a new marker.' );
+			}
+
+			// If marker changes to marker that do not use operations then we need to create additional operation
+			// that removes that marker first.
+			if ( currentMarker && currentMarker.managedUsingOperations && !usingOperation ) {
+				applyMarkerOperation( this, markerName, currentMarker.getRange(), null );
+			}
+
+			return this.model.markers._set( markerName, newRange, usingOperation );
+		}
+
+		if ( !newRange && !currentMarker ) {
+			throw new CKEditorError( 'writer-setMarker-no-range: Range parameter is required when adding a new marker.' );
+		}
+
+		const currentRange = currentMarker ? currentMarker.getRange() : null;
+
+		if ( !newRange ) {
+			// If `newRange` is not given, treat this as synchronizing existing marker.
+			// Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
+			applyMarkerOperation( this, markerName, null, currentRange );
+		} else {
+			// Just change marker range.
+			applyMarkerOperation( this, markerName, currentRange, newRange );
+		}
+
+		return this.model.markers.get( markerName );
+	}
+
+	/**
+	 * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
+	 * The marker is removed accordingly to how it has been created, so if the marker was created using operation,
+	 * it will be destroyed using operation.
+	 *
+	 * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove.
+	 */
+	removeMarker( markerOrName ) {
+		this._assertWriterUsedCorrectly();
+
+		const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
+
+		if ( !this.model.markers.has( name ) ) {
+			/**
+			 * Trying to remove marker which does not exist.
+			 *
+			 * @error writer-removeMarker-no-marker
+			 */
+			throw new CKEditorError( 'writer-removeMarker-no-marker: Trying to remove marker which does not exist.' );
+		}
+
+		const marker = this.model.markers.get( name );
+
+		if ( !marker.managedUsingOperations ) {
+			this.model.markers._remove( name );
+
+			return;
+		}
+
+		const oldRange = marker.getRange();
+
+		applyMarkerOperation( this, name, oldRange, null );
+	}
+
+	/**
+	 * Sets this selection's ranges and direction to the specified location based on the given
+	 * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/node~Node node}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
+	 *
+	 *		// Sets selection to the given range.
+	 *		const range = new Range( start, end );
+	 *		writer.setSelection( range );
+	 *
+	 *		// Sets selection to given ranges.
+	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		writer.setSelection( range );
+	 *
+	 *		// Sets selection to other selection.
+	 *		const otherSelection = new Selection();
+	 *		writer.setSelection( otherSelection );
+	 *
+	 * 		// Sets selection to the given document selection.
+	 *		const documentSelection = new DocumentSelection( doc );
+	 *		writer.setSelection( documentSelection );
+	 *
+	 * 		// Sets collapsed selection at the given position.
+	 *		const position = new Position( root, path );
+	 *		writer.setSelection( position );
+	 *
+	 * 		// Sets collapsed selection at the position of the given node and an offset.
+	 *		writer.setSelection( paragraph, offset );
+	 *
+	 * Creates a range inside an {@link module:engine/model/element~Element element} which starts before the first child of
+ 	 * that element and ends after the last child of that element.
+	 *
+	 *		writer.setSelection( paragraph, 'in' );
+	 *
+	 * Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends just after the item.
+	 *
+	 *		writer.setSelection( paragraph, 'on' );
+	 *
+	 * 		// Removes all selection's ranges.
+	 *		writer.setSelection( null );
+	 *
+	 * `Writer#setSelection()` allow passing additional options (`backward`) as the last argument.
+	 *
+	 * 		// Sets selection as backward.
+	 *		writer.setSelection( range, { backward: true } );
+	 *
+	 * Throws `writer-incorrect-use` error when the writer is used outside the `change()` block.
+	 *
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+	 * module:engine/model/position~Position|module:engine/model/node~Node|
+	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
+	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
+	 * @param {Object} [options]
+	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
+	 */
+	setSelection( selectable, placeOrOffset, options ) {
+		this._assertWriterUsedCorrectly();
+
+		this.model.document.selection._setTo( selectable, placeOrOffset, options );
+	}
+
+	/**
+	 * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
+	 *
+	 * The location can be specified in the same form as {@link module:engine/model/position~Position.createAt} parameters.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * first parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	setSelectionFocus( itemOrPosition, offset ) {
+		this._assertWriterUsedCorrectly();
+
+		this.model.document.selection._setFocus( itemOrPosition, offset );
+	}
+
+	/**
+	 * Sets attribute(s) on the selection. If attribute with the same key already is set, it's value is overwritten.
+	 *
+	 * Using key and value pair:
+	 *
+	 * 	writer.setSelectionAttribute( 'italic', true );
+	 *
+	 * Using key-value object:
+	 *
+	 * 	writer.setSelectionAttribute( { italic: true, bold: false } );
+	 *
+	 * Using iterable object:
+	 *
+	 * 	writer.setSelectionAttribute( new Map( [ [ 'italic', true ] ] ) );
+	 *
+	 * @param {String|Object|Iterable.<*>} keyOrObjectOrIterable Key of the attribute to set
+	 * or object / iterable of key - value attribute pairs.
+	 * @param {*} [value] Attribute value.
+	 */
+	setSelectionAttribute( keyOrObjectOrIterable, value ) {
+		this._assertWriterUsedCorrectly();
+
+		if ( typeof keyOrObjectOrIterable === 'string' ) {
+			this._setSelectionAttribute( keyOrObjectOrIterable, value );
+		} else {
+			for ( const [ key, value ] of toMap( keyOrObjectOrIterable ) ) {
+				this._setSelectionAttribute( key, value );
+			}
+		}
+	}
+
+	/**
+	 * Removes attribute(s) with given key(s) from the selection.
+	 *
+	 * Using key
+	 *
+	 * 	writer.removeSelectionAttribute( 'italic' );
+	 *
+	 * Using iterable of keys
+	 *
+	 * 	writer.removeSelectionAttribute( [ 'italic', 'bold' ] );
+	 *
+	 * @param {String|Iterable.<String>} keyOrIterableOfKeys Key of the attribute to remove or an iterable of attribute keys to remove.
+	 */
+	removeSelectionAttribute( keyOrIterableOfKeys ) {
+		this._assertWriterUsedCorrectly();
+
+		if ( typeof keyOrIterableOfKeys === 'string' ) {
+			this._removeSelectionAttribute( keyOrIterableOfKeys );
+		} else {
+			for ( const key of keyOrIterableOfKeys ) {
+				this._removeSelectionAttribute( key );
+			}
+		}
+	}
+
+	/**
+	 * Temporarily changes the {@link module:engine/model/documentselection~DocumentSelection#isGravityOverridden gravity}
+	 * of the selection from left to right.
+	 *
+	 * The gravity defines from which direction the selection inherits its attributes. If it's the default left gravity,
+	 * then the selection (after being moved by the user) inherits attributes from its left-hand side.
+	 * This method allows to temporarily override this behavior by forcing the gravity to the right.
+	 *
+	 * For the following model fragment:
+	 *
+	 *		<$text bold="true" linkHref="url">bar[]</$text><$text bold="true">biz</$text>
+	 *
+	 * * Default gravity: selection will have the `bold` and `linkHref` attributes.
+	 * * Overridden gravity: selection will have `bold` attribute.
+	 *
+	 * By default the selection's gravity is automatically restored just after a direct selection change (when user
+	 * moved the caret) but you can pass `customRestore = true` in which case you will have to call
+	 * {@link ~Writer#restoreSelectionGravity} manually.
+	 *
+	 * When the selection's gravity is overridden more than once without being restored in the meantime then it needs
+	 * to be restored the same number of times. This is to prevent conflicts when
+	 * more than one feature want to independently override and restore the selection's gravity.
+	 *
+	 * @param {Boolean} [customRestore=false] When `true` then gravity won't be restored until
+	 * {@link ~Writer#restoreSelectionGravity} will be called directly. When `false` then gravity is restored
+	 * after selection is moved by user.
+	 */
+	overrideSelectionGravity( customRestore ) {
+		this.model.document.selection._overrideGravity( customRestore );
+	}
+
+	/**
+	 * Restores {@link ~Writer#overrideSelectionGravity} gravity to default.
+	 *
+	 * Note that the gravity remains overridden as long as will not be restored the same number of times as it was overridden.
+	 */
+	restoreSelectionGravity() {
+		this.model.document.selection._restoreGravity();
+	}
+
+	/**
+	 * @private
+	 * @param {String} key Key of the attribute to remove.
+	 * @param {*} value Attribute value.
+	 */
+	_setSelectionAttribute( key, value ) {
+		const selection = this.model.document.selection;
+
+		// Store attribute in parent element if the selection is collapsed in an empty node.
+		if ( selection.isCollapsed && selection.anchor.parent.isEmpty ) {
+			const storeKey = DocumentSelection._getStoreAttributeKey( key );
+
+			this.setAttribute( storeKey, value, selection.anchor.parent );
+		}
+
+		selection._setAttribute( key, value );
+	}
+
+	/**
+	 * @private
+	 * @param {String} key Key of the attribute to remove.
+	 */
+	_removeSelectionAttribute( key ) {
+		const selection = this.model.document.selection;
+
+		// Remove stored attribute from parent element if the selection is collapsed in an empty node.
+		if ( selection.isCollapsed && selection.anchor.parent.isEmpty ) {
+			const storeKey = DocumentSelection._getStoreAttributeKey( key );
+
+			this.removeAttribute( storeKey, selection.anchor.parent );
+		}
+
+		selection._removeAttribute( key );
+	}
+
+	/**
+	 * Throws `writer-detached-writer-tries-to-modify-model` error when the writer is used outside of the `change()` block.
+	 *
+	 * @private
+	 */
+	_assertWriterUsedCorrectly() {
+		/**
+		 * Trying to use a writer outside a {@link module:engine/model/model~Model#change `change()` or
+		 * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`} blocks.
+		 *
+		 * The writer can only be used inside these blocks which ensures that the model
+		 * can only be changed during such "sessions".
+		 *
+		 * @error writer-incorrect-use
+		 */
+		if ( this.model._currentWriter !== this ) {
+			throw new CKEditorError( 'writer-incorrect-use: Trying to use a writer outside the change() block.' );
+		}
 	}
 }
 
-/**
- * Checks if given position is in a text node, and if so, splits the text node in two text nodes, each of them
- * containing a part of original text node.
- *
- * @ignore
- * @private
- * @param {module:engine/model/position~Position} position Position at which node should be split.
- */
-function _splitNodeAtPosition( position ) {
-	const textNode = position.textNode;
-	const element = position.parent;
+// Sets given attribute to each node in given range. When attribute value is null then attribute will be removed.
+//
+// Because attribute operation needs to have the same attribute value on the whole range, this function splits
+// the range into smaller parts.
+//
+// @private
+// @param {module:engine/model/writer~Writer} writer
+// @param {String} key Attribute key.
+// @param {*} value Attribute new value.
+// @param {module:engine/model/range~Range} range Model range on which the attribute will be set.
+function setAttributeOnRange( writer, key, value, range ) {
+	const delta = new AttributeDelta();
+	const model = writer.model;
+	const doc = model.document;
+
+	// Position of the last split, the beginning of the new range.
+	let lastSplitPosition = range.start;
+
+	// Currently position in the scanning range. Because we need value after the position, it is not a current
+	// position of the iterator but the previous one (we need to iterate one more time to get the value after).
+	let position;
+
+	// Value before the currently position.
+	let valueBefore;
+
+	// Value after the currently position.
+	let valueAfter;
+
+	for ( const val of range ) {
+		valueAfter = val.item.getAttribute( key );
+
+		// At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
+		// because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).
+		if ( position && valueBefore != valueAfter ) {
+			// if valueBefore == value there is nothing to change, so we add operation only if these values are different.
+			if ( valueBefore != value ) {
+				addOperation();
+			}
+
+			lastSplitPosition = position;
+		}
 
-	if ( textNode ) {
-		const offsetDiff = position.offset - textNode.startOffset;
-		const index = textNode.index;
+		position = val.nextPosition;
+		valueBefore = valueAfter;
+	}
 
-		element.removeChildren( index, 1 );
+	// Because position in the loop is not the iterator position (see let position comment), the last position in
+	// the while loop will be last but one position in the range. We need to check the last position manually.
+	if ( position instanceof Position && position != lastSplitPosition && valueBefore != value ) {
+		addOperation();
+	}
+
+	function addOperation() {
+		// Add delta to the batch only if there is at least operation in the delta. Add delta only once.
+		if ( delta.operations.length === 0 ) {
+			writer.batch.addDelta( delta );
+		}
 
-		const firstPart = new Text( textNode.data.substr( 0, offsetDiff ), textNode.getAttributes() );
-		const secondPart = new Text( textNode.data.substr( offsetDiff ), textNode.getAttributes() );
+		const range = new Range( lastSplitPosition, position );
+		const version = range.root.document ? doc.version : null;
+		const operation = new AttributeOperation( range, key, valueBefore, value, version );
 
-		element.insertChildren( index, [ firstPart, secondPart ] );
+		delta.addOperation( operation );
+		model.applyOperation( operation );
 	}
 }
 
-/**
- * Checks whether two given nodes have same attributes.
- *
- * @ignore
- * @private
- * @param {module:engine/model/node~Node} nodeA Node to check.
- * @param {module:engine/model/node~Node} nodeB Node to check.
- * @returns {Boolean} `true` if nodes have same attributes, `false` otherwise.
- */
-function _haveSameAttributes( nodeA, nodeB ) {
-	const iteratorA = nodeA.getAttributes();
-	const iteratorB = nodeB.getAttributes();
+// Sets given attribute to the given node. When attribute value is null then attribute will be removed.
+//
+// @private
+// @param {module:engine/model/writer~Writer} writer
+// @param {String} key Attribute key.
+// @param {*} value Attribute new value.
+// @param {module:engine/model/item~Item} item Model item on which the attribute will be set.
+function setAttributeOnItem( writer, key, value, item ) {
+	const model = writer.model;
+	const doc = model.document;
+	const previousValue = item.getAttribute( key );
+	let range, operation;
+
+	if ( previousValue != value ) {
+		const isRootChanged = item.root === item;
 
-	for ( const attr of iteratorA ) {
-		if ( attr[ 1 ] !== nodeB.getAttribute( attr[ 0 ] ) ) {
-			return false;
+		const delta = isRootChanged ? new RootAttributeDelta() : new AttributeDelta();
+		writer.batch.addDelta( delta );
+
+		if ( isRootChanged ) {
+			// If we change attributes of root element, we have to use `RootAttributeOperation`.
+			const version = item.document ? doc.version : null;
+
+			operation = new RootAttributeOperation( item, key, previousValue, value, version );
+		} else {
+			if ( item.is( 'element' ) ) {
+				// If we change the attribute of the element, we do not want to change attributes of its children, so
+				// the end of the range cannot be after the closing tag, it should be inside that element, before any of
+				// it's children, so the range will contain only the opening tag.
+				range = new Range( Position.createBefore( item ), Position.createFromParentAndOffset( item, 0 ) );
+			} else {
+				// If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change
+				// all characters represented by it.
+				range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
+			}
+
+			const version = range.root.document ? doc.version : null;
+
+			operation = new AttributeOperation( range, key, previousValue, value, version );
 		}
 
-		iteratorB.next();
+		delta.addOperation( operation );
+		model.applyOperation( operation );
+	}
+}
+
+// Creates and applies marker operation to {@link module:engine/model/delta/delta~Delta delta}.
+//
+// @private
+// @param {module:engine/model/writer~Writer} writer
+// @param {String} name Marker name.
+// @param {module:engine/model/range~Range} oldRange Marker range before the change.
+// @param {module:engine/model/range~Range} newRange Marker range after the change.
+function applyMarkerOperation( writer, name, oldRange, newRange ) {
+	const model = writer.model;
+	const doc = model.document;
+	const delta = new MarkerDelta();
+
+	const operation = new MarkerOperation( name, oldRange, newRange, model.markers, doc.version );
+
+	writer.batch.addDelta( delta );
+	delta.addOperation( operation );
+	model.applyOperation( operation );
+}
+
+// Creates `RemoveOperation` or `DetachOperation` that removes `howMany` nodes starting from `position`.
+// The operation will be applied on given model instance and added to given delta instance.
+//
+// @private
+// @param {module:engine/model/position~Position} position Position from which nodes are removed.
+// @param {Number} howMany Number of nodes to remove.
+// @param {module:engine/model/delta~Delta} delta Delta to add new operation to.
+// @param {module:engine/model/model~Model} model Model instance on which operation will be applied.
+function applyRemoveOperation( position, howMany, delta, model ) {
+	let operation;
+
+	if ( position.root.document ) {
+		const doc = model.document;
+		const graveyardPosition = new Position( doc.graveyard, [ 0 ] );
+
+		operation = new RemoveOperation( position, howMany, graveyardPosition, doc.version );
+	} else {
+		operation = new DetachOperation( position, howMany );
 	}
 
-	return iteratorB.next().done;
+	delta.addOperation( operation );
+	model.applyOperation( operation );
 }
 
-/**
- * Value that can be normalized to an array of {@link module:engine/model/node~Node nodes}.
- *
- * Non-arrays are normalized as follows:
- * * {@link module:engine/model/node~Node Node} is left as is,
- * * {@link module:engine/model/textproxy~TextProxy TextProxy} and `String` are normalized to {@link module:engine/model/text~Text Text},
- * * {@link module:engine/model/nodelist~NodeList NodeList} is normalized to an array containing all nodes that are in that node list,
- * * {@link module:engine/model/documentfragment~DocumentFragment DocumentFragment} is normalized to an array containing all of it's
- * * children.
- *
- * Arrays are processed item by item like non-array values and flattened to one array. Normalization always results in
- * a flat array of {@link module:engine/model/node~Node nodes}. Consecutive text nodes (or items normalized to text nodes) will be
- * merged if they have same attributes.
- *
- * @typedef {module:engine/model/node~Node|module:engine/model/textproxy~TextProxy|String|
- * module:engine/model/nodelist~NodeList|module:engine/model/documentfragment~DocumentFragment|Iterable}
- * module:engine/model/node~NodeSet
- */
+// Returns `true` if both root elements are the same element or both are documents root elements.
+//
+// Elements in the same tree can be moved (for instance you can move element form one documents root to another, or
+// within the same document fragment), but when element supposed to be moved from document fragment to the document, or
+// to another document it should be removed and inserted to avoid problems with OT. This is because features like undo or
+// collaboration may track changes on the document but ignore changes on detached fragments and should not get
+// unexpected `move` operation.
+function isSameTree( rootA, rootB ) {
+	// If it is the same root this is the same tree.
+	if ( rootA === rootB ) {
+		return true;
+	}
+
+	// If both roots are documents root it is operation within the document what we still treat as the same tree.
+	if ( rootA instanceof RootElement && rootB instanceof RootElement ) {
+		return true;
+	}
+
+	return false;
+}

+ 141 - 0
packages/ckeditor5-engine/src/utils/bindtwostepcarettoattribute.js

@@ -0,0 +1,141 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/utils/bindtwostepcarettoattribute
+ */
+
+import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+
+/**
+ * This helper adds two-step caret movement behavior for the given attribute.
+ *
+ * For example, when this behavior is enabled for the `linkHref` attribute (which converts to `<a>` element in the view)
+ * and the caret is just before an `<a>` element (at a link boundary), then pressing
+ * the right arrow key will move caret into that `<a>`element instead of moving it after the next character:
+ *
+ * * With two-step caret movement: `<p>foo{}<a>bar</a>biz<p>` + <kbd>→</kbd> => `<p>foo<a>{}bar</a>biz<p>`
+ * * Without two-step caret movement: `<p>foo{}<a>bar</a>biz<p>` + <kbd>→</kbd> => `<p>foo<a>b{}ar</a>biz<p>`
+ *
+ * The same behavior will be changed fo "leaving" an attribute element:
+ *
+ * * With two-step caret movement: `<p>foo<a>bar{}</a>biz<p>` + <kbd>→</kbd> => `<p>foo<a>bar</a>{}biz<p>`
+ * * Without two-step caret movement: `<p>foo<a>bar{}</a>biz<p>` + <kbd>→</kbd> => `<p>foo<a>bar</a>b{}iz<p>`
+ *
+ * And when moving left:
+ *
+ * * With two-step caret movement: `<p>foo<a>bar</a>b{}iz<p>` + <kbd>←</kbd> => `<p>foo<a>bar</a>{}biz<p>` +
+ * <kbd>←</kbd> => `<p>foo<a>bar{}</a>biz<p>`
+ * * Without two-step caret movement: `<p>foo<a>bar</a>b{}iz<p>` + <kbd>←</kbd> => `<p>foo<a>bar{}</a>biz<p>`
+ *
+ * @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
+ * (e.g. a plugin instance).
+ * @param {String} attribute Attribute for which this behavior will be added.
+ */
+export default function bindTwoStepCaretToAttribute( view, model, emitter, attribute ) {
+	const modelSelection = model.document.selection;
+
+	// Listen to keyboard events and handle cursor before the move.
+	emitter.listenTo( view.document, 'keydown', ( evt, data ) => {
+		const arrowRightPressed = data.keyCode == keyCodes.arrowright;
+		const arrowLeftPressed = data.keyCode == keyCodes.arrowleft;
+
+		// When neither left or right arrow has been pressed then do noting.
+		if ( !arrowRightPressed && !arrowLeftPressed ) {
+			return;
+		}
+
+		// This implementation works only for collapsed selection.
+		if ( !modelSelection.isCollapsed ) {
+			return;
+		}
+
+		// When user tries to expand selection or jump over the whole word or to the beginning/end then
+		// two-steps movement is not necessary.
+		if ( data.shiftKey || data.altKey || data.ctrlKey ) {
+			return;
+		}
+
+		const position = modelSelection.getFirstPosition();
+
+		// Moving right.
+		if ( arrowRightPressed ) {
+			// If gravity is already overridden then do nothing.
+			// It means that we already enter `foo<a>{}bar</a>biz` or left `foo<a>bar</a>{}biz` text with attribute
+			// and gravity will be restored just after caret movement.
+			if ( modelSelection.isGravityOverridden ) {
+				return;
+			}
+
+			// If caret sticks to the bound of Text with attribute it means that we are going to
+			// enter `foo{}<a>bar</a>biz` or leave `foo<a>bar{}</a>biz` the text with attribute.
+			if ( isAtAttributeBoundary( position.nodeAfter, position.nodeBefore, attribute ) ) {
+				// So we need to prevent caret from being moved.
+				data.preventDefault();
+				// And override default selection gravity.
+				model.change( writer => writer.overrideSelectionGravity() );
+			}
+
+		// Moving left.
+		} else {
+			// If caret sticks to the bound of Text with attribute and gravity is already overridden it means that
+			// we are going to enter `foo<a>bar</a>{}biz` or leave `foo<a>{}bar</a>biz` text with attribute.
+			if ( modelSelection.isGravityOverridden && isAtAttributeBoundary( position.nodeBefore, position.nodeAfter, attribute ) ) {
+				// So we need to prevent cater from being moved.
+				data.preventDefault();
+				// And restore the gravity.
+				model.change( writer => writer.restoreSelectionGravity() );
+
+				return;
+			}
+
+			// If we are here we need to check if caret is a one character before the text with attribute bound
+			// `foo<a>bar</a>b{}iz` or `foo<a>b{}ar</a>biz`.
+			const nextPosition = position.getShiftedBy( -1 );
+
+			// When position is the same it means that parent bound has been reached.
+			if ( !nextPosition.isBefore( position ) ) {
+				return;
+			}
+
+			// When caret is going stick to the bound of Text with attribute after movement then we need to override
+			// the gravity before the move. But we need to do it in a custom way otherwise `selection#change:range`
+			// event following the overriding will restore the gravity.
+			if ( isAtAttributeBoundary( nextPosition.nodeBefore, nextPosition.nodeAfter, attribute ) ) {
+				model.change( writer => {
+					let counter = 0;
+
+					// So let's override the gravity.
+					writer.overrideSelectionGravity( true );
+
+					// But skip the following `change:range` event and restore the gravity on the next one.
+					emitter.listenTo( modelSelection, 'change:range', ( evt, data ) => {
+						if ( counter++ && data.directChange ) {
+							writer.restoreSelectionGravity();
+							evt.off();
+						}
+					} );
+				} );
+			}
+		}
+	} );
+}
+
+// @param {module:engine/model/node~Node} nextNode Node before the position.
+// @param {module:engine/model/node~Node} prevNode Node after the position.
+// @param {String} attribute Attribute name.
+// @returns {Boolean} `true` when position between the nodes sticks to the bound of text with given attribute.
+function isAtAttributeBoundary( nextNode, prevNode, attribute ) {
+	const isAttrInNext = nextNode ? nextNode.hasAttribute( attribute ) : false;
+	const isAttrInPrev = prevNode ? prevNode.hasAttribute( attribute ) : false;
+
+	if ( isAttrInNext && isAttrInPrev && nextNode.getAttributeKeys( attribute ) !== prevNode.getAttribute( attribute ) ) {
+		return true;
+	}
+
+	return isAttrInNext && !isAttrInPrev || !isAttrInNext && isAttrInPrev;
+}

+ 32 - 18
packages/ckeditor5-engine/src/view/attributeelement.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -14,7 +14,7 @@ const DEFAULT_PRIORITY = 10;
 
 /**
  * Attributes are elements which define document presentation. They are mostly elements like `<b>` or `<span>`.
- * Attributes can be broken and merged by the {@link module:engine/view/writer~writer view writer}.
+ * Attributes can be broken and merged by the {@link module:engine/view/writer~Writer view writer}.
  *
  * Editing engine does not define fixed HTML DTD. This is why the type of the {@link module:engine/view/element~Element} need to
  * be defined by the feature developer. Creating an element you should use {@link module:engine/view/containerelement~ContainerElement}
@@ -26,6 +26,8 @@ export default class AttributeElement extends Element {
 	/**
 	 * Creates a attribute element.
 	 *
+	 * @see module:engine/view/writer~Writer#createAttributeElement
+	 * @protected
 	 * @see module:engine/view/element~Element
 	 */
 	constructor( name, attrs, children ) {
@@ -36,9 +38,10 @@ export default class AttributeElement extends Element {
 		 * {@link module:engine/view/element~Element#isSimilar similar}. Setting different priorities on similar
  		 * nodes may prevent merging, e.g. two `<abbr>` nodes next each other shouldn't be merged.
 		 *
+		 * @protected
 		 * @member {Number}
 		 */
-		this.priority = DEFAULT_PRIORITY;
+		this._priority = DEFAULT_PRIORITY;
 
 		/**
 		 * Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed.
@@ -49,6 +52,16 @@ export default class AttributeElement extends Element {
 		this.getFillerOffset = getFillerOffset;
 	}
 
+	/**
+	 * Priority of this element.
+	 *
+	 * @readonly
+	 * @return {Number}
+	 */
+	get priority() {
+		return this._priority;
+	}
+
 	/**
 	 * @inheritDoc
 	 */
@@ -60,33 +73,34 @@ export default class AttributeElement extends Element {
 		}
 	}
 
+	/**
+	 * Checks if this element is similar to other element.
+	 * Both elements should have the same name, attributes and priority to be considered as similar.
+	 * Two similar elements can contain different set of children nodes.
+	 *
+	 * @param {module:engine/view/element~Element} otherElement
+	 * @returns {Boolean}
+	 */
+	isSimilar( otherElement ) {
+		return super.isSimilar( otherElement ) && this.priority == otherElement.priority;
+	}
+
 	/**
 	 * Clones provided element with priority.
 	 *
+	 * @protected
 	 * @param {Boolean} deep If set to `true` clones element and all its children recursively. When set to `false`,
 	 * element will be cloned without any children.
 	 * @returns {module:engine/view/attributeelement~AttributeElement} Clone of this element.
 	 */
-	clone( deep ) {
-		const cloned = super.clone( deep );
+	_clone( deep ) {
+		const cloned = super._clone( deep );
 
 		// Clone priority too.
-		cloned.priority = this.priority;
+		cloned._priority = this._priority;
 
 		return cloned;
 	}
-
-	/**
-	 * Checks if this element is similar to other element.
-	 * Both elements should have the same name, attributes and priority to be considered as similar.
-	 * Two similar elements can contain different set of children nodes.
-	 *
-	 * @param {module:engine/view/element~Element} otherElement
-	 * @returns {Boolean}
-	 */
-	isSimilar( otherElement ) {
-		return super.isSimilar( otherElement ) && this.priority == otherElement.priority;
-	}
 }
 
 /**

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

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -23,8 +23,8 @@ import Element from './element';
  * DOM properly. {@link module:engine/view/domconverter~DomConverter} will ensure that `ContainerElement` is editable and it is possible
  * to put caret inside it, even if the container is empty.
  *
- * Secondly, {@link module:engine/view/writer~writer view writer} uses this information.
- * Nodes {@link module:engine/view/writer~writer.breakAttributes breaking} and {@link module:engine/view/writer~writer.mergeAttributes
+ * Secondly, {@link module:engine/view/writer~Writer view writer} uses this information.
+ * Nodes {@link module:engine/view/writer~Writer#breakAttributes breaking} and {@link module:engine/view/writer~Writer#mergeAttributes
  * merging}
  * is performed only in a bounds of a container nodes.
  *
@@ -32,7 +32,7 @@ import Element from './element';
  *
  *		<p><b>fo^o</b></p>
  *
- * {@link module:engine/view/writer~writer.breakAttributes breakAttributes} will create:
+ * {@link module:engine/view/writer~Writer#breakAttributes breakAttributes} will create:
  *
  *		<p><b>fo</b><b>o</b></p>
  *
@@ -49,6 +49,8 @@ export default class ContainerElement extends Element {
 	 * Creates a container element.
 	 *
 	 * @see module:engine/view/element~Element
+	 * @see module:engine/view/writer~Writer#createContainerElement
+	 * @protected
 	 */
 	constructor( name, attrs, children ) {
 		super( name, attrs, children );

+ 52 - 262
packages/ckeditor5-engine/src/view/document.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,37 +8,13 @@
  */
 
 import Selection from './selection';
-import Renderer from './renderer';
-import DomConverter from './domconverter';
-import RootEditableElement from './rooteditableelement';
-import { injectQuirksHandling } from './filler';
-import { injectUiElementHandling } from './uielement';
-import log from '@ckeditor/ckeditor5-utils/src/log';
-import MutationObserver from './observer/mutationobserver';
-import SelectionObserver from './observer/selectionobserver';
-import FocusObserver from './observer/focusobserver';
-import KeyObserver from './observer/keyobserver';
-import FakeSelectionObserver from './observer/fakeselectionobserver';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
-import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll';
 
 /**
- * Document class creates an abstract layer over the content editable area.
- * It combines the actual tree of view elements, tree of DOM elements,
- * {@link module:engine/view/domconverter~DomConverter DOM Converter}, {@link module:engine/view/renderer~Renderer renderer} and all
- * {@link module:engine/view/observer/observer~Observer observers}.
- *
- * If you want to only transform the tree of view elements to the DOM elements you can use the
- * {@link module:engine/view/domconverter~DomConverter DomConverter}.
- *
- * Note that the following observers are added by the class constructor and are always available:
- *
- * * {@link module:engine/view/observer/mutationobserver~MutationObserver},
- * * {@link module:engine/view/observer/selectionobserver~SelectionObserver},
- * * {@link module:engine/view/observer/focusobserver~FocusObserver},
- * * {@link module:engine/view/observer/keyobserver~KeyObserver},
- * * {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}.
+ * Document class creates an abstract layer over the content editable area, contains a tree of view elements and
+ * {@link module:engine/view/selection~Selection view selection} associated with this document.
  *
  * @mixes module:utils/observablemixin~ObservableMixin
  */
@@ -47,14 +23,6 @@ export default class Document {
 	 * Creates a Document instance.
 	 */
 	constructor() {
-		/**
-		 * Roots of the DOM tree. Map on the `HTMLElement`s with roots names as keys.
-		 *
-		 * @readonly
-		 * @member {Map} module:engine/view/document~Document#domRoots
-		 */
-		this.domRoots = new Map();
-
 		/**
 		 * Selection done on this document.
 		 *
@@ -64,22 +32,17 @@ export default class Document {
 		this.selection = new Selection();
 
 		/**
-		 * Instance of the {@link module:engine/view/domconverter~DomConverter domConverter} use by
-		 * {@link module:engine/view/document~Document#renderer renderer}
-		 * and {@link module:engine/view/observer/observer~Observer observers}.
+		 * Roots of the view tree. Collection of the {module:engine/view/element~Element view elements}.
 		 *
-		 * @readonly
-		 * @member {module:engine/view/domconverter~DomConverter} module:engine/view/document~Document#domConverter
-		 */
-		this.domConverter = new DomConverter();
-
-		/**
-		 * Roots of the view tree. Map of the {module:engine/view/element~Element view elements} with roots names as keys.
+		 * View roots are created as a result of binding between {@link module:engine/view/document~Document#roots} and
+		 * {@link module:engine/model/document~Document#roots} and this is handled by
+		 * {@link module:engine/controller/editingcontroller~EditingController}, so to create view root we need to create
+		 * model root using {@link module:engine/model/document~Document#createRoot}.
 		 *
 		 * @readonly
-		 * @member {Map} module:engine/view/document~Document#roots
+		 * @member {Collection} module:engine/view/document~Document#roots
 		 */
-		this.roots = new Map();
+		this.roots = new Collection( { idProperty: 'rootName' } );
 
 		/**
 		 * Defines whether document is in read-only mode.
@@ -104,145 +67,24 @@ export default class Document {
 		this.set( 'isFocused', false );
 
 		/**
-		 * Instance of the {@link module:engine/view/document~Document#renderer renderer}.
+		 * True if composition is in progress inside the document.
+		 *
+		 * This property is updated by the {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
+		 * If the {@link module:engine/view/observer/compositionobserver~CompositionObserver} is disabled this property will not change.
 		 *
 		 * @readonly
-		 * @member {module:engine/view/renderer~Renderer} module:engine/view/document~Document#renderer
+		 * @observable
+		 * @member {Boolean} module:engine/view/document~Document#isComposing
 		 */
-		this.renderer = new Renderer( this.domConverter, this.selection );
-		this.renderer.bind( 'isFocused' ).to( this );
+		this.set( 'isComposing', false );
 
 		/**
-		 * Map of registered {@link module:engine/view/observer/observer~Observer observers}.
+		 * Post-fixer callbacks registered to the view document.
 		 *
 		 * @private
-		 * @member {Map.<Function, module:engine/view/observer/observer~Observer>} module:engine/view/document~Document#_observers
+		 * @member {Set}
 		 */
-		this._observers = new Map();
-
-		// Add default observers.
-		this.addObserver( MutationObserver );
-		this.addObserver( SelectionObserver );
-		this.addObserver( FocusObserver );
-		this.addObserver( KeyObserver );
-		this.addObserver( FakeSelectionObserver );
-
-		injectQuirksHandling( this );
-		injectUiElementHandling( this );
-
-		this.decorate( 'render' );
-	}
-
-	/**
-	 * Creates observer of the given type if not yet created, {@link module:engine/view/observer/observer~Observer#enable enables} it
-	 * and {@link module:engine/view/observer/observer~Observer#observe attaches} to all existing and future
-	 * {@link module:engine/view/document~Document#domRoots DOM roots}.
-	 *
-	 * Note: Observers are recognized by their constructor (classes). A single observer will be instantiated and used only
-	 * when registered for the first time. This means that features and other components can register a single observer
-	 * multiple times without caring whether it has been already added or not.
-	 *
-	 * @param {Function} Observer The constructor of an observer to add.
-	 * Should create an instance inheriting from {@link module:engine/view/observer/observer~Observer}.
-	 * @returns {module:engine/view/observer/observer~Observer} Added observer instance.
-	 */
-	addObserver( Observer ) {
-		let observer = this._observers.get( Observer );
-
-		if ( observer ) {
-			return observer;
-		}
-
-		observer = new Observer( this );
-
-		this._observers.set( Observer, observer );
-
-		for ( const [ name, domElement ] of this.domRoots ) {
-			observer.observe( domElement, name );
-		}
-
-		observer.enable();
-
-		return observer;
-	}
-
-	/**
-	 * Returns observer of the given type or `undefined` if such observer has not been added yet.
-	 *
-	 * @param {Function} Observer The constructor of an observer to get.
-	 * @returns {module:engine/view/observer/observer~Observer|undefined} Observer instance or undefined.
-	 */
-	getObserver( Observer ) {
-		return this._observers.get( Observer );
-	}
-
-	/**
-	 * Creates a {@link module:engine/view/document~Document#roots view root element}.
-	 *
-	 * If the DOM element is passed as a first parameter it will be automatically
-	 * {@link module:engine/view/document~Document#attachDomRoot attached}:
-	 *
-	 *		document.createRoot( document.querySelector( 'div#editor' ) ); // Will call document.attachDomRoot.
-	 *
-	 * However, if the string is passed, then only the view element will be created and the DOM element have to be
-	 * attached separately:
-	 *
-	 *		document.createRoot( 'body' );
-	 *		document.attachDomRoot( document.querySelector( 'body#editor' ) );
-	 *
-	 * In both cases, {@link module:engine/view/rooteditableelement~RootEditableElement#rootName element name} is always
-	 * transformed to lower
-	 * case.
-	 *
-	 * @param {Element|String} domRoot DOM root element or the tag name of view root element if the DOM element will be
-	 * attached later.
-	 * @param {String} [name='main'] Name of the root.
-	 * @returns {module:engine/view/rooteditableelement~RootEditableElement} The created view root element.
-	 */
-	createRoot( domRoot, name = 'main' ) {
-		const rootTag = typeof domRoot == 'string' ? domRoot : domRoot.tagName;
-
-		const viewRoot = new RootEditableElement( rootTag.toLowerCase(), name );
-		viewRoot.document = this;
-
-		this.roots.set( name, viewRoot );
-
-		// Mark changed nodes in the renderer.
-		viewRoot.on( 'change:children', ( evt, node ) => this.renderer.markToSync( 'children', node ) );
-		viewRoot.on( 'change:attributes', ( evt, node ) => this.renderer.markToSync( 'attributes', node ) );
-		viewRoot.on( 'change:text', ( evt, node ) => this.renderer.markToSync( 'text', node ) );
-
-		if ( this.domConverter.isElement( domRoot ) ) {
-			this.attachDomRoot( domRoot, name );
-		}
-
-		return viewRoot;
-	}
-
-	/**
-	 * Attaches DOM root element to the view element and enable all observers on that element. This method also
-	 * {@link module:engine/view/renderer~Renderer#markToSync mark element} to be synchronized with the view what means that all child
-	 * nodes will be removed and replaced with content of the view root.
-	 *
-	 * Note that {@link module:engine/view/document~Document#createRoot} will call this method automatically if the DOM element is
-	 * passed to it.
-	 *
-	 * @param {Element|String} domRoot DOM root element.
-	 * @param {String} [name='main'] Name of the root.
-	 */
-	attachDomRoot( domRoot, name = 'main' ) {
-		const viewRoot = this.getRoot( name );
-
-		this.domRoots.set( name, domRoot );
-
-		this.domConverter.bindElements( domRoot, viewRoot );
-
-		this.renderer.markToSync( 'children', viewRoot );
-		this.renderer.domDocuments.add( domRoot.ownerDocument );
-
-		for ( const observer of this._observers.values() ) {
-			observer.observe( domRoot, name );
-		}
+		this._postFixers = new Set();
 	}
 
 	/**
@@ -250,98 +92,53 @@ export default class Document {
 	 * specific "main" root is returned.
 	 *
 	 * @param {String} [name='main'] Name of the root.
-	 * @returns {module:engine/view/rooteditableelement~RootEditableElement} The view root element with the specified name.
+	 * @returns {module:engine/view/rooteditableelement~RootEditableElement|null} The view root element with the specified name
+	 * or null when there is no root of given name.
 	 */
 	getRoot( name = 'main' ) {
 		return this.roots.get( name );
 	}
 
 	/**
-	 * Gets DOM root element.
+	 * Used to register a post-fixer callback. A post-fixers mechanism allows to update view tree just before rendering
+	 * to the DOM.
 	 *
-	 * @param {String} [name='main']  Name of the root.
-	 * @returns {Element} DOM root element instance.
+	 * Post-fixers are fired just 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}.
+	 *
+	 * As a parameter, a post-fixer callback receives a {@link module:engine/view/writer~Writer writer} instance connected with the
+	 * executed changes block.
+	 *
+	 * @param {Function} postFixer
 	 */
-	getDomRoot( name = 'main' ) {
-		return this.domRoots.get( name );
+	registerPostFixer( postFixer ) {
+		this._postFixers.add( postFixer );
 	}
 
 	/**
-	 * Renders all changes. In order to avoid triggering the observers (e.g. mutations) all observers are disabled
-	 * before rendering and re-enabled after that.
+	 * Performs post-fixer loops. Executes post-fixer callbacks as long as none of them has done any changes to the model.
 	 *
-	 * @fires render
+	 * @protected
+	 * @param {module:engine/view/writer~Writer} writer
 	 */
-	render() {
-		this.disableObservers();
-		this.renderer.render();
-		this.enableObservers();
-	}
+	_callPostFixers( writer ) {
+		let wasFixed = false;
 
-	/**
-	 * Focuses document. It will focus {@link module:engine/view/editableelement~EditableElement EditableElement} that is currently having
-	 * selection inside.
-	 */
-	focus() {
-		if ( !this.isFocused ) {
-			const editable = this.selection.editableElement;
+		do {
+			for ( const callback of this._postFixers ) {
+				wasFixed = callback( writer );
 
-			if ( editable ) {
-				this.domConverter.focus( editable );
-				this.render();
-			} 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.' );
+				if ( wasFixed ) {
+					break;
+				}
 			}
-		}
-	}
-
-	/**
-	 * Scrolls the page viewport and {@link #domRoots} with their ancestors to reveal the
-	 * caret, if not already visible to the user.
-	 */
-	scrollToTheSelection() {
-		const range = this.selection.getFirstRange();
-
-		if ( range ) {
-			scrollViewportToShowTarget( {
-				target: this.domConverter.viewRangeToDom( range ),
-				viewportOffset: 20
-			} );
-		}
-	}
-
-	/**
-	 * Disables all added observers.
-	 */
-	disableObservers() {
-		for ( const observer of this._observers.values() ) {
-			observer.disable();
-		}
-	}
-
-	/**
-	 * Enables all added observers.
-	 */
-	enableObservers() {
-		for ( const observer of this._observers.values() ) {
-			observer.enable();
-		}
-	}
-
-	/**
-	 * Destroys all observers created by view `Document`.
-	 */
-	destroy() {
-		for ( const observer of this._observers.values() ) {
-			observer.destroy();
-		}
+		} while ( wasFixed );
 	}
 }
 
@@ -358,10 +155,3 @@ mix( Document, ObservableMixin );
  *
  * @typedef {String} module:engine/view/document~ChangeType
  */
-
-/**
- * Fired when {@link #render render} method is called. Actual rendering is executed as a listener to
- * this event with default priority. This way other listeners can be used to run code before or after rendering.
- *
- * @event render
- */

+ 28 - 14
packages/ckeditor5-engine/src/view/documentfragment.js

@@ -1,5 +1,5 @@
 /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  * For licensing, see LICENSE.md.
  */
 
@@ -8,6 +8,7 @@
  */
 
 import Text from './text';
+import TextProxy from './textproxy';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
@@ -19,6 +20,7 @@ export default class DocumentFragment {
 	/**
 	 * Creates new DocumentFragment instance.
 	 *
+	 * @protected
 	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children] List of nodes to be inserted into
 	 * created document fragment.
 	 */
@@ -32,12 +34,16 @@ export default class DocumentFragment {
 		this._children = [];
 
 		if ( children ) {
-			this.insertChildren( 0, children );
+			this._insertChildren( 0, children );
 		}
 	}
 
 	/**
-	 * Iterates over nodes added to this DocumentFragment.
+	 * Iterable interface.
+	 *
+	 * Iterates over nodes added to this document fragment.
+	 *
+	 * @returns {Iterable.<module:engine/view/node~Node>}
 	 */
 	[ Symbol.iterator ]() {
 		return this._children[ Symbol.iterator ]();
@@ -96,14 +102,14 @@ export default class DocumentFragment {
 	}
 
 	/**
-	 * {@link module:engine/view/documentfragment~DocumentFragment#insertChildren Insert} a child node or a list of child nodes at the end
+	 * {@link module:engine/view/documentfragment~DocumentFragment#_insertChildren Insert} a child node or a list of child nodes at the end
 	 * and sets the parent of these nodes to this fragment.
 	 *
-	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} nodes Node or the list of nodes to be inserted.
+	 * @param {module:engine/view/item~Item|Iterable.<module:engine/view/item~Item>} items Items to be inserted.
 	 * @returns {Number} Number of appended nodes.
 	 */
-	appendChildren( nodes ) {
-		return this.insertChildren( this.childCount, nodes );
+	_appendChildren( items ) {
+		return this._insertChildren( this.childCount, items );
 	}
 
 	/**
@@ -140,19 +146,19 @@ export default class DocumentFragment {
 	 * this fragment.
 	 *
 	 * @param {Number} index Position where nodes should be inserted.
-	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} nodes Node or list of nodes to be inserted.
+	 * @param {module:engine/view/item~Item|Iterable.<module:engine/view/item~Item>} items Items to be inserted.
 	 * @returns {Number} Number of inserted nodes.
 	 */
-	insertChildren( index, nodes ) {
+	_insertChildren( index, items ) {
 		this._fireChange( 'children', this );
 		let count = 0;
 
-		nodes = normalize( nodes );
+		const nodes = normalize( items );
 
 		for ( const node of nodes ) {
 			// If node that is being added to this element is already inside another element, first remove it from the old parent.
 			if ( node.parent !== null ) {
-				node.remove();
+				node._remove();
 			}
 
 			node.parent = this;
@@ -172,7 +178,7 @@ export default class DocumentFragment {
 	 * @param {Number} [howMany=1] Number of nodes to remove.
 	 * @returns {Array.<module:engine/view/node~Node>} The array of removed nodes.
 	 */
-	removeChildren( index, howMany = 1 ) {
+	_removeChildren( index, howMany = 1 ) {
 		this._fireChange( 'children', this );
 
 		for ( let i = index; i < index + howMany; i++ ) {
@@ -199,7 +205,7 @@ mix( DocumentFragment, EmitterMixin );
 
 // Converts strings to Text and non-iterables to arrays.
 //
-// @param {String|module:engine/view/node~Node|Iterable.<String|module:engine/view/node~Node>}
+// @param {String|module:engine/view/item~Item|Iterable.<String|module:engine/view/item~Item>}
 // @return {Iterable.<module:engine/view/node~Node>}
 function normalize( nodes ) {
 	// Separate condition because string is iterable.
@@ -214,6 +220,14 @@ function normalize( nodes ) {
 	// Array.from to enable .map() on non-arrays.
 	return Array.from( nodes )
 		.map( node => {
-			return typeof node == 'string' ? new Text( node ) : node;
+			if ( typeof node == 'string' ) {
+				return new Text( node );
+			}
+
+			if ( node instanceof TextProxy ) {
+				return new Text( node.data );
+			}
+
+			return node;
 		} );
 }

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff