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

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

Maciej Gołaszewski 6 лет назад
Родитель
Сommit
a55cbf719a
100 измененных файлов с 4457 добавлено и 2753 удалено
  1. 2 2
      packages/ckeditor5-engine/.eslintrc.js
  2. 11 11
      packages/ckeditor5-engine/.travis.yml
  3. 186 0
      packages/ckeditor5-engine/CHANGELOG.md
  4. 1 1
      packages/ckeditor5-engine/LICENSE.md
  5. 1 1
      packages/ckeditor5-engine/README.md
  6. 1 1
      packages/ckeditor5-engine/docs/api/engine.md
  7. 2 1
      packages/ckeditor5-engine/docs/framework/guides/deep-dive/schema.md
  8. 28 23
      packages/ckeditor5-engine/package.json
  9. 154 24
      packages/ckeditor5-engine/src/controller/datacontroller.js
  10. 6 8
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  11. 151 137
      packages/ckeditor5-engine/src/conversion/conversion.js
  12. 40 0
      packages/ckeditor5-engine/src/conversion/conversionhelpers.js
  13. 0 1061
      packages/ckeditor5-engine/src/conversion/downcast-converters.js
  14. 0 129
      packages/ckeditor5-engine/src/conversion/downcast-selection-converters.js
  15. 66 19
      packages/ckeditor5-engine/src/conversion/downcastdispatcher.js
  16. 1347 0
      packages/ckeditor5-engine/src/conversion/downcasthelpers.js
  17. 25 11
      packages/ckeditor5-engine/src/conversion/mapper.js
  18. 3 3
      packages/ckeditor5-engine/src/conversion/modelconsumable.js
  19. 0 609
      packages/ckeditor5-engine/src/conversion/upcast-converters.js
  20. 0 48
      packages/ckeditor5-engine/src/conversion/upcast-selection-converters.js
  21. 150 45
      packages/ckeditor5-engine/src/conversion/upcastdispatcher.js
  22. 763 0
      packages/ckeditor5-engine/src/conversion/upcasthelpers.js
  23. 2 2
      packages/ckeditor5-engine/src/conversion/viewconsumable.js
  24. 2 2
      packages/ckeditor5-engine/src/dataprocessor/basichtmlwriter.js
  25. 2 2
      packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc
  26. 2 2
      packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js
  27. 2 2
      packages/ckeditor5-engine/src/dataprocessor/htmlwriter.js
  28. 2 2
      packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js
  29. 2 2
      packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js
  30. 19 6
      packages/ckeditor5-engine/src/dev-utils/model.js
  31. 2 2
      packages/ckeditor5-engine/src/dev-utils/operationreplayer.js
  32. 3 3
      packages/ckeditor5-engine/src/dev-utils/view.js
  33. 2 2
      packages/ckeditor5-engine/src/model/batch.js
  34. 49 3
      packages/ckeditor5-engine/src/model/differ.js
  35. 74 33
      packages/ckeditor5-engine/src/model/document.js
  36. 2 2
      packages/ckeditor5-engine/src/model/documentfragment.js
  37. 128 41
      packages/ckeditor5-engine/src/model/documentselection.js
  38. 4 4
      packages/ckeditor5-engine/src/model/element.js
  39. 2 2
      packages/ckeditor5-engine/src/model/history.js
  40. 2 2
      packages/ckeditor5-engine/src/model/item.jsdoc
  41. 15 2
      packages/ckeditor5-engine/src/model/liveposition.js
  42. 2 2
      packages/ckeditor5-engine/src/model/liverange.js
  43. 27 9
      packages/ckeditor5-engine/src/model/markercollection.js
  44. 68 48
      packages/ckeditor5-engine/src/model/model.js
  45. 6 3
      packages/ckeditor5-engine/src/model/node.js
  46. 2 2
      packages/ckeditor5-engine/src/model/nodelist.js
  47. 2 2
      packages/ckeditor5-engine/src/model/operation/attributeoperation.js
  48. 2 2
      packages/ckeditor5-engine/src/model/operation/detachoperation.js
  49. 2 2
      packages/ckeditor5-engine/src/model/operation/insertoperation.js
  50. 2 2
      packages/ckeditor5-engine/src/model/operation/markeroperation.js
  51. 2 2
      packages/ckeditor5-engine/src/model/operation/mergeoperation.js
  52. 2 2
      packages/ckeditor5-engine/src/model/operation/moveoperation.js
  53. 2 2
      packages/ckeditor5-engine/src/model/operation/nooperation.js
  54. 2 2
      packages/ckeditor5-engine/src/model/operation/operation.js
  55. 2 2
      packages/ckeditor5-engine/src/model/operation/operationfactory.js
  56. 2 2
      packages/ckeditor5-engine/src/model/operation/renameoperation.js
  57. 2 2
      packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js
  58. 2 2
      packages/ckeditor5-engine/src/model/operation/splitoperation.js
  59. 163 30
      packages/ckeditor5-engine/src/model/operation/transform.js
  60. 2 2
      packages/ckeditor5-engine/src/model/operation/utils.js
  61. 19 15
      packages/ckeditor5-engine/src/model/position.js
  62. 27 2
      packages/ckeditor5-engine/src/model/range.js
  63. 2 2
      packages/ckeditor5-engine/src/model/rootelement.js
  64. 166 50
      packages/ckeditor5-engine/src/model/schema.js
  65. 111 20
      packages/ckeditor5-engine/src/model/selection.js
  66. 4 3
      packages/ckeditor5-engine/src/model/text.js
  67. 2 2
      packages/ckeditor5-engine/src/model/textproxy.js
  68. 2 2
      packages/ckeditor5-engine/src/model/treewalker.js
  69. 44 16
      packages/ckeditor5-engine/src/model/utils/deletecontent.js
  70. 2 2
      packages/ckeditor5-engine/src/model/utils/getselectedcontent.js
  71. 148 14
      packages/ckeditor5-engine/src/model/utils/insertcontent.js
  72. 2 2
      packages/ckeditor5-engine/src/model/utils/modifyselection.js
  73. 46 28
      packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js
  74. 134 47
      packages/ckeditor5-engine/src/model/writer.js
  75. 2 2
      packages/ckeditor5-engine/src/utils/bindtwostepcarettoattribute.js
  76. 5 5
      packages/ckeditor5-engine/src/view/attributeelement.js
  77. 2 2
      packages/ckeditor5-engine/src/view/containerelement.js
  78. 14 3
      packages/ckeditor5-engine/src/view/document.js
  79. 2 2
      packages/ckeditor5-engine/src/view/documentfragment.js
  80. 23 11
      packages/ckeditor5-engine/src/view/documentselection.js
  81. 4 2
      packages/ckeditor5-engine/src/view/domconverter.js
  82. 83 95
      packages/ckeditor5-engine/src/view/downcastwriter.js
  83. 6 2
      packages/ckeditor5-engine/src/view/editableelement.js
  84. 13 11
      packages/ckeditor5-engine/src/view/element.js
  85. 2 2
      packages/ckeditor5-engine/src/view/elementdefinition.jsdoc
  86. 4 2
      packages/ckeditor5-engine/src/view/emptyelement.js
  87. 3 3
      packages/ckeditor5-engine/src/view/filler.js
  88. 2 2
      packages/ckeditor5-engine/src/view/item.jsdoc
  89. 4 4
      packages/ckeditor5-engine/src/view/matcher.js
  90. 6 4
      packages/ckeditor5-engine/src/view/node.js
  91. 2 2
      packages/ckeditor5-engine/src/view/observer/clickobserver.js
  92. 2 2
      packages/ckeditor5-engine/src/view/observer/compositionobserver.js
  93. 2 2
      packages/ckeditor5-engine/src/view/observer/domeventdata.js
  94. 2 2
      packages/ckeditor5-engine/src/view/observer/domeventobserver.js
  95. 2 2
      packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js
  96. 4 4
      packages/ckeditor5-engine/src/view/observer/focusobserver.js
  97. 2 2
      packages/ckeditor5-engine/src/view/observer/keyobserver.js
  98. 2 2
      packages/ckeditor5-engine/src/view/observer/mouseobserver.js
  99. 4 4
      packages/ckeditor5-engine/src/view/observer/mutationobserver.js
  100. 4 4
      packages/ckeditor5-engine/src/view/observer/observer.js

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /* eslint-env node */

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

@@ -1,33 +1,33 @@
 sudo: required
 dist: trusty
 addons:
+  chrome: stable
   firefox: latest
-  apt:
-    sources:
-    - google-chrome
-    packages:
-    - google-chrome-stable
 language: node_js
 node_js:
 - '8'
 cache:
-- node_modules
+  yarn: true
 branches:
-   except:
-   - stable
+  except:
+  - stable
 before_install:
+- export START_TIME=$( date +%s )
 - export DISPLAY=:99.0
 - sh -e /etc/init.d/xvfb start
-- npm config set package-lock false
-- npm i -g npm@^5.7.1
+- npm i -g yarn
 install:
-- npm install @ckeditor/ckeditor5-dev-tests
+- yarn add @ckeditor/ckeditor5-dev-tests
 - ckeditor5-dev-tests-install-dependencies
 script:
 - ckeditor5-dev-tests-travis
 after_success:
 - ckeditor5-dev-tests-save-revision
+after_script:
+- export END_TIME=$( date +%s )
+- ckeditor5-dev-tests-notify-travis-status
 env:
   global:
   - secure: CSFKXnTb2oQW8YIjh+SWXYSYzx1jcIg1wGcl6jWrbaOU06JHEO77YLKlhMxLfRniFtilBEksK4ugIcn+lihykhHleMxI27+kifILdLLZKvwWlZiDrtpJSzLk08SpRNDwwthSlHVKrxbHVFE1K9SYpX4s3qTpxullDMPgc13OP4c=
   - secure: WGdwDEavTOIE/yw8pdpWmx3Cehm2/GN8gZMimuot8sWPbOZRuPh+2lnVWpASLe25qJUKKeLWYLRHccpMLKbyweiordSEBwzNrOGOFyUuaDPITNZZrp5ekh2vHBp1+mwhRpRekdyWdzzVsm01RkFrnJqQlaXxX9i/r9Tu4EsK54M=
+  - secure: BKoFnOWcfKn8zaTvilMn9UORF+gR1zBqetqai5FKfSKraJXqiBrz1AI3ecytCjJOKS1g6HoRtZHwVS4a+uG3JjK3bRERQF2NyHiyjbrsL+2HaKaRnVmox8PC/59m1PJlcRKAV9RTYJqL/oqa7N8OBegt4LpIVnEcil8WCErCF6Q=

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

@@ -1,6 +1,192 @@
 Changelog
 =========
 
+## [13.1.1](https://github.com/ckeditor/ckeditor5-engine/compare/v13.1.0...v13.1.1) (2019-06-05)
+
+### Bug fixes
+
+* Prevented from losing selection attributes between operations (fixes a bug with text composition). Closes https://github.com/ckeditor/ckeditor5-typing/issues/188. ([42dcb25](https://github.com/ckeditor/ckeditor5-engine/commit/42dcb25))
+
+### Other changes
+
+* Added more cases of affected markers on merging in `model.Writer`. Closes [#1738](https://github.com/ckeditor/ckeditor5-engine/issues/1738). ([01ff6e6](https://github.com/ckeditor/ckeditor5-engine/commit/01ff6e6))
+
+
+## [13.1.0](https://github.com/ckeditor/ckeditor5-engine/compare/v13.0.0...v13.1.0) (2019-04-10)
+
+### Features
+
+* `Model#insertContent()` will return a range affected by the insertion. ([f4e4644](https://github.com/ckeditor/ckeditor5-engine/commit/f4e4644))
+
+  In `Model#deleteContent()`, added `doNotAutoparagraph` flag to `options`.
+  `Position` and `LivePosition` static creators should handle `stickiness` param.
+* Added possibility to refresh the marker with no changes through `Writer#updateMarker()` method. Closes [#1649](https://github.com/ckeditor/ckeditor5-engine/issues/1649). ([cf56d90](https://github.com/ckeditor/ckeditor5-engine/commit/cf56d90))
+* Introduced `Schema#setAttributeProperties()` and `Schema#getAttributeProperties()` methods. Closes [ckeditor/ckeditor5#1659](https://github.com/ckeditor/ckeditor5/issues/1659). ([1c6f83a](https://github.com/ckeditor/ckeditor5-engine/commit/1c6f83a))
+* Introduced `UpcastConversionApi#getSplitParts()`. Also, provided a way to set upcast conversion helper fired for every view element. Closes https://github.com/ckeditor/ckeditor5/issues/1580. Closes https://github.com/ckeditor/ckeditor5/issues/1581. ([d0ee3f4](https://github.com/ckeditor/ckeditor5-engine/commit/d0ee3f4))
+
+### Bug fixes
+
+* `view.DowncastWriter` will now correctly wrap and unwrap nested attribute elements. Closes [#1716](https://github.com/ckeditor/ckeditor5-engine/issues/1716). Closes [ckeditor/ckeditor5-font#30](https://github.com/ckeditor/ckeditor5-font/issues/30). ([4126359](https://github.com/ckeditor/ckeditor5-engine/commit/4126359))
+* Attribute and remove change on intersecting ranges done in the same change block will be correctly saved in `Differ` and downcasted. Closes [ckeditor/ckeditor5#1645](https://github.com/ckeditor/ckeditor5/issues/1645). ([b2a9d86](https://github.com/ckeditor/ckeditor5-engine/commit/b2a9d86))
+* Editor will no longer crash during undo in some pasting+remove scenarios. Closes [#1701](https://github.com/ckeditor/ckeditor5-engine/issues/1701). ([ca619e7](https://github.com/ckeditor/ckeditor5-engine/commit/ca619e7))
+* Made sure that `Schema#getAttributeProperties()` always returns an object. Closes [#1717](https://github.com/ckeditor/ckeditor5-engine/issues/1717). ([b3f5da3](https://github.com/ckeditor/ckeditor5-engine/commit/b3f5da3))
+* Markers should be now correctly upcasted inside any element. Closes [#1697](https://github.com/ckeditor/ckeditor5-engine/issues/1697). ([3706324](https://github.com/ckeditor/ckeditor5-engine/commit/3706324))
+* `Model#deleteContent()` will not throw anymore if the passed selection is in the graveyard root. Closes [#1706](https://github.com/ckeditor/ckeditor5-engine/issues/1706). ([bd875c7](https://github.com/ckeditor/ckeditor5-engine/commit/bd875c7))
+* The editor will not throw an error when updating an empty fake selection container. Closes [#1714](https://github.com/ckeditor/ckeditor5-engine/issues/1714). ([c48f5a4](https://github.com/ckeditor/ckeditor5-engine/commit/c48f5a4))
+
+
+## [13.0.0](https://github.com/ckeditor/ckeditor5-engine/compare/v12.0.0...v13.0.0) (2019-02-28)
+
+### Features
+
+* Added an additional event in markers conversion. Improved how `MarkerOperation` is transformed during undo. Closes [#1604](https://github.com/ckeditor/ckeditor5-engine/issues/1604). ([da5a390](https://github.com/ckeditor/ckeditor5-engine/commit/da5a390))
+* Implemented `Selection#is()` and `DocumentSelection#is()` methods in both the model and the view. Closes [#1663](https://github.com/ckeditor/ckeditor5-engine/issues/1663). ([aac4948](https://github.com/ckeditor/ckeditor5-engine/commit/aac4948))
+* Introduce the `selection.getTopMostBlocks()` method. ([a9c41c8](https://github.com/ckeditor/ckeditor5-engine/commit/a9c41c8))
+* Introduce the read-only `View#isRenderingInProgress` flag to check if the document is in the rendering phase. Closes https://github.com/ckeditor/ckeditor5/issues/1530. ([6577d04](https://github.com/ckeditor/ckeditor5-engine/commit/6577d04))
+* Introduced `Differ#getChangedMarkers`. Closes [#1658](https://github.com/ckeditor/ckeditor5-engine/issues/1658). ([2e04af7](https://github.com/ckeditor/ckeditor5-engine/commit/2e04af7))
+* Introduced `DocumentSelection#markers` collection. Closes [#1615](https://github.com/ckeditor/ckeditor5-engine/issues/1615). ([b2c1d72](https://github.com/ckeditor/ckeditor5-engine/commit/b2c1d72))
+* Introduced support for inline objects (enables support for inline widgets). Introduced `Schema#isInline()`. Closes [[ckeditor/ckeditor5#1049](https://github.com/ckeditor/ckeditor5/issues/1049)](https://github.com/ckeditor/ckeditor5/issues/1049). Closes [[ckeditor/ckeditor5#1426](https://github.com/ckeditor/ckeditor5/issues/1426)](https://github.com/ckeditor/ckeditor5/issues/1426). ([6b36bf1](https://github.com/ckeditor/ckeditor5-engine/commit/6b36bf1))
+* Introduced whitespace trimming to `Model#hasContent()`. `DataController#get()` method can now trim empty data (so it returns empty string instead of `<p>&nbsp;</p>`). Closes [[ckeditor/ckeditor5#401](https://github.com/ckeditor/ckeditor5/issues/401)](https://github.com/ckeditor/ckeditor5/issues/401). ([2b95dc3](https://github.com/ckeditor/ckeditor5-engine/commit/2b95dc3))
+* Moved the root element DOM attributes management from the UI to the engine. Made it possible to use `addPlaceholder()` (now `enablePlaceholder()`) on the root editable. Introduced the `View.detachDomRoot()` method. Implemented additional placeholder helpers (`showPlaceholder()`, `hidePlaceholder()`, `needsPlaceholder()`) (see [ckeditor/ckeditor5#479](https://github.com/ckeditor/ckeditor5/issues/479)). Closes [#899](https://github.com/ckeditor/ckeditor5-engine/issues/899). ([21dee6b](https://github.com/ckeditor/ckeditor5-engine/commit/21dee6b))
+
+### Bug fixes
+
+* `MarkerOperation` OT cases for undo. Closes [#1650](https://github.com/ckeditor/ckeditor5-engine/issues/1650). ([649cae0](https://github.com/ckeditor/ckeditor5-engine/commit/649cae0))
+* `MarkerOperation` transformation in undo. Closes [#1668](https://github.com/ckeditor/ckeditor5-engine/issues/1668). ([c9932b8](https://github.com/ckeditor/ckeditor5-engine/commit/c9932b8))
+* `Selection#getTopMostBlocks()` should not leak from limit elements. Closes [ckeditor/ckeditor5-table#163](https://github.com/ckeditor/ckeditor5-table/issues/163). ([7bc0338](https://github.com/ckeditor/ckeditor5-engine/commit/7bc0338))
+* All content is properly removed after undoing paste in some scenarios. Closes [[ckeditor/ckeditor5#1540](https://github.com/ckeditor/ckeditor5/issues/1540)](https://github.com/ckeditor/ckeditor5/issues/1540). ([08855d3](https://github.com/ckeditor/ckeditor5-engine/commit/08855d3))
+* Converter priority passing in `conversion.attributeToElement()`. Closes [#1617](https://github.com/ckeditor/ckeditor5-engine/issues/1617). ([fe6d17d](https://github.com/ckeditor/ckeditor5-engine/commit/fe6d17d))
+* Fake selection container should be correctly appended to the new editable element when creating a new fake selection in a different editable element than the one which was focused before. Closes [[ckeditor/ckeditor5#1523](https://github.com/ckeditor/ckeditor5/issues/1523)](https://github.com/ckeditor/ckeditor5/issues/1523). ([3b53d5a](https://github.com/ckeditor/ckeditor5-engine/commit/3b53d5a))
+* Filter out fake selection container before comparing DOM view root children in view renderer. Closes [ckeditor/ckeditor5#1578](https://github.com/ckeditor/ckeditor5/issues/1578). ([6591f87](https://github.com/ckeditor/ckeditor5-engine/commit/6591f87))
+* Moving to the same position is not handled by the `Differ` as a change. ([7dfaae6](https://github.com/ckeditor/ckeditor5-engine/commit/7dfaae6))
+* Prevented `model.Writer` from inserting empty text nodes. Closes [#1320](https://github.com/ckeditor/ckeditor5-engine/issues/1320). ([47070b5](https://github.com/ckeditor/ckeditor5-engine/commit/47070b5))
+* Prevented `View` from firing the `render` event if there were no changes since the last rendering. Closes [#1653](https://github.com/ckeditor/ckeditor5-engine/issues/1653). Closes [#1660](https://github.com/ckeditor/ckeditor5-engine/issues/1660). ([558638c](https://github.com/ckeditor/ckeditor5-engine/commit/558638c))
+* Renamed the event during selection attributes conversion. `attribute:key` becomes to `attribute:key:$text`. Closes [#1597](https://github.com/ckeditor/ckeditor5-engine/issues/1597). ([fd7734e](https://github.com/ckeditor/ckeditor5-engine/commit/fd7734e))
+* Stopped invoking `view.render()` by `EditingController` when the model document isn't changed. Closes [#1653](https://github.com/ckeditor/ckeditor5-engine/issues/1653). ([5d97fd6](https://github.com/ckeditor/ckeditor5-engine/commit/5d97fd6))
+* Fixed memory leaks during editor initialization and destruction (see [ckeditor/ckeditor5#1341](https://github.com/ckeditor/ckeditor5/issues/1341)). ([bf86ffa](https://github.com/ckeditor/ckeditor5-engine/commit/bf86ffa))
+* Undo and redo no longer crashes in scenarios featuring pasting content into earlier pasted content. Closes [[ckeditor/ckeditor5#1385](https://github.com/ckeditor/ckeditor5/issues/1385)](https://github.com/ckeditor/ckeditor5/issues/1385). ([551ab50](https://github.com/ckeditor/ckeditor5-engine/commit/551ab50))
+* Update model selection attributes and markers after each change that affects the selection. Closes [#1673](https://github.com/ckeditor/ckeditor5-engine/issues/1673). ([4f9ac0e](https://github.com/ckeditor/ckeditor5-engine/commit/4f9ac0e))
+
+* Add selection post-fixer improvements. Closes [#1593](https://github.com/ckeditor/ckeditor5-engine/issues/1593). ([7f40831](https://github.com/ckeditor/ckeditor5-engine/commit/7f40831))
+
+### Other changes
+
+* Added support for handling data in multiple roots in `DataController`. Closes [#1626](https://github.com/ckeditor/ckeditor5-engine/issues/1626). ([0fb4295](https://github.com/ckeditor/ckeditor5-engine/commit/0fb4295))
+* Change `Conversion` class API. Closes [#1640](https://github.com/ckeditor/ckeditor5-engine/issues/1640). ([e7d09cd](https://github.com/ckeditor/ckeditor5-engine/commit/e7d09cd))
+* Introduced `editor.data#ready` event. ([46d9243](https://github.com/ckeditor/ckeditor5-engine/commit/46d9243))
+* Removed `wrap()` from public API. Closes [#1616](https://github.com/ckeditor/ckeditor5-engine/issues/1616). ([1c7ef68](https://github.com/ckeditor/ckeditor5-engine/commit/1c7ef68))
+* Swapped the order of parameters in `Schema#findAllowedParent()`. Now those parameters match to parameters in other methods of the `Schema` class. Closes [#1636](https://github.com/ckeditor/ckeditor5-engine/issues/1636). ([6515558](https://github.com/ckeditor/ckeditor5-engine/commit/6515558))
+* Upcast element to attribute defaults to `low` priority instead of `normal`. Closes [ckeditor/ckeditor5#1399](https://github.com/ckeditor/ckeditor5/issues/1399). ([c33c49c](https://github.com/ckeditor/ckeditor5-engine/commit/c33c49c))
+* Expose conversion utilities. Closes [#1556](https://github.com/ckeditor/ckeditor5-engine/issues/1556). ([9306c22](https://github.com/ckeditor/ckeditor5-engine/commit/9306c22))
+
+### BREAKING CHANGES
+
+* Upgraded minimal versions of Node to `8.0.0` and npm to `5.7.1`. See: [ckeditor/ckeditor5#1507](https://github.com/ckeditor/ckeditor5/issues/1507). ([612ea3c](https://github.com/ckeditor/ckeditor5-cloud-services/commit/612ea3c))
+* `DataController#get()` method now returns an empty string when the editor content is empty (instead of returning e.g. `<p>&nbsp;</p>`).
+* The wrap() conversion helper was removed from public API.
+* The `attachPlaceholder()` has been renamed to `enablePlaceholder()`.
+* `enablePlaceholder()` accepts a configuration object instead of separate parameters.
+* The `detachPlaceholder()` has been renamed to `disablePlaceholder()`.
+* The `Conversion#register()` method was removed from the public API. Use constructor parameters to pass dispatchers and `Conversion#addAlias()` to register an alternative conversion group for registered upcast or downcast dispatchers.
+* The `editor#dataReady` event was removed. The `editor.data#ready` event has been introduced and should be used instead.
+* Swapped the order of parameters in `Schema#findAllowedParent()`.
+* The second parameter (`rootName`) from `DataController#init()` method has been removed. To initialize data on a root different than default one an object with `rootName` - `data` pair should be passed.
+* The second parameter (`rootName`) from `DataController#set()` method has been removed. To set data on a root different than default one an object with `rootName` - `data` pair should be passed.
+* The `editing.view.render()` method was renamed to `editing.view.forceRender()`. It should be used with caution as it will re-render editing view and repaint the UI.
+* The `conversion.register()` method now accepts single options object as a parameter.
+* The `downcastElementToElement()` helper was removed from public API. Use `conversion.for( 'downcast' ).elementToElement()` instead.
+* The `downcastAttributeToElement()` helper was removed from public API. Use `conversion.for( 'downcast' ).attributeToElement()` instead.
+* The `downcastAttributeToAttribute()` helper was removed from public API. Use `conversion.for( 'downcast' ).attributeToAttribute()` instead.
+* The `downcastMarkerToElement()` helper was removed from public API. Use `conversion.for( 'downcast' ).markerToElement()` instead.
+* The `downcastMarkerToHighlight()` helper was removed from public API. Use `conversion.for( 'downcast' ).markerToHighlight()` instead.
+* The `upcastElementToElement()` helper was removed from public API. Use `conversion.for( 'upcast' ).elementToElement()` instead.
+* The `upcastElementToAttribute()` helper was removed from public API. Use `conversion.for( 'upcast' ).elementToAttribute()` instead.
+* The `upcastAttributeToAttribute()` helper was removed from public API. Use `conversion.for( 'upcast' ).attributeToAttribute()` instead.
+* The `upcastElementToMarker()` helper was removed from public API. Use `conversion.for( 'upcast' ).elementToMarker()` instead.
+* The `insertUIElement()` and `removeUIElement()` downcast converters were removed from public API. Use `conversion.for( 'downcast' ).markerToElement()` instead.
+* The `highlightText()`, `highlightElement()` and `removeHighlight()` downcast converters were removed from public API. Use `conversion.for( 'downcast' ).markerToHighlight()` instead.
+* The `insertElement()` downcast converter was removed from public API. Use `conversion.for( 'downcast' ).elementToElement()` instead.
+* The `changeAttribute()` downcast converter was removed from public API. Use `conversion.for( 'downcast' ).attributeToAttribute()` instead.
+
+
+## [12.0.0](https://github.com/ckeditor/ckeditor5-engine/compare/v11.0.0...v12.0.0) (2018-12-05)
+
+### Features
+
+* Introduced `createDocumentFragment()`, `createElement()` and `createText()` methods in `UpcastWriter`. Additionally, the `View#change()` method now returns the return value of its callback. Closes [#1549](https://github.com/ckeditor/ckeditor5-engine/issues/1549). ([ec13c85](https://github.com/ckeditor/ckeditor5-engine/commit/ec13c85))
+* The `Model#insertContent()` method will accept offset parameter. ([00dd70c](https://github.com/ckeditor/ckeditor5-engine/commit/00dd70c))
+* Made `Position.createAt()` require the `offset` when the first parameter is a model/view item. This change affected the following methods too. Closes [#1554](https://github.com/ckeditor/ckeditor5-engine/issues/1554). ([00dd70c](https://github.com/ckeditor/ckeditor5-engine/commit/00dd70c))
+
+  * `model.Position.createAt()`
+  * `model.Range.createCollapsedAt()`
+  * `model.Selection#setFocus()`
+  * `model.Writer#insert()`
+  * `model.Writer#insertText()`
+  * `model.Writer#insertElement()`
+  * `model.Writer#move()`
+  * `model.Writer#setSelectionFocus()`
+  * `view.Writer#setSelectionFocus()`
+  * `view.Position.createAt()`
+  * `view.Range.createCollapsedAt()`
+  * `view.Selection#setFocus()`
+
+  See breaking changes.
+
+### Bug fixes
+
+* `Model#deleteContent()` will properly merge elements inside a limit element. Closes [ckeditor/ckeditor5#1265](https://github.com/ckeditor/ckeditor5/issues/1265). ([5d26bc3](https://github.com/ckeditor/ckeditor5-engine/commit/5d26bc3))
+* Added `MoveOperation` x `SplitOperation` transformation for a case when graveyard element is moved. Closes [#1580](https://github.com/ckeditor/ckeditor5-engine/issues/1580). ([f88c918](https://github.com/ckeditor/ckeditor5-engine/commit/f88c918))
+* Better handling for `MoveOperation` x `SplitOperation` transformation special case. Closes [ckeditor/ckeditor5#1288](https://github.com/ckeditor/ckeditor5/issues/1288). ([b92a800](https://github.com/ckeditor/ckeditor5-engine/commit/b92a800))
+* Corrected transformations for pasting and undo scenarios. Closes [ckeditor/ckeditor5#1287](https://github.com/ckeditor/ckeditor5/issues/1287). ([b1e8975](https://github.com/ckeditor/ckeditor5-engine/commit/b1e8975))
+* Do not run attribute-to-attribute downcast conversion on text node attributes. Closes [#1587](https://github.com/ckeditor/ckeditor5-engine/issues/1587). ([6659582](https://github.com/ckeditor/ckeditor5-engine/commit/6659582))
+* Firefox should visually move the caret to a new line after a soft break. Closes [#1439](https://github.com/ckeditor/ckeditor5-engine/issues/1439). ([80392ad](https://github.com/ckeditor/ckeditor5-engine/commit/80392ad))
+* Made markers created by `Writer#insert()` affect the data. Closes [#1583](https://github.com/ckeditor/ckeditor5-engine/issues/1583). ([72aaaf0](https://github.com/ckeditor/ckeditor5-engine/commit/72aaaf0))
+
+### Other changes
+
+* `ContainerElement#getFillerOffset()` can now be re-used in other places in the code (it is now exported by the module). See [ckeditor/ckeditor5-list#117](https://github.com/ckeditor/ckeditor5-list/issues/117). ([12f28bb](https://github.com/ckeditor/ckeditor5-engine/commit/12f28bb))
+* Moved `Position`, `Range` and `Selection` static factories from those classes to the model/view writers and `Model`/`View` instances. Previously, those factories were available as static methods of the `Position`, `Range` and `Selection` classes which meant that you needed to import those classes to your plugin's code to create new instances. That required your package to depend on `@ckeditor/ckeditor5-engine` and was not very useful in general. After this change, you can create instances of those classes without importing anything. See the "Breaking changes" section for more details. Closes [#1555](https://github.com/ckeditor/ckeditor5-engine/issues/1555). ([e7f8467](https://github.com/ckeditor/ckeditor5-engine/commit/e7f8467))
+* Vairous fixes in the API docs. Thanks to [@denisname](https://github.com/denisname)!
+
+### BREAKING CHANGES
+
+* The model `Position.createAt()` method was removed from the public API. Use `writer.createPositionAt()` instead. This method is also available on the `Model` instance.
+* The `offset` parameter of the following methods does not default to `0` and hence is no longer optional when `itemOrPosition` is a model/view item:
+  - `model.Position.createAt()`
+  - `model.Range.createCollapsedAt()`
+  - `model.Selection#setFocus()`
+  - `model.Writer#insert()`
+  - `model.Writer#insertText()`
+  - `model.Writer#insertElement()`
+  - `model.Writer#move()`
+  - `model.Writer#setSelectionFocus()`
+  - `view.Writer#setSelectionFocus()`
+  - `view.Position.createAt()`
+  - `view.Range.createCollapsedAt()`
+  - `view.Selection#setFocus()`
+* The model `Position.createBefore()` method was removed from the public API. Use `writer.createPositionBefore()` instead. This method is also available on the `Model` instance.
+* The model `Position.createFromPosition()` method was removed. Use `writer.createPositionAt( position )` to create a new position instance. This method is also available on the `Model` instance.
+* The model `Position.createFromParentAndOffset()` method was removed. Use `writer.createPositionAt( parent, offset )` instead. This method is also available on the `Model` instance.
+* The model `Range.createIn()` method was removed from the public API. Use `writer.createRangeIn()` instead. This method is also available on the `Model` instance.
+* The model `Range.createOn()` method was removed from the public API. Use `writer.createRangeOn()` instead. This method is also available on the `Model` instance.
+* The model `Range.createFromRange()` method was removed from the public API.
+* The model `Range.createFromParentsAndOffsets()` method was removed from the public API.
+* The model `Range.createFromPositionAndShift()` method was removed from the public API.
+* The model `Range.createCollapsedAt()` method removed method was removed. Use `writer.createRange( position )` to create collapsed range. This method is also available on the `Model` instance.
+* The model `Position.createAfter()` method was removed from the public API. Use `writer.createPositionAfter()` instead. This method is also available on the `Model` instance.
+* The view `Position.createAt()` method was removed from the public API. Use `writer.createPositionAt()` instead. This method is also available on the `View` instance.
+* The view `Position.createAfter()` method was removed from the public API. Use `writer.createPositionAfter()` instead. This method is also available on the `View` instance.
+* The view `Position.createBefore()` method was removed from the public API. Use `writer.createPositionBefore()` instead. This method is also available on the `View` instance.
+* The view `Position.createFromPosition()` method was removed. Use `writer.createPositionAt( position )` to create a new position instance. This method is also available on the `View` instance.
+* The view `Range.createIn()` method was removed from the public API. Use `writer.createRangeIn()` instead. This method is also available on the `View` instance.
+* The view `Range.createOn()` method was removed from the public API. Use `writer.createRangeOn()` instead. This method is also available on the `View` instance.
+* The view `Range.createFromRange()` method was removed from the public API.
+* The view `Range.createFromPositionAndShift()` method was removed from the public API.
+* The view `Range.createFromParentsAndOffsets()` method was removed from the public API.
+* The view `Range.createCollapsedAt()` method removed method was removed. Use `writer.createRange( position )` to create a collapsed range. This method is also available on the `View` instance.
+* The model `Range.createFromRanges()` method was removed from the public API.
+
+
 ## [11.0.0](https://github.com/ckeditor/ckeditor5-engine/compare/v10.2.0...v11.0.0) (2018-10-08)
 
 ### 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-2018, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
+Copyright (c) 2003-2019, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
 
 Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html).
 

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

@@ -32,4 +32,4 @@ Additionally, refer to the [`@ckeditor/ckeditor5-engine` package](https://ckedit
 
 ## License
 
-Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file.
+Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). For full details about the license, please check the `LICENSE.md` file or [https://ckeditor.com/legal/ckeditor-oss-license](https://ckeditor.com/legal/ckeditor-oss-license).

+ 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/editing-engine editing engine's architecture}.
 
 You can also browse the API documentation of this package by using the module tree on the left.
 

+ 2 - 1
packages/ckeditor5-engine/docs/framework/guides/deep-dive/schema.md

@@ -57,7 +57,8 @@ schema.register( '$block', {
 	isBlock: true
 } );
 schema.register( '$text', {
-	allowIn: '$block'
+	allowIn: '$block',
+	isInline: true
 } );
 ```
 

+ 28 - 23
packages/ckeditor5-engine/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@ckeditor/ckeditor5-engine",
-  "version": "11.0.0",
+  "version": "13.1.1",
   "description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
   "keywords": [
     "wysiwyg",
@@ -21,35 +21,36 @@
     "ckeditor 5"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-utils": "^11.0.0",
+    "@ckeditor/ckeditor5-utils": "^12.1.1",
     "lodash-es": "^4.17.10"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^10.0.3",
-    "@ckeditor/ckeditor5-block-quote": "^10.1.0",
-    "@ckeditor/ckeditor5-core": "^11.0.1",
-    "@ckeditor/ckeditor5-editor-classic": "^11.0.1",
-    "@ckeditor/ckeditor5-enter": "^10.1.2",
-    "@ckeditor/ckeditor5-essentials": "^10.1.2",
-    "@ckeditor/ckeditor5-heading": "^10.1.0",
-    "@ckeditor/ckeditor5-link": "^10.0.4",
-    "@ckeditor/ckeditor5-list": "^11.0.2",
-    "@ckeditor/ckeditor5-paragraph": "^10.0.3",
-    "@ckeditor/ckeditor5-typing": "^11.0.1",
-    "@ckeditor/ckeditor5-undo": "^10.0.3",
-    "@ckeditor/ckeditor5-widget": "^10.3.0",
+    "@ckeditor/ckeditor5-basic-styles": "^11.1.1",
+    "@ckeditor/ckeditor5-block-quote": "^11.1.0",
+    "@ckeditor/ckeditor5-core": "^12.1.1",
+    "@ckeditor/ckeditor5-editor-classic": "^12.1.1",
+    "@ckeditor/ckeditor5-enter": "^11.0.2",
+    "@ckeditor/ckeditor5-essentials": "^11.0.2",
+    "@ckeditor/ckeditor5-heading": "^11.0.2",
+    "@ckeditor/ckeditor5-link": "^11.0.2",
+    "@ckeditor/ckeditor5-list": "^12.0.2",
+    "@ckeditor/ckeditor5-paragraph": "^11.0.2",
+    "@ckeditor/ckeditor5-theme-lark": "^14.0.0",
+    "@ckeditor/ckeditor5-typing": "^12.0.2",
+    "@ckeditor/ckeditor5-undo": "^11.0.2",
+    "@ckeditor/ckeditor5-widget": "^11.0.2",
     "eslint": "^5.5.0",
-    "eslint-config-ckeditor5": "^1.0.7",
-    "husky": "^0.14.3",
+    "eslint-config-ckeditor5": "^1.0.11",
+    "husky": "^1.3.1",
     "lint-staged": "^7.0.0"
   },
   "engines": {
-    "node": ">=6.9.0",
-    "npm": ">=3.0.0"
+    "node": ">=8.0.0",
+    "npm": ">=5.7.1"
   },
   "author": "CKSource (http://cksource.com/)",
   "license": "GPL-2.0-or-later",
-  "homepage": "https://ckeditor.com",
+  "homepage": "https://ckeditor.com/ckeditor-5",
   "bugs": "https://github.com/ckeditor/ckeditor5-engine/issues",
   "repository": {
     "type": "git",
@@ -61,8 +62,7 @@
     "theme"
   ],
   "scripts": {
-    "lint": "eslint --quiet '**/*.js'",
-    "precommit": "lint-staged"
+    "lint": "eslint --quiet '**/*.js'"
   },
   "lint-staged": {
     "**/*.js": [
@@ -72,5 +72,10 @@
   "eslintIgnore": [
     "src/lib/**",
     "packages/**"
-  ]
+  ],
+  "husky": {
+    "hooks": {
+      "pre-commit": "lint-staged"
+    }
+  }
 }

+ 154 - 24
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -14,10 +14,10 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import Mapper from '../conversion/mapper';
 
 import DowncastDispatcher from '../conversion/downcastdispatcher';
-import { insertText } from '../conversion/downcast-converters';
+import { insertText } from '../conversion/downcasthelpers';
 
 import UpcastDispatcher from '../conversion/upcastdispatcher';
-import { convertText, convertToModelFragment } from '../conversion/upcast-converters';
+import { convertText, convertToModelFragment } from '../conversion/upcasthelpers';
 
 import ViewDocumentFragment from '../view/documentfragment';
 import ViewDocument from '../view/document';
@@ -35,6 +35,11 @@ import ModelRange from '../model/range';
  * * downcast converters,
  * * upcast converters.
  *
+ * An instance of the data controller is always available in the {@link module:core/editor/editor~Editor#data `editor.data`}
+ * property:
+ *
+ *		editor.data.get( { rootName: 'customRoot' } ); // -> '<p>Hello!</p>'
+ *
  * @mixes module:utils/observablemixin~ObservableMixin
  */
 export default class DataController {
@@ -58,7 +63,7 @@ export default class DataController {
 		 * Data processor used during the conversion.
 		 *
 		 * @readonly
-		 * @member {module:engine/dataProcessor~DataProcessor}
+		 * @member {module:engine/dataprocessor/dataprocessor~DataProcessor}
 		 */
 		this.processor = dataProcessor;
 
@@ -103,18 +108,50 @@ export default class DataController {
 		this.upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
 
 		this.decorate( 'init' );
+
+		// Fire `ready` event when initialisation has completed. Such low level listener gives possibility
+		// to plug into initialisation pipeline without interrupting the initialisation flow.
+		this.on( 'init', () => {
+			this.fire( 'ready' );
+		}, { priority: 'lowest' } );
 	}
 
 	/**
 	 * 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.
+	 * @param {Object} [options]
+	 * @param {String} [options.rootName='main'] Root name.
+	 * @param {String} [options.trim='empty'] Whether returned data should be trimmed. This option is set to `empty` by default,
+	 * which means whenever editor content is considered empty, an empty string will be returned. To turn off trimming completely
+	 * use `'none'`. In such cases exact content will be returned (for example `<p>&nbsp;</p>` for an empty editor).
 	 * @returns {String} Output data.
 	 */
-	get( rootName = 'main' ) {
-		// Get model range.
-		return this.stringify( this.model.document.getRoot( rootName ) );
+	get( options ) {
+		const { rootName = 'main', trim = 'empty' } = options || {};
+
+		if ( !this._checkIfRootsExists( [ rootName ] ) ) {
+			/**
+			 * Cannot get data from a non-existing root. This error is thrown when {@link #get DataController#get() method}
+			 * is called with non-existent root name. For example, if there is an editor instance with only `main` root,
+			 * calling {@link #get} like:
+			 *
+			 *		data.get( { rootName: 'root2' } );
+			 *
+			 * will throw this error.
+			 *
+			 * @error datacontroller-get-non-existent-root
+			 */
+			throw new CKEditorError( 'datacontroller-get-non-existent-root: Attempting to get data from a non-existing root.' );
+		}
+
+		const root = this.model.document.getRoot( rootName );
+
+		if ( trim === 'empty' && !this.model.hasContent( root, { ignoreWhitespaces: true } ) ) {
+			return '';
+		}
+
+		return this.stringify( root );
 	}
 
 	/**
@@ -181,12 +218,20 @@ export default class DataController {
 	 * **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.
 	 *
+	 * When data is passed as a string it is initialized on a default `main` root:
+	 *
+	 *		dataController.init( '<p>Foo</p>' ); // Initializes data on the `main` root.
+	 *
+	 * To initialize data on a different root or multiple roots at once, object containing `rootName` - `data` pairs should be passed:
+	 *
+	 *		dataController.init( { main: '<p>Foo</p>', title: '<h1>Bar</h1>' } ); // Initializes data on the `main` and `title` roots.
+	 *
 	 * @fires init
-	 * @param {String} data Input data.
-	 * @param {String} [rootName='main'] Root name.
+	 * @param {String|Object.<String,String>} data Input data as a string or an object containing `rootName` - `data`
+	 * pairs to initialize data on multiple roots at once.
 	 * @returns {Promise} Promise that is resolved after the data is set on the editor.
 	 */
-	init( data, rootName = 'main' ) {
+	init( data ) {
 		if ( this.model.document.version ) {
 			/**
 			 * Cannot set initial data to not empty {@link module:engine/model/document~Document}.
@@ -198,10 +243,33 @@ export default class DataController {
 			throw new CKEditorError( 'datacontroller-init-document-not-empty: Trying to set initial data to not empty document.' );
 		}
 
-		const modelRoot = this.model.document.getRoot( rootName );
+		let initialData = {};
+		if ( typeof data === 'string' ) {
+			initialData.main = data; // Default root is 'main'. To initiate data on a different root, object should be passed.
+		} else {
+			initialData = data;
+		}
+
+		if ( !this._checkIfRootsExists( Object.keys( initialData ) ) ) {
+			/**
+			 * Cannot init data on a non-existing root. This error is thrown when {@link #init DataController#init() method}
+			 * is called with non-existent root name. For example, if there is an editor instance with only `main` root,
+			 * calling {@link #init} like:
+			 *
+			 * 		data.init( { main: '<p>Foo</p>', root2: '<p>Bar</p>' } );
+			 *
+			 * will throw this error.
+			 *
+			 * @error datacontroller-init-non-existent-root
+			 */
+			throw new CKEditorError( 'datacontroller-init-non-existent-root: Attempting to init data on a non-existing root.' );
+		}
 
 		this.model.enqueueChange( 'transparent', writer => {
-			writer.insert( this.parse( data, modelRoot ), modelRoot, 0 );
+			for ( const rootName of Object.keys( initialData ) ) {
+				const modelRoot = this.model.document.getRoot( rootName );
+				writer.insert( this.parse( initialData[ rootName ], modelRoot ), modelRoot, 0 );
+			}
 		} );
 
 		return Promise.resolve();
@@ -216,19 +284,52 @@ export default class DataController {
 	 * 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.
-	 * @param {String} [rootName='main'] Root name.
+	 * When data is passed as a string it is set on a default `main` root:
+	 *
+	 *		dataController.set( '<p>Foo</p>' ); // Sets data on the `main` root.
+	 *
+	 * To set data on a different root or multiple roots at once, object containing `rootName` - `data` pairs should be passed:
+	 *
+	 *		dataController.set( { main: '<p>Foo</p>', title: '<h1>Bar</h1>' } ); // Sets data on the `main` and `title` roots.
+	 *
+	 * @param {String|Object.<String,String>} data Input data as a string or an object containing `rootName` - `data`
+	 * pairs to set data on multiple roots at once.
 	 */
-	set( data, rootName = 'main' ) {
-		// Save to model.
-		const modelRoot = this.model.document.getRoot( rootName );
+	set( data ) {
+		let newData = {};
+
+		if ( typeof data === 'string' ) {
+			newData.main = data; // Default root is 'main'. To set data on a different root, object should be passed.
+		} else {
+			newData = data;
+		}
+
+		if ( !this._checkIfRootsExists( Object.keys( newData ) ) ) {
+			/**
+			 * Cannot set data on a non-existing root. This error is thrown when {@link #set DataController#set() method}
+			 * is called with non-existent root name. For example, if there is an editor instance with only `main` root,
+			 * calling {@link #set} like:
+			 *
+			 * 		data.set( { main: '<p>Foo</p>', root2: '<p>Bar</p>' } );
+			 *
+			 * will throw this error.
+			 *
+			 * @error datacontroller-set-non-existent-root
+			 */
+			throw new CKEditorError( 'datacontroller-set-non-existent-root: Attempting to set data on a non-existing root.' );
+		}
 
 		this.model.enqueueChange( 'transparent', writer => {
 			writer.setSelection( null );
 			writer.removeSelectionAttribute( this.model.document.selection.getAttributeKeys() );
 
-			writer.remove( writer.createRangeIn( modelRoot ) );
-			writer.insert( this.parse( data, modelRoot ), modelRoot, 0 );
+			for ( const rootName of Object.keys( newData ) ) {
+				// Save to model.
+				const modelRoot = this.model.document.getRoot( rootName );
+
+				writer.remove( writer.createRangeIn( modelRoot ) );
+				writer.insert( this.parse( newData[ rootName ], modelRoot ), modelRoot, 0 );
+			}
 		} );
 	}
 
@@ -273,11 +374,40 @@ export default class DataController {
 	/**
 	 * Removes all event listeners set by the DataController.
 	 */
-	destroy() {}
+	destroy() {
+		this.stopListening();
+	}
+
+	/**
+	 * Checks if all provided root names are existing editor roots.
+	 *
+	 * @private
+	 * @param {Array.<String>} rootNames Root names to check.
+	 * @returns {Boolean} Whether all provided root names are existing editor roots.
+	 */
+	_checkIfRootsExists( rootNames ) {
+		for ( const rootName of rootNames ) {
+			if ( !this.model.document.getRootNames().includes( rootName ) ) {
+				return false;
+			}
+		}
+
+		return true;
+	}
 
 	/**
-	 * Event fired by decorated {@link #init} method.
-	 * See {@link module:utils/observablemixin~ObservableMixin.decorate} for more information and samples.
+	 * Event fired once data initialisation has finished.
+	 *
+	 * @event ready
+	 */
+
+	/**
+	 * Event fired after {@link #init init() method} has been run. It can be {@link #listenTo listened to} to adjust/modify
+	 * the initialisation flow. However, if the `init` event is stopped or prevented, the {@link #event:ready ready event}
+	 * should be fired manually.
+	 *
+	 * The `init` event is fired by decorated {@link #init} method.
+	 * See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples.
 	 *
 	 * @event init
 	 */

+ 6 - 8
packages/ckeditor5-engine/src/controller/editingcontroller.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -11,12 +11,11 @@ import RootEditableElement from '../view/rooteditableelement';
 import View from '../view/view';
 import Mapper from '../conversion/mapper';
 import DowncastDispatcher from '../conversion/downcastdispatcher';
-import { insertText, remove } from '../conversion/downcast-converters';
-import { convertSelectionChange } from '../conversion/upcast-selection-converters';
-import { clearAttributes, convertCollapsedSelection, convertRangeSelection } from '../conversion/downcast-selection-converters';
+import { clearAttributes, convertCollapsedSelection, convertRangeSelection, insertText, remove } from '../conversion/downcasthelpers';
 
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import { convertSelectionChange } from '../conversion/upcasthelpers';
 
 /**
  * Controller for the editing pipeline. The editing pipeline controls {@link ~EditingController#model model} rendering,
@@ -76,12 +75,11 @@ export default class EditingController {
 		//
 		// See  https://github.com/ckeditor/ckeditor5-engine/issues/1528
 		this.listenTo( this.model, '_beforeChanges', () => {
-			this.view._renderingDisabled = true;
+			this.view._disableRendering( true );
 		}, { priority: 'highest' } );
 
 		this.listenTo( this.model, '_afterChanges', () => {
-			this.view._renderingDisabled = false;
-			this.view.render();
+			this.view._disableRendering( false );
 		}, { priority: 'lowest' } );
 
 		// Whenever model document is changed, convert those changes to the view (using model.Document#differ).

+ 151 - 137
packages/ckeditor5-engine/src/conversion/conversion.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -8,18 +8,8 @@
  */
 
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
-import {
-	downcastElementToElement,
-	downcastAttributeToElement,
-	downcastAttributeToAttribute
-} from './downcast-converters';
-
-import {
-	upcastElementToElement,
-	upcastElementToAttribute,
-	upcastAttributeToAttribute
-} from './upcast-converters';
+import UpcastHelpers from './upcasthelpers';
+import DowncastHelpers from './downcasthelpers';
 
 /**
  * A utility class that helps add converters to upcast and downcast dispatchers.
@@ -27,7 +17,7 @@ import {
  * We recommend reading the {@glink framework/guides/architecture/editing-engine Editing engine architecture} guide first to
  * understand the core concepts of the conversion mechanisms.
  *
- * The instance of the conversion manager is available in the
+ * An instance of the conversion manager is available in the
  * {@link module:core/editor/editor~Editor#conversion `editor.conversion`} property
  * and by default has the following groups of dispatchers (i.e. directions of conversion):
  *
@@ -36,23 +26,26 @@ import {
  * * `dataDowncast`
  * * `upcast`
  *
+ * # One-way converters
+ *
  * To add a converter to a specific group, use the {@link module:engine/conversion/conversion~Conversion#for `for()`}
  * method:
  *
  *		// Add a converter to editing downcast and data downcast.
- *		editor.conversion.for( 'downcast' ).add( downcastElementToElement( config ) );
+ *		editor.conversion.for( 'downcast' ).elementToElement( config ) );
  *
  *		// Add a converter to the data pipepline only:
- *		editor.conversion.for( 'dataDowncast' ).add( downcastElementToElement( dataConversionConfig ) );
+ *		editor.conversion.for( 'dataDowncast' ).elementToElement( dataConversionConfig ) );
+ *
  *		// And a slightly different one for the editing pipeline:
- *		editor.conversion.for( 'editingDowncast' ).add( downcastElementToElement( editingConversionConfig ) );
+ *		editor.conversion.for( 'editingDowncast' ).elementToElement( editingConversionConfig ) );
+ *
+ * See {@link module:engine/conversion/conversion~Conversion#for `for()`} method documentation to learn more about
+ * available conversion helpers and how to use your custom ones.
  *
- * The functions used in `add()` calls are one-way converters (i.e. you need to remember yourself to add
- * a converter in the other direction, if your feature requires that). They are also called "conversion helpers".
- * You can find a set of them in the {@link module:engine/conversion/downcast-converters} and
- * {@link module:engine/conversion/upcast-converters} modules.
+ * # Two-way converters
  *
- * Besides allowing to register converters to specific dispatchers, you can also use methods available in this
+ * Besides using one-way converters via the `for()` method, you can also use other methods available in this
  * class to add two-way converters (upcast and downcast):
  *
  * * {@link module:engine/conversion/conversion~Conversion#elementToElement `elementToElement()`} &ndash;
@@ -65,95 +58,132 @@ import {
 export default class Conversion {
 	/**
 	 * Creates a new conversion instance.
+	 *
+	 * @param {module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+	 * Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher>} downcastDispatchers
+	 * @param {module:engine/conversion/upcastdispatcher~UpcastDispatcher|
+	 * Array.<module:engine/conversion/upcastdispatcher~UpcastDispatcher>} upcastDispatchers
 	 */
-	constructor() {
+	constructor( downcastDispatchers, upcastDispatchers ) {
 		/**
+		 * Maps dispatchers group name to ConversionHelpers instances.
+		 *
 		 * @private
-		 * @member {Map}
+		 * @member {Map.<String,module:engine/conversion/conversionhelpers~ConversionHelpers>}
 		 */
-		this._dispatchersGroups = new Map();
+		this._helpers = new Map();
+
+		// Define default 'downcast' & 'upcast' dispatchers groups. Those groups are always available as two-way converters needs them.
+		this._downcast = Array.isArray( downcastDispatchers ) ? downcastDispatchers : [ downcastDispatchers ];
+		this._createConversionHelpers( { name: 'downcast', dispatchers: this._downcast, isDowncast: true } );
+
+		this._upcast = Array.isArray( upcastDispatchers ) ? upcastDispatchers : [ upcastDispatchers ];
+		this._createConversionHelpers( { name: 'upcast', dispatchers: this._upcast, isDowncast: false } );
 	}
 
 	/**
-	 * Registers one or more converters under a given group name. The group name can then be used to assign a converter
-	 * to multiple dispatchers at once.
+	 * Define an alias for registered dispatcher.
 	 *
-	 * If a given group name is used for the second time, the
-	 * {@link module:utils/ckeditorerror~CKEditorError `conversion-register-group-exists` error} is thrown.
+	 *		const conversion = new Conversion(
+	 *			[ dataDowncastDispatcher, editingDowncastDispatcher ],
+	 *			upcastDispatcher
+	 *		);
 	 *
-	 * @param {String} groupName The name for dispatchers group.
-	 * @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
-	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers Dispatchers to register
-	 * under the given name.
+	 *		conversion.addAlias( 'dataDowncast', dataDowncastDispatcher );
+	 *
+	 * @param {String} alias An alias of a dispatcher.
+	 * @param {module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher} dispatcher Dispatcher which should have an alias.
 	 */
-	register( groupName, dispatchers ) {
-		if ( this._dispatchersGroups.has( groupName ) ) {
+	addAlias( alias, dispatcher ) {
+		const isDowncast = this._downcast.includes( dispatcher );
+		const isUpcast = this._upcast.includes( dispatcher );
+
+		if ( !isUpcast && !isDowncast ) {
 			/**
-			 * Trying to register a group name that was already registered.
+			 * Trying to register and alias for a dispatcher that nas not been registered.
 			 *
-			 * @error conversion-register-group-exists
+			 * @error conversion-add-alias-dispatcher-not-registered
 			 */
-			throw new CKEditorError( 'conversion-register-group-exists: Trying to register a group name that was already registered.' );
+			throw new CKEditorError( 'conversion-add-alias-dispatcher-not-registered: ' +
+				'Trying to register and alias for a dispatcher that nas not been registered.' );
 		}
 
-		this._dispatchersGroups.set( groupName, dispatchers );
+		this._createConversionHelpers( { name: alias, dispatchers: [ dispatcher ], isDowncast } );
 	}
 
 	/**
-	 * Provides chainable API to assign converters to dispatchers registered under a given group name. Converters are added
-	 * by calling the `.add()` method of an object returned by this function.
+	 * Provides a chainable API to assign converters to conversion dispatchers group.
 	 *
-	 *		conversion.for( 'downcast' )
-	 *			.add( conversionHelperA )
-	 *			.add( conversionHelperB );
+	 * If the given group name has not been registered, the
+	 * {@link module:utils/ckeditorerror~CKEditorError `conversion-for-unknown-group` error} is thrown.
 	 *
-	 * In this example `conversionHelperA` and `conversionHelperB` will be called for all dispatchers from the `'model'` group.
+	 * You can use conversion helpers available directly in the `for()` chain or your custom ones via
+	 * the {@link module:engine/conversion/conversionhelpers~ConversionHelpers#add `add()`} method.
 	 *
-	 * The `.add()` method takes exactly one parameter, which is a function. This function should accept one parameter that
-	 * is a dispatcher instance. The function should add an actual converter to the passed dispatcher instance.
+	 * # Using bulit-in conversion helpers
+	 *
+	 * The `for()` chain comes with a set of conversion helpers which you can use like this:
+	 *
+	 *		editor.conversion.for( 'downcast' )
+	 *			.elementToElement( config1 )        // Adds an element-to-element downcast converter.
+	 *			.attributeToElement( config2 );     // Adds an attribute-to-element downcast converter.
+	 *
+	 *		editor.conversion.for( 'upcast' )
+	 *			.elementToAttribute( config3 );     // Adds an element-to-attribute upcast converter.
 	 *
-	 * Conversion helpers for most common cases are already provided. They are flexible enough to cover most use cases.
-	 * See the documentation to learn how they can be configured.
+	 * Refer to the documentation of built-in conversion helpers to learn about their configuration options.
 	 *
-	 * For downcast (model-to-view conversion), these are:
+	 * * downcast (model-to-view) conversion helpers:
 	 *
-	 * * {@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}.
+	 *	* {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`},
+	 *	* {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement `attributeToElement()`},
+	 *	* {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToAttribute `attributeToAttribute()`}.
+	 *	* {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToElement `markerToElement()`}.
+	 *	* {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToHighlight `markerToHighlight()`}.
 	 *
-	 * For upcast (view-to-model conversion), these are:
+	 * * upcast (view-to-model) conversion helpers:
 	 *
-	 * * {@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}.
+	 *	* {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToElement `elementToElement()`},
+	 *	* {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToAttribute `elementToAttribute()`},
+	 *	* {@link module:engine/conversion/upcasthelpers~UpcastHelpers#attributeToAttribute `attributeToAttribute()`}.
+	 *	* {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToMarker `elementToMarker()`}.
 	 *
-	 * An example of using conversion helpers to convert the `paragraph` model element to the `p` view element (and back):
+	 * # Using custom conversion helpers
 	 *
-	 *		// Define conversion configuration - model element 'paragraph' should be converted to view element 'p'.
-	 *		const config = { model: 'paragraph', view: 'p' };
+	 * If you need to implement a nontypical converter, you can do so by calling:
 	 *
-	 *		// Add converters to proper dispatchers using conversion helpers.
-	 *		conversion.for( 'downcast' ).add( downcastElementToElement( config ) );
-	 *		conversion.for( 'upcast' ).add( upcastElementToElement( config ) );
+	 *		editor.conversion.for( direction ).add( customHelper );
 	 *
-	 * An example of providing a custom conversion helper that uses a custom converter function:
+	 * The `.add()` method takes exactly one parameter, which is a function. This function should accept one parameter that
+	 * is a dispatcher instance. The function should add an actual converter to the passed dispatcher instance.
+	 *
+	 * Example:
+	 *
+	 *		editor.conversion.for( 'upcast' ).add( dispatcher => {
+	 *			dispatcher.on( 'element:a',  ( evt, data, conversionApi ) => {
+	 *				// Do something with a view <a> element.
+	 *			} );
+	 *		} );
 	 *
-	 *		// Adding a custom `myConverter` converter for 'paragraph' element insertion, with the default priority ('normal').
-	 *		conversion.for( 'downcast' ).add( conversion.customConverter( 'insert:paragraph', myConverter ) );
+	 * Refer to the documentation of {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}
+	 * and {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} to learn how to write
+	 * custom converters.
 	 *
 	 * @param {String} groupName The name of dispatchers group to add the converters to.
-	 * @returns {Object} An object with the `.add()` method, providing a way to add converters.
+	 * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers}
 	 */
 	for( groupName ) {
-		const dispatchers = this._getDispatchers( groupName );
-
-		return {
-			add( conversionHelper ) {
-				_addToDispatchers( dispatchers, conversionHelper );
+		if ( !this._helpers.has( groupName ) ) {
+			/**
+			 * 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 this;
-			}
-		};
+		return this._helpers.get( groupName );
 	}
 
 	/**
@@ -161,13 +191,13 @@ export default class Conversion {
 	 * For example, the model `<paragraph>Foo</paragraph>` is `<p>Foo</p>` in the view.
 	 *
 	 *		// A simple conversion from the `paragraph` model element to the `<p>` view element (and vice versa).
-	 *		conversion.elementToElement( { model: 'paragraph', view: 'p' } );
+	 *		editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 	 *
 	 *		// Override other converters by specifying a converter definition with a higher priority.
-	 *		conversion.elementToElement( { model: 'paragraph', view: 'div', converterPriority: 'high' } );
+	 *		editor.conversion.elementToElement( { model: 'paragraph', view: 'div', converterPriority: 'high' } );
 	 *
 	 *		// View specified as an object instead of a string.
-	 *		conversion.elementToElement( {
+	 *		editor.conversion.elementToElement( {
 	 *			model: 'fancyParagraph',
 	 *			view: {
 	 *				name: 'p',
@@ -176,7 +206,7 @@ export default class Conversion {
 	 *		} );
 	 *
 	 *		// Use `upcastAlso` to define other view elements that should also be converted to a `paragraph` element.
-	 *		conversion.elementToElement( {
+	 *		editor.conversion.elementToElement( {
 	 *			model: 'paragraph',
 	 *			view: 'p',
 	 *			upcastAlso: [
@@ -191,7 +221,7 @@ export default class Conversion {
 	 *		} );
 	 *
 	 *		// `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
-	 *		conversion.elementToElement( {
+	 *		editor.conversion.elementToElement( {
 	 *			model: 'heading',
 	 *			view: 'h2',
 	 *			// Convert "headling-like" paragraphs to headings.
@@ -229,17 +259,16 @@ export default class Conversion {
 	 */
 	elementToElement( definition ) {
 		// Set up downcast converter.
-		this.for( 'downcast' ).add( downcastElementToElement( definition ) );
+		this.for( 'downcast' ).elementToElement( definition );
 
 		// Set up upcast converter.
 		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
-			this.for( 'upcast' ).add(
-				upcastElementToElement( {
+			this.for( 'upcast' )
+				.elementToElement( {
 					model,
 					view,
 					converterPriority: definition.converterPriority
-				} )
-			);
+				} );
 		}
 	}
 
@@ -248,13 +277,13 @@ export default class Conversion {
 	 * For example, a model text node with `"Foo"` as data and the `bold` attribute is `<strong>Foo</strong>` in the view.
 	 *
 	 *		// A simple conversion from the `bold=true` attribute to the `<strong>` view element (and vice versa).
-	 *		conversion.attributeToElement( { model: 'bold', view: 'strong' } );
+	 *		editor.conversion.attributeToElement( { model: 'bold', view: 'strong' } );
 	 *
 	 *		// Override other converters by specifying a converter definition with a higher priority.
-	 *		conversion.attributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } );
+	 *		editor.conversion.attributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } );
 	 *
 	 *		// View specified as an object instead of a string.
-	 *		conversion.attributeToElement( {
+	 *		editor.conversion.attributeToElement( {
 	 *			model: 'bold',
 	 *			view: {
 	 *				name: 'span',
@@ -264,7 +293,7 @@ export default class Conversion {
 	 *
 	 *		// Use `config.model.name` to define the conversion only from a given node type, `$text` in this case.
 	 *		// The same attribute on different elements may then be handled by a different converter.
-	 *		conversion.attributeToElement( {
+	 *		editor.conversion.attributeToElement( {
 	 *			model: {
 	 *				key: 'textDecoration',
 	 *				values: [ 'underline', 'lineThrough' ],
@@ -287,7 +316,7 @@ export default class Conversion {
 	 *		} );
 	 *
 	 *		// Use `upcastAlso` to define other view elements that should also be converted to the `bold` attribute.
-	 *		conversion.attributeToElement( {
+	 *		editor.conversion.attributeToElement( {
 	 *			model: 'bold',
 	 *			view: 'strong',
 	 *			upcastAlso: [
@@ -321,7 +350,7 @@ export default class Conversion {
 	 *
 	 *		// Conversion from and to a model attribute key whose value is an enum (`fontSize=big|small`).
 	 *		// `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
-	 *		conversion.attributeToElement( {
+	 *		editor.conversion.attributeToElement( {
 	 *			model: {
 	 *				key: 'fontSize',
 	 *				values: [ 'big', 'small' ]
@@ -402,29 +431,31 @@ export default class Conversion {
 	 */
 	attributeToElement( definition ) {
 		// Set up downcast converter.
-		this.for( 'downcast' ).add( downcastAttributeToElement( definition ) );
+		this.for( 'downcast' ).attributeToElement( definition );
 
 		// Set up upcast converter.
 		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
-			this.for( 'upcast' ).add(
-				upcastElementToAttribute( {
+			this.for( 'upcast' )
+				.elementToAttribute( {
 					view,
 					model,
-					priority: definition.priority
-				} )
-			);
+					converterPriority: definition.converterPriority
+				} );
 		}
 	}
 
 	/**
 	 * Sets up converters between the model and the view that 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>` (the same attribute key and value).
+	 * This type of converters is intended to be used with {@link module:engine/model/element~Element model element} nodes.
+	 * To convert text attributes {@link module:engine/conversion/conversion~Conversion#attributeToElement `attributeToElement converter`}
+	 * should be set up.
 	 *
 	 *		// A simple conversion from the `source` model attribute to the `src` view attribute (and vice versa).
-	 *		conversion.attributeToAttribute( { model: 'source', view: 'src' } );
+	 *		editor.conversion.attributeToAttribute( { model: 'source', view: 'src' } );
 	 *
 	 *		// Attribute values are strictly specified.
-	 *		conversion.attributeToAttribute( {
+	 *		editor.conversion.attributeToAttribute( {
 	 *			model: {
 	 *				name: 'image',
 	 *				key: 'aside',
@@ -440,7 +471,7 @@ export default class Conversion {
 	 *		} );
 	 *
 	 *		// Set the style attribute.
-	 *		conversion.attributeToAttribute( {
+	 *		editor.conversion.attributeToAttribute( {
 	 *			model: {
 	 *				name: 'image',
 	 *				key: 'aside',
@@ -461,7 +492,7 @@ export default class Conversion {
 	 *
 	 *		// Conversion from and to a model attribute key whose value is an enum (`align=right|center`).
 	 *		// Use `upcastAlso` to define other view elements that should also be converted to the `align=right` attribute.
-	 *		conversion.attributeToAttribute( {
+	 *		editor.conversion.attributeToAttribute( {
 	 *			model: {
 	 *				key: 'align',
 	 *				values: [ 'right', 'center' ]
@@ -525,43 +556,41 @@ export default class Conversion {
 	 */
 	attributeToAttribute( definition ) {
 		// Set up downcast converter.
-		this.for( 'downcast' ).add( downcastAttributeToAttribute( definition ) );
+		this.for( 'downcast' ).attributeToAttribute( definition );
 
 		// Set up upcast converter.
 		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
-			this.for( 'upcast' ).add(
-				upcastAttributeToAttribute( {
+			this.for( 'upcast' )
+				.attributeToAttribute( {
 					view,
 					model
-				} )
-			);
+				} );
 		}
 	}
 
 	/**
-	 * Returns dispatchers registered under a given group name.
-	 *
-	 * If the given group name has not been registered, the
-	 * {@link module:utils/ckeditorerror~CKEditorError `conversion-for-unknown-group` error} is thrown.
+	 * Creates and caches conversion helpers for given dispatchers group.
 	 *
 	 * @private
-	 * @param {String} groupName
-	 * @returns {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
-	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>}
+	 * @param {Object} options
+	 * @param {String} options.name Group name.
+	 * @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} options.dispatchers
+	 * @param {Boolean} options.isDowncast
 	 */
-	_getDispatchers( groupName ) {
-		const dispatchers = this._dispatchersGroups.get( groupName );
-
-		if ( !dispatchers ) {
+	_createConversionHelpers( { name, dispatchers, isDowncast } ) {
+		if ( this._helpers.has( name ) ) {
 			/**
-			 * Trying to add a converter to an unknown dispatchers group.
+			 * Trying to register a group name that has already been registered.
 			 *
-			 * @error conversion-for-unknown-group
+			 * @error conversion-group-exists
 			 */
-			throw new CKEditorError( 'conversion-for-unknown-group: Trying to add a converter to an unknown dispatchers group.' );
+			throw new CKEditorError( 'conversion-group-exists: Trying to register a group name that has already been registered.' );
 		}
 
-		return dispatchers;
+		const helpers = isDowncast ? new DowncastHelpers( dispatchers ) : new UpcastHelpers( dispatchers );
+
+		this._helpers.set( name, helpers );
 	}
 }
 
@@ -582,21 +611,6 @@ export default class Conversion {
  * @property {module:utils/priorities~PriorityString} [converterPriority] The converter priority.
  */
 
-// Helper function for the `Conversion` `.add()` method.
-//
-// Calls `conversionHelper` on each dispatcher from the group specified earlier in the `.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`.
 //

+ 40 - 0
packages/ckeditor5-engine/src/conversion/conversionhelpers.js

@@ -0,0 +1,40 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module engine/conversion/conversionhelpers
+ */
+
+/**
+ * Base class for conversion helpers.
+ */
+export default class ConversionHelpers {
+	/**
+	 * Creates a conversion helpers instance.
+	 *
+	 * @param {Array.<module:engine/conversion/downcastdispatcher~DowncastDispatcher|
+	 * module:engine/conversion/upcastdispatcher~UpcastDispatcher>} dispatchers
+	 */
+	constructor( dispatchers ) {
+		this._dispatchers = dispatchers;
+	}
+
+	/**
+	 * Registers a conversion helper.
+	 *
+	 * **Note**: See full usage example in the `{@link module:engine/conversion/conversion~Conversion#for conversion.for()}`
+	 * method description.
+	 *
+	 * @param {Function} conversionHelper The function to be called on event.
+	 * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers|module:engine/conversion/upcasthelpers~UpcastHelpers}
+	 */
+	add( conversionHelper ) {
+		for ( const dispatcher of this._dispatchers ) {
+			conversionHelper( dispatcher );
+		}
+
+		return this;
+	}
+}

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

@@ -1,1061 +0,0 @@
-/**
- * @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 DocumentSelection from '../model/documentselection';
-
-import { cloneDeep } from 'lodash-es';
-
-/**
- * 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', converterPriority: 'high' } );
- *
- *		downcastElementToElement( {
- *			model: 'fancyParagraph',
- *			view: {
- *				name: 'p',
- *				classes: 'fancy'
- *			}
- *		} );
- *
- * 		downcastElementToElement( {
- * 			model: 'heading',
- * 			view: ( modelElement, viewWriter ) => viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
- * 		} );
- *
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
- * to the conversion process.
- *
- * @param {Object} config Conversion configuration.
- * @param {String} config.model The name of the model element to convert.
- * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
- * that takes the model element and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}
- * as parameters and returns a view container element.
- * @returns {Function} Conversion helper.
- */
-export function downcastElementToElement( config ) {
-	config = cloneDeep( config );
-
-	config.view = _normalizeToElementConfig( config.view, 'container' );
-
-	return dispatcher => {
-		dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.converterPriority || 'normal' } );
-	};
-}
-
-/**
- * Model attribute to view element conversion helper.
- *
- * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with
- * `"Foo"` as data and the `bold` attribute becomes `<strong>Foo</strong>` in the view.
- *
- *		downcastAttributeToElement( { model: 'bold', view: 'strong' } );
- *
- *		downcastAttributeToElement( { model: 'bold', view: 'b', converterPriority: 'high' } );
- *
- *		downcastAttributeToElement( {
- *			model: 'invert',
- *			view: {
- *				name: 'span',
- *				classes: [ 'font-light', 'bg-dark' ]
- *			}
- *		} );
- *
- *		downcastAttributeToElement( {
- *			model: {
- *				key: 'fontSize',
- *				values: [ 'big', 'small' ]
- *			},
- *			view: {
- *				big: {
- *					name: 'span',
- *					styles: {
- *						'font-size': '1.2em'
- *					}
- *				},
- *				small: {
- *					name: 'span',
- *					styles: {
- *						'font-size': '0.8em'
- *					}
- *				}
- *			}
- *		} );
- *
- * 		downcastAttributeToElement( {
- * 			model: 'bold',
- * 			view: ( modelAttributeValue, viewWriter ) => {
- * 				return viewWriter.createAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } );
- * 			}
- * 		} );
- *
- *		downcastAttributeToElement( {
- *			model: {
- *				key: 'color',
- *				name: '$text'
- *			},
- *			view: ( modelAttributeValue, viewWriter ) => {
- *				return viewWriter.createAttributeElement( 'span', { style: 'color:' + modelAttributeValue } );
- *			}
- *		} );
- *
- * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
- * to the conversion process.
- *
- * @param {Object} config Conversion configuration.
- * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
- * of `String`s with possible values if the model attribute is an enumerable.
- * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function
- * that takes the model attribute value and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}
- * as parameters and returns a view attribute element. If `config.model.values` is
- * given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions.
- * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
- * @returns {Function} Conversion helper.
- */
-export function downcastAttributeToElement( 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 ] = _normalizeToElementConfig( config.view[ modelValue ], 'attribute' );
-		}
-	} else {
-		config.view = _normalizeToElementConfig( config.view, 'attribute' );
-	}
-
-	const elementCreator = _getFromAttributeCreator( config );
-
-	return dispatcher => {
-		dispatcher.on( eventName, wrap( elementCreator ), { priority: config.converterPriority || 'normal' } );
-	};
-}
-
-/**
- * Model attribute to view attribute conversion helper.
- *
- * This conversion results in adding an attribute to 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', converterPriority: '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 `conversion.for()`} to learn how to add a converter
- * to the conversion process.
- *
- * @param {Object} config Conversion configuration.
- * @param {String|Object} config.model The 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 A view attribute key, or a `{ key, value }` object or a function that takes
- * the 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.converterPriority='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.converterPriority || 'normal' } );
-	};
-}
-
-/**
- * Model marker to view element conversion helper.
- *
- * This conversion results in creating a view element on the boundaries of the converted marker. If the 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', converterPriority: 'high' } );
- *
- *		downcastMarkerToElement( {
- *			model: 'search',
- *			view: {
- *				name: 'span',
- *				attributes: {
- *					'data-marker': 'search'
- *				}
- *			}
- *		} );
- *
- * 		downcastMarkerToElement( {
- * 			model: 'search',
- * 			view: ( markerData, viewWriter ) => {
- *	 			return viewWriter.createUIElement( 'span', { 'data-marker': 'search', 'data-start': markerData.isOpening } );
- * 			}
- * 		} );
- *
- * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function
- * receives the `data` object as a parameter and should return an instance of the
- * {@link module:engine/view/uielement~UIElement view UI element}. The `data` and `conversionApi` objects are passed from
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
- * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` to
- * the marker end boundary element.
- *
- * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline.
- *
- * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add a converter to the conversion process.
- *
- * @param {Object} config Conversion configuration.
- * @param {String} config.model The name of the model marker (or model marker group) to convert.
- * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
- * that takes the model marker data as a parameter and returns a view UI element.
- * @param {module:utils/priorities~PriorityString} [config.converterPriority='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.converterPriority || 'normal' } );
-		dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority: config.converterPriority || '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, a 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 a 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, a 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 the highlight descriptor and passes it
- * to a container element, it is the container element instance itself that applies values from the highlight descriptor.
- * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that.
- *
- *		downcastMarkerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
- *
- *		downcastMarkerToHighlight( { model: 'comment', view: { classes: 'new-comment' }, converterPriority: 'high' } );
- *
- * 		downcastMarkerToHighlight( {
- * 			model: 'comment',
- * 			view: data => {
- * 				// Assuming that the marker name is in a form of comment:commentType.
- *	 			const commentType = data.markerName.split( ':' )[ 1 ];
- *
- *	 			return {
- *	 				classes: [ 'comment', 'comment-' + commentType ]
- *	 			};
- * 			}
- * 		} );
- *
- * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function
- * receives the `data` object as a parameter and should return a
- * {@link module:engine/conversion/downcast-converters~HighlightDescriptor highlight descriptor}.
- * 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 a converter to the conversion process.
- *
- * @param {Object} config Conversion configuration.
- * @param {String} config.model The name of the model marker (or model marker group) to convert.
- * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} config.view A highlight descriptor
- * that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor.
- * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
- * @returns {Function} Conversion helper.
- */
-export function downcastMarkerToHighlight( config ) {
-	return dispatcher => {
-		dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority: config.converterPriority || 'normal' } );
-		dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority: config.converterPriority || 'normal' } );
-		dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority: config.converterPriority || 'normal' } );
-	};
-}
-
-// Takes `config.view`, and if it is an {@link module:engine/view/elementdefinition~ElementDefinition}, converts it
-// to a function (because lower level converters accept 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 a view element instance from the provided {@link module:engine/view/elementdefinition~ElementDefinition} and class.
-//
-// @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition
-// @param {module:engine/view/downcastwriter~DowncastWriter} 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;
-	const attributes = Object.assign( {}, viewElementDefinition.attributes );
-
-	if ( viewElementType == 'container' ) {
-		element = viewWriter.createContainerElement( viewElementDefinition.name, attributes );
-	} else if ( viewElementType == 'attribute' ) {
-		const options = {
-			priority: viewElementDefinition.priority || ViewAttributeElement.DEFAULT_PRIORITY
-		};
-
-		element = viewWriter.createAttributeElement( viewElementDefinition.name, attributes, options );
-	} else {
-		// 'ui'.
-		element = viewWriter.createUIElement( viewElementDefinition.name, attributes );
-	}
-
-	if ( viewElementDefinition.styles ) {
-		const keys = Object.keys( viewElementDefinition.styles );
-
-		for ( const key of keys ) {
-			viewWriter.setStyle( key, viewElementDefinition.styles[ key ], element );
-		}
-	}
-
-	if ( viewElementDefinition.classes ) {
-		const classes = viewElementDefinition.classes;
-
-		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 the configuration, adds default parameters if they do not exist and normalizes other parameters to be used in downcast converters
-// for generating a 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' ) {
-		// { key, value, ... }
-		if ( view.value ) {
-			return () => view;
-		}
-		// { key, ... }
-		else {
-			return modelAttributeValue => ( { key: view.key, value: modelAttributeValue } );
-		}
-	} else {
-		// function.
-		return view;
-	}
-}
-
-/**
- * Function factory that creates a converter which converts node insertion changes from the model to the view.
- * The function passed will be provided with all the parameters of the dispatcher's
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}.
- * It is expected that the function returns an {@link module:engine/view/element~Element}.
- * The result of the function will be inserted into the view.
- *
- * The converter automatically consumes the corresponding value from the consumables list, stops the event (see
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and binds the 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 that creates a default downcast converter for text insertion changes.
- *
- * The converter automatically consumes the corresponding value from the 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 that 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 = conversionApi.writer.createRange( viewStart, viewEnd );
-
-		// Trim the range to remove in case some UI elements are on the view range boundaries.
-		const removed = conversionApi.writer.remove( viewRange.getTrimmed() );
-
-		// After the range is removed, unbind all view elements from the model.
-		// Range inside view document fragment is used to unbind deeply.
-		for ( const child of conversionApi.writer.createRangeIn( removed ).getItems() ) {
-			conversionApi.mapper.unbindViewElement( child );
-		}
-	};
-}
-
-/**
- * Function factory that creates a converter which converts marker adding change to the
- * {@link module:engine/view/uielement~UIElement view UI element}.
- *
- * The view UI element that will be added to the view depends on the passed parameter. See {@link ~insertElement}.
- * In case of a non-collapsed range, the UI element will not wrap nodes but separate elements will be placed at the beginning
- * and at the end of the range.
- *
- * This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
- *
- * @param {module:engine/view/uielement~UIElement|Function} elementCreator A view UI element or a function returning the view element
- * that 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 );
-		conversionApi.mapper.bindElementToMarker( viewStartElement, data.markerName );
-
-		// Add "closing" element only if range is not collapsed.
-		if ( !markerRange.isCollapsed ) {
-			viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
-			conversionApi.mapper.bindElementToMarker( viewEndElement, data.markerName );
-		}
-
-		evt.stop();
-	};
-}
-
-/**
- * Function factory that returns a default downcast converter for removing a {@link module:engine/view/uielement~UIElement UI element}
- * basing on marker remove change.
- *
- * This converter unbinds elements from the marker name.
- *
- * @returns {Function} Removed UI element converter.
- */
-export function removeUIElement() {
-	return ( evt, data, conversionApi ) => {
-		const elements = conversionApi.mapper.markerNameToElements( data.markerName );
-
-		if ( !elements ) {
-			return;
-		}
-
-		conversionApi.mapper.unbindElementsFromMarkerName( data.markerName );
-
-		for ( const element of elements ) {
-			conversionApi.writer.clear( conversionApi.writer.createRangeOn( element ), element );
-		}
-
-		conversionApi.writer.clearClonedElementsGroup( data.markerName );
-
-		evt.stop();
-	};
-}
-
-/**
- * Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view.
- *
- * Attributes from the 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 element
- * attributes on a one-to-one basis.
- *
- * **Note:** The provided attribute creator should always return the same `key` for a given attribute from the model.
- *
- * The converter automatically consumes the corresponding value from the 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 the 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 the key-value pair.
- *			return { key, value };
- *		} ) );
- *
- * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
- * represent the attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
- * The function is passed the model attribute value as the first parameter and additional data about the change as the second parameter.
- * @returns {Function} Set/change attribute converter.
- */
-export function changeAttribute( attributeCreator ) {
-	attributeCreator = attributeCreator || ( ( value, data ) => ( { value, key: data.attributeKey } ) );
-
-	return ( evt, data, conversionApi ) => {
-		const oldAttribute = attributeCreator( data.attributeOldValue, data );
-		const newAttribute = attributeCreator( data.attributeNewValue, data );
-
-		if ( !oldAttribute && !newAttribute ) {
-			return;
-		}
-
-		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.
-		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 set the new attribute.
-		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 that creates a converter which converts set/change/remove attribute changes from the model to the view.
- * It can also 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 the model are converted to a view element that will be wrapping these view nodes that are bound to
- * model elements having the given attribute. This is useful for attributes like `bold` that may be set on text nodes in the 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 the attribute value and then all the parameters of the
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute` event}.
- * It is expected that the function returns an {@link module:engine/view/element~Element}.
- * The result of the function will be the wrapping element.
- * When the provided `Function` does not return any element, no conversion will take place.
- *
- * The converter automatically consumes the corresponding value from the consumables list and 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 that 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 that creates a converter which converts the text inside marker's range. The converter wraps the text with
- * {@link module:engine/view/attributeelement~AttributeElement} created from the provided descriptor.
- * See {link module:engine/conversion/downcast-converters~createViewElementFromHighlightDescriptor}.
- *
- * It can also be used to convert the 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 does not provide the `priority` property, `10` will be used.
- *
- * If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
- *
- * This converter binds the created {@link module:engine/view/attributeelement~AttributeElement attribute elemens} with the marker name
- * using the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
- *
- * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} highlightDescriptor
- * @returns {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 );
-			const rangeAfterWrap = viewWriter.wrap( viewRange, viewElement );
-
-			for ( const element of rangeAfterWrap.getItems() ) {
-				if ( element.is( 'attributeElement' ) && element.isSimilar( viewElement ) ) {
-					conversionApi.mapper.bindElementToMarker( element, data.markerName );
-
-					// One attribute element is enough, because all of them are bound together by the view writer.
-					// Mapper uses this binding to get all the elements no matter how many of them are registered in the mapper.
-					break;
-				}
-			}
-		}
-	};
-}
-
-/**
- * Converter function factory. It creates a function which applies the marker's highlight to an element inside the marker's range.
- *
- * The converter checks if an element has the `addHighlight` function stored as a
- * {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight.
- * In such case the converter will consume all element's children, assuming that they were handled by the element itself.
- *
- * When the `addHighlight` custom property is not present, the element is not converted in any special way.
- * This means that converters will proceed to convert the element's child nodes.
- *
- * If the highlight descriptor does not provide the `priority` property, `10` will be used.
- *
- * If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
- *
- * This converter binds altered {@link module:engine/view/containerelement~ContainerElement container elements} with the marker name using
- * the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
- *
- * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} highlightDescriptor
- * @returns {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 );
-
-			conversionApi.mapper.bindElementToMarker( viewElement, data.markerName );
-		}
-	};
-}
-
-/**
- * Function factory that creates a converter which converts the removing model marker to the view.
- *
- * Both text nodes and elements are handled by this converter but they are handled a bit differently.
- *
- * Text nodes are unwrapped using the {@link module:engine/view/attributeelement~AttributeElement attribute element} created from the
- * provided highlight descriptor. See {link module:engine/conversion/downcast-converters~HighlightDescriptor}.
- *
- * For elements, the converter checks if an element has the `removeHighlight` function stored as a
- * {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight.
- * In such case, the children of that element will not be converted.
- *
- * When `removeHighlight` is not present, the element is not converted in any special way.
- * The converter will proceed to convert the element's child nodes instead.
- *
- * If the highlight descriptor does not provide the `priority` property, `10` will be used.
- *
- * If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
- *
- * This converter unbinds elements from the marker name.
- *
- * @param {module:engine/conversion/downcast-converters~HighlightDescriptor|Function} highlightDescriptor
- * @returns {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;
-		}
-
-		// View element that will be used to unwrap `AttributeElement`s.
-		const viewHighlightElement = createViewElementFromHighlightDescriptor( descriptor );
-
-		// Get all elements bound with given marker name.
-		const elements = conversionApi.mapper.markerNameToElements( data.markerName );
-
-		if ( !elements ) {
-			return;
-		}
-
-		conversionApi.mapper.unbindElementsFromMarkerName( data.markerName );
-
-		for ( const element of elements ) {
-			if ( element.is( 'attributeElement' ) ) {
-				conversionApi.writer.unwrap( conversionApi.writer.createRangeOn( element ), viewHighlightElement );
-			} else {
-				// if element.is( 'containerElement' ).
-				element.getCustomProperty( 'removeHighlight' )( element, descriptor.id, conversionApi.writer );
-			}
-		}
-
-		conversionApi.writer.clearClonedElementsGroup( data.markerName );
-
-		evt.stop();
-	};
-}
-
-// 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 a `<span>` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from the information
- * provided by the {@link module:engine/conversion/downcast-converters~HighlightDescriptor highlight descriptor} object. If a priority
- * is not provided in the descriptor, the default priority will be used.
- *
- * @param {module:engine/conversion/downcast-converters~HighlightDescriptor} descriptor
- * @returns {module:engine/view/attributeelement~AttributeElement}
- */
-export function createViewElementFromHighlightDescriptor( descriptor ) {
-	const viewElement = new ViewAttributeElement( 'span', descriptor.attributes );
-
-	if ( descriptor.classes ) {
-		viewElement._addClass( descriptor.classes );
-	}
-
-	if ( descriptor.priority ) {
-		viewElement._priority = descriptor.priority;
-	}
-
-	viewElement._id = descriptor.id;
-
-	return viewElement;
-}
-
-/**
- * An 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 view attribute element} with CSS class(es), attributes and a priority
- * described by this object.
- *
- * Additionally, each {@link module:engine/view/containerelement~ContainerElement container element} can handle displaying the highlight
- * separately by providing the `addHighlight` and `removeHighlight` custom properties. In this case:
- *
- *  * The `HighlightDescriptor` object is passed to the `addHighlight` function upon conversion and should be used to apply the highlight to
- *  the element.
- *  * The descriptor `id` is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight with the
- *  given ID from the element.
- *
- * @typedef {Object} module:engine/conversion/downcast-converters~HighlightDescriptor
- *
- * @property {String|Array.<String>} classes A CSS class or an array of classes to set. If the descriptor is used to
- * create an {@link module:engine/view/attributeelement~AttributeElement attribute element} over text nodes, these classes will be set
- * on that attribute element. If the descriptor is applied to an element, usually these classes will be set on that element, however,
- * this depends on how the element converts the descriptor.
- *
- * @property {String} [id] Descriptor identifier. If not provided, it defaults to the converted marker's name.
- *
- * @property {Number} [priority] Descriptor priority. If not provided, it defaults to `10`. If the descriptor is used to create
- * an {@link module:engine/view/attributeelement~AttributeElement attribute element}, it will be that element's
- * {@link module:engine/view/attributeelement~AttributeElement#priority priority}. If the 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 the descriptor is used to create
- * an {@link module:engine/view/attributeelement~AttributeElement attribute element} over text nodes, these attributes will be set on that
- * attribute element. If the descriptor is applied to an element, usually these attributes will be set on that element, however,
- * this depends on how the element converts the descriptor.
- */

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

@@ -1,129 +0,0 @@
-/**
- * @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/documentselection~DocumentSelection view selection} converters for
- * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher}.
- *
- * @module engine/conversion/downcast-selection-converters
- */
-
-/**
- * Function factory that creates a converter which converts a non-collapsed {@link module:engine/model/selection~Selection model selection}
- * to a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
- * value from the `consumable` object and maps model positions from the 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 that creates a converter which converts a collapsed {@link module:engine/model/selection~Selection model selection} to
- * a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
- * value from the `consumable` object, maps the model selection position to the view position and breaks
- * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position.
- *
- *		modelDispatcher.on( 'selection', convertCollapsedSelection() );
- *
- * An example of the view state before and after converting the collapsed selection:
- *
- *		   <p><strong>f^oo<strong>bar</p>
- *		-> <p><strong>f</strong>^<strong>oo</strong>bar</p>
- *
- * By breaking attribute elements like `<strong>`, the selection is in a correct element. Then, when the selection attribute is
- * converted, broken attributes might be merged again, or the position where the selection is may be wrapped
- * with 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 that creates a converter which 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 merges 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 does 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 );
-	};
-}

+ 66 - 19
packages/ckeditor5-engine/src/conversion/downcastdispatcher.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -105,13 +105,15 @@ export default class DowncastDispatcher {
 	/**
 	 * Creates a `DowncastDispatcher` instance.
 	 *
-	 * @param {Object} [conversionApi] Interface passed by dispatcher to the events calls.
+	 * @see module:engine/conversion/downcastdispatcher~DowncastConversionApi
+	 * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
+	 * by `DowncastDispatcher`.
 	 */
 	constructor( conversionApi = {} ) {
 		/**
 		 * Interface passed by dispatcher to the events callbacks.
 		 *
-		 * @member {Object}
+		 * @member {module:engine/conversion/downcastdispatcher~DowncastConversionApi}
 		 */
 		this.conversionApi = extend( { dispatcher: this }, conversionApi );
 	}
@@ -294,7 +296,7 @@ export default class DowncastDispatcher {
 
 			// 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.fire( 'attribute:' + data.attributeKey + ':$text', data, this.conversionApi );
 			}
 		}
 
@@ -321,22 +323,28 @@ export default class DowncastDispatcher {
 		// 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 );
+		//
+		// First, fire an event for the whole marker.
+		//
+		const consumable = new Consumable();
+		consumable.add( markerRange, eventName );
 
-			this.conversionApi.consumable = consumable;
+		this.conversionApi.consumable = consumable;
 
-			this.fire( eventName, { markerName, markerRange }, this.conversionApi );
+		this.fire( eventName, { markerName, markerRange }, this.conversionApi );
 
+		//
+		// Do not fire events for each item inside the range if the range got consumed.
+		//
+		if ( !consumable.test( markerRange, eventName ) ) {
 			return;
 		}
 
-		// Create consumable for each item in range.
+		//
+		// Then, fire an event for each item inside the marker 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 ) ) {
@@ -481,7 +489,8 @@ export default class DowncastDispatcher {
 	 * @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.
+	 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
+	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 
 	/**
@@ -497,7 +506,8 @@ export default class DowncastDispatcher {
 	 * @param {Object} data Additional information about the change.
 	 * @param {module:engine/model/position~Position} data.position Position from which the node has been removed.
 	 * @param {Number} data.length Offset size of the removed node.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
+	 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
+	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 
 	/**
@@ -523,7 +533,8 @@ export default class DowncastDispatcher {
 	 * @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.
+	 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
+	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 
 	/**
@@ -532,7 +543,8 @@ export default class DowncastDispatcher {
 	 * @event selection
 	 * @param {module:engine/model/selection~Selection} selection Selection that is converted.
 	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
-	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `DowncastDispatcher` constructor.
+	 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
+	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 
 	/**
@@ -567,7 +579,8 @@ export default class DowncastDispatcher {
 	 * @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.
+	 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
+	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 
 	/**
@@ -582,7 +595,8 @@ export default class DowncastDispatcher {
 	 * @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.
+	 * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
+	 * to be used by callback, passed in `DowncastDispatcher` constructor.
 	 */
 }
 
@@ -611,3 +625,36 @@ function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
 
 	return !hasCustomHandling;
 }
+
+/**
+ * Conversion interface that is registered for given {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}
+ * and is passed as one of parameters when {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher dispatcher}
+ * fires it's events.
+ *
+ * @interface module:engine/conversion/downcastdispatcher~DowncastConversionApi
+ */
+
+/**
+ * The {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} instance.
+ *
+ * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #dispatcher
+ */
+
+/**
+ * Stores information about what parts of processed model item are still waiting to be handled. After a piece of model item
+ * was converted, appropriate consumable value should be {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}.
+ *
+ * @member {module:engine/conversion/modelconsumable~ModelConsumable} #consumable
+ */
+
+/**
+ * The {@link module:engine/conversion/mapper~Mapper} instance.
+ *
+ * @member {module:engine/conversion/mapper~Mapper} #mapper
+ */
+
+/**
+ * The {@link module:engine/view/downcastwriter~DowncastWriter} instance used to manipulate data during conversion.
+ *
+ * @member {module:engine/view/downcastwriter~DowncastWriter} #writer
+ */

+ 1347 - 0
packages/ckeditor5-engine/src/conversion/downcasthelpers.js

@@ -0,0 +1,1347 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import ModelRange from '../model/range';
+import ModelSelection from '../model/selection';
+import ModelElement from '../model/element';
+
+import ViewAttributeElement from '../view/attributeelement';
+import DocumentSelection from '../model/documentselection';
+import ConversionHelpers from './conversionhelpers';
+
+import log from '@ckeditor/ckeditor5-utils/src/log';
+import { cloneDeep } from 'lodash-es';
+
+/**
+ * Contains downcast (model-to-view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
+ *
+ * @module engine/conversion/downcasthelpers
+ */
+
+/**
+ * Downcast conversion helper functions.
+ *
+ * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
+ */
+export default class DowncastHelpers extends ConversionHelpers {
+	/**
+	 * 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.
+	 *
+	 *		editor.conversion.for( 'downcast' ).elementToElement( {
+	 *			model: 'paragraph',
+	 *			view: 'p'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).elementToElement( {
+	 *			model: 'paragraph',
+	 *			view: 'div',
+	 *			converterPriority: 'high'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).elementToElement( {
+	 *			model: 'fancyParagraph',
+	 *			view: {
+	 *				name: 'p',
+	 *				classes: 'fancy'
+	 *			}
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).elementToElement( {
+	 *			model: 'heading',
+	 *			view: ( modelElement, viewWriter ) => {
+	 *				return viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) )
+	 *			}
+	 *		} );
+	 *
+	 * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
+	 * to the conversion process.
+	 *
+	 * @method #elementToElement
+	 * @param {Object} config Conversion configuration.
+	 * @param {String} config.model The name of the model element to convert.
+	 * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
+	 * that takes the model element and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}
+	 * as parameters and returns a view container element.
+	 * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
+	 */
+	elementToElement( config ) {
+		return this.add( downcastElementToElement( config ) );
+	}
+
+	/**
+	 * Model attribute to view element conversion helper.
+	 *
+	 * This conversion results in wrapping view nodes with a view attribute element. For example, a model text node with
+	 * `"Foo"` as data and the `bold` attribute becomes `<strong>Foo</strong>` in the view.
+	 *
+	 *		editor.conversion.for( 'downcast' ).attributeToElement( {
+	 *			model: 'bold',
+	 *			view: 'strong'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).attributeToElement( {
+	 *			model: 'bold',
+	 *			view: 'b',
+	 *			converterPriority: 'high'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).attributeToElement( {
+	 *			model: 'invert',
+	 *			view: {
+	 *				name: 'span',
+	 *				classes: [ 'font-light', 'bg-dark' ]
+	 *			}
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).attributeToElement( {
+	 *			model: {
+	 *				key: 'fontSize',
+	 *				values: [ 'big', 'small' ]
+	 *			},
+	 *			view: {
+	 *				big: {
+	 *					name: 'span',
+	 *					styles: {
+	 *						'font-size': '1.2em'
+	 *					}
+	 *				},
+	 *				small: {
+	 *					name: 'span',
+	 *					styles: {
+	 *						'font-size': '0.8em'
+	 *					}
+	 *				}
+	 *			}
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).attributeToElement( {
+	 *			model: 'bold',
+	 *			view: ( modelAttributeValue, viewWriter ) => {
+	 *				return viewWriter.createAttributeElement( 'span', {
+	 *					style: 'font-weight:' + modelAttributeValue
+	 *				} );
+	 *			}
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).attributeToElement( {
+	 *			model: {
+	 *				key: 'color',
+	 *				name: '$text'
+	 *			},
+	 *			view: ( modelAttributeValue, viewWriter ) => {
+	 *				return viewWriter.createAttributeElement( 'span', {
+	 *					style: 'color:' + modelAttributeValue
+	 *				} );
+	 *			}
+	 *		} );
+	 *
+	 * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
+	 * to the conversion process.
+	 *
+	 * @method #attributeToElement
+	 * @param {Object} config Conversion configuration.
+	 * @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
+	 * of `String`s with possible values if the model attribute is an enumerable.
+	 * @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function
+	 * that takes the model attribute value and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}
+	 * as parameters and returns a view attribute element. If `config.model.values` is
+	 * given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions.
+	 * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
+	 * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
+	 */
+	attributeToElement( config ) {
+		return this.add( downcastAttributeToElement( config ) );
+	}
+
+	/**
+	 * Model attribute to view attribute conversion helper.
+	 *
+	 * This conversion results in adding an attribute to 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>`.
+	 *
+	 *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
+	 *			model: 'source',
+	 *			view: 'src'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
+	 *			model: 'source',
+	 *			view: 'href',
+	 *			converterPriority: 'high'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
+	 *			model: {
+	 *				name: 'image',
+	 *				key: 'source'
+	 *			},
+	 *			view: 'src'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
+	 *			model: {
+	 *				name: 'styled',
+	 *				values: [ 'dark', 'light' ]
+	 *			},
+	 *			view: {
+	 *				dark: {
+	 *					key: 'class',
+	 *					value: [ 'styled', 'styled-dark' ]
+	 *				},
+	 *				light: {
+	 *					key: 'class',
+	 *					value: [ 'styled', 'styled-light' ]
+	 *				}
+	 *			}
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
+	 *			model: 'styled',
+	 *			view: modelAttributeValue => ( { key: 'class', value: 'styled-' + modelAttributeValue } )
+	 *		} );
+	 *
+	 * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
+	 * to the conversion process.
+	 *
+	 * @method #attributeToAttribute
+	 * @param {Object} config Conversion configuration.
+	 * @param {String|Object} config.model The 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 A view attribute key, or a `{ key, value }` object or a function that takes
+	 * the 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.converterPriority='normal'] Converter priority.
+	 * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
+	 */
+	attributeToAttribute( config ) {
+		return this.add( downcastAttributeToAttribute( config ) );
+	}
+
+	/**
+	 * Model marker to view element conversion helper.
+	 *
+	 * This conversion results in creating a view element on the boundaries of the converted marker. If the 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.
+	 *
+	 *		editor.conversion.for( 'downcast' ).markerToElement( {
+	 *			model: 'search',
+	 *			view: 'marker-search'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).markerToElement( {
+	 *			model: 'search',
+	 *			view: 'search-result',
+	 *			converterPriority: 'high'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).markerToElement( {
+	 *			model: 'search',
+	 *			view: {
+	 *				name: 'span',
+	 *				attributes: {
+	 *					'data-marker': 'search'
+	 *				}
+	 *			}
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).markerToElement( {
+	 *			model: 'search',
+	 *			view: ( markerData, viewWriter ) => {
+	 *				return viewWriter.createUIElement( 'span', {
+	 *					'data-marker': 'search',
+	 *					'data-start': markerData.isOpening
+	 *				} );
+	 *			}
+	 *		} );
+	 *
+	 * If a function is passed as the `config.view` parameter, it will be used to generate both boundary elements. The function
+	 * receives the `data` object as a parameter and should return an instance of the
+	 * {@link module:engine/view/uielement~UIElement view UI element}. The `data` object and
+	 * {@link module:engine/conversion/downcastdispatcher~DowncastConversionApi `conversionApi`} are passed from
+	 * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally,
+	 * the `data.isOpening` parameter is passed, which is set to `true` for the marker start boundary element, and `false` to
+	 * the marker end boundary element.
+	 *
+	 * This kind of conversion is useful for saving data into the database, so it should be used in the data conversion pipeline.
+	 *
+	 * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
+	 * to the conversion process.
+	 *
+	 * @method #markerToElement
+	 * @param {Object} config Conversion configuration.
+	 * @param {String} config.model The name of the model marker (or model marker group) to convert.
+	 * @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
+	 * that takes the model marker data as a parameter and returns a view UI element.
+	 * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
+	 * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
+	 */
+	markerToElement( config ) {
+		return this.add( downcastMarkerToElement( config ) );
+	}
+
+	/**
+	 * 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/downcasthelpers~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, a 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 a 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, a 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 the highlight descriptor and passes it
+	 * to a container element, it is the container element instance itself that applies values from the highlight descriptor.
+	 * So, in a sense, the converter takes care of stating what should be applied on what, while the element decides how to apply that.
+	 *
+	 *		editor.conversion.for( 'downcast' ).markerToHighlight( { model: 'comment', view: { classes: 'comment' } } );
+	 *
+	 *		editor.conversion.for( 'downcast' ).markerToHighlight( {
+	 *			model: 'comment',
+	 *			view: { classes: 'new-comment' },
+	 *			converterPriority: 'high'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'downcast' ).markerToHighlight( {
+	 *			model: 'comment',
+	 *			view: data => {
+	 *				// Assuming that the marker name is in a form of comment:commentType.
+	 *				const commentType = data.markerName.split( ':' )[ 1 ];
+	 *
+	 *				return {
+	 *					classes: [ 'comment', 'comment-' + commentType ]
+	 *				};
+	 *			}
+	 *		} );
+	 *
+	 * If a function is passed as the `config.view` parameter, it will be used to generate the highlight descriptor. The function
+	 * receives the `data` object as a parameter and should return a
+	 * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor}.
+	 * The `data` object properties are passed from {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}.
+	 *
+	 * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
+	 * to the conversion process.
+	 *
+	 * @method #markerToHighlight
+	 * @param {Object} config Conversion configuration.
+	 * @param {String} config.model The name of the model marker (or model marker group) to convert.
+	 * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} config.view A highlight descriptor
+	 * that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor.
+	 * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
+	 * @returns {module:engine/conversion/downcasthelpers~DowncastHelpers}
+	 */
+	markerToHighlight( config ) {
+		return this.add( downcastMarkerToHighlight( config ) );
+	}
+}
+
+/**
+ * Function factory that creates a default downcast converter for text insertion changes.
+ *
+ * The converter automatically consumes the corresponding value from the 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 that 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 = conversionApi.writer.createRange( viewStart, viewEnd );
+
+		// Trim the range to remove in case some UI elements are on the view range boundaries.
+		const removed = conversionApi.writer.remove( viewRange.getTrimmed() );
+
+		// After the range is removed, unbind all view elements from the model.
+		// Range inside view document fragment is used to unbind deeply.
+		for ( const child of conversionApi.writer.createRangeIn( removed ).getItems() ) {
+			conversionApi.mapper.unbindViewElement( child );
+		}
+	};
+}
+
+/**
+ * Creates a `<span>` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from the information
+ * provided by the {@link module:engine/conversion/downcasthelpers~HighlightDescriptor highlight descriptor} object. If a priority
+ * is not provided in the descriptor, the default priority will be used.
+ *
+ * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor
+ * @returns {module:engine/view/attributeelement~AttributeElement}
+ */
+export function createViewElementFromHighlightDescriptor( descriptor ) {
+	const viewElement = new ViewAttributeElement( 'span', descriptor.attributes );
+
+	if ( descriptor.classes ) {
+		viewElement._addClass( descriptor.classes );
+	}
+
+	if ( descriptor.priority ) {
+		viewElement._priority = descriptor.priority;
+	}
+
+	viewElement._id = descriptor.id;
+
+	return viewElement;
+}
+
+/**
+ * Function factory that creates a converter which converts a non-collapsed {@link module:engine/model/selection~Selection model selection}
+ * to a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
+ * value from the `consumable` object and maps model positions from the 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 that creates a converter which converts a collapsed {@link module:engine/model/selection~Selection model selection} to
+ * a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
+ * value from the `consumable` object, maps the model selection position to the view position and breaks
+ * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position.
+ *
+ *		modelDispatcher.on( 'selection', convertCollapsedSelection() );
+ *
+ * An example of the view state before and after converting the collapsed selection:
+ *
+ *		   <p><strong>f^oo<strong>bar</p>
+ *		-> <p><strong>f</strong>^<strong>oo</strong>bar</p>
+ *
+ * By breaking attribute elements like `<strong>`, the selection is in a correct element. Then, when the selection attribute is
+ * converted, broken attributes might be merged again, or the position where the selection is may be wrapped
+ * with different, appropriate attribute elements.
+ *
+ * See also {@link module:engine/conversion/downcasthelpers~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 that creates a converter which 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 merges 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/downcasthelpers~convertCollapsedSelection}
+ * which does 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 );
+	};
+}
+
+/**
+ * Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view.
+ * It can also 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 the model are converted to a view element that will be wrapping these view nodes that are bound to
+ * model elements having the given attribute. This is useful for attributes like `bold` that may be set on text nodes in the 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 the attribute value and then all the parameters of the
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute `attribute` event}.
+ * It is expected that the function returns an {@link module:engine/view/element~Element}.
+ * The result of the function will be the wrapping element.
+ * When the provided `Function` does not return any element, no conversion will take place.
+ *
+ * The converter automatically consumes the corresponding value from the consumables list and stops the event (see
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}).
+ *
+ *		modelDispatcher.on( 'attribute:bold', wrap( ( modelAttributeValue, viewWriter ) => {
+ *			return viewWriter.createAttributeElement( 'strong' );
+ *		} );
+ *
+ * @protected
+ * @param {Function} elementCreator Function returning a view element that 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 that creates a converter which converts node insertion changes from the model to the view.
+ * The function passed will be provided with all the parameters of the dispatcher's
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert `insert` event}.
+ * It is expected that the function returns an {@link module:engine/view/element~Element}.
+ * The result of the function will be inserted into the view.
+ *
+ * The converter automatically consumes the corresponding value from the consumables list, stops the event (see
+ * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and binds the 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;
+ *			}
+ *		) );
+ *
+ * @protected
+ * @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 that creates a converter which converts marker adding change to the
+ * {@link module:engine/view/uielement~UIElement view UI element}.
+ *
+ * The view UI element that will be added to the view depends on the passed parameter. See {@link ~insertElement}.
+ * In case of a non-collapsed range, the UI element will not wrap nodes but separate elements will be placed at the beginning
+ * and at the end of the range.
+ *
+ * This converter binds created UI elements with the marker name using {@link module:engine/conversion/mapper~Mapper#bindElementToMarker}.
+ *
+ * @protected
+ * @param {module:engine/view/uielement~UIElement|Function} elementCreator A view UI element or a function returning the view element
+ * that 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 );
+		conversionApi.mapper.bindElementToMarker( viewStartElement, data.markerName );
+
+		// Add "closing" element only if range is not collapsed.
+		if ( !markerRange.isCollapsed ) {
+			viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
+			conversionApi.mapper.bindElementToMarker( viewEndElement, data.markerName );
+		}
+
+		evt.stop();
+	};
+}
+
+// Function factory that returns a default downcast converter for removing a {@link module:engine/view/uielement~UIElement UI element}
+// basing on marker remove change.
+//
+// This converter unbinds elements from the marker name.
+//
+// @returns {Function} Removed UI element converter.
+function removeUIElement() {
+	return ( evt, data, conversionApi ) => {
+		const elements = conversionApi.mapper.markerNameToElements( data.markerName );
+
+		if ( !elements ) {
+			return;
+		}
+
+		conversionApi.mapper.unbindElementsFromMarkerName( data.markerName );
+
+		for ( const element of elements ) {
+			conversionApi.writer.clear( conversionApi.writer.createRangeOn( element ), element );
+		}
+
+		conversionApi.writer.clearClonedElementsGroup( data.markerName );
+
+		evt.stop();
+	};
+}
+
+// Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view.
+//
+// Attributes from the 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 element
+// attributes on a one-to-one basis.
+//
+// *Note:** The provided attribute creator should always return the same `key` for a given attribute from the model.
+//
+// The converter automatically consumes the corresponding value from the 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 the 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 the key-value pair.
+//			return { key, value };
+//		} ) );
+//
+// @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
+// represent the attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
+// The function is passed the model attribute value as the first parameter and additional data about the change as the second parameter.
+// @returns {Function} Set/change attribute converter.
+function changeAttribute( attributeCreator ) {
+	return ( evt, data, conversionApi ) => {
+		const oldAttribute = attributeCreator( data.attributeOldValue, data );
+		const newAttribute = attributeCreator( data.attributeNewValue, data );
+
+		if ( !oldAttribute && !newAttribute ) {
+			return;
+		}
+
+		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
+			return;
+		}
+
+		const viewElement = conversionApi.mapper.toViewElement( data.item );
+		const viewWriter = conversionApi.writer;
+
+		// If model item cannot be mapped to a view element, it means item is not an `Element` instance but a `TextProxy` node.
+		// Only elements can have attributes in a view so do not proceed for anything else (#1587).
+		if ( !viewElement ) {
+			/**
+			 * This error occurs when a {@link module:engine/model/textproxy~TextProxy text node's} attribute is to be downcasted
+			 * by {@link module:engine/conversion/conversion~Conversion#attributeToAttribute `Attribute to Attribute converter`}.
+			 * In most cases it is caused by converters misconfiguration when only "generic" converter is defined:
+			 *
+			 *		editor.conversion.for( 'downcast' ).attributeToAttribute( {
+			 *			model: 'attribute-name',
+			 *			view: 'attribute-name'
+			 *		} ) );
+			 *
+			 * and given attribute is used on text node, for example:
+			 *
+			 *		model.change( writer => {
+			 *			writer.insertText( 'Foo', { 'attribute-name': 'bar' }, parent, 0 );
+			 *		} );
+			 *
+			 * In such cases, to convert the same attribute for both {@link module:engine/model/element~Element}
+			 * and {@link module:engine/model/textproxy~TextProxy `Text`} nodes, text specific
+			 * {@link module:engine/conversion/conversion~Conversion#attributeToElement `Attribute to Element converter`}
+			 * with higher {@link module:utils/priorities~PriorityString priority} must also be defined:
+			 *
+			 *		editor.conversion.for( 'downcast' ).attributeToElement( {
+			 *			model: {
+			 *				key: 'attribute-name',
+			 *				name: '$text'
+			 *			},
+			 *			view: ( value, writer ) => {
+			 *				return writer.createAttributeElement( 'span', { 'attribute-name': value } );
+			 *			},
+			 *			converterPriority: 'high'
+			 *		} ) );
+			 *
+			 * @error conversion-attribute-to-attribute-on-text
+			 */
+			log.warn( 'conversion-attribute-to-attribute-on-text: ' +
+				'Trying to convert text node\'s attribute with attribute-to-attribute converter.' );
+
+			return;
+		}
+
+		// First remove the old attribute if there was one.
+		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 set the new attribute.
+		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 that creates a converter which converts the text inside marker's range. The converter wraps the text with
+// {@link module:engine/view/attributeelement~AttributeElement} created from the provided descriptor.
+// See {link module:engine/conversion/downcasthelpers~createViewElementFromHighlightDescriptor}.
+//
+// It can also be used to convert the 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 does not provide the `priority` property, `10` will be used.
+//
+// If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
+//
+// This converter binds the created {@link module:engine/view/attributeelement~AttributeElement attribute elemens} with the marker name
+// using the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
+//
+// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
+// @returns {Function}
+function highlightText( highlightDescriptor ) {
+	return ( evt, data, conversionApi ) => {
+		if ( !data.item ) {
+			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 );
+			const rangeAfterWrap = viewWriter.wrap( viewRange, viewElement );
+
+			for ( const element of rangeAfterWrap.getItems() ) {
+				if ( element.is( 'attributeElement' ) && element.isSimilar( viewElement ) ) {
+					conversionApi.mapper.bindElementToMarker( element, data.markerName );
+
+					// One attribute element is enough, because all of them are bound together by the view writer.
+					// Mapper uses this binding to get all the elements no matter how many of them are registered in the mapper.
+					break;
+				}
+			}
+		}
+	};
+}
+
+// Converter function factory. It creates a function which applies the marker's highlight to an element inside the marker's range.
+//
+// The converter checks if an element has the `addHighlight` function stored as a
+// {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight.
+// In such case the converter will consume all element's children, assuming that they were handled by the element itself.
+//
+// When the `addHighlight` custom property is not present, the element is not converted in any special way.
+// This means that converters will proceed to convert the element's child nodes.
+//
+// If the highlight descriptor does not provide the `priority` property, `10` will be used.
+//
+// If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
+//
+// This converter binds altered {@link module:engine/view/containerelement~ContainerElement container elements} with the marker name using
+// the {@link module:engine/conversion/mapper~Mapper#bindElementToMarker} method.
+//
+// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
+// @returns {Function}
+function highlightElement( highlightDescriptor ) {
+	return ( evt, data, conversionApi ) => {
+		if ( !data.item ) {
+			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 );
+
+			conversionApi.mapper.bindElementToMarker( viewElement, data.markerName );
+		}
+	};
+}
+
+// Function factory that creates a converter which converts the removing model marker to the view.
+//
+// Both text nodes and elements are handled by this converter but they are handled a bit differently.
+//
+// Text nodes are unwrapped using the {@link module:engine/view/attributeelement~AttributeElement attribute element} created from the
+// provided highlight descriptor. See {link module:engine/conversion/downcasthelpers~HighlightDescriptor}.
+//
+// For elements, the converter checks if an element has the `removeHighlight` function stored as a
+// {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight.
+// In such case, the children of that element will not be converted.
+//
+// When `removeHighlight` is not present, the element is not converted in any special way.
+// The converter will proceed to convert the element's child nodes instead.
+//
+// If the highlight descriptor does not provide the `priority` property, `10` will be used.
+//
+// If the highlight descriptor does not provide the `id` property, the name of the marker will be used.
+//
+// This converter unbinds elements from the marker name.
+//
+// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} highlightDescriptor
+// @returns {Function}
+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;
+		}
+
+		// View element that will be used to unwrap `AttributeElement`s.
+		const viewHighlightElement = createViewElementFromHighlightDescriptor( descriptor );
+
+		// Get all elements bound with given marker name.
+		const elements = conversionApi.mapper.markerNameToElements( data.markerName );
+
+		if ( !elements ) {
+			return;
+		}
+
+		conversionApi.mapper.unbindElementsFromMarkerName( data.markerName );
+
+		for ( const element of elements ) {
+			if ( element.is( 'attributeElement' ) ) {
+				conversionApi.writer.unwrap( conversionApi.writer.createRangeOn( element ), viewHighlightElement );
+			} else {
+				// if element.is( 'containerElement' ).
+				element.getCustomProperty( 'removeHighlight' )( element, descriptor.id, conversionApi.writer );
+			}
+		}
+
+		conversionApi.writer.clearClonedElementsGroup( data.markerName );
+
+		evt.stop();
+	};
+}
+
+// Model element to view element conversion helper.
+//
+// See {@link ~DowncastHelpers#elementToElement `.elementToElement()` downcast helper} for examples.
+//
+// @param {Object} config Conversion configuration.
+// @param {String} config.model The name of the model element to convert.
+// @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
+// that takes the model element and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}
+// as parameters and returns a view container element.
+// @returns {Function} Conversion helper.
+function downcastElementToElement( config ) {
+	config = cloneDeep( config );
+
+	config.view = normalizeToElementConfig( config.view, 'container' );
+
+	return dispatcher => {
+		dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.converterPriority || 'normal' } );
+	};
+}
+
+// Model attribute to view element conversion helper.
+//
+// See {@link ~DowncastHelpers#attributeToElement `.attributeToElement()` downcast helper} for examples.
+//
+// @param {Object} config Conversion configuration.
+// @param {String|Object} config.model The key of the attribute to convert from or a `{ key, values }` object. `values` is an array
+// of `String`s with possible values if the model attribute is an enumerable.
+// @param {module:engine/view/elementdefinition~ElementDefinition|Function|Object} config.view A view element definition or a function
+// that takes the model attribute value and {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}
+// as parameters and returns a view attribute element. If `config.model.values` is
+// given, `config.view` should be an object assigning values from `config.model.values` to view element definitions or functions.
+// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
+// @returns {Function} Conversion helper.
+function downcastAttributeToElement( 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 ] = normalizeToElementConfig( config.view[ modelValue ], 'attribute' );
+		}
+	} else {
+		config.view = normalizeToElementConfig( config.view, 'attribute' );
+	}
+
+	const elementCreator = getFromAttributeCreator( config );
+
+	return dispatcher => {
+		dispatcher.on( eventName, wrap( elementCreator ), { priority: config.converterPriority || 'normal' } );
+	};
+}
+
+// Model attribute to view attribute conversion helper.
+//
+// See {@link ~DowncastHelpers#attributeToAttribute `.attributeToAttribute()` downcast helper} for examples.
+//
+// @param {Object} config Conversion configuration.
+// @param {String|Object} config.model The 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 A view attribute key, or a `{ key, value }` object or a function that takes
+// the 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.converterPriority='normal'] Converter priority.
+// @returns {Function} Conversion helper.
+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.converterPriority || 'normal' } );
+	};
+}
+
+// Model marker to view element conversion helper.
+//
+// See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
+//
+// @param {Object} config Conversion configuration.
+// @param {String} config.model The name of the model marker (or model marker group) to convert.
+// @param {module:engine/view/elementdefinition~ElementDefinition|Function} config.view A view element definition or a function
+// that takes the model marker data as a parameter and returns a view UI element.
+// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
+// @returns {Function} Conversion helper.
+function downcastMarkerToElement( config ) {
+	config = cloneDeep( config );
+
+	config.view = normalizeToElementConfig( config.view, 'ui' );
+
+	return dispatcher => {
+		dispatcher.on( 'addMarker:' + config.model, insertUIElement( config.view ), { priority: config.converterPriority || 'normal' } );
+		dispatcher.on( 'removeMarker:' + config.model, removeUIElement( config.view ), { priority: config.converterPriority || 'normal' } );
+	};
+}
+
+// Model marker to highlight conversion helper.
+//
+// See {@link ~DowncastHelpers#markerToElement `.markerToElement()` downcast helper} for examples.
+//
+// @param {Object} config Conversion configuration.
+// @param {String} config.model The name of the model marker (or model marker group) to convert.
+// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor|Function} config.view A highlight descriptor
+// that will be used for highlighting or a function that takes the model marker data as a parameter and returns a highlight descriptor.
+// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
+// @returns {Function} Conversion helper.
+function downcastMarkerToHighlight( config ) {
+	return dispatcher => {
+		dispatcher.on( 'addMarker:' + config.model, highlightText( config.view ), { priority: config.converterPriority || 'normal' } );
+		dispatcher.on( 'addMarker:' + config.model, highlightElement( config.view ), { priority: config.converterPriority || 'normal' } );
+		dispatcher.on( 'removeMarker:' + config.model, removeHighlight( config.view ), { priority: config.converterPriority || 'normal' } );
+	};
+}
+
+// Takes `config.view`, and if it is an {@link module:engine/view/elementdefinition~ElementDefinition}, converts it
+// to a function (because lower level converters accept 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 a view element instance from the provided {@link module:engine/view/elementdefinition~ElementDefinition} and class.
+//
+// @param {module:engine/view/elementdefinition~ElementDefinition} viewElementDefinition
+// @param {module:engine/view/downcastwriter~DowncastWriter} 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;
+	const attributes = Object.assign( {}, viewElementDefinition.attributes );
+
+	if ( viewElementType == 'container' ) {
+		element = viewWriter.createContainerElement( viewElementDefinition.name, attributes );
+	} else if ( viewElementType == 'attribute' ) {
+		const options = {
+			priority: viewElementDefinition.priority || ViewAttributeElement.DEFAULT_PRIORITY
+		};
+
+		element = viewWriter.createAttributeElement( viewElementDefinition.name, attributes, options );
+	} else {
+		// 'ui'.
+		element = viewWriter.createUIElement( viewElementDefinition.name, attributes );
+	}
+
+	if ( viewElementDefinition.styles ) {
+		const keys = Object.keys( viewElementDefinition.styles );
+
+		for ( const key of keys ) {
+			viewWriter.setStyle( key, viewElementDefinition.styles[ key ], element );
+		}
+	}
+
+	if ( viewElementDefinition.classes ) {
+		const classes = viewElementDefinition.classes;
+
+		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 the configuration, adds default parameters if they do not exist and normalizes other parameters to be used in downcast converters
+// for generating a 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' ) {
+		// { key, value, ... }
+		if ( view.value ) {
+			return () => view;
+		}
+		// { key, ... }
+		else {
+			return modelAttributeValue => ( { key: view.key, value: modelAttributeValue } );
+		}
+	} else {
+		// function.
+		return view;
+	}
+}
+
+// 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;
+}
+
+/**
+ * An 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 view attribute element} with CSS class(es), attributes and a priority
+ * described by this object.
+ *
+ * Additionally, each {@link module:engine/view/containerelement~ContainerElement container element} can handle displaying the highlight
+ * separately by providing the `addHighlight` and `removeHighlight` custom properties. In this case:
+ *
+ *  * The `HighlightDescriptor` object is passed to the `addHighlight` function upon conversion and should be used to apply the highlight to
+ *  the element.
+ *  * The descriptor `id` is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight with the
+ *  given ID from the element.
+ *
+ * @typedef {Object} module:engine/conversion/downcasthelpers~HighlightDescriptor
+ *
+ * @property {String|Array.<String>} classes A CSS class or an array of classes to set. If the descriptor is used to
+ * create an {@link module:engine/view/attributeelement~AttributeElement attribute element} over text nodes, these classes will be set
+ * on that attribute element. If the descriptor is applied to an element, usually these classes will be set on that element, however,
+ * this depends on how the element converts the descriptor.
+ *
+ * @property {String} [id] Descriptor identifier. If not provided, it defaults to the converted marker's name.
+ *
+ * @property {Number} [priority] Descriptor priority. If not provided, it defaults to `10`. If the descriptor is used to create
+ * an {@link module:engine/view/attributeelement~AttributeElement attribute element}, it will be that element's
+ * {@link module:engine/view/attributeelement~AttributeElement#priority priority}. If the 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 the descriptor is used to create
+ * an {@link module:engine/view/attributeelement~AttributeElement attribute element} over text nodes, these attributes will be set on that
+ * attribute element. If the descriptor is applied to an element, usually these attributes will be set on that element, however,
+ * this depends on how the element converts the descriptor.
+ */

+ 25 - 11
packages/ckeditor5-engine/src/conversion/mapper.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -21,7 +21,10 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
  * Maps elements, positions and markers between {@link module:engine/view/document~Document the view} and
  * {@link module:engine/model/model the model}.
  *
- * Mapper use bound elements to find corresponding elements and positions, so, to get proper results,
+ * The instance of the Mapper used for the editing pipeline is available in
+ * {@link module:engine/controller/editingcontroller~EditingController#mapper `editor.editing.mapper`}.
+ *
+ * Mapper uses bound elements to find corresponding elements and positions, so, to get proper results,
  * all model elements should be {@link module:engine/conversion/mapper~Mapper#bindElements bound}.
  *
  * To map complex model to/from view relations, you may provide custom callbacks for
@@ -91,14 +94,8 @@ export default class Mapper {
 				return;
 			}
 
-			let viewBlock = data.viewPosition.parent;
-			let modelParent = this._viewToModelMapping.get( viewBlock );
-
-			while ( !modelParent ) {
-				viewBlock = viewBlock.parent;
-				modelParent = this._viewToModelMapping.get( viewBlock );
-			}
-
+			const viewBlock = this.findMappedViewAncestor( data.viewPosition );
+			const modelParent = this._viewToModelMapping.get( viewBlock );
 			const modelOffset = this._toModelOffset( data.viewPosition.parent, data.viewPosition.offset, viewBlock );
 
 			data.modelPosition = ModelPosition._createAt( modelParent, modelOffset );
@@ -338,6 +335,23 @@ export default class Mapper {
 		this._viewToModelLengthCallbacks.set( viewElementName, lengthCallback );
 	}
 
+	/**
+	 * For given `viewPosition`, finds and returns the closest ancestor of this position that has a mapping to
+	 * the model.
+	 *
+	 * @param {module:engine/view/position~Position} viewPosition Position for which mapped ancestor should be found.
+	 * @returns {module:engine/view/element~Element}
+	 */
+	findMappedViewAncestor( viewPosition ) {
+		let parent = viewPosition.parent;
+
+		while ( !this._viewToModelMapping.has( parent ) ) {
+			parent = parent.parent;
+		}
+
+		return parent;
+	}
+
 	/**
 	 * Calculates model offset based on the view position and the block element.
 	 *

+ 3 - 3
packages/ckeditor5-engine/src/conversion/modelconsumable.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -29,7 +29,7 @@ import TextProxy from '../model/textproxy';
  * {@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/downcast-selection-converters default downcast converters} for more information.
+ * See {@link module:engine/conversion/downcasthelpers 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

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

@@ -1,609 +0,0 @@
-/**
- * @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 { cloneDeep } from 'lodash-es';
-
-/**
- * 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', converterPriority: 'high' } );
- *
- *		upcastElementToElement( {
- *			view: {
- *				name: 'p',
- *				classes: 'fancy'
- *			},
- *			model: 'fancyParagraph'
- *		} );
- *
- *		upcastElementToElement( {
- * 			view: {
- *				name: 'p',
- *				classes: '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.converterPriority='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.converterPriority || '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`.
- *
- * This helper is meant to set a model attribute on all the elements that are inside the converted element:
- *
- *		<strong>Foo</strong>   -->   <strong><p>Foo</p></strong>   -->   <paragraph><$text bold="true">Foo</$text></paragraph>
- *
- * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
- * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text. See
- * {@link module:engine/conversion/upcast-converters~upcastAttributeToAttribute} for comparison.
- *
- * 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', converterPriority: 'high' } );
- *
- *		upcastElementToAttribute( {
- *			view: {
- *				name: 'span',
- *				classes: 'bold'
- *			},
- *			model: 'bold'
- *		} );
- *
- *		upcastElementToAttribute( {
- *			view: {
- *				name: 'span',
- *				classes: [ 'styled', 'styled-dark' ]
- *			},
- *			model: {
- *				key: 'styled',
- *				value: 'dark'
- *			}
- *		} );
- *
- * 		upcastElementToAttribute( {
- *			view: {
- *				name: 'span',
- *				styles: {
- *					'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.converterPriority='normal'] Converter priority.
- * @returns {Function} Conversion helper.
- */
-export function upcastElementToAttribute( config ) {
-	config = cloneDeep( config );
-
-	_normalizeModelAttributeConfig( config );
-
-	const converter = _prepareToAttributeConverter( config, false );
-
-	const elementName = _getViewElementNameFromConfig( config );
-	const eventName = elementName ? 'element:' + elementName : 'element';
-
-	return dispatcher => {
-		dispatcher.on( eventName, converter, { priority: config.converterPriority || '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.
- *
- * This helper is meant to convert view attributes from view elements which got converted to the model, so the view attribute
- * is set only on the corresponding model node:
- *
- *		<div class="dark"><div>foo</div></div>    -->    <div dark="true"><div>foo</div></div>
- *
- * Above, `class="dark"` attribute is added only to the `<div>` elements that has it. This is in contrary to
- * {@link module:engine/conversion/upcast-converters~upcastElementToAttribute} which sets attributes for all the children in the model:
- *
- *		<strong>Foo</strong>   -->   <strong><p>Foo</p></strong>   -->   <paragraph><$text bold="true">Foo</$text></paragraph>
- *
- * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
- * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text.
- *
- * 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', converterPriority: '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.converterPriority='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, true );
-
-	return dispatcher => {
-		dispatcher.on( 'element', converter, { priority: config.converterPriority || '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', converterPriority: 'high' } );
- *
- *		upcastElementToMarker( {
- *			view: 'marker-search',
- *			model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
- *		} );
- *
- *		upcastElementToMarker( {
- *			view: {
- *				name: 'span',
- *				attributes: {
- *					'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.converterPriority='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, conversionApi.writer.createPositionAt( modelElement, 0 ) );
-
-		// 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
-			conversionApi.writer.createPositionBefore( modelElement ),
-			// Should end after but we need to take into consideration that children could split our
-			// element, so we need to move range after parent of the last converted child.
-			// before: <allowed>[]</allowed>
-			// after: <allowed>[<converted><child></child></converted><child></child><converted>]</converted></allowed>
-			conversionApi.writer.createPositionAfter( 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 = conversionApi.writer.createPositionAt( splitResult.cursorParent, 0 );
-
-			// 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' ) {
-		const keyName = key == 'class' ? 'classes' : 'styles';
-
-		normalized = {
-			[ keyName ]: config.view.value
-		};
-	} else {
-		const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value;
-
-		normalized = {
-			attributes: {
-				[ 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' || typeof config.model.value == 'undefined' ? 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} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
-// on all elements in the range.
-function _prepareToAttributeConverter( config, shallow ) {
-	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 ( _onlyViewNameIsDefined( config ) ) {
-			match.match.name = true;
-		} else {
-			// Do not test or consume `name` consumable.
-			delete match.match.name;
-		}
-
-		// 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 }, shallow, conversionApi );
-
-		if ( attributeWasSet ) {
-			conversionApi.consumable.consume( data.viewItem, match.match );
-		}
-	};
-}
-
-// Helper function that checks if element name should be consumed in attribute converters.
-//
-// @param {Object} config Conversion config.
-// @returns {Boolean}
-function _onlyViewNameIsDefined( config ) {
-	if ( typeof config.view == 'object' && !_getViewElementNameFromConfig( config ) ) {
-		return false;
-	}
-
-	return !config.view.classes && !config.view.attributes && !config.view.styles;
-}
-
-// 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.
-// @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
-// on all elements in the range.
-// @returns {Boolean} `true` if attribute was set on at least one node from given `modelRange`.
-function _setAttributeOn( modelRange, modelAttribute, shallow, conversionApi ) {
-	let result = false;
-
-	// Set attribute on each item in range according to Schema.
-	for ( const node of Array.from( modelRange.getItems( { shallow } ) ) ) {
-		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;
-			}
-		}
-	};
-}

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

@@ -1,48 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * Contains {@link module:engine/view/documentselection~DocumentSelection view selection}
- * to {@link module:engine/model/selection~Selection model selection} conversion helpers.
- *
- * @module engine/conversion/upcast-selection-converters
- */
-
-import ModelSelection from '../model/selection';
-
-/**
- * Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection
- * view selection} taken from the {@link module:engine/view/document~Document#event:selectionChange} event
- * and sets in on the {@link module:engine/model/document~Document#selection model}.
- *
- * **Note**: because there is no view selection change dispatcher nor any other advanced view selection to model
- * conversion mechanism, the callback should be set directly on view document.
- *
- *		view.document.on( 'selectionChange', convertSelectionChange( modelDocument, mapper ) );
- *
- * @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( model, mapper ) {
-	return ( evt, data ) => {
-		const viewSelection = data.newSelection;
-		const modelSelection = new ModelSelection();
-
-		const ranges = [];
-
-		for ( const viewRange of viewSelection.getRanges() ) {
-			ranges.push( mapper.toModelRange( viewRange ) );
-		}
-
-		modelSelection.setTo( ranges, { backward: viewSelection.isBackward } );
-
-		if ( !modelSelection.isEqual( model.document.selection ) ) {
-			model.change( writer => {
-				writer.setSelection( modelSelection );
-			} );
-		}
-	};
-}

+ 150 - 45
packages/ckeditor5-engine/src/conversion/upcastdispatcher.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -41,7 +41,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
  * Examples of providing callbacks for `UpcastDispatcher`:
  *
  *		// Converter for links (<a>).
- *		upcastDispatcher.on( 'element:a', ( evt, data, conversionApi ) => {
+ *		editor.data.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.
@@ -56,7 +56,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
  *		} );
  *
  *		// Convert all elements which have no custom converter into paragraph (autoparagraphing).
- *  	data.viewToModel.on( 'element', ( evt, data, conversionApi ) => {
+ *  	editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
  *  	 	// When element is already consumed by higher priority converters then do nothing.
  *  	 	if ( conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
  *  	 			const paragraph = conversionApi.writer.createElement( 'paragraph' );
@@ -102,21 +102,20 @@ export default class UpcastDispatcher {
 	/**
 	 * Creates a `UpcastDispatcher` that operates using passed API.
 	 *
-	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi
+	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi
 	 * @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.
+		 * List of the elements that were created during splitting.
 		 *
-		 * After conversion process list is cleared.
+		 * After conversion process the list is cleared.
 		 *
-		 * @protected
-		 * @type {Set<module:engine/model/element~Element>}
+		 * @private
+		 * @type {Map.<module:engine/model/element~Element,Array.<module:engine/model/element~Element>>}
 		 */
-		this._removeIfEmpty = new Set();
+		this._splitParts = new Map();
 
 		/**
 		 * Position in the temporary structure where the converted content is inserted. The structure reflect the context of
@@ -131,7 +130,7 @@ export default class UpcastDispatcher {
 		/**
 		 * Interface passed by dispatcher to the events callbacks.
 		 *
-		 * @member {module:engine/conversion/upcastdispatcher~ViewConversionApi}
+		 * @member {module:engine/conversion/upcastdispatcher~UpcastConversionApi}
 		 */
 		this.conversionApi = Object.assign( {}, conversionApi );
 
@@ -140,6 +139,7 @@ export default class UpcastDispatcher {
 		this.conversionApi.convertItem = this._convertItem.bind( this );
 		this.conversionApi.convertChildren = this._convertChildren.bind( this );
 		this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.bind( this );
+		this.conversionApi.getSplitParts = this._getSplitParts.bind( this );
 	}
 
 	/**
@@ -176,15 +176,15 @@ export default class UpcastDispatcher {
 		// Do the conversion.
 		const { modelRange } = this._convertItem( viewItem, this._modelCursor );
 
-		// Conversion result is always a document fragment so let's create this fragment.
+		// Conversion result is always a document fragment so let's create it.
 		const documentFragment = writer.createDocumentFragment();
 
 		// When there is a conversion result.
 		if ( modelRange ) {
-			// Remove all empty elements that was added to #_removeIfEmpty list.
+			// Remove all empty elements that were create while splitting.
 			this._removeEmptyElements();
 
-			// Move all items that was converted to context tree to document fragment.
+			// Move all items that were converted in context tree to the document fragment.
 			for ( const item of Array.from( this._modelCursor.parent.getChildren() ) ) {
 				writer.append( item, documentFragment );
 			}
@@ -196,8 +196,8 @@ export default class UpcastDispatcher {
 		// Clear context position.
 		this._modelCursor = null;
 
-		// Clear split elements.
-		this._removeIfEmpty.clear();
+		// Clear split elements lists.
+		this._splitParts.clear();
 
 		// Clear conversion API.
 		this.conversionApi.writer = null;
@@ -209,7 +209,7 @@ export default class UpcastDispatcher {
 
 	/**
 	 * @private
-	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#convertItem
+	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertItem
 	 */
 	_convertItem( viewItem, modelCursor ) {
 		const data = Object.assign( { viewItem, modelCursor, modelRange: null } );
@@ -239,7 +239,7 @@ export default class UpcastDispatcher {
 
 	/**
 	 * @private
-	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#convertChildren
+	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertChildren
 	 */
 	_convertChildren( viewItem, modelCursor ) {
 		const modelRange = new ModelRange( modelCursor );
@@ -259,11 +259,11 @@ export default class UpcastDispatcher {
 
 	/**
 	 * @private
-	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#splitToAllowedParent
+	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#splitToAllowedParent
 	 */
 	_splitToAllowedParent( node, modelCursor ) {
 		// Try to find allowed parent.
-		const allowedParent = this.conversionApi.schema.findAllowedParent( node, modelCursor );
+		const allowedParent = this.conversionApi.schema.findAllowedParent( modelCursor, node );
 
 		// When there is no parent that allows to insert node then return `null`.
 		if ( !allowedParent ) {
@@ -283,14 +283,31 @@ export default class UpcastDispatcher {
 		// 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.
+		// Using the range returned by `model.Writer#split`, we will pair original elements with their split parts.
+		//
+		// The range returned from the writer spans "over the split" or, precisely saying, from the end of the original element (the one
+		// that got split) to the beginning of the other part of that element:
+		//
+		// <limit><a><b><c>X[]Y</c></b><a></limit> ->
+		// <limit><a><b><c>X[</c></b></a><a><b><c>]Y</c></b></a>
 		//
-		// 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 );
+		// After the split there cannot be any full node between the positions in `splitRange`. The positions are touching.
+		// Also, because of how splitting works, it is easy to notice, that "closing tags" are in the reverse order than "opening tags".
+		// Also, since we split all those elements, each of them has to have the other part.
+		//
+		// With those observations in mind, we will pair the original elements with their split parts by saving "closing tags" and matching
+		// them with "opening tags" in the reverse order. For that we can use a stack.
+		const stack = [];
+
+		for ( const treeWalkerValue of splitResult.range.getWalker() ) {
+			if ( treeWalkerValue.type == 'elementEnd' ) {
+				stack.push( treeWalkerValue.item );
+			} else {
+				// There should not be any text nodes after the element is split, so the only other value is `elementStart`.
+				const originalPart = stack.pop();
+				const splitPart = treeWalkerValue.item;
+
+				this._registerSplitPair( originalPart, splitPart );
 			}
 		}
 
@@ -301,25 +318,62 @@ export default class UpcastDispatcher {
 	}
 
 	/**
-	 * 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.
+	 * Registers that `splitPart` element is a split part of the `originalPart` element.
+	 *
+	 * Data set by this method is used by {@link #_getSplitParts} and {@link #_removeEmptyElements}.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} originalPart
+	 * @param {module:engine/model/element~Element} splitPart
+	 */
+	_registerSplitPair( originalPart, splitPart ) {
+		if ( !this._splitParts.has( originalPart ) ) {
+			this._splitParts.set( originalPart, [ originalPart ] );
+		}
+
+		const list = this._splitParts.get( originalPart );
+
+		this._splitParts.set( splitPart, list );
+		list.push( splitPart );
+	}
+
+	/**
+	 * @private
+	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#getSplitParts
+	 */
+	_getSplitParts( element ) {
+		let parts;
+
+		if ( !this._splitParts.has( element ) ) {
+			parts = [ element ];
+		} else {
+			parts = this._splitParts.get( element );
+		}
+
+		return parts;
+	}
+
+	/**
+	 * Checks if there are any empty elements created while splitting and removes them.
+	 *
+	 * This method works recursively to re-check empty elements again after at least one element was removed in the initial call,
+	 * as some elements might have become empty after other empty elements were removed from them.
 	 *
 	 * @private
 	 */
 	_removeEmptyElements() {
-		let removed = false;
+		let anyRemoved = false;
 
-		for ( const element of this._removeIfEmpty ) {
+		for ( const element of this._splitParts.keys() ) {
 			if ( element.isEmpty ) {
 				this.conversionApi.writer.remove( element );
-				this._removeIfEmpty.delete( element );
-				removed = true;
+				this._splitParts.delete( element );
+
+				anyRemoved = true;
 			}
 		}
 
-		if ( removed ) {
+		if ( anyRemoved ) {
 			this._removeEmptyElements();
 		}
 	}
@@ -348,7 +402,7 @@ export default class UpcastDispatcher {
 	 * 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.
+	 * @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion utilities to be used by callback.
 	 */
 
 	/**
@@ -408,7 +462,7 @@ function extractMarkersFromModelFragment( modelItem, writer ) {
 	return markers;
 }
 
-// Creates model fragment according to given context and returns position in top element.
+// Creates model fragment according to given context and returns position in the bottom (the deepest) element.
 function createContextTree( contextDefinition, writer ) {
 	let position;
 
@@ -436,7 +490,7 @@ function createContextTree( contextDefinition, writer ) {
  * and is passed as one of parameters when {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher dispatcher}
  * fires it's events.
  *
- * @interface ViewConversionApi
+ * @interface module:engine/conversion/upcastdispatcher~UpcastConversionApi
  */
 
 /**
@@ -465,7 +519,7 @@ function createContextTree( contextDefinition, writer ) {
  * @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/view/item~Item} viewItem Element which children should be converted.
  * @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.
@@ -505,11 +559,50 @@ function createContextTree( contextDefinition, writer ) {
  */
 
 /**
- * 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
+ * Returns all the split parts of given `element` that were created during upcasting through using {@link #splitToAllowedParent}.
+ * It enables you to easily track those elements and continue processing them after they are split during their children conversion.
+ *
+ *		<paragraph>Foo<image />bar<image />baz</paragraph> ->
+ *		<paragraph>Foo</paragraph><image /><paragraph>bar</paragraph><image /><paragraph>baz</paragraph>
+ *
+ * For a reference to any of above paragraphs, the function will return all three paragraphs (the original element included),
+ * sorted in the order of their creation (the original element is the first one).
+ *
+ * If given `element` was not split, an array with single element is returned.
+ *
+ * Example of a usage in a converter code:
+ *
+ *		const myElement = conversionApi.writer.createElement( 'myElement' );
+ *
+ *		// Children conversion may split `myElement`.
+ *		conversionApi.convertChildren( myElement, modelCursor );
+ *
+ *		const splitParts = conversionApi.getSplitParts( myElement );
+ *		const lastSplitPart = splitParts[ splitParts.length - 1 ];
+ *
+ *		// Setting `data.modelRange` basing on split parts:
+ *		data.modelRange = conversionApi.writer.createRange(
+ *			conversionApi.writer.createPositionBefore( myElement ),
+ *			conversionApi.writer.createPositionAfter( lastSplitPart )
+ *		);
+ *
+ *		// Setting `data.modelCursor` to continue after the last split element:
+ *		data.modelCursor = conversionApi.writer.createPositionAfter( lastSplitPart );
+ *
+ * **Tip:** if you are unable to get a reference to the original element (for example because the code is split into multiple converters
+ * or even classes) but it was already converted, you might want to check first element in `data.modelRange`. This is a common situation
+ * if an attribute converter is separated from an element converter.
+ *
+ * @method #getSplitParts
+ * @param {module:engine/model/element~Element} element
+ * @returns {Array.<module:engine/model/element~Element>}
+ */
+
+/**
+ * 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
+ * @member {module:engine/conversion/viewconsumable~ViewConsumable} #consumable
  */
 
 /**
@@ -520,5 +613,17 @@ function createContextTree( contextDefinition, writer ) {
  * {@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
+ * @member {Object} #store
+ */
+
+/**
+ * The model's schema instance.
+ *
+ * @member {module:engine/model/schema~Schema} #schema
+ */
+
+/**
+ * The {@link module:engine/model/writer~Writer} instance used to manipulate data during conversion.
+ *
+ * @member {module:engine/model/writer~Writer} #writer
  */

+ 763 - 0
packages/ckeditor5-engine/src/conversion/upcasthelpers.js

@@ -0,0 +1,763 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import Matcher from '../view/matcher';
+import ModelRange from '../model/range';
+import ConversionHelpers from './conversionhelpers';
+
+import { cloneDeep } from 'lodash-es';
+import ModelSelection from '../model/selection';
+
+/**
+ * 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/upcasthelpers
+ */
+
+/**
+ * Upcast conversion helper functions.
+ *
+ * @extends module:engine/conversion/conversionhelpers~ConversionHelpers
+ */
+export default class UpcastHelpers extends ConversionHelpers {
+	/**
+	 * 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.
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToElement( {
+	 *			view: 'p',
+	 *			model: 'paragraph'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToElement( {
+	 *			view: 'p',
+	 *			model: 'paragraph',
+	 *			converterPriority: 'high'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToElement( {
+	 *			view: {
+	 *				name: 'p',
+	 *				classes: 'fancy'
+	 *			},
+	 *			model: 'fancyParagraph'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToElement( {
+	 * 			view: {
+	 *				name: 'p',
+	 *				classes: 'heading'
+	 * 			},
+	 * 			model: ( viewElement, modelWriter ) => {
+	 * 				return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } );
+	 * 			}
+	 * 		} );
+	 *
+	 * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
+	 * to the conversion process.
+	 *
+	 * @method #elementToElement
+	 * @param {Object} config Conversion configuration.
+	 * @param {module:engine/view/matcher~MatcherPattern} [config.view] Pattern matching all view elements which should be converted. If not
+	 * set, the converter will fire for every view element.
+	 * @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.converterPriority='normal'] Converter priority.
+	 * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
+	 */
+	elementToElement( config ) {
+		return this.add( upcastElementToElement( config ) );
+	}
+
+	/**
+	 * 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`.
+	 *
+	 * This helper is meant to set a model attribute on all the elements that are inside the converted element:
+	 *
+	 *		<strong>Foo</strong>   -->   <strong><p>Foo</p></strong>   -->   <paragraph><$text bold="true">Foo</$text></paragraph>
+	 *
+	 * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
+	 * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text. See
+	 * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#attributeToAttribute} for comparison.
+	 *
+	 * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToAttribute( {
+	 *			view: 'strong',
+	 *			model: 'bold'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToAttribute( {
+	 *			view: 'strong',
+	 *			model: 'bold',
+	 *			converterPriority: 'high'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToAttribute( {
+	 *			view: {
+	 *				name: 'span',
+	 *				classes: 'bold'
+	 *			},
+	 *			model: 'bold'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToAttribute( {
+	 *			view: {
+	 *				name: 'span',
+	 *				classes: [ 'styled', 'styled-dark' ]
+	 *			},
+	 *			model: {
+	 *				key: 'styled',
+	 *				value: 'dark'
+	 *			}
+	 *		} );
+	 *
+	 * 		editor.conversion.for( 'upcast' ).elementToAttribute( {
+	 *			view: {
+	 *				name: 'span',
+	 *				styles: {
+	 *					'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 `conversion.for()`} to learn how to add a converter
+	 * to the conversion process.
+	 *
+	 * @method #elementToAttribute
+	 * @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.converterPriority='normal'] Converter priority.
+	 * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
+	 */
+	elementToAttribute( config ) {
+		return this.add( upcastElementToAttribute( config ) );
+	}
+
+	/**
+	 * 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.
+	 *
+	 * This helper is meant to convert view attributes from view elements which got converted to the model, so the view attribute
+	 * is set only on the corresponding model node:
+	 *
+	 *		<div class="dark"><div>foo</div></div>    -->    <div dark="true"><div>foo</div></div>
+	 *
+	 * Above, `class="dark"` attribute is added only to the `<div>` elements that has it. This is in contrary to
+	 * {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToAttribute} which sets attributes for
+	 * all the children in the model:
+	 *
+	 *		<strong>Foo</strong>   -->   <strong><p>Foo</p></strong>   -->   <paragraph><$text bold="true">Foo</$text></paragraph>
+	 *
+	 * Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
+	 * Even though `<strong>` is over `<p>` element, `bold="true"` was added to the text.
+	 *
+	 * Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
+	 *
+	 *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
+	 *			view: 'src',
+	 *			model: 'source'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
+	 *			view: { key: 'src' },
+	 *			model: 'source'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
+	 *			view: { key: 'src' },
+	 *			model: 'source',
+	 *			converterPriority: 'normal'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
+	 *			view: {
+	 *				key: 'data-style',
+	 *				value: /[\s\S]+/
+	 *			},
+	 *			model: 'styled'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
+	 *			view: {
+	 *				name: 'img',
+	 *				key: 'class',
+	 *				value: 'styled-dark'
+	 *			},
+	 *			model: {
+	 *				key: 'styled',
+	 *				value: 'dark'
+	 *			}
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).attributeToAttribute( {
+	 *			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 `conversion.for()`} to learn how to add a converter
+	 * to the conversion process.
+	 *
+	 * @method #attributeToAttribute
+	 * @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.converterPriority='low'] Converter priority.
+	 * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
+	 */
+	attributeToAttribute( config ) {
+		return this.add( upcastAttributeToAttribute( config ) );
+	}
+
+	/**
+	 * 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}.
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToMarker( {
+	 *			view: 'marker-search',
+	 *			model: 'search'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToMarker( {
+	 *			view: 'marker-search',
+	 *			model: 'search',
+	 *			converterPriority: 'high'
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToMarker( {
+	 *			view: 'marker-search',
+	 *			model: viewElement => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
+	 *		} );
+	 *
+	 *		editor.conversion.for( 'upcast' ).elementToMarker( {
+	 *			view: {
+	 *				name: 'span',
+	 *				attributes: {
+	 *					'data-marker': 'search'
+	 *				}
+	 *			},
+	 *			model: 'search'
+	 *		} );
+	 *
+	 * See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
+	 * to the conversion process.
+	 *
+	 * @method #elementToMarker
+	 * @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.converterPriority='normal'] Converter priority.
+	 * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
+	 */
+	elementToMarker( config ) {
+		return this.add( upcastElementToMarker( config ) );
+	}
+}
+
+/**
+ * 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;
+			}
+		}
+	};
+}
+
+/**
+ * Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection
+ * view selection} taken from the {@link module:engine/view/document~Document#event:selectionChange} event
+ * and sets in on the {@link module:engine/model/document~Document#selection model}.
+ *
+ * **Note**: because there is no view selection change dispatcher nor any other advanced view selection to model
+ * conversion mechanism, the callback should be set directly on view document.
+ *
+ *		view.document.on( 'selectionChange', convertSelectionChange( modelDocument, mapper ) );
+ *
+ * @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( model, mapper ) {
+	return ( evt, data ) => {
+		const viewSelection = data.newSelection;
+		const modelSelection = new ModelSelection();
+
+		const ranges = [];
+
+		for ( const viewRange of viewSelection.getRanges() ) {
+			ranges.push( mapper.toModelRange( viewRange ) );
+		}
+
+		modelSelection.setTo( ranges, { backward: viewSelection.isBackward } );
+
+		if ( !modelSelection.isEqual( model.document.selection ) ) {
+			model.change( writer => {
+				writer.setSelection( modelSelection );
+			} );
+		}
+	};
+}
+
+// View element to model element conversion helper.
+//
+// See {@link ~UpcastHelpers#elementToElement `.elementToElement()` upcast helper} for examples.
+//
+// @param {Object} config Conversion configuration.
+// @param {module:engine/view/matcher~MatcherPattern} [config.view] Pattern matching all view elements which should be converted. If not
+// set, the converter will fire for every view element.
+// @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.converterPriority='normal'] Converter priority.
+// @returns {Function} Conversion helper.
+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.converterPriority || 'normal' } );
+	};
+}
+
+// View element to model attribute conversion helper.
+//
+// See {@link ~UpcastHelpers#elementToAttribute `.elementToAttribute()` upcast helper} for examples.
+//
+// @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.converterPriority='normal'] Converter priority.
+// @returns {Function} Conversion helper.
+function upcastElementToAttribute( config ) {
+	config = cloneDeep( config );
+
+	normalizeModelAttributeConfig( config );
+
+	const converter = prepareToAttributeConverter( config, false );
+
+	const elementName = getViewElementNameFromConfig( config );
+	const eventName = elementName ? 'element:' + elementName : 'element';
+
+	return dispatcher => {
+		dispatcher.on( eventName, converter, { priority: config.converterPriority || 'low' } );
+	};
+}
+
+// View attribute to model attribute conversion helper.
+//
+// See {@link ~UpcastHelpers#attributeToAttribute `.attributeToAttribute()` upcast helper} for examples.
+//
+// @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.converterPriority='low'] Converter priority.
+// @returns {Function} Conversion helper.
+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, true );
+
+	return dispatcher => {
+		dispatcher.on( 'element', converter, { priority: config.converterPriority || 'low' } );
+	};
+}
+
+// View element to model marker conversion helper.
+//
+// See {@link ~UpcastHelpers#elementToMarker `.elementToMarker()` upcast helper} for examples.
+//
+// @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.converterPriority='normal'] Converter priority.
+// @returns {Function} Conversion helper.
+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 = config.view ? new Matcher( config.view ) : null;
+
+	return ( evt, data, conversionApi ) => {
+		let match = {};
+
+		// If `config.view` has not been passed do not try matching. In this case, the converter should fire for all elements.
+		if ( matcher ) {
+			// This will be usually just one pattern but we support matchers with many patterns too.
+			const matcherResult = matcher.match( data.viewItem );
+
+			// If there is no match, this callback should not do anything.
+			if ( !matcherResult ) {
+				return;
+			}
+
+			match = matcherResult.match;
+		}
+
+		// Force consuming element's name.
+		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 ) ) {
+			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.
+		conversionApi.convertChildren( data.viewItem, conversionApi.writer.createPositionAt( modelElement, 0 ) );
+
+		// Consume appropriate value from consumable values list.
+		conversionApi.consumable.consume( data.viewItem, match );
+
+		const parts = conversionApi.getSplitParts( modelElement );
+
+		// Set conversion result range.
+		data.modelRange = new ModelRange(
+			conversionApi.writer.createPositionBefore( modelElement ),
+			conversionApi.writer.createPositionAfter( parts[ parts.length - 1 ] )
+		);
+
+		// Now we need to check where the `modelCursor` should be.
+		if ( splitResult.cursorParent ) {
+			// If we split parent to insert our element then we want to continue conversion in the new part of the split parent.
+			//
+			// before: <allowed><notAllowed>foo[]</notAllowed></allowed>
+			// after:  <allowed><notAllowed>foo</notAllowed><converted></converted><notAllowed>[]</notAllowed></allowed>
+
+			data.modelCursor = conversionApi.writer.createPositionAt( splitResult.cursorParent, 0 );
+		} else {
+			// Otherwise just continue after inserted element.
+
+			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' ) {
+		const keyName = key == 'class' ? 'classes' : 'styles';
+
+		normalized = {
+			[ keyName ]: config.view.value
+		};
+	} else {
+		const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value;
+
+		normalized = {
+			attributes: {
+				[ 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' || typeof config.model.value == 'undefined' ? 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} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
+// on all elements in the range.
+function prepareToAttributeConverter( config, shallow ) {
+	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 ( onlyViewNameIsDefined( config ) ) {
+			match.match.name = true;
+		} else {
+			// Do not test or consume `name` consumable.
+			delete match.match.name;
+		}
+
+		// 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 }, shallow, conversionApi );
+
+		if ( attributeWasSet ) {
+			conversionApi.consumable.consume( data.viewItem, match.match );
+		}
+	};
+}
+
+// Helper function that checks if element name should be consumed in attribute converters.
+//
+// @param {Object} config Conversion config.
+// @returns {Boolean}
+function onlyViewNameIsDefined( config ) {
+	if ( typeof config.view == 'object' && !getViewElementNameFromConfig( config ) ) {
+		return false;
+	}
+
+	return !config.view.classes && !config.view.attributes && !config.view.styles;
+}
+
+// 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 {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion API.
+// @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
+// on all elements in the range.
+// @returns {Boolean} `true` if attribute was set on at least one node from given `modelRange`.
+function setAttributeOn( modelRange, modelAttribute, shallow, conversionApi ) {
+	let result = false;
+
+	// Set attribute on each item in range according to Schema.
+	for ( const node of Array.from( modelRange.getItems( { shallow } ) ) ) {
+		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 } );
+	};
+}

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

+ 19 - 6
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -29,10 +29,13 @@ import DowncastDispatcher from '../conversion/downcastdispatcher';
 import UpcastDispatcher from '../conversion/upcastdispatcher';
 import Mapper from '../conversion/mapper';
 import {
-	convertRangeSelection,
 	convertCollapsedSelection,
-} from '../conversion/downcast-selection-converters';
-import { insertText, insertElement, wrap, insertUIElement } from '../conversion/downcast-converters';
+	convertRangeSelection,
+	insertElement,
+	insertText,
+	insertUIElement,
+	wrap
+} from '../conversion/downcasthelpers';
 
 import { isPlainObject } from 'lodash-es';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
@@ -40,10 +43,15 @@ import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 /**
  * Writes the content of a model {@link module:engine/model/document~Document document} to an HTML-like string.
  *
+ *		getData( editor.model ); // -> '<paragraph>Foo![]</paragraph>'
+ *
  * **Note:** A {@link module:engine/model/text~Text text} node that contains attributes will be represented as:
  *
  *		<$text attribute="value">Text data</$text>
  *
+ * **Note:** Using this tool in production-grade code is not recommended. It was designed for development, prototyping,
+ * debugging and testing.
+ *
  * @param {module:engine/model/model~Model} model
  * @param {Object} [options]
  * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true`, the selection will
@@ -74,6 +82,8 @@ getData._stringify = stringify;
 /**
  * Sets the content of a model {@link module:engine/model/document~Document document} provided as an HTML-like string.
  *
+ *		setData( editor.model, '<paragraph>Foo![]</paragraph>' );
+ *
  * **Note:** Remember to register elements in the {@link module:engine/model/model~Model#schema model's schema} before
  * trying to use them.
  *
@@ -81,6 +91,9 @@ getData._stringify = stringify;
  *
  *		<$text attribute="value">Text data</$text>
  *
+ * **Note:** Using this tool in production-grade code is not recommended. It was designed for development, prototyping,
+ * debugging and testing.
+ *
  * @param {module:engine/model/model~Model} model
  * @param {String} data HTML-like string to write into the document.
  * @param {Object} options
@@ -231,6 +244,7 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu
 
 		return new ViewContainerElement( modelItem.name, attributes );
 	} ) );
+
 	downcastDispatcher.on( 'selection', convertRangeSelection() );
 	downcastDispatcher.on( 'selection', convertCollapsedSelection() );
 	downcastDispatcher.on( 'addMarker', insertUIElement( ( data, writer ) => {
@@ -278,7 +292,6 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu
  *
  * @param {String} data HTML-like string to be parsed.
  * @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.

+ 2 - 2
packages/ckeditor5-engine/src/dev-utils/operationreplayer.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -1053,7 +1053,7 @@ function _convertType( type ) {
 // Checks if a given priority is allowed. Returns null if the priority cannot be converted.
 //
 // @param {String} priorityString
-// returns {Number|Null}
+// returns {Number|null}
 function _convertPriority( priorityString ) {
 	const priority = parseInt( priorityString, 10 );
 

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

+ 49 - 3
packages/ckeditor5-engine/src/model/differ.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -149,6 +149,15 @@ export default class Differ {
 			case 'remove':
 			case 'move':
 			case 'reinsert': {
+				// When range is moved to the same position then not mark it as a change.
+				// See: https://github.com/ckeditor/ckeditor5-engine/issues/1664.
+				if (
+					operation.sourcePosition.isEqual( operation.targetPosition ) ||
+					operation.sourcePosition.getShiftedBy( operation.howMany ).isEqual( operation.targetPosition )
+				) {
+					return;
+				}
+
 				const sourceParentInserted = this._isInInsertedElement( operation.sourcePosition.parent );
 				const targetParentInserted = this._isInInsertedElement( operation.targetPosition.parent );
 
@@ -292,6 +301,23 @@ export default class Differ {
 		return result;
 	}
 
+	/**
+	 * Returns all markers which changed.
+	 *
+	 * @returns {Array.<Object>}
+	 */
+	getChangedMarkers() {
+		return Array.from( this._changedMarkers ).map( item => (
+			{
+				name: item[ 0 ],
+				data: {
+					oldRange: item[ 1 ].oldRange,
+					newRange: item[ 1 ].newRange
+				}
+			}
+		) );
+	}
+
 	/**
 	 * Checks whether some of the buffered changes affect the editor data.
 	 *
@@ -795,6 +821,26 @@ export default class Differ {
 					}
 				}
 
+				if ( old.type == 'remove' ) {
+					// This is a case when attribute change "contains" remove change.
+					// The attribute change needs to be split into two because changes cannot intersect.
+					if ( inc.offset < old.offset && incEnd > old.offset ) {
+						const attributePart = {
+							type: 'attribute',
+							offset: old.offset,
+							howMany: incEnd - old.offset,
+							count: this._changeCount++
+						};
+
+						this._handleChange( attributePart, changes );
+
+						changes.push( attributePart );
+
+						inc.nodesToHandle = old.offset - inc.offset;
+						inc.howMany = inc.nodesToHandle;
+					}
+				}
+
 				if ( old.type == 'attribute' ) {
 					// There are only two conflicting scenarios possible here:
 					if ( inc.offset >= old.offset && incEnd <= oldEnd ) {
@@ -1061,7 +1107,7 @@ function _generateActionsFromChanges( oldChildrenLength, changes ) {
 		} else {
 			actions.push( ...'a'.repeat( change.howMany ).split( '' ) );
 
-			// The last handled offset isa at the position after the changed range.
+			// The last handled offset is at the position after the changed range.
 			offset = change.offset + change.howMany;
 			// We changed `howMany` old nodes, update `oldChildrenHandled`.
 			oldChildrenHandled += change.howMany;

+ 74 - 33
packages/ckeditor5-engine/src/model/document.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -46,7 +46,7 @@ export default class Document {
 		 * The {@link module:engine/model/model~Model model} that the document is a part of.
 		 *
 		 * @readonly
-		 * @member {module:engine/model/model~Model}
+		 * @type {module:engine/model/model~Model}
 		 */
 		this.model = model;
 
@@ -58,7 +58,7 @@ export default class Document {
 		 * a {@link module:utils/ckeditorerror~CKEditorError model-document-applyOperation-wrong-version} error is thrown.
 		 *
 		 * @readonly
-		 * @member {Number}
+		 * @type {Number}
 		 */
 		this.version = 0;
 
@@ -66,7 +66,7 @@ export default class Document {
 		 * The document's history.
 		 *
 		 * @readonly
-		 * @member {module:engine/model/history~History}
+		 * @type {module:engine/model/history~History}
 		 */
 		this.history = new History( this );
 
@@ -74,7 +74,7 @@ export default class Document {
 		 * The selection in this document.
 		 *
 		 * @readonly
-		 * @member {module:engine/model/documentselection~DocumentSelection}
+		 * @type {module:engine/model/documentselection~DocumentSelection}
 		 */
 		this.selection = new DocumentSelection( this );
 
@@ -83,7 +83,7 @@ export default class Document {
 		 * {@link #getRoot} to manipulate it.
 		 *
 		 * @readonly
-		 * @member {module:utils/collection~Collection}
+		 * @type {module:utils/collection~Collection}
 		 */
 		this.roots = new Collection( { idProperty: 'rootName' } );
 
@@ -91,7 +91,7 @@ export default class Document {
 		 * The model differ object. Its role is to buffer changes done on the model document and then calculate a diff of those changes.
 		 *
 		 * @readonly
-		 * @member {module:engine/model/differ~Differ}
+		 * @type {module:engine/model/differ~Differ}
 		 */
 		this.differ = new Differ( model.markers );
 
@@ -99,10 +99,18 @@ export default class Document {
 		 * Post-fixer callbacks registered to the model document.
 		 *
 		 * @private
-		 * @member {Set}
+		 * @type {Set.<Function>}
 		 */
 		this._postFixers = new Set();
 
+		/**
+		 * A boolean indicates whether the selection has changed until
+		 *
+		 * @private
+		 * @type {Boolean}
+		 */
+		this._hasSelectionChangedFromTheLastChangeBlock = false;
+
 		// Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
 		this.createRoot( '$root', graveyardName );
 
@@ -144,29 +152,8 @@ export default class Document {
 		}, { priority: 'low' } );
 
 		// Listen to selection changes. If selection changed, mark it.
-		let hasSelectionChanged = false;
-
 		this.listenTo( this.selection, 'change', () => {
-			hasSelectionChanged = true;
-		} );
-
-		// 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.
-		// Fire `change:data` event when at least one operation or buffered marker changes the data.
-		this.listenTo( model, '_change', ( evt, writer ) => {
-			if ( !this.differ.isEmpty || hasSelectionChanged ) {
-				this._callPostFixers( writer );
-
-				if ( this.differ.hasDataChanges() ) {
-					this.fire( 'change:data', writer.batch );
-				} else {
-					this.fire( 'change', writer.batch );
-				}
-
-				this.differ.reset();
-				hasSelectionChanged = false;
-			}
+			this._hasSelectionChangedFromTheLastChangeBlock = true;
 		} );
 
 		// Buffer marker changes.
@@ -253,8 +240,8 @@ export default class Document {
 	}
 
 	/**
-	 * 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.
+	 * Used to register a post-fixer callback. A post-fixer mechanism guarantees that the features
+	 * will operate on a correct model state.
 	 *
 	 * 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
@@ -306,6 +293,51 @@ export default class Document {
 		return json;
 	}
 
+	/**
+	 * 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.
+	 * Fire `change:data` event when at least one operation or buffered marker changes the data.
+	 *
+	 * @protected
+	 * @fires change
+	 * @fires change:data
+	 * @param {module:engine/model/writer~Writer} writer The writer on which post-fixers will be called.
+	 */
+	_handleChangeBlock( writer ) {
+		if ( this._hasDocumentChangedFromTheLastChangeBlock() ) {
+			this._callPostFixers( writer );
+
+			// Refresh selection attributes according to the final position in the model after the change.
+			this.selection.refresh();
+
+			if ( this.differ.hasDataChanges() ) {
+				this.fire( 'change:data', writer.batch );
+			} else {
+				this.fire( 'change', writer.batch );
+			}
+
+			// Theoretically, it is not necessary to refresh selection after change event because
+			// post-fixers are the last who should change the model, but just in case...
+			this.selection.refresh();
+
+			this.differ.reset();
+		}
+
+		this._hasSelectionChangedFromTheLastChangeBlock = false;
+	}
+
+	/**
+	 * Returns whether there is a buffered change or if the selection has changed from the last
+	 * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()` block}
+	 * or {@link module:engine/model/model~Model#change `change()` block}.
+	 *
+	 * @protected
+	 * @returns {Boolean} Returns `true` if document has changed from the last `change()` or `enqueueChange()` block.
+	 */
+	_hasDocumentChangedFromTheLastChangeBlock() {
+		return !this.differ.isEmpty || this._hasSelectionChangedFromTheLastChangeBlock;
+	}
+
 	/**
 	 * 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.
@@ -359,12 +391,21 @@ export default class Document {
 	 * Performs post-fixer loops. Executes post-fixer callbacks as long as none of them has done any changes to the model.
 	 *
 	 * @private
+	 * @param {module:engine/model/writer~Writer} writer The writer on which post-fixer callbacks will be called.
 	 */
 	_callPostFixers( writer ) {
 		let wasFixed = false;
 
 		do {
 			for ( const callback of this._postFixers ) {
+				// Ensure selection attributes are up to date before each post-fixer.
+				// https://github.com/ckeditor/ckeditor5-engine/issues/1673.
+				//
+				// It might be good to refresh the selection after each operation but at the moment it leads
+				// to losing attributes for composition or and spell checking
+				// https://github.com/ckeditor/ckeditor5-typing/issues/188
+				this.selection.refresh();
+
 				wasFixed = callback( writer );
 
 				if ( wasFixed ) {

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

+ 128 - 41
packages/ckeditor5-engine/src/model/documentselection.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -15,6 +15,7 @@ import LiveRange from './liverange';
 import Text from './text';
 import TextProxy from './textproxy';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import log from '@ckeditor/ckeditor5-utils/src/log';
 import uid from '@ckeditor/ckeditor5-utils/src/uid';
@@ -149,6 +150,17 @@ export default class DocumentSelection {
 		return this._selection.isGravityOverridden;
 	}
 
+	/**
+	 * A collection of selection markers.
+	 * Marker is a selection marker when selection range is inside the marker range.
+	 *
+	 * @readonly
+	 * @type {module:utils/collection~Collection.<module:engine/model/markercollection~Marker>}
+	 */
+	get markers() {
+		return this._selection.markers;
+	}
+
 	/**
 	 * Used for the compatibility with the {@link module:engine/model/selection~Selection#isEqual} method.
 	 *
@@ -247,12 +259,32 @@ export default class DocumentSelection {
 	 *		<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() {
 		return this._selection.getSelectedBlocks();
 	}
 
+	/**
+	 * Returns blocks that aren't nested in other selected blocks.
+	 *
+	 * In this case the method will return blocks A, B and E because C & D are children of block B:
+	 *
+	 *		[<blockA></blockA>
+	 *		<blockB>
+	 *			<blockC></blockC>
+	 *			<blockD></blockD>
+	 *		</blockB>
+	 *		<blockE></blockE>]
+	 *
+	 * **Note:** To get all selected blocks use {@link #getSelectedBlocks `getSelectedBlocks()`}.
+	 *
+	 * @returns {Iterable.<module:engine/model/element~Element>}
+	 */
+	getTopMostBlocks() {
+		return this._selection.getTopMostBlocks();
+	}
+
 	/**
 	 * 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.
@@ -327,6 +359,32 @@ export default class DocumentSelection {
 		return this._selection.hasAttribute( key );
 	}
 
+	/**
+	 * Refreshes selection attributes and markers according to the current position in the model.
+	 */
+	refresh() {
+		this._selection._updateMarkers();
+		this._selection._updateAttributes( false );
+	}
+
+	/**
+	 * Checks whether object is of given type following the convention set by
+	 * {@link module:engine/model/node~Node#is `Node#is()`}.
+	 *
+	 *		const selection = new DocumentSelection( ... );
+	 *
+	 *		selection.is( 'selection' ); // true
+	 *		selection.is( 'documentSelection' ); // true
+	 *		selection.is( 'node' ); // false
+	 *		selection.is( 'element' ); // false
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'selection' || type == 'documentSelection';
+	}
+
 	/**
 	 * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
 	 * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionFocus} method.
@@ -346,16 +404,12 @@ export default class DocumentSelection {
 
 	/**
 	 * 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.
+	 * {@link module:engine/model/selection~Selectable selectable}.
 	 * Should be used only within the {@link module:engine/model/writer~Writer#setSelection} method.
 	 *
 	 * @see module:engine/model/writer~Writer#setSelection
 	 * @protected
-	 * @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 {module:engine/model/selection~Selectable} 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.
@@ -506,6 +560,12 @@ class LiveSelection extends Selection {
 	constructor( doc ) {
 		super();
 
+		// List of selection markers.
+		// Marker is a selection marker when selection range is inside the marker range.
+		//
+		// @type {module:utils/collection~Collection}
+		this.markers = new Collection( { idProperty: 'name' } );
+
 		// Document which owns this selection.
 		//
 		// @protected
@@ -546,7 +606,27 @@ class LiveSelection extends Selection {
 		// @type {Set}
 		this._overriddenGravityRegister = new Set();
 
-		// Add events that will ensure selection correctness.
+		// Ensure selection is correct after each operation.
+		this.listenTo( this._model, 'applyOperation', ( evt, args ) => {
+			const operation = args[ 0 ];
+
+			if ( !operation.isDocumentOperation || operation.type == 'marker' || operation.type == 'rename' || operation.type == 'noop' ) {
+				return;
+			}
+
+			while ( this._fixGraveyardRangesData.length ) {
+				const { liveRange, sourcePosition } = this._fixGraveyardRangesData.shift();
+
+				this._fixGraveyardSelection( liveRange, sourcePosition );
+			}
+
+			if ( this._hasChangedRange ) {
+				this._hasChangedRange = false;
+				this.fire( 'change:range', { directChange: false } );
+			}
+		}, { priority: 'lowest' } );
+
+		// Ensure selection is correct and up to date after each range change.
 		this.on( 'change:range', () => {
 			for ( const range of this.getRanges() ) {
 				if ( !this._document._validateSelectionRange( range ) ) {
@@ -565,27 +645,13 @@ class LiveSelection extends Selection {
 			}
 		} );
 
-		this.listenTo( this._document, 'change', ( evt, batch ) => {
-			// Update selection's attributes.
-			this._updateAttributes( false );
+		// Update markers data stored by the selection after each marker change.
+		this.listenTo( this._model.markers, 'update', () => this._updateMarkers() );
 
-			// Clear selection attributes from element if no longer empty.
+		// Ensure selection is up to date after each change block.
+		this.listenTo( this._document, 'change', ( evt, batch ) => {
 			clearAttributesStoredInElement( this._model, batch );
 		} );
-
-		this.listenTo( this._model, 'applyOperation', () => {
-			while ( this._fixGraveyardRangesData.length ) {
-				const { liveRange, sourcePosition } = this._fixGraveyardRangesData.shift();
-
-				this._fixGraveyardSelection( liveRange, sourcePosition );
-			}
-
-			if ( this._hasChangedRange ) {
-				this._hasChangedRange = false;
-
-				this.fire( 'change:range', { directChange: false } );
-			}
-		}, { priority: 'lowest' } );
 	}
 
 	get isCollapsed() {
@@ -651,12 +717,12 @@ class LiveSelection extends Selection {
 
 	setTo( selectable, optionsOrPlaceOrOffset, options ) {
 		super.setTo( selectable, optionsOrPlaceOrOffset, options );
-		this._refreshAttributes();
+		this._updateAttributes( true );
 	}
 
 	setFocus( itemOrPosition, offset ) {
 		super.setFocus( itemOrPosition, offset );
-		this._refreshAttributes();
+		this._updateAttributes( true );
 	}
 
 	setAttribute( key, value ) {
@@ -683,7 +749,7 @@ class LiveSelection extends Selection {
 		this._overriddenGravityRegister.add( overrideUid );
 
 		if ( this._overriddenGravityRegister.size === 1 ) {
-			this._refreshAttributes();
+			this._updateAttributes( true );
 		}
 
 		return overrideUid;
@@ -696,7 +762,8 @@ class LiveSelection extends Selection {
 			 * UID obtained from the {@link module:engine/model/writer~Writer#overrideSelectionGravity} to restore.
 			 *
 			 * @error document-selection-gravity-wrong-restore
-			 * @param {String} uid The unique identifier returned by {@link #overrideGravity}.
+			 * @param {String} uid The unique identifier returned by
+			 * {@link module:engine/model/documentselection~DocumentSelection#_overrideGravity}.
 			 */
 			throw new CKEditorError(
 				'document-selection-gravity-wrong-restore: Attempting to restore the selection gravity for an unknown UID.',
@@ -708,15 +775,10 @@ class LiveSelection extends Selection {
 
 		// Restore gravity only when all overriding have been restored.
 		if ( !this.isGravityOverridden ) {
-			this._refreshAttributes();
+			this._updateAttributes( true );
 		}
 	}
 
-	// Removes all attributes from the selection and sets attributes according to the surrounding nodes.
-	_refreshAttributes() {
-		this._updateAttributes( true );
-	}
-
 	_popRange() {
 		this._ranges.pop().detach();
 	}
@@ -767,6 +829,32 @@ class LiveSelection extends Selection {
 		return liveRange;
 	}
 
+	_updateMarkers() {
+		const markers = [];
+
+		for ( const marker of this._model.markers ) {
+			const markerRange = marker.getRange();
+
+			for ( const selectionRange of this.getRanges() ) {
+				if ( markerRange.containsRange( selectionRange, !selectionRange.isCollapsed ) ) {
+					markers.push( marker );
+				}
+			}
+		}
+
+		for ( const marker of markers ) {
+			if ( !this.markers.has( marker ) ) {
+				this.markers.add( marker );
+			}
+		}
+
+		for ( const marker of Array.from( this.markers ) ) {
+			if ( !markers.includes( marker ) ) {
+				this.markers.remove( marker );
+			}
+		}
+	}
+
 	// Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.
 	//
 	// @protected
@@ -953,10 +1041,9 @@ class LiveSelection extends Selection {
 					break;
 				}
 
-				// This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454.
-				// It can be done better by using `break;` instead of checking `attrs === null`.
-				if ( value.type == 'text' && attrs === null ) {
+				if ( value.type == 'text' ) {
 					attrs = value.item.getAttributes();
+					break;
 				}
 			}
 		} else {

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -89,7 +89,7 @@ export default class Element extends Node {
 	}
 
 	/**
-	 * Checks whether given model tree object is of given type.
+	 * Checks whether this model object is of the given type.
 	 *
 	 *		obj.name; // 'listItem'
 	 *		obj instanceof Element; // true
@@ -100,7 +100,7 @@ export default class Element extends Node {
 	 *		obj.is( 'text' ); // false
 	 *		obj.is( 'element', 'image' ); // false
 	 *
-	 * Read more in {@link module:engine/model/node~Node#is}.
+	 * Read more in {@link module:engine/model/node~Node#is `Node#is()`}.
 	 *
 	 * @param {String} type Type to check when `name` parameter is present.
 	 * Otherwise, it acts like the `name` parameter.

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

+ 15 - 2
packages/ckeditor5-engine/src/model/liveposition.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -88,6 +88,7 @@ export default class LivePosition extends Position {
 	 * @method module:engine/model/liveposition~LivePosition._createAfter
 	 * @see module:engine/model/position~Position._createAfter
 	 * @param {module:engine/model/node~Node} node
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone']
 	 * @returns {module:engine/model/liveposition~LivePosition}
 	 */
 
@@ -97,6 +98,18 @@ export default class LivePosition extends Position {
 	 * @method module:engine/model/liveposition~LivePosition._createBefore
 	 * @see module:engine/model/position~Position._createBefore
 	 * @param {module:engine/model/node~Node} node
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone']
+	 * @returns {module:engine/model/liveposition~LivePosition}
+	 */
+
+	/**
+	 * @static
+	 * @protected
+	 * @method module:engine/model/liveposition~LivePosition._createAt
+	 * @see module:engine/model/position~Position._createAt
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset]
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone']
 	 * @returns {module:engine/model/liveposition~LivePosition}
 	 */
 

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -153,6 +153,28 @@ export default class MarkerCollection {
 		return false;
 	}
 
+	/**
+	 * Fires an {@link module:engine/model/markercollection~MarkerCollection#event:update} event for the given {@link ~Marker marker}
+	 * but does not change the marker. Useful to force {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast
+	 * conversion} for the marker.
+	 *
+	 * @protected
+	 * @fires module:engine/model/markercollection~MarkerCollection#event:update
+	 * @param {String} markerOrName Marker or name of a marker to refresh.
+	 */
+	_refresh( markerOrName ) {
+		const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
+		const marker = this._markers.get( markerName );
+
+		if ( !marker ) {
+			throw new CKEditorError( 'markercollection-refresh-marker-not-exists: Marker with provided name does not exists.' );
+		}
+
+		const range = marker.getRange();
+
+		this.fire( 'update:' + markerName, marker, range, range, marker.managedUsingOperations, marker.affectsData );
+	}
+
 	/**
 	 * Returns iterator that iterates over all markers, which ranges contain given {@link module:engine/model/position~Position position}.
 	 *
@@ -230,7 +252,7 @@ export default class MarkerCollection {
 	 * Fired whenever marker is added, updated or removed from `MarkerCollection`.
 	 *
 	 * @event update
-	 * @param {module:engine/model/markercollection~Marker} Updated Marker.
+	 * @param {module:engine/model/markercollection~Marker} 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
@@ -300,15 +322,11 @@ mix( MarkerCollection, EmitterMixin );
  *
  * 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.
+ * Use {@link module:engine/conversion/downcasthelpers 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
+ * Then, it can be upcasted back to a marker. Again, use {@link module:engine/conversion/upcasthelpers 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
- * to the range which it is marking at the moment.
- *
  * `Marker` instances are created and destroyed only by {@link ~MarkerCollection MarkerCollection}.
  */
 class Marker {

+ 68 - 48
packages/ckeditor5-engine/src/model/model.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -94,7 +94,8 @@ export default class Model {
 			isBlock: true
 		} );
 		this.schema.register( '$text', {
-			allowIn: '$block'
+			allowIn: '$block',
+			isInline: true
 		} );
 		this.schema.register( '$clipboardHolder', {
 			allowContentOf: '$root',
@@ -102,12 +103,15 @@ export default class Model {
 		} );
 		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' ]
+		// An element needed by the `upcastElementToMarker` converter.
+		// This element temporarily represents a marker boundary during the conversion process and is removed
+		// at the end of the conversion. `UpcastDispatcher` or at least `Conversion` class looks like a
+		// better place for this registration but both know nothing about `Schema`.
+		this.schema.register( '$marker' );
+		this.schema.addChildCheck( ( context, childDefinition ) => {
+			if ( childDefinition.name === '$marker' ) {
+				return true;
+			}
 		} );
 
 		injectSelectionPostFixer( this );
@@ -331,15 +335,16 @@ export default class Model {
 	 *
 	 * @fires insertContent
 	 * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
-	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
-	 * module:engine/model/position~Position|module:engine/model/item~Item|
-	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} [selectable=model.document.selection]
+	 * @param {module:engine/model/selection~Selectable} [selectable=model.document.selection]
 	 * The selection into which the content should be inserted. If not provided the current model document selection will be used.
 	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] To be used when a model item was passed as `selectable`.
 	 * This param defines a position in relation to that item.
+	 * @returns {module:engine/model/range~Range} Range which contains all the performed changes. This is a range that, if removed,
+	 * would return the model to the state before the insertion. If no changes were preformed by `insertContent`, returns a range collapsed
+	 * at the insertion position.
 	 */
 	insertContent( content, selectable, placeOrOffset ) {
-		insertContent( this, content, selectable, placeOrOffset );
+		return insertContent( this, content, selectable, placeOrOffset );
 	}
 
 	/**
@@ -374,6 +379,18 @@ export default class Model {
 	 *
 	 * * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
 	 * * `<heading1>^</heading1>` with enabled (`doNotResetEntireContent == true`)
+	 *
+	 * @param {Boolean} [options.doNotAutoparagraph=false] Whether to create a paragraph if after content deletion selection is moved
+	 * to a place where text cannot be inserted.
+	 *
+	 * For example `<paragraph>x</paragraph>[<image src="foo.jpg"></image>]` will become:
+	 *
+	 * * `<paragraph>x</paragraph><paragraph>[]</paragraph>` with the option disabled (`doNotAutoparagraph == false`)
+	 * * `<paragraph>x[]</paragraph>` with the option enabled (`doNotAutoparagraph == true`).
+	 *
+	 * **Note:** if there is no valid position for the selection, the paragraph will always be created:
+	 *
+	 * `[<image src="foo.jpg"></image>]` -> `<paragraph>[]</paragraph>`.
 	 */
 	deleteContent( selection, options ) {
 		deleteContent( this, selection, options );
@@ -447,26 +464,49 @@ export default class Model {
 
 	/**
 	 * Checks whether the given {@link module:engine/model/range~Range range} or
-	 * {@link module:engine/model/element~Element element}
-	 * has any content.
+	 * {@link module:engine/model/element~Element element} has any meaningful content.
+	 *
+	 * Meaningful content is:
+	 *
+	 * * any text node (`options.ignoreWhitespaces` allows controlling whether this text node must also contain
+	 * any non-whitespace characters),
+	 * * or any {@link module:engine/model/schema~Schema#isObject object element},
+	 * * or any {@link module:engine/model/markercollection~Marker marker} which
+	 * {@link module:engine/model/markercollection~Marker#_affectsData affects data}.
 	 *
-	 * Content is any text node or element which is registered in the {@link module:engine/model/schema~Schema schema}.
+	 * This means that a range containing an empty `<paragraph></paragraph>` is not considered to have a meaningful content.
+	 * However, a range containing an `<image></image>` (which would normally be marked in the schema as an object element)
+	 * is considered non-empty.
 	 *
 	 * @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
+	 * @param {Object} [options]
+	 * @param {Boolean} [options.ignoreWhitespaces] Whether text node with whitespaces only should be considered empty.
 	 * @returns {Boolean}
 	 */
-	hasContent( rangeOrElement ) {
-		if ( rangeOrElement instanceof ModelElement ) {
-			rangeOrElement = ModelRange._createIn( rangeOrElement );
-		}
+	hasContent( rangeOrElement, options ) {
+		const range = rangeOrElement instanceof ModelElement ? ModelRange._createIn( rangeOrElement ) : rangeOrElement;
 
-		if ( rangeOrElement.isCollapsed ) {
+		if ( range.isCollapsed ) {
 			return false;
 		}
 
-		for ( const item of rangeOrElement.getItems() ) {
-			// Remember, `TreeWalker` returns always `textProxy` nodes.
-			if ( item.is( 'textProxy' ) || this.schema.isObject( item ) ) {
+		// Check if there are any markers which affects data in this given range.
+		for ( const intersectingMarker of this.markers.getMarkersIntersectingRange( range ) ) {
+			if ( intersectingMarker.affectsData ) {
+				return true;
+			}
+		}
+
+		const { ignoreWhitespaces = false } = options || {};
+
+		for ( const item of range.getItems() ) {
+			if ( item.is( 'textProxy' ) ) {
+				if ( !ignoreWhitespaces ) {
+					return true;
+				} else if ( item.data.search( /\S/ ) !== -1 ) {
+					return true;
+				}
+			} else if ( this.schema.isObject( item ) ) {
 				return true;
 			}
 		}
@@ -596,14 +636,8 @@ export default class Model {
 	}
 
 	/**
-	 * Creates a new selection instance based on:
-	 *
-	 * * the given {@link module:engine/model/selection~Selection selection},
-	 * * or based on the given {@link module:engine/model/range~Range range},
-	 * * or based on the given iterable collection of {@link module:engine/model/range~Range ranges}
-	 * * or at the given {@link module:engine/model/position~Position position},
-	 * * or on the given {@link module:engine/model/element~Element element},
-	 * * or creates an empty selection if no arguments were passed.
+	 * Creates a new selection instance based on the given {@link module:engine/model/selection~Selectable selectable}
+	 * or creates an empty selection if no arguments were passed.
 	 *
 	 * Note: This method is also available as
 	 * {@link module:engine/model/writer~Writer#createSelection `Writer#createSelection()`}.
@@ -650,9 +684,7 @@ export default class Model {
 	 *		// Creates backward selection.
 	 *		const selection = writer.createSelection( range, { backward: true } );
 	 *
-	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
-	 * module:engine/model/position~Position|module:engine/model/element~Element|
-	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
+	 * @param {module:engine/model/selection~Selectable} 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.
@@ -705,8 +737,7 @@ export default class Model {
 			const callbackReturnValue = this._pendingChanges[ 0 ].callback( this._currentWriter );
 			ret.push( callbackReturnValue );
 
-			// Fire internal `_change` event.
-			this.fire( '_change', this._currentWriter );
+			this.document._handleChangeBlock( this._currentWriter );
 
 			this._pendingChanges.shift();
 			this._currentWriter = null;
@@ -717,17 +748,6 @@ export default class Model {
 		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 when entering the outermost {@link module:engine/model/model~Model#enqueueChange} or
 	 * {@link module:engine/model/model~Model#change} block.

+ 6 - 3
packages/ckeditor5-engine/src/model/node.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -11,6 +11,9 @@ import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
 
+// To check if component is loaded more than once.
+import '@ckeditor/ckeditor5-utils/src/version';
+
 /**
  * Model node. Most basic structure of model tree.
  *
@@ -99,7 +102,7 @@ export default class Node {
 	 * This means that model tree got broken.
 	 *
 	 * @readonly
-	 * @type {Number|Null}
+	 * @type {Number|null}
 	 */
 	get startOffset() {
 		let pos;

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,3 +1,8 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
 import InsertOperation from './insertoperation';
 import AttributeOperation from './attributeoperation';
 import RenameOperation from './renameoperation';
@@ -301,7 +306,7 @@ export function transformSets( operationsA, operationsB, options ) {
 		originalOperationsBCount: operationsB.length
 	};
 
-	const contextFactory = new ContextFactory( options.document, options.useRelations );
+	const contextFactory = new ContextFactory( options.document, options.useRelations, options.forceWeakRemove );
 	contextFactory.setOriginalOperations( operationsA );
 	contextFactory.setOriginalOperations( operationsB );
 
@@ -381,13 +386,17 @@ class ContextFactory {
 	// @param {module:engine/model/document~Document} document Document which the operations change.
 	// @param {Boolean} useRelations Whether during transformation relations should be used (used during undo for
 	// better conflict resolution).
-	constructor( document, useRelations ) {
+	// @param {Boolean} [forceWeakRemove=false] If set to `false`, remove operation will be always stronger than move operation,
+	// so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
+	constructor( document, useRelations, forceWeakRemove = false ) {
 		// `model.History` instance which information about undone operations will be taken from.
 		this._history = document.history;
 
 		// Whether additional context should be used.
 		this._useRelations = useRelations;
 
+		this._forceWeakRemove = !!forceWeakRemove;
+
 		// For each operation that is created during transformation process, we keep a reference to the original operation
 		// which it comes from. The original operation works as a kind of "identifier". Every contextual information
 		// gathered during transformation that we want to save for given operation, is actually saved for the original operation.
@@ -499,6 +508,10 @@ class ContextFactory {
 							this._setRelation( opA, opB, 'mergeTargetNotMoved' );
 						}
 
+						if ( opA.sourcePosition.isEqual( opB.targetPosition ) ) {
+							this._setRelation( opA, opB, 'mergeSourceNotMoved' );
+						}
+
 						if ( opA.sourcePosition.isEqual( opB.sourcePosition ) ) {
 							this._setRelation( opA, opB, 'mergeSameElement' );
 						}
@@ -515,6 +528,55 @@ class ContextFactory {
 
 				break;
 			}
+
+			case MarkerOperation: {
+				const markerRange = opA.newRange;
+
+				if ( !markerRange ) {
+					return;
+				}
+
+				switch ( opB.constructor ) {
+					case MoveOperation: {
+						const movedRange = Range._createFromPositionAndShift( opB.sourcePosition, opB.howMany );
+
+						const affectedLeft = movedRange.containsPosition( markerRange.start ) ||
+							movedRange.start.isEqual( markerRange.start );
+
+						const affectedRight = movedRange.containsPosition( markerRange.end ) ||
+							movedRange.end.isEqual( markerRange.end );
+
+						if ( ( affectedLeft || affectedRight ) && !movedRange.containsRange( markerRange ) ) {
+							this._setRelation( opA, opB, {
+								side: affectedLeft ? 'left' : 'right',
+								path: affectedLeft ? markerRange.start.path.slice() : markerRange.end.path.slice()
+							} );
+						}
+
+						break;
+					}
+
+					case MergeOperation: {
+						const wasInLeftElement = markerRange.start.isEqual( opB.targetPosition );
+						const wasStartBeforeMergedElement = markerRange.start.isEqual( opB.deletionPosition );
+						const wasEndBeforeMergedElement = markerRange.end.isEqual( opB.deletionPosition );
+						const wasInRightElement = markerRange.end.isEqual( opB.sourcePosition );
+
+						if ( wasInLeftElement || wasStartBeforeMergedElement || wasEndBeforeMergedElement || wasInRightElement ) {
+							this._setRelation( opA, opB, {
+								wasInLeftElement,
+								wasStartBeforeMergedElement,
+								wasEndBeforeMergedElement,
+								wasInRightElement
+							} );
+						}
+
+						break;
+					}
+				}
+
+				break;
+			}
 		}
 	}
 
@@ -529,7 +591,8 @@ class ContextFactory {
 			aWasUndone: this._wasUndone( opA ),
 			bWasUndone: this._wasUndone( opB ),
 			abRelation: this._useRelations ? this._getRelation( opA, opB ) : null,
-			baRelation: this._useRelations ? this._getRelation( opB, opA ) : null
+			baRelation: this._useRelations ? this._getRelation( opB, opA ) : null,
+			forceWeakRemove: this._forceWeakRemove
 		};
 	}
 
@@ -1068,24 +1131,60 @@ setTransformation( MarkerOperation, MergeOperation, ( a, b ) => {
 	return [ a ];
 } );
 
-setTransformation( MarkerOperation, MoveOperation, ( a, b ) => {
+setTransformation( MarkerOperation, MoveOperation, ( a, b, context ) => {
 	if ( a.oldRange ) {
 		a.oldRange = Range._createFromRanges( a.oldRange._getTransformedByMoveOperation( b ) );
 	}
 
 	if ( a.newRange ) {
+		if ( context.abRelation ) {
+			const aNewRange = Range._createFromRanges( a.newRange._getTransformedByMoveOperation( b ) );
+
+			if ( context.abRelation.side == 'left' && b.targetPosition.isEqual( a.newRange.start ) ) {
+				a.newRange.start.path = context.abRelation.path;
+				a.newRange.end = aNewRange.end;
+
+				return [ a ];
+			} else if ( context.abRelation.side == 'right' && b.targetPosition.isEqual( a.newRange.end ) ) {
+				a.newRange.start = aNewRange.start;
+				a.newRange.end.path = context.abRelation.path;
+
+				return [ a ];
+			}
+		}
+
 		a.newRange = Range._createFromRanges( a.newRange._getTransformedByMoveOperation( b ) );
 	}
 
 	return [ a ];
 } );
 
-setTransformation( MarkerOperation, SplitOperation, ( a, b ) => {
+setTransformation( MarkerOperation, SplitOperation, ( a, b, context ) => {
 	if ( a.oldRange ) {
 		a.oldRange = a.oldRange._getTransformedBySplitOperation( b );
 	}
 
 	if ( a.newRange ) {
+		if ( context.abRelation ) {
+			const aNewRange = a.newRange._getTransformedBySplitOperation( b );
+
+			if ( a.newRange.start.isEqual( b.splitPosition ) && context.abRelation.wasStartBeforeMergedElement ) {
+				a.newRange.start = Position._createAt( b.insertionPosition );
+			} else if ( a.newRange.start.isEqual( b.splitPosition ) && !context.abRelation.wasInLeftElement ) {
+				a.newRange.start = Position._createAt( b.moveTargetPosition );
+			}
+
+			if ( a.newRange.end.isEqual( b.splitPosition ) && context.abRelation.wasInRightElement ) {
+				a.newRange.end = Position._createAt( b.moveTargetPosition );
+			} else if ( a.newRange.end.isEqual( b.splitPosition ) && context.abRelation.wasEndBeforeMergedElement ) {
+				a.newRange.end = Position._createAt( b.insertionPosition );
+			} else {
+				a.newRange.end = aNewRange.end;
+			}
+
+			return [ a ];
+		}
+
 		a.newRange = a.newRange._getTransformedBySplitOperation( b );
 	}
 
@@ -1223,7 +1322,7 @@ setTransformation( MergeOperation, MoveOperation, ( a, b, context ) => {
 	//
 	const removedRange = Range._createFromPositionAndShift( b.sourcePosition, b.howMany );
 
-	if ( b.type == 'remove' && !context.bWasUndone ) {
+	if ( b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove ) {
 		if ( a.deletionPosition.hasSameParentAs( b.sourcePosition ) && removedRange.containsPosition( a.sourcePosition ) ) {
 			return [ new NoOperation( 0 ) ];
 		}
@@ -1339,19 +1438,34 @@ setTransformation( MergeOperation, SplitOperation, ( a, b, context ) => {
 
 	// Case 2:
 	//
-	// Merge source is at the same position as split position. This sometimes happen during undo. This merge operation
-	// might have been earlier transformed by a merge operation which both merged the same element. See case in
-	// `MergeOperation` x `MergeOperation` transformation. In that case, if the merge operation has been undone, the special
-	// case is not applied.
+	// Merge source is at the same position as split position. This sometimes happen, mostly during undo.
+	// The decision here is mostly to choose whether merge source position should stay where it is (so it will be at the end of the
+	// split element) or should be move to the beginning of the new element.
 	//
-	// In this scenario the merge operation is now transformed by the split which has undone the previous merge operation.
-	// So now we are fixing situation which was skipped in `MergeOperation` x `MergeOperation` case.
-	//
-	if ( a.sourcePosition.isEqual( b.splitPosition ) && ( context.abRelation == 'mergeSameElement' || a.sourcePosition.offset > 0 ) ) {
-		a.sourcePosition = b.moveTargetPosition.clone();
-		a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b );
+	if ( a.sourcePosition.isEqual( b.splitPosition ) ) {
+		// Use context to check if `SplitOperation` is not undoing a merge operation, that didn't change the `a` operation.
+		// This scenario happens the undone merge operation moved nodes at the source position of `a` operation.
+		// In that case `a` operation source position should stay where it is.
+		if ( context.abRelation == 'mergeSourceNotMoved' ) {
+			a.howMany = 0;
+			a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b );
 
-		return [ a ];
+			return [ a ];
+		}
+
+		// This merge operation might have been earlier transformed by a merge operation which both merged the same element.
+		// See that case in `MergeOperation` x `MergeOperation` transformation. In that scenario, if the merge operation has been undone,
+		// the special case is not applied.
+		//
+		// Now, the merge operation is transformed by the split which has undone that previous merge operation.
+		// So now we are fixing situation which was skipped in `MergeOperation` x `MergeOperation` case.
+		//
+		if ( context.abRelation == 'mergeSameElement' || a.sourcePosition.offset > 0 ) {
+			a.sourcePosition = b.moveTargetPosition.clone();
+			a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b );
+
+			return [ a ];
+		}
 	}
 
 	// The default case.
@@ -1506,9 +1620,9 @@ setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
 	//
 	// If only one of operations is a remove operation, we force remove operation to be the "stronger" one
 	// to provide more expected results.
-	if ( a.type == 'remove' && b.type != 'remove' && !context.aWasUndone ) {
+	if ( a.type == 'remove' && b.type != 'remove' && !context.aWasUndone && !context.forceWeakRemove ) {
 		aIsStrong = true;
-	} else if ( a.type != 'remove' && b.type == 'remove' && !context.bWasUndone ) {
+	} else if ( a.type != 'remove' && b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove ) {
 		aIsStrong = false;
 	}
 
@@ -1678,7 +1792,7 @@ setTransformation( MoveOperation, SplitOperation, ( a, b, context ) => {
 	if ( b.graveyardPosition ) {
 		const movesGraveyardElement = moveRange.start.isEqual( b.graveyardPosition ) || moveRange.containsPosition( b.graveyardPosition );
 
-		if ( a.howMany > 1 && movesGraveyardElement ) {
+		if ( a.howMany > 1 && movesGraveyardElement && !context.aWasUndone ) {
 			ranges.push( Range._createFromPositionAndShift( b.insertionPosition, 1 ) );
 		}
 	}
@@ -1690,7 +1804,7 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
 	const movedRange = Range._createFromPositionAndShift( a.sourcePosition, a.howMany );
 
 	if ( b.deletionPosition.hasSameParentAs( a.sourcePosition ) && movedRange.containsPosition( b.sourcePosition ) ) {
-		if ( a.type == 'remove' ) {
+		if ( a.type == 'remove' && !context.forceWeakRemove ) {
 			// Case 1:
 			//
 			// The element to remove got merged.
@@ -1704,21 +1818,23 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
 				const results = [];
 
 				let gyMoveSource = b.graveyardPosition.clone();
-				let splitNodesMoveSource = b.targetPosition.clone();
+				let splitNodesMoveSource = b.targetPosition._getTransformedByMergeOperation( b );
 
 				if ( a.howMany > 1 ) {
 					results.push( new MoveOperation( a.sourcePosition, a.howMany - 1, a.targetPosition, 0 ) );
-					gyMoveSource = gyMoveSource._getTransformedByInsertion( a.targetPosition, a.howMany - 1 );
+
+					gyMoveSource = gyMoveSource._getTransformedByMove( a.sourcePosition, a.targetPosition, a.howMany - 1 );
 					splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( a.sourcePosition, a.targetPosition, a.howMany - 1 );
 				}
 
 				const gyMoveTarget = b.deletionPosition._getCombined( a.sourcePosition, a.targetPosition );
 				const gyMove = new MoveOperation( gyMoveSource, 1, gyMoveTarget, 0 );
 
-				const targetPositionPath = gyMove.getMovedRangeStart().path.slice();
-				targetPositionPath.push( 0 );
+				const splitNodesMoveTargetPath = gyMove.getMovedRangeStart().path.slice();
+				splitNodesMoveTargetPath.push( 0 );
 
-				const splitNodesMoveTarget = new Position( gyMove.targetPosition.root, targetPositionPath );
+				const splitNodesMoveTarget = new Position( gyMove.targetPosition.root, splitNodesMoveTargetPath );
+				splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( gyMoveSource, gyMoveTarget, 1 );
 				const splitNodesMove = new MoveOperation( splitNodesMoveSource, b.howMany, splitNodesMoveTarget, 0 );
 
 				results.push( gyMove );
@@ -1961,7 +2077,9 @@ setTransformation( SplitOperation, MoveOperation, ( a, b, context ) => {
 		// is already moved to the correct position, we need to only move the nodes after the split position.
 		// This will be done by `MoveOperation` instead of `SplitOperation`.
 		//
-		if ( rangeToMove.start.isEqual( a.graveyardPosition ) || rangeToMove.containsPosition( a.graveyardPosition ) ) {
+		const gyElementMoved = rangeToMove.start.isEqual( a.graveyardPosition ) || rangeToMove.containsPosition( a.graveyardPosition );
+
+		if ( !context.bWasUndone && gyElementMoved ) {
 			const sourcePosition = a.splitPosition._getTransformedByMoveOperation( b );
 
 			const newParentPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
@@ -2066,6 +2184,9 @@ setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => {
 	//
 	// So we cancel split operation only if it was really identical.
 	//
+	// Also, there is additional case, where split operations aren't identical and should not be cancelled, however the
+	// default transformation is incorrect too.
+	//
 	if ( a.splitPosition.isEqual( b.splitPosition ) ) {
 		if ( !a.graveyardPosition && !b.graveyardPosition ) {
 			return [ new NoOperation( 0 ) ];
@@ -2074,6 +2195,20 @@ setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => {
 		if ( a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual( b.graveyardPosition ) ) {
 			return [ new NoOperation( 0 ) ];
 		}
+
+		// Use context to know that the `a.splitPosition` should stay where it is.
+		// This happens during undo when first a merge operation moved nodes to `a.splitPosition` and now `b` operation undoes that merge.
+		if ( context.abRelation == 'splitBefore' ) {
+			// Since split is at the same position, there are no nodes left to split.
+			a.howMany = 0;
+
+			// Note: there was `if ( a.graveyardPosition )` here but it was uncovered in tests and I couldn't find any scenarios for now.
+			// That would have to be a `SplitOperation` that didn't come from undo but is transformed by operations that were undone.
+			// It could happen if `context` is enabled in collaboration.
+			a.graveyardPosition = a.graveyardPosition._getTransformedBySplitOperation( b );
+
+			return [ a ];
+		}
 	}
 
 	// Case 2:
@@ -2205,9 +2340,7 @@ function _makeMoveOperationsFromRanges( ranges, targetPosition ) {
 		const op = new MoveOperation(
 			range.start,
 			range.end.offset - range.start.offset,
-			// If the target is the end of the move range this operation doesn't really move anything.
-			// In this case, it is better for OT to use range start instead of range end.
-			targetPosition.isEqual( range.end ) ? range.start : targetPosition,
+			targetPosition,
 			0
 		);
 

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

+ 19 - 15
packages/ckeditor5-engine/src/model/position.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -13,6 +13,9 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import Text from './text';
 import { last } from 'lodash-es';
 
+// To check if component is loaded more than once.
+import '@ckeditor/ckeditor5-utils/src/version';
+
 /**
  * Represents a position in the model tree.
  *
@@ -828,11 +831,13 @@ export default class Position {
 	 * * {@link module:engine/model/position~Position._createAfter}.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when the
+	 * first parameter is a {@link module:engine/model/item~Item model item}.
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness. Used only when the
 	 * first parameter is a {@link module:engine/model/item~Item model item}.
 	 * @protected
 	 */
-	static _createAt( itemOrPosition, offset ) {
+	static _createAt( itemOrPosition, offset, stickiness = 'toNone' ) {
 		if ( itemOrPosition instanceof Position ) {
 			return new Position( itemOrPosition.root, itemOrPosition.path, itemOrPosition.stickiness );
 		} else {
@@ -841,9 +846,9 @@ export default class Position {
 			if ( offset == 'end' ) {
 				offset = node.maxOffset;
 			} else if ( offset == 'before' ) {
-				return this._createBefore( node );
+				return this._createBefore( node, stickiness );
 			} else if ( offset == 'after' ) {
-				return this._createAfter( node );
+				return this._createAfter( node, stickiness );
 			} else if ( offset !== 0 && !offset ) {
 				/**
 				 * {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}
@@ -869,7 +874,7 @@ export default class Position {
 
 			path.push( offset );
 
-			return new this( node.root, path );
+			return new this( node.root, path, stickiness );
 		}
 	}
 
@@ -877,10 +882,11 @@ export default class Position {
 	 * Creates a new position, after given {@link module:engine/model/item~Item model item}.
 	 *
 	 * @param {module:engine/model/item~Item} item Item after which the position should be placed.
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
 	 * @returns {module:engine/model/position~Position}
 	 * @protected
 	 */
-	static _createAfter( item ) {
+	static _createAfter( item, stickiness ) {
 		if ( !item.parent ) {
 			/**
 			 * You can not make a position after a root element.
@@ -891,17 +897,18 @@ export default class Position {
 			throw new CKEditorError( 'model-position-after-root: You cannot make a position after root.', { root: item } );
 		}
 
-		return this._createAt( item.parent, item.endOffset );
+		return this._createAt( item.parent, item.endOffset, stickiness );
 	}
 
 	/**
 	 * Creates a new position, before the given {@link module:engine/model/item~Item model item}.
 	 *
 	 * @param {module:engine/model/item~Item} item Item before which the position should be placed.
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
 	 * @returns {module:engine/model/position~Position}
 	 * @protected
 	 */
-	static _createBefore( item ) {
+	static _createBefore( item, stickiness ) {
 		if ( !item.parent ) {
 			/**
 			 * You can not make a position before a root element.
@@ -912,7 +919,7 @@ export default class Position {
 			throw new CKEditorError( 'model-position-before-root: You cannot make a position before root.', { root: item } );
 		}
 
-		return this._createAt( item.parent, item.startOffset );
+		return this._createAt( item.parent, item.startOffset, stickiness );
 	}
 
 	/**
@@ -943,10 +950,7 @@ export default class Position {
 			);
 		}
 
-		const pos = new Position( doc.getRoot( json.root ), json.path );
-		pos.stickiness = json.stickiness;
-
-		return pos;
+		return new Position( doc.getRoot( json.root ), json.path, json.stickiness );
 	}
 }
 

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -336,6 +336,16 @@ export default class Range {
 	/**
 	 * Creates a {@link module:engine/model/treewalker~TreeWalker TreeWalker} instance with this range as a boundary.
 	 *
+	 * For example, to iterate over all items in the entire document root:
+	 *
+	 *		// Create a range spanning over the entire root content:
+	 *		const range = editor.model.createRangeIn( editor.model.document.getRoot() );
+	 *
+	 *		// Iterate over all items in this range:
+	 *		for ( const value of range.getWalker() ) {
+	 *			console.log( value.item );
+	 *		}
+	 *
 	 * @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
 	 * @param {module:engine/model/position~Position} [options.startPosition]
 	 * @param {Boolean} [options.singleCharacters=false]
@@ -562,6 +572,21 @@ export default class Range {
 	 * @returns {module:engine/model/range~Range}
 	 */
 	_getTransformedByMergeOperation( operation ) {
+		// Special case when the marker is set on "the closing tag" of an element. Marker can be set like that during
+		// transformations, especially when a content of a few block elements were removed. For example:
+		//
+		// {} is the transformed range, [] is the removed range.
+		// <p>F[o{o</p><p>B}ar</p><p>Xy]z</p>
+		//
+		// <p>Fo{o</p><p>B}ar</p><p>z</p>
+		// <p>F{</p><p>B}ar</p><p>z</p>
+		// <p>F{</p>}<p>z</p>
+		// <p>F{}z</p>
+		//
+		if ( this.start.isEqual( operation.targetPosition ) && this.end.isEqual( operation.deletionPosition ) ) {
+			return new Range( this.start );
+		}
+
 		let start = this.start._getTransformedByMergeOperation( operation );
 		let end = this.end._getTransformedByMergeOperation( operation );
 

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

+ 166 - 50
packages/ckeditor5-engine/src/model/schema.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -39,6 +39,14 @@ export default class Schema {
 	constructor() {
 		this._sourceDefinitions = {};
 
+		/**
+		 * A dictionary containing attribute properties.
+		 *
+		 * @private
+		 * @member {Object.<String,String>}
+		 */
+		this._attributeProperties = {};
+
 		this.decorate( 'checkChild' );
 		this.decorate( 'checkAttribute' );
 
@@ -64,7 +72,25 @@ export default class Schema {
 	 */
 	register( itemName, definition ) {
 		if ( this._sourceDefinitions[ itemName ] ) {
-			// TODO docs
+			/**
+			 * A single item cannot be registered twice in the schema.
+			 *
+			 * This situation may happen when:
+			 *
+			 * * Two or more plugins called {@link #register `register()`} with the same name. This will usually mean that
+			 * there is a collision between plugins which try to use the same element in the model. Unfortunately,
+			 * the only way to solve this is by modifying one of these plugins to use a unique model element name.
+			 * * A single plugin was loaded twice. This happens when it is installed by npm/yarn in two versions
+			 * and usually means one or more of the following issues:
+			 *     * a version mismatch (two of your dependencies require two different versions of this plugin),
+			 *     * incorrect imports (this plugin is somehow imported twice in a way which confuses webpack),
+			 *     * mess in `node_modules/` (`rm -rf node_modules/` may help).
+			 *
+			 * **Note:** Check the logged `itemName` to better understand which plugin was duplicated/conflicting.
+			 *
+			 * @param itemName The name of the model element that is being registered twice.
+			 * @error schema-cannot-register-item-twice
+			 */
 			throw new CKEditorError( 'schema-cannot-register-item-twice: A single item cannot be registered twice in the schema.', {
 				itemName
 			} );
@@ -103,7 +129,15 @@ export default class Schema {
 	 */
 	extend( itemName, definition ) {
 		if ( !this._sourceDefinitions[ itemName ] ) {
-			// TODO docs
+			/**
+			 * Cannot extend an item which was not registered yet.
+			 *
+			 * This error happens when a plugin tries to extend the schema definition of an item which was not
+			 * {@link #register registered} yet.
+			 *
+			 * @param itemName The name of the model element which is being extended.
+			 * @error schema-cannot-register-item-twice
+			 */
 			throw new CKEditorError( 'schema-cannot-extend-missing-item: Cannot extend an item which was not registered yet.', {
 				itemName
 			} );
@@ -204,7 +238,7 @@ export default class Schema {
 
 	/**
 	 * Returns `true` if the given item is defined to be
-	 * a object element by {@link module:engine/model/schema~SchemaItemDefinition}'s `isObject` property.
+	 * an object element by {@link module:engine/model/schema~SchemaItemDefinition}'s `isObject` property.
 	 *
 	 *		schema.isObject( 'paragraph' ); // -> false
 	 *		schema.isObject( 'image' ); // -> true
@@ -220,6 +254,24 @@ export default class Schema {
 		return !!( def && def.isObject );
 	}
 
+	/**
+	 * Returns `true` if the given item is defined to be
+	 * an inline element by {@link module:engine/model/schema~SchemaItemDefinition}'s `isInline` property.
+	 *
+	 *		schema.isInline( 'paragraph' ); // -> false
+	 *		schema.isInline( 'softBreak' ); // -> true
+	 *
+	 *		const text = writer.createText('foo' );
+	 *		schema.isInline( text ); // -> true
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
+	 */
+	isInline( item ) {
+		const def = this.getDefinition( item );
+
+		return !!( def && def.isInline );
+	}
+
 	/**
 	 * Checks whether the given node (`child`) can be a child of the given context.
 	 *
@@ -395,7 +447,7 @@ export default class Schema {
 	 * Example:
 	 *
 	 *		// Disallow bold on $text inside heading1.
-	 *		schema.addChildCheck( ( context, attributeName ) => {
+	 *		schema.addAttributeCheck( ( context, attributeName ) => {
 	 *			if ( context.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
 	 *				return false;
 	 *			}
@@ -431,6 +483,58 @@ export default class Schema {
 		}, { priority: 'high' } );
 	}
 
+	/**
+	 * This method allows assigning additional metadata to the model attributes. For example,
+	 * {@link module:engine/model/schema~AttributeProperties `AttributeProperties#isFormatting` property} is
+	 * used to mark formatting attributes (like `bold` or `italic`).
+	 *
+	 *		// Mark bold as a formatting attribute.
+	 *		schema.setAttributeProperties( 'bold', {
+	 *			isFormatting: true
+	 *		} );
+	 *
+	 *		// Override code not to be considered a formatting markup.
+	 *		schema.setAttributeProperties( 'code', {
+	 *			isFormatting: false
+	 *		} );
+	 *
+	 * Properties are not limited to members defined in the
+	 * {@link module:engine/model/schema~AttributeProperties `AttributeProperties` type} and you can also use custom properties:
+	 *
+	 *		schema.setAttributeProperties( 'blockQuote', {
+	 *			customProperty: 'value'
+	 *		} );
+	 *
+	 * Subsequent calls with the same attribute will extend its custom properties:
+	 *
+	 *		schema.setAttributeProperties( 'blockQuote', {
+	 *			one: 1
+	 *		} );
+	 *
+	 *		schema.setAttributeProperties( 'blockQuote', {
+	 *			two: 2
+	 *		} );
+	 *
+	 *		console.log( schema.getAttributeProperties( 'blockQuote' ) );
+	 *		// Logs: { one: 1, two: 2 }
+	 *
+	 * @param {String} attributeName A name of the attribute to receive the properties.
+	 * @param {module:engine/model/schema~AttributeProperties} properties A dictionary of properties.
+	 */
+	setAttributeProperties( attributeName, properties ) {
+		this._attributeProperties[ attributeName ] = Object.assign( this.getAttributeProperties( attributeName ), properties );
+	}
+
+	/**
+	 * Returns properties associated with a given model attribute. See {@link #setAttributeProperties `setAttributeProperties()`}.
+	 *
+	 * @param {String} attributeName A name of the attribute.
+	 * @returns {module:engine/model/schema~AttributeProperties}
+	 */
+	getAttributeProperties( attributeName ) {
+		return this._attributeProperties[ attributeName ] || {};
+	}
+
 	/**
 	 * Returns the lowest {@link module:engine/model/schema~Schema#isLimit limit element} containing the entire
 	 * selection/range/position or the root otherwise.
@@ -520,7 +624,7 @@ export default class Schema {
 	 *
 	 * @param {Array.<module:engine/model/range~Range>} ranges Ranges to be validated.
 	 * @param {String} attribute The name of the attribute to check.
-	 * @returns {Iterator.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
+	 * @returns {Iterable.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
 	 */
 	* getValidRanges( ranges, attribute ) {
 		ranges = convertToMinimalFlatRanges( ranges );
@@ -530,42 +634,6 @@ export default class Schema {
 		}
 	}
 
-	/**
-	 * Takes a flat range and an attribute name. Traverses the range recursively and deeply to find and return all ranges
-	 * inside the given range on which the attribute can be applied.
-	 *
-	 * This is a helper function for {@link ~Schema#getValidRanges}.
-	 *
-	 * @private
-	 * @param {module:engine/model/range~Range} range Range to process.
-	 * @param {String} attribute The name of the attribute to check.
-	 * @returns {Iterator.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
-	 */
-	* _getValidRangesForRange( range, attribute ) {
-		let start = range.start;
-		let end = range.start;
-
-		for ( const item of range.getItems( { shallow: true } ) ) {
-			if ( item.is( 'element' ) ) {
-				yield* this._getValidRangesForRange( Range._createIn( item ), attribute );
-			}
-
-			if ( !this.checkAttribute( item, attribute ) ) {
-				if ( !start.isEqual( end ) ) {
-					yield new Range( start, end );
-				}
-
-				start = Position._createAfter( item );
-			}
-
-			end = Position._createAfter( item );
-		}
-
-		if ( !start.isEqual( end ) ) {
-			yield new Range( start, end );
-		}
-	}
-
 	/**
 	 * Basing on given `position`, finds and returns a {@link module:engine/model/range~Range range} which is
 	 * nearest to that `position` and is a correct range for selection.
@@ -624,11 +692,11 @@ export default class Schema {
 	 * 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.
+	 * @params {module:engine/model/node~Node|String} node Node for which allowed parent should be found or its name.
 	 * @returns {module:engine/model/element~Element|null} element Allowed parent or null if nothing was found.
 	 */
-	findAllowedParent( node, position ) {
+	findAllowedParent( position, node ) {
 		let parent = position.parent;
 
 		while ( parent ) {
@@ -738,6 +806,42 @@ export default class Schema {
 			return false;
 		}
 	}
+
+	/**
+	 * Takes a flat range and an attribute name. Traverses the range recursively and deeply to find and return all ranges
+	 * inside the given range on which the attribute can be applied.
+	 *
+	 * This is a helper function for {@link ~Schema#getValidRanges}.
+	 *
+	 * @private
+	 * @param {module:engine/model/range~Range} range Range to process.
+	 * @param {String} attribute The name of the attribute to check.
+	 * @returns {Iterable.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
+	 */
+	* _getValidRangesForRange( range, attribute ) {
+		let start = range.start;
+		let end = range.start;
+
+		for ( const item of range.getItems( { shallow: true } ) ) {
+			if ( item.is( 'element' ) ) {
+				yield* this._getValidRangesForRange( Range._createIn( item ), attribute );
+			}
+
+			if ( !this.checkAttribute( item, attribute ) ) {
+				if ( !start.isEqual( end ) ) {
+					yield new Range( start, end );
+				}
+
+				start = Position._createAfter( item );
+			}
+
+			end = Position._createAfter( item );
+		}
+
+		if ( !start.isEqual( end ) ) {
+			yield new Range( start, end );
+		}
+	}
 }
 
 mix( Schema, ObservableMixin );
@@ -873,7 +977,7 @@ mix( Schema, ObservableMixin );
  * * `allowAttributesOf` &ndash; A string or an array of strings. Inherits attributes from other items.
  * * `inheritTypesFrom` &ndash; A string or an array of strings. Inherits `is*` properties of other items.
  * * `inheritAllFrom` &ndash; A string. A shorthand for `allowContentOf`, `allowWhere`, `allowAttributesOf`, `inheritTypesFrom`.
- * * Additionally, you can define the following `is*` properties: `isBlock`, `isLimit`, `isObject`. Read about them below.
+ * * Additionally, you can define the following `is*` properties: `isBlock`, `isLimit`, `isObject`, `isInline`. Read about them below.
  *
  * # The is* properties
  *
@@ -889,8 +993,9 @@ mix( Schema, ObservableMixin );
  * a limit element are limited to its content. **Note:** All objects (`isObject`) are treated as limit elements, too.
  * * `isObject` &ndash; Whether an item is "self-contained" and should be treated as a whole. Examples of object elements:
  * `image`, `table`, `video`, etc. **Note:** An object is also a limit, so
- * {@link module:engine/model/schema~Schema#isLimit `isLimit()`}
- * returns `true` for object elements automatically.
+ * {@link module:engine/model/schema~Schema#isLimit `isLimit()`} returns `true` for object elements automatically.
+ * * `isInline` &ndash; Whether an item is "text-like" and should be treated as an inline node. Examples of inline elements:
+ * `$text`, `softBreak` (`<br>`), etc.
  *
  * # Generic items
  *
@@ -905,7 +1010,8 @@ mix( Schema, ObservableMixin );
  *			isBlock: true
  *		} );
  *		this.schema.register( '$text', {
- *			allowIn: '$block'
+ *			allowIn: '$block',
+ *			isInline: true
  *		} );
  *
  * They reflect typical editor content that is contained within one root, consists of several blocks
@@ -1239,6 +1345,16 @@ export class SchemaContext {
  * @typedef {Object} module:engine/model/schema~SchemaContextItem
  */
 
+/**
+ * A structure containing additional metadata describing the attribute.
+ *
+ * See {@link module:engine/model/schema~Schema#setAttributeProperties `Schema#setAttributeProperties()`} for usage examples.
+ *
+ * @typedef {Object} module:engine/model/schema~AttributeProperties
+ * @property {Boolean} [isFormatting] Indicates that the attribute should be considered as a visual formatting, like `bold`, `italic` or
+ * `fontSize` rather than semantic attribute (such as `src`, `listType`, etc.). For example, it is used by the "Remove format" feature.
+ */
+
 function compileBaseItemRule( sourceItemRules, itemName ) {
 	const itemRule = {
 		name: itemName,
@@ -1459,7 +1575,7 @@ function* combineWalkers( backward, forward ) {
 // all those minimal flat ranges.
 //
 // @param {Array.<module:engine/model/range~Range>} ranges Ranges to process.
-// @returns {Iterator.<module:engine/model/range~Range>} Minimal flat ranges of given `ranges`.
+// @returns {Iterable.<module:engine/model/range~Range>} Minimal flat ranges of given `ranges`.
 function* convertToMinimalFlatRanges( ranges ) {
 	for ( const range of ranges ) {
 		yield* range.getMinimalFlatRanges();

+ 111 - 20
packages/ckeditor5-engine/src/model/selection.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -27,12 +27,7 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  */
 export default class Selection {
 	/**
-	 * Creates a new selection instance
-	 * based on the given {@link module:engine/model/selection~Selection selection},
-	 * or based on the given {@link module:engine/model/range~Range range},
-	 * or based on an iterable collection of {@link module:engine/model/range~Range ranges}
-	 * or at the given {@link module:engine/model/position~Position position},
-	 * or on the given {@link module:engine/model/element~Element element},
+	 * Creates a new selection instance based on the given {@link module:engine/model/selection~Selectable selectable}
 	 * or creates an empty selection if no arguments were passed.
 	 *
 	 *		// Creates empty selection without ranges.
@@ -77,9 +72,7 @@ export default class Selection {
 	 *		// Creates backward selection.
 	 *		const selection = writer.createSelection( range, { backward: true } );
 	 *
-	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
-	 * module:engine/model/position~Position|module:engine/model/element~Element|
-	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
+	 * @param {module:engine/model/selection~Selectable} 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.
@@ -322,9 +315,7 @@ export default class Selection {
 
 	/**
 	 * 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.
+	 * {@link module:engine/model/selection~Selectable selectable}.
 	 *
 	 *		// Removes all selection's ranges.
 	 *		selection.setTo( null );
@@ -368,9 +359,7 @@ export default class Selection {
 	 *		// Sets backward selection.
 	 *		const selection = writer.createSelection( range, { backward: true } );
 	 *
-	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
-	 * module:engine/model/position~Position|module:engine/model/node~Node|
-	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
+	 * @param {module:engine/model/selection~Selectable} 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.
@@ -415,11 +404,18 @@ export default class Selection {
 			this._setRanges( selectable, placeOrOffset && !!placeOrOffset.backward );
 		} else {
 			/**
-			 * Cannot set selection to given place.
+			 * Cannot set the selection to the given place.
+			 *
+			 * Invalid parameters were specified when setting the selection. Common issues:
+			 *
+			 * * A {@link module:engine/model/textproxy~TextProxy} instance was passed instead of
+			 * a real {@link module:engine/model/text~Text}.
+			 * * View nodes were passed instead of model nodes.
+			 * * `null`/`undefined` was passed.
 			 *
 			 * @error model-selection-setTo-not-selectable
 			 */
-			throw new CKEditorError( 'model-selection-setTo-not-selectable: Cannot set selection to given place.' );
+			throw new CKEditorError( 'model-selection-setTo-not-selectable: Cannot set the selection to the given place.' );
 		}
 	}
 
@@ -619,6 +615,23 @@ export default class Selection {
 		return ( nodeAfterStart instanceof Element && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : null;
 	}
 
+	/**
+	 * Checks whether object is of given type following the convention set by
+	 * {@link module:engine/model/node~Node#is `Node#is()`}.
+	 *
+	 *		const selection = new Selection( ... );
+	 *
+	 *		selection.is( 'selection' ); // true
+	 *		selection.is( 'node' ); // false
+	 *		selection.is( 'element' ); // false
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'selection';
+	}
+
 	/**
 	 * Gets elements of type "block" touched by the selection.
 	 *
@@ -672,6 +685,35 @@ export default class Selection {
 		}
 	}
 
+	/**
+	 * Returns blocks that aren't nested in other selected blocks.
+	 *
+	 * In this case the method will return blocks A, B and E because C & D are children of block B:
+	 *
+	 *		[<blockA></blockA>
+	 *		<blockB>
+	 *			<blockC></blockC>
+	 *			<blockD></blockD>
+	 *		</blockB>
+	 *		<blockE></blockE>]
+	 *
+	 * **Note:** To get all selected blocks use {@link #getSelectedBlocks `getSelectedBlocks()`}.
+	 *
+	 * @returns {Iterable.<module:engine/model/element~Element>}
+	 */
+	* getTopMostBlocks() {
+		const selected = Array.from( this.getSelectedBlocks() );
+
+		for ( const block of selected ) {
+			const parentBlock = findAncestorBlock( block );
+
+			// Filter out blocks that are nested in other selected blocks (like paragraphs in tables).
+			if ( !parentBlock || !selected.includes( parentBlock ) ) {
+				yield block;
+			}
+		}
+	}
+
 	/**
 	 * Checks whether the selection contains the entire content of the given element. This means that selection must start
 	 * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
@@ -791,10 +833,25 @@ function isUnvisitedBlockContainer( element, visited ) {
 }
 
 // Finds the lowest element in position's ancestors which is a block.
+// It will search until first ancestor that is a limit element.
 // Marks all ancestors as already visited to not include any of them later on.
 function getParentBlock( position, visited ) {
+	const schema = position.parent.document.model.schema;
+
 	const ancestors = position.parent.getAncestors( { parentFirst: true, includeSelf: true } );
-	const block = ancestors.find( element => isUnvisitedBlockContainer( element, visited ) );
+
+	let hasParentLimit = false;
+
+	const block = ancestors.find( element => {
+		// Stop searching after first parent node that is limit element.
+		if ( hasParentLimit ) {
+			return false;
+		}
+
+		hasParentLimit = schema.isLimit( element );
+
+		return !hasParentLimit && isUnvisitedBlockContainer( element, visited );
+	} );
 
 	// Mark all ancestors of this position's parent, because find() might've stopped early and
 	// the found block may be a child of another block.
@@ -802,3 +859,37 @@ function getParentBlock( position, visited ) {
 
 	return block;
 }
+
+// Returns first ancestor block of a node.
+//
+// @param {module:engine/model/node~Node} node
+// @returns {module:engine/model/node~Node|undefined}
+function findAncestorBlock( node ) {
+	const schema = node.document.model.schema;
+
+	let parent = node.parent;
+
+	while ( parent ) {
+		if ( schema.isBlock( parent ) ) {
+			return parent;
+		}
+
+		parent = parent.parent;
+	}
+}
+
+/**
+ * An entity that is used to set selection.
+ *
+ * See also {@link module:engine/model/selection~Selection#setTo}
+ *
+ * @typedef {
+ *     module:engine/model/selection~Selection|
+ *     module:engine/model/documentselection~DocumentSelection|
+ *     module:engine/model/position~Position|
+ *     module:engine/model/range~Range|
+ *     module:engine/model/node~Node|
+ *     Iterable.<module:engine/model/range~Range>|
+ *     null
+ * } module:engine/model/selection~Selectable
+ */

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -55,7 +55,8 @@ export default class Text extends Node {
 	/**
 	 * Returns a text data contained in the node.
 	 *
-	 * @returns {String}
+	 * @readonly
+	 * @type {String}
 	 */
 	get data() {
 		return this._data;

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

+ 44 - 16
packages/ckeditor5-engine/src/model/utils/deletecontent.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -42,12 +42,31 @@ import DocumentSelection from '../documentselection';
  *
  * * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
  * * `<heading>^</heading>` with enabled (`doNotResetEntireContent == true`).
+ *
+ * @param {Boolean} [options.doNotAutoparagraph=false] Whether to create a paragraph if after content deletion selection is moved
+ * to a place where text cannot be inserted.
+ *
+ * For example `<paragraph>x</paragraph>[<image src="foo.jpg"></image>]` will become:
+ *
+ * * `<paragraph>x</paragraph><paragraph>[]</paragraph>` with the option disabled (`doNotAutoparagraph == false`)
+ * * `<paragraph>x[]</paragraph>` with the option enabled (`doNotAutoparagraph == true`).
+ *
+ * **Note:** if there is no valid position for the selection, the paragraph will always be created:
+ *
+ * `[<image src="foo.jpg"></image>]` -> `<paragraph>[]</paragraph>`.
  */
 export default function deleteContent( model, selection, options = {} ) {
 	if ( selection.isCollapsed ) {
 		return;
 	}
 
+	const selRange = selection.getFirstRange();
+
+	// If the selection is already removed, don't do anything.
+	if ( selRange.root.rootName == '$graveyard' ) {
+		return;
+	}
+
 	const schema = model.schema;
 
 	model.change( writer => {
@@ -59,7 +78,6 @@ export default function deleteContent( model, selection, options = {} ) {
 			return;
 		}
 
-		const selRange = selection.getFirstRange();
 		const startPos = selRange.start;
 		const endPos = LivePosition.fromPosition( selRange.end, 'toNext' );
 
@@ -79,7 +97,7 @@ export default function deleteContent( model, selection, options = {} ) {
 		if ( !options.leaveUnmerged ) {
 			mergeBranches( writer, startPos, endPos );
 
-			// TMP this will be replaced with a postifxer.
+			// TMP this will be replaced with a postfixer.
 			// 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.
 			//
@@ -88,16 +106,20 @@ export default function deleteContent( model, selection, options = {} ) {
 			schema.removeDisallowedAttributes( startPos.parent.getChildren(), writer );
 		}
 
-		if ( selection instanceof DocumentSelection ) {
-			writer.setSelection( startPos );
-		} else {
-			selection.setTo( startPos );
-		}
+		collapseSelectionAt( writer, selection, startPos );
 
-		// 4. Autoparagraphing.
+		// 4. Add a paragraph to set selection in it.
 		// 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 );
+			// If auto-paragraphing is off, find the closest valid selection range and collapse the selection there.
+			// If there is no valid selection range, create paragraph anyway and set selection there.
+			const validSelectionRange = schema.getNearestSelectionRange( startPos );
+
+			if ( options.doNotAutoparagraph && validSelectionRange ) {
+				collapseSelectionAt( writer, selection, validSelectionRange );
+			} else {
+				insertParagraph( writer, startPos, selection );
+			}
 		}
 
 		endPos.detach();
@@ -195,11 +217,7 @@ function insertParagraph( writer, position, selection ) {
 
 	writer.insert( paragraph, position );
 
-	if ( selection instanceof DocumentSelection ) {
-		writer.setSelection( paragraph, 0 );
-	} else {
-		selection.setTo( paragraph, 0 );
-	}
+	collapseSelectionAt( writer, selection, writer.createPositionAt( paragraph, 0 ) );
 }
 
 function replaceEntireContentWithParagraph( writer, selection ) {
@@ -228,3 +246,13 @@ function shouldEntireContentBeReplacedWithParagraph( schema, selection ) {
 
 	return schema.checkChild( limitElement, 'paragraph' );
 }
+
+// Helper function that sets the selection. Depending whether given `selection` is a document selection or not,
+// uses a different method to set it.
+function collapseSelectionAt( writer, selection, positionOrRange ) {
+	if ( selection instanceof DocumentSelection ) {
+		writer.setSelection( positionOrRange );
+	} else {
+		selection.setTo( positionOrRange );
+	}
+}

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -22,6 +22,8 @@ import Selection from '../selection';
  * If an instance of {@link module:engine/model/selection~Selection} is passed as `selectable` it will be modified
  * to the insertion selection (equal to a range to be selected after insertion).
  *
+ * If `selectable` is not passed, the content will be inserted using the current selection of the model document.
+ *
  * **Note:** Use {@link module:engine/model/model~Model#insertContent} instead of this function.
  * This function is only exposed to be reusable in algorithms which change the {@link module:engine/model/model~Model#insertContent}
  * method's behavior.
@@ -29,14 +31,15 @@ import Selection from '../selection';
  * @param {module:engine/model/model~Model} model The model in context of which the insertion
  * should be performed.
  * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
- * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
- * module:engine/model/position~Position|module:engine/model/element~Element|
- * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} [selectable=model.document.selection]
+ * @param {module:engine/model/selection~Selectable} [selectable=model.document.selection]
  * Selection into which the content should be inserted.
  * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
+ * @returns {module:engine/model/range~Range} Range which contains all the performed changes. This is a range that, if removed,
+ * would return the model to the state before the insertion. If no changes were preformed by `insertContent`, returns a range collapsed
+ * at the insertion position.
  */
 export default function insertContent( model, content, selectable, placeOrOffset ) {
-	model.change( writer => {
+	return model.change( writer => {
 		let selection;
 
 		if ( !selectable ) {
@@ -47,11 +50,13 @@ export default function insertContent( model, content, selectable, placeOrOffset
 			selection = writer.createSelection( selectable, placeOrOffset );
 		}
 
+		const insertionPosition = selection.getFirstPosition();
+
 		if ( !selection.isCollapsed ) {
-			model.deleteContent( selection );
+			model.deleteContent( selection, { doNotAutoparagraph: true } );
 		}
 
-		const insertion = new Insertion( model, writer, selection.anchor );
+		const insertion = new Insertion( model, writer, insertionPosition );
 
 		let nodesToInsert;
 
@@ -88,6 +93,12 @@ export default function insertContent( model, content, selectable, placeOrOffset
 			 */
 			log.warn( 'insertcontent-no-range: Cannot determine a proper selection range after insertion.' );
 		}
+
+		const affectedRange = insertion.getAffectedRange() || model.createRange( insertionPosition );
+
+		insertion.destroy();
+
+		return affectedRange;
 	} );
 }
 
@@ -140,6 +151,22 @@ class Insertion {
 		this.schema = model.schema;
 
 		this._filterAttributesOf = [];
+
+		/**
+		 * Beginning of the affected range. See {@link module:engine/model/utils/insertcontent~Insertion#getAffectedRange}.
+		 *
+		 * @private
+		 * @member {module:engine/model/liveposition~LivePosition|null} #_affectedStart
+		 */
+		this._affectedStart = null;
+
+		/**
+		 * End of the affected range. See {@link module:engine/model/utils/insertcontent~Insertion#getAffectedRange}.
+		 *
+		 * @private
+		 * @member {module:engine/model/liveposition~LivePosition|null} #_affectedEnd
+		 */
+		this._affectedEnd = null;
 	}
 
 	/**
@@ -168,7 +195,7 @@ class Insertion {
 
 	/**
 	 * Returns range to be selected after insertion.
-	 * Returns null if there is no valid range to select after insertion.
+	 * Returns `null` if there is no valid range to select after insertion.
 	 *
 	 * @returns {module:engine/model/range~Range|null}
 	 */
@@ -180,6 +207,33 @@ class Insertion {
 		return this.model.schema.getNearestSelectionRange( this.position );
 	}
 
+	/**
+	 * Returns a range which contains all the performed changes. This is a range that, if removed, would return the model to the state
+	 * before the insertion. Returns `null` if no changes were done.
+	 *
+	 * @returns {module:engine/model/range~Range|null}
+	 */
+	getAffectedRange() {
+		if ( !this._affectedStart ) {
+			return null;
+		}
+
+		return new Range( this._affectedStart, this._affectedEnd );
+	}
+
+	/**
+	 * Destroys `Insertion` instance.
+	 */
+	destroy() {
+		if ( this._affectedStart ) {
+			this._affectedStart.detach();
+		}
+
+		if ( this._affectedEnd ) {
+			this._affectedEnd.detach();
+		}
+	}
+
 	/**
 	 * Handles insertion of a single node.
 	 *
@@ -219,10 +273,10 @@ class Insertion {
 		// E.g.:
 		// <p>x^</p> + <p>y</p> => <p>x</p><p>y</p> => <p>xy[]</p>
 		// and:
-		// <p>x^y</p> + <p>z</p> => <p>x</p>^<p>y</p> + <p>z</p> => <p>x</p><p>y</p><p>z</p> => <p>xy[]z</p>
+		// <p>x^y</p> + <p>z</p> => <p>x</p>^<p>y</p> + <p>z</p> => <p>x</p><p>z</p><p>y</p> => <p>xz[]y</p>
 		// but:
 		// <p>x</p><p>^</p><p>z</p> + <p>y</p> => <p>x</p><p>y</p><p>z</p> (no merging)
-		// <p>x</p>[<img>]<p>z</p> + <p>y</p> => <p>x</p><p>y</p><p>z</p> (no merging, note: after running deletetContents
+		// <p>x</p>[<img>]<p>z</p> + <p>y</p> => <p>x</p><p>y</p><p>z</p> (no merging, note: after running deleteContents
 		//																	 it's exactly the same case as above)
 		this._mergeSiblingsOf( node, context );
 	}
@@ -278,6 +332,7 @@ class Insertion {
 
 		const livePos = LivePosition.fromPosition( this.position, 'toNext' );
 
+		this._setAffectedBoundaries( this.position );
 		this.writer.insert( node, this.position );
 
 		this.position = livePos.toPosition();
@@ -293,6 +348,37 @@ class Insertion {
 		this._filterAttributesOf.push( node );
 	}
 
+	/**
+	 * Sets `_affectedStart` and `_affectedEnd` to the given `position`. Should be used before a change is done during insertion process to
+	 * mark the affected range.
+	 *
+	 * This method is used before inserting a node or splitting a parent node. `_affectedStart` and `_affectedEnd` are also changed
+	 * during merging, but the logic there is more complicated so it is left out of this function.
+	 *
+	 * @private
+	 * @param {module:engine/model/position~Position} position
+	 */
+	_setAffectedBoundaries( position ) {
+		// Set affected boundaries stickiness so that those position will "expand" when something is inserted in between them:
+		// <paragraph>Foo][bar</paragraph> -> <paragraph>Foo]xx[bar</paragraph>
+		// This is why it cannot be a range but two separate positions.
+		if ( !this._affectedStart ) {
+			this._affectedStart = LivePosition.fromPosition( position, 'toPrevious' );
+		}
+
+		// If `_affectedEnd` is before the new boundary position, expand `_affectedEnd`. This can happen if first inserted node was
+		// inserted into the parent but the next node is moved-out of that parent:
+		// (1) <paragraph>Foo][</paragraph> -> <paragraph>Foo]xx[</paragraph>
+		// (2) <paragraph>Foo]xx[</paragraph> -> <paragraph>Foo]xx</paragraph><widget></widget>[
+		if ( !this._affectedEnd || this._affectedEnd.isBefore( position ) ) {
+			if ( this._affectedEnd ) {
+				this._affectedEnd.detach();
+			}
+
+			this._affectedEnd = LivePosition.fromPosition( position, 'toNext' );
+		}
+	}
+
 	/**
 	 * @private
 	 * @param {module:engine/model/node~Node} node The node which could potentially be merged.
@@ -314,8 +400,38 @@ class Insertion {
 			const livePosition = LivePosition.fromPosition( this.position );
 			livePosition.stickiness = 'toNext';
 
+			// If `_affectedStart` is sames as merge position, it means that the element "marked" by `_affectedStart` is going to be
+			// removed and its contents will be moved. This won't transform `LivePosition` so `_affectedStart` needs to be moved
+			// by hand to properly reflect affected range. (Due to `_affectedStart` and `_affectedEnd` stickiness, the "range" is
+			// shown as `][`).
+			//
+			// Example - insert `<paragraph>Abc</paragraph><paragraph>Xyz</paragraph>` at the end of `<paragraph>Foo^</paragraph>`:
+			//
+			// <paragraph>Foo</paragraph><paragraph>Bar</paragraph>   -->
+			// <paragraph>Foo</paragraph>]<paragraph>Abc</paragraph><paragraph>Xyz</paragraph>[<paragraph>Bar</paragraph>   -->
+			// <paragraph>Foo]Abc</paragraph><paragraph>Xyz</paragraph>[<paragraph>Bar</paragraph>
+			//
+			// Note, that if we are here then something must have been inserted, so `_affectedStart` and `_affectedEnd` have to be set.
+			if ( this._affectedStart.isEqual( mergePosLeft ) ) {
+				this._affectedStart.detach();
+				this._affectedStart = LivePosition._createAt( mergePosLeft.nodeBefore, 'end', 'toPrevious' );
+			}
+
 			this.writer.merge( mergePosLeft );
 
+			// If only one element (the merged one) is in the "affected range", also move the affected range end appropriately.
+			//
+			// Example - insert `<paragraph>Abc</paragraph>` at the of `<paragraph>Foo^</paragraph>`:
+			//
+			// <paragraph>Foo</paragraph><paragraph>Bar</paragraph>   -->
+			// <paragraph>Foo</paragraph>]<paragraph>Abc</paragraph>[<paragraph>Bar</paragraph>   -->
+			// <paragraph>Foo]Abc</paragraph>[<paragraph>Bar</paragraph>   -->
+			// <paragraph>Foo]Abc[</paragraph><paragraph>Bar</paragraph>
+			if ( mergePosLeft.isEqual( this._affectedEnd ) && context.isLast ) {
+				this._affectedEnd.detach();
+				this._affectedEnd = LivePosition._createAt( mergePosLeft.nodeBefore, 'end', 'toNext' );
+			}
+
 			this.position = livePosition.toPosition();
 			livePosition.detach();
 		}
@@ -335,10 +451,22 @@ class Insertion {
 
 			// OK:  <p>xx[]</p> + <p>yy</p> => <p>xx[]yy</p> (when sticks to previous)
 			// NOK: <p>xx[]</p> + <p>yy</p> => <p>xxyy[]</p> (when sticks to next)
-			const livePosition = new LivePosition( this.position.root, this.position.path, 'toPrevious' );
+			const livePosition = LivePosition.fromPosition( this.position, 'toPrevious' );
+
+			// See comment above on moving `_affectedStart`.
+			if ( this._affectedEnd.isEqual( mergePosRight ) ) {
+				this._affectedEnd.detach();
+				this._affectedEnd = LivePosition._createAt( mergePosRight.nodeBefore, 'end', 'toNext' );
+			}
 
 			this.writer.merge( mergePosRight );
 
+			// See comment above on moving `_affectedStart`.
+			if ( mergePosRight.getShiftedBy( -1 ).isEqual( this._affectedStart ) && context.isFirst ) {
+				this._affectedStart.detach();
+				this._affectedStart = LivePosition._createAt( mergePosRight.nodeBefore, 0, 'toPrevious' );
+			}
+
 			this.position = livePosition.toPosition();
 			livePosition.detach();
 		}
@@ -426,19 +554,25 @@ class Insertion {
 			}
 
 			if ( this.position.isAtStart ) {
+				// If insertion position is at the beginning of the parent, move it out instead of splitting.
+				// <p>^Foo</p> -> ^<p>Foo</p>
 				const parent = this.position.parent;
+
 				this.position = this.writer.createPositionBefore( parent );
 
-				// Special case – parent is empty (<p>^</p>) so isAtStart == isAtEnd == true.
-				// We can remove the element after moving selection out of it.
+				// Special case – parent is empty (<p>^</p>).
+				// We can remove the element after moving insertion position out of it.
 				if ( parent.isEmpty ) {
 					this.writer.remove( parent );
 				}
 			} else if ( this.position.isAtEnd ) {
+				// If insertion position is at the end of the parent, move it out instead of splitting.
+				// <p>Foo^</p> -> <p>Foo</p>^
 				this.position = this.writer.createPositionAfter( this.position.parent );
 			} else {
 				const tempPos = this.writer.createPositionAfter( this.position.parent );
 
+				this._setAffectedBoundaries( this.position );
 				this.writer.split( this.position );
 
 				this.position = tempPos;

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

+ 46 - 28
packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -187,11 +187,19 @@ function tryFixingNonCollapsedRage( range, schema ) {
 		// - [<block>foo</block>]    ->    <block>[foo]</block>
 		// - [<block>foo]</block>    ->    <block>[foo]</block>
 		// - <block>f[oo</block>]    ->    <block>f[oo]</block>
+		// - [<block>foo</block><object></object>]    ->    <block>[foo</block><object></object>]
 		if ( checkSelectionOnNonLimitElements( start, end, schema ) ) {
-			const fixedStart = schema.getNearestSelectionRange( start, 'forward' );
-			const fixedEnd = schema.getNearestSelectionRange( end, 'backward' );
+			const isStartObject = start.nodeAfter && schema.isObject( start.nodeAfter );
+			const fixedStart = isStartObject ? null : schema.getNearestSelectionRange( start, 'forward' );
 
-			return new Range( fixedStart ? fixedStart.start : start, fixedEnd ? fixedEnd.start : end );
+			const isEndObject = end.nodeBefore && schema.isObject( end.nodeBefore );
+			const fixedEnd = isEndObject ? null : schema.getNearestSelectionRange( end, 'backward' );
+
+			// The schema.getNearestSelectionRange might return null - if that happens use original position.
+			const rangeStart = fixedStart ? fixedStart.start : start;
+			const rangeEnd = fixedEnd ? fixedEnd.start : end;
+
+			return new Range( rangeStart, rangeEnd );
 		}
 	}
 
@@ -201,21 +209,23 @@ function tryFixingNonCollapsedRage( range, schema ) {
 	// At this point we eliminated valid positions on text nodes so if one of range positions is placed inside a limit element
 	// then the range crossed limit element boundaries and needs to be fixed.
 	if ( isStartInLimit || isEndInLimit ) {
-		const isStartObject = start.nodeAfter && schema.isObject( start.nodeAfter );
-		const isEndObject = end.nodeBefore && schema.isObject( end.nodeBefore );
-
-		const bothInSameParent = ( !!start.nodeAfter && !!end.nodeBefore ) && start.nodeAfter === end.nodeBefore;
+		const bothInSameParent = ( start.nodeAfter && end.nodeBefore ) && start.nodeAfter.parent === end.nodeBefore.parent;
 
-		const expandStart = isStartInLimit && ( !bothInSameParent || !isStartObject );
-		const expandEnd = isEndInLimit && ( !bothInSameParent || !isEndObject );
+		const expandStart = isStartInLimit && ( !bothInSameParent || !isInObject( start.nodeAfter, schema ) );
+		const expandEnd = isEndInLimit && ( !bothInSameParent || !isInObject( end.nodeBefore, schema ) );
 
 		// Although we've already found limit element on start/end positions we must find the outer-most limit element.
 		// as limit elements might be nested directly inside (ie table > tableRow > tableCell).
-		const startPosition = Position._createAt( startLimitElement, 0 );
-		const endPosition = Position._createAt( endLimitElement, 0 );
+		let fixedStart = start;
+		let fixedEnd = end;
 
-		const fixedStart = expandStart ? expandSelectionOnIsLimitNode( startPosition, schema, 'start' ) : start;
-		const fixedEnd = expandEnd ? expandSelectionOnIsLimitNode( endPosition, schema, 'end' ) : end;
+		if ( expandStart ) {
+			fixedStart = Position._createBefore( findOutermostLimitAncestor( startLimitElement, schema ) );
+		}
+
+		if ( expandEnd ) {
+			fixedEnd = Position._createAfter( findOutermostLimitAncestor( endLimitElement, schema ) );
+		}
 
 		return new Range( fixedStart, fixedEnd );
 	}
@@ -224,40 +234,39 @@ function tryFixingNonCollapsedRage( range, schema ) {
 	return null;
 }
 
-// Expands selection so it contains whole limit node.
+// Finds the outer-most ancestor.
 //
-// @param {module:engine/model/position~Position} position
+// @param {module:engine/model/node~Node} startingNode
 // @param {module:engine/model/schema~Schema} schema
 // @param {String} expandToDirection Direction of expansion - either 'start' or 'end' of the range.
-// @returns {module:engine/model/position~Position}
-function expandSelectionOnIsLimitNode( position, schema, expandToDirection ) {
-	let node = position.parent;
-	let parent = node;
+// @returns {module:engine/model/node~Node}
+function findOutermostLimitAncestor( startingNode, schema ) {
+	let isLimitNode = startingNode;
+	let parent = isLimitNode;
 
 	// Find outer most isLimit block as such blocks might be nested (ie. in tables).
 	while ( schema.isLimit( parent ) && parent.parent ) {
-		node = parent;
+		isLimitNode = parent;
 		parent = parent.parent;
 	}
 
-	// Depending on direction of expanding selection return position before or after found node.
-	return expandToDirection === 'start' ? Position._createBefore( node ) : Position._createAfter( node );
+	return isLimitNode;
 }
 
-// Checks whether both range ends are placed around non-limit elements.
+// Checks whether any of range boundaries is placed around non-limit elements.
 //
 // @param {module:engine/model/position~Position} start
 // @param {module:engine/model/position~Position} end
 // @param {module:engine/model/schema~Schema} schema
+// @returns {Boolean}
 function checkSelectionOnNonLimitElements( start, end, schema ) {
 	const startIsOnBlock = ( start.nodeAfter && !schema.isLimit( start.nodeAfter ) ) || schema.checkChild( start, '$text' );
 	const endIsOnBlock = ( end.nodeBefore && !schema.isLimit( end.nodeBefore ) ) || schema.checkChild( end, '$text' );
 
-	return startIsOnBlock && endIsOnBlock;
+	// We should fix such selection when one of those nodes needs fixing.
+	return startIsOnBlock || endIsOnBlock;
 }
 
-// Merges intersecting ranges to make ranges safe for setting in selection.
-//
 // @param {Array.<module:engine/model/range~Range>} ranges
 // @returns {Array.<module:engine/model/range~Range>}
 function mergeIntersectingRanges( ranges ) {
@@ -282,3 +291,12 @@ function mergeIntersectingRanges( ranges ) {
 
 	return deIntersected;
 }
+
+// Checks if node exists and if it's an object.
+//
+// @param {module:engine/model/node~Node} node
+// @param {module:engine/model/schema~Schema} schema
+// @returns {Boolean}
+function isInObject( node, schema ) {
+	return node && schema.isObject( node );
+}

+ 134 - 47
packages/ckeditor5-engine/src/model/writer.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -159,6 +159,10 @@ export default class Writer {
 	insert( item, itemOrPosition, offset = 0 ) {
 		this._assertWriterUsedCorrectly();
 
+		if ( item instanceof Text && item.data == '' ) {
+			return;
+		}
+
 		const position = Position._createAt( itemOrPosition, offset );
 
 		// If item has a parent already.
@@ -466,6 +470,14 @@ export default class Writer {
 
 		const position = Position._createAt( itemOrPosition, offset );
 
+		// Do not move anything if the move target is same as moved range start.
+		if ( position.isEqual( range.start ) ) {
+			return;
+		}
+
+		// If part of the marker is removed, create additional marker operation for undo purposes.
+		this._addOperationForAffectedMarkers( 'move', range );
+
 		if ( !isSameTree( range.root, position.root ) ) {
 			/**
 			 * Range is going to be moved within not the same document. Please use
@@ -491,17 +503,14 @@ export default class Writer {
 	remove( itemOrRange ) {
 		this._assertWriterUsedCorrectly();
 
-		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();
+		const rangeToRemove = itemOrRange instanceof Range ? itemOrRange : Range._createOn( itemOrRange );
+		const ranges = rangeToRemove.getMinimalFlatRanges().reverse();
 
-			for ( const flat of ranges ) {
-				applyRemoveOperation( flat.start, flat.end.offset - flat.start.offset, this.batch, this.model );
-			}
-		} else {
-			const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1;
+		for ( const flat of ranges ) {
+			// If part of the marker is removed, create additional marker operation for undo purposes.
+			this._addOperationForAffectedMarkers( 'move', flat );
 
-			applyRemoveOperation( Position._createBefore( itemOrRange ), howMany, this.batch, this.model );
+			applyRemoveOperation( flat.start, flat.end.offset - flat.start.offset, this.batch, this.model );
 		}
 	}
 
@@ -519,6 +528,9 @@ export default class Writer {
 		const nodeBefore = position.nodeBefore;
 		const nodeAfter = position.nodeAfter;
 
+		// If part of the marker is removed, create additional marker operation for undo purposes.
+		this._addOperationForAffectedMarkers( 'merge', position );
+
 		if ( !( nodeBefore instanceof Element ) ) {
 			/**
 			 * Node before merge position must be an element.
@@ -623,9 +635,7 @@ export default class Writer {
 	/**
 	 * Shortcut for {@link module:engine/model/model~Model#createSelection `Model#createSelection()`}.
 	 *
-	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
-	 * module:engine/model/position~Position|module:engine/model/element~Element|
-	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
+	 * @param {module:engine/model/selection~Selectable} 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.
@@ -707,8 +717,8 @@ export default class Writer {
 	 * @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
+	 * @returns {module:engine/model/position~Position} result.position Position between split elements.
+	 * @returns {module:engine/model/range~Range} result.range Range that stars from the end of the first split element and ends
 	 * at the beginning of the first copy element.
 	 */
 	split( position, limitElement ) {
@@ -804,22 +814,12 @@ export default class Writer {
 			throw new CKEditorError( 'writer-wrap-element-attached: Element to wrap with is already attached to tree model.' );
 		}
 
-		const version = range.root.document ? range.root.document.version : null;
+		this.insert( element, range.start );
 
-		// Has to be `range.start` not `range.end` for better transformations.
-		const insert = new InsertOperation( range.start, element, version );
-		this.batch.addOperation( insert );
-		this.model.applyOperation( insert );
+		// Shift the range-to-wrap because we just inserted an element before that range.
+		const shiftedRange = new Range( range.start.getShiftedBy( 1 ), range.end.getShiftedBy( 1 ) );
 
-		const move = new MoveOperation(
-			range.start.getShiftedBy( 1 ),
-			range.end.offset - range.start.offset,
-			Position._createAt( element, 0 ),
-			version === null ? null : version + 1
-		);
-
-		this.batch.addOperation( move );
-		this.model.applyOperation( move );
+		this.move( shiftedRange, Position._createAt( element, 0 ) );
 	}
 
 	/**
@@ -888,12 +888,12 @@ export default class Writer {
 
 		if ( !options || typeof options.usingOperation != 'boolean' ) {
 			/**
-			 * The `options.usingOperations` parameter is required when adding a new marker.
+			 * The `options.usingOperation` parameter is required when adding a new marker.
 			 *
-			 * @error writer-addMarker-no-usingOperations
+			 * @error writer-addMarker-no-usingOperation
 			 */
 			throw new CKEditorError(
-				'writer-addMarker-no-usingOperations: The options.usingOperations parameter is required when adding a new marker.'
+				'writer-addMarker-no-usingOperation: The options.usingOperation parameter is required when adding a new marker.'
 			);
 		}
 
@@ -929,13 +929,38 @@ export default class Writer {
 	}
 
 	/**
-	 * Adds or updates a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
+	 * Adds, updates or refreshes 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.
 	 *
+	 * As the second parameter you can set the new marker data or leave this parameter as empty which will just refresh
+	 * the marker by triggering downcast conversion for it. Refreshing the marker is useful when you want to change
+	 * the marker {@link module:engine/view/element~Element view element} without changing any marker data.
+	 *
+	 * 		let isCommentActive = false;
+	 *
+	 * 		model.conversion.markerToHighlight( {
+	 * 			model: 'comment',
+	 *			view: data => {
+	 *				const classes = [ 'comment-marker' ];
+	 *
+	 *				if ( isCommentActive ) {
+	 *					classes.push( 'comment-marker--active' );
+	 *				}
+	 *
+	 *				return { classes };
+	 *			}
+	 * 		} );
+	 *
+	 * 		// Change the property that indicates if marker is displayed as active or not.
+	 * 		isCommentActive = true;
+	 *
+	 * 		// And refresh the marker to convert it with additional class.
+	 * 		model.change( writer => writer.updateMarker( 'comment' ) );
+	 *
 	 * The `options.usingOperation` parameter lets you change 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.
@@ -965,13 +990,14 @@ export default class Writer {
 	 *
 	 * @see module:engine/model/markercollection~Marker
 	 * @param {String} markerOrName Name of a marker to update, or a marker instance.
-	 * @param {Object} options
+	 * @param {Object} [options] If options object is not defined then marker will be refreshed by triggering
+	 * downcast conversion for this marker with the same data.
 	 * @param {module:engine/model/range~Range} [options.range] Marker range to update.
 	 * @param {Boolean} [options.usingOperation] Flag indicated whether the marker should be added by MarkerOperation.
 	 * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
 	 * @param {Boolean} [options.affectsData] Flag indicating that the marker changes the editor data.
 	 */
-	updateMarker( markerOrName, options = {} ) {
+	updateMarker( markerOrName, options ) {
 		this._assertWriterUsedCorrectly();
 
 		const markerName = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
@@ -986,6 +1012,12 @@ export default class Writer {
 			throw new CKEditorError( 'writer-updateMarker-marker-not-exists: Marker with provided name does not exists.' );
 		}
 
+		if ( !options ) {
+			this.model.markers._refresh( currentMarker );
+
+			return;
+		}
+
 		const hasUsingOperationDefined = typeof options.usingOperation == 'boolean';
 		const affectsDataDefined = typeof options.affectsData == 'boolean';
 
@@ -1067,14 +1099,8 @@ export default class Writer {
 	}
 
 	/**
-	 * Sets the document's selection (ranges and direction) to the specified location based on:
-	 *
-	 * * the given {@link module:engine/model/selection~Selection selection},
-	 * * or the given {@link module:engine/model/position~Position position},
-	 * * or the given {@link module:engine/model/range~Range range},
-	 * * or the given iterable of {@link module:engine/model/range~Range ranges},
-	 * * or the given {@link module:engine/model/node~Node node},
-	 * * or `null`.
+	 * Sets the document's selection (ranges and direction) to the specified location based on the given
+	 * {@link module:engine/model/selection~Selectable selectable} or creates an empty selection if no arguments were passed.
 	 *
 	 *		// Sets selection to the given range.
 	 *		const range = writer.createRange( start, end );
@@ -1118,9 +1144,7 @@ export default class Writer {
 	 *
 	 * 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 {module:engine/model/selection~Selectable} 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.
@@ -1294,6 +1318,69 @@ export default class Writer {
 			throw new CKEditorError( 'writer-incorrect-use: Trying to use a writer outside the change() block.' );
 		}
 	}
+
+	/**
+	 * For given action `type` and `positionOrRange` where the action happens, this function finds all affected markers
+	 * and applies a marker operation with the new marker range equal to the current range. Thanks to this, the marker range
+	 * can be later correctly processed during undo.
+	 *
+	 * @private
+	 * @param {'move'|'merge'} type Writer action type.
+	 * @param {module:engine/model/position~Position|module:engine/model/range~Range} positionOrRange Position or range
+	 * where the writer action happens.
+	 */
+	_addOperationForAffectedMarkers( type, positionOrRange ) {
+		for ( const marker of this.model.markers ) {
+			if ( !marker.managedUsingOperations ) {
+				continue;
+			}
+
+			const markerRange = marker.getRange();
+			let isAffected = false;
+
+			if ( type == 'move' ) {
+				isAffected =
+					positionOrRange.containsPosition( markerRange.start ) ||
+					positionOrRange.start.isEqual( markerRange.start ) ||
+					positionOrRange.containsPosition( markerRange.end ) ||
+					positionOrRange.end.isEqual( markerRange.end );
+			} else {
+				// if type == 'merge'.
+				const elementBefore = positionOrRange.nodeBefore;
+				const elementAfter = positionOrRange.nodeAfter;
+
+				//               Start:  <p>Foo[</p><p>Bar]</p>
+				//         After merge:  <p>Foo[Bar]</p>
+				// After undoing split:  <p>Foo</p><p>[Bar]</p>     <-- incorrect, needs remembering for undo.
+				//
+				const affectedInLeftElement = markerRange.start.parent == elementBefore && markerRange.start.isAtEnd;
+
+				//               Start:  <p>[Foo</p><p>]Bar</p>
+				//         After merge:  <p>[Foo]Bar</p>
+				// After undoing split:  <p>[Foo]</p><p>Bar</p>     <-- incorrect, needs remembering for undo.
+				//
+				const affectedInRightElement = markerRange.end.parent == elementAfter && markerRange.end.offset == 0;
+
+				//               Start:  <p>[Foo</p>]<p>Bar</p>
+				//         After merge:  <p>[Foo]Bar</p>
+				// After undoing split:  <p>[Foo]</p><p>Bar</p>     <-- incorrect, needs remembering for undo.
+				//
+				const affectedAfterLeftElement = markerRange.end.nodeAfter == elementAfter;
+
+				//               Start:  <p>Foo</p>[<p>Bar]</p>
+				//         After merge:  <p>Foo[Bar]</p>
+				// After undoing split:  <p>Foo</p><p>[Bar]</p>     <-- incorrect, needs remembering for undo.
+				//
+				const affectedBeforeRightElement = markerRange.start.nodeAfter == elementAfter;
+
+				isAffected = affectedInLeftElement || affectedInRightElement || affectedAfterLeftElement || affectedBeforeRightElement;
+			}
+
+			if ( isAffected ) {
+				this.updateMarker( marker.name, { range: markerRange } );
+			}
+		}
+	}
 }
 
 // Sets given attribute to each node in given range. When attribute value is null then attribute will be removed.

+ 2 - 2
packages/ckeditor5-engine/src/utils/bindtwostepcarettoattribute.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

+ 5 - 5
packages/ckeditor5-engine/src/view/attributeelement.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -69,7 +69,7 @@ export default class AttributeElement extends Element {
 		 * This property is managed by {@link module:engine/view/downcastwriter~DowncastWriter}.
 		 *
 		 * @protected
-		 * @member {Set|null}
+		 * @member {Set.<module:engine/view/attributeelement~AttributeElement>|null}
 		 */
 		this._clonesGroup = null;
 	}
@@ -78,7 +78,7 @@ export default class AttributeElement extends Element {
 	 * Element priority. Decides in what order elements are wrapped by {@link module:engine/view/downcastwriter~DowncastWriter}.
 	 *
 	 * @readonly
-	 * @returns {Number}
+	 * @type {Number}
 	 */
 	get priority() {
 		return this._priority;
@@ -89,7 +89,7 @@ export default class AttributeElement extends Element {
 	 * and then two elements are considered similar if, and only if they have the same `id`.
 	 *
 	 * @readonly
-	 * @returns {String|Number}
+	 * @type {String|Number}
 	 */
 	get id() {
 		return this._id;

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -40,7 +40,7 @@ export default class Document {
 		 * model root using {@link module:engine/model/document~Document#createRoot}.
 		 *
 		 * @readonly
-		 * @member {Collection} module:engine/view/document~Document#roots
+		 * @member {module:utils/collection~Collection} module:engine/view/document~Document#roots
 		 */
 		this.roots = new Collection( { idProperty: 'rootName' } );
 
@@ -115,12 +115,23 @@ export default class Document {
 	 * As a parameter, a post-fixer callback receives a {@link module:engine/view/downcastwriter~DowncastWriter downcast writer}
 	 * instance connected with the executed changes block.
 	 *
+	 * Note that registering a post-fixer won't re-render the editor's view. If the view should change after registering the post-fixer then
+	 * it should be done manually calling `view.forceRender();`.
+	 *
 	 * @param {Function} postFixer
 	 */
 	registerPostFixer( postFixer ) {
 		this._postFixers.add( postFixer );
 	}
 
+	/**
+	 * Destroys this instance. Makes sure that all observers are destroyed and listeners removed.
+	 */
+	destroy() {
+		this.roots.map( root => root.destroy() );
+		this.stopListening();
+	}
+
 	/**
 	 * Performs post-fixer loops. Executes post-fixer callbacks as long as none of them has done any changes to the model.
 	 *

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -71,9 +71,7 @@ export default class DocumentSelection {
 	 *		// Creates fake selection with label.
 	 *		const selection = new DocumentSelection( range, { fake: true, label: 'foo' } );
 	 *
-	 * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|
-	 * Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|
-	 * module:engine/view/item~Item|null} [selectable=null]
+	 * @param {module:engine/view/selection~Selectable} [selectable=null]
 	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
@@ -276,12 +274,27 @@ export default class DocumentSelection {
 		return this._selection.isSimilar( otherSelection );
 	}
 
+	/**
+	 * Checks whether object is of given type following the convention set by
+	 * {@link module:engine/view/node~Node#is `Node#is()`}.
+	 *
+	 *		const selection = new DocumentSelection( ... );
+	 *
+	 *		selection.is( 'selection' ); // true
+	 *		selection.is( 'documentSelection' ); // true
+	 *		selection.is( 'node' ); // false
+	 *		selection.is( 'element' ); // false
+	 *
+	 * @param {String} type
+	 * @returns {Boolean}
+	 */
+	is( type ) {
+		return type == 'selection' || type == 'documentSelection';
+	}
+
 	/**
 	 * Sets this selection's ranges and direction to the specified location based on the given
-	 * {@link module:engine/view/documentselection~DocumentSelection document selection},
-	 * {@link module:engine/view/selection~Selection selection}, {@link module:engine/view/position~Position position},
-	 * {@link module:engine/view/item~Item item}, {@link module:engine/view/range~Range range},
-	 * an iterable of {@link module:engine/view/range~Range ranges} or null.
+	 * {@link module:engine/view/selection~Selectable selectable}.
 	 *
 	 *		// Sets selection to the given range.
 	 *		const range = writer.createRange( start, end );
@@ -331,8 +344,7 @@ export default class DocumentSelection {
 	 *
 	 * @protected
 	 * @fires change
-	 * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|
-	 * Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|module:engine/view/item~Item|null} selectable
+	 * @param {module:engine/view/selection~Selectable} 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.

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -29,6 +29,8 @@ import { isElement } from 'lodash-es';
  * DomConverter is a set of tools to do transformations between DOM nodes and view nodes. It also handles
  * {@link module:engine/view/domconverter~DomConverter#bindElements binding} these nodes.
  *
+ * The instance of DOMConverter is available in {@link module:engine/view/view~View#domConverter `editor.editing.view.domConverter`}.
+ *
  * DomConverter does not check which nodes should be rendered (use {@link module:engine/view/renderer~Renderer}), does not keep a
  * state of a tree nor keeps synchronization between tree view and DOM tree (use {@link module:engine/view/document~Document}).
  *

+ 83 - 95
packages/ckeditor5-engine/src/view/downcastwriter.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -56,10 +56,7 @@ export default class DowncastWriter {
 
 	/**
 	 * Sets {@link module:engine/view/documentselection~DocumentSelection selection's} ranges and direction to the
-	 * specified location based on the given {@link module:engine/view/documentselection~DocumentSelection document selection},
-	 * {@link module:engine/view/selection~Selection selection}, {@link module:engine/view/position~Position position},
-	 * {@link module:engine/view/item~Item item}, {@link module:engine/view/range~Range range},
-	 * an iterable of {@link module:engine/view/range~Range ranges} or null.
+	 * specified location based on the given {@link module:engine/view/selection~Selectable selectable}.
 	 *
 	 * Usage:
 	 *
@@ -114,8 +111,7 @@ export default class DowncastWriter {
 	 * 		// (and be  properly handled by screen readers).
 	 *		writer.setSelection( range, { fake: true, label: 'foo' } );
 	 *
-	 * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|
-	 * Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|module:engine/view/item~Item|null} selectable
+	 * @param {module:engine/view/selection~Selectable} 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.
@@ -870,27 +866,6 @@ export default class DowncastWriter {
 
 		// Break attributes at range start and end.
 		const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
-
-		// Range around one element - check if AttributeElement can be unwrapped partially when it's not similar.
-		// For example:
-		// <b class="foo bar" title="baz"></b> unwrap with:	<b class="foo"></p> result: <b class"bar" title="baz"></b>
-		if ( breakEnd.isEqual( breakStart.getShiftedBy( 1 ) ) ) {
-			const node = breakStart.nodeAfter;
-
-			// Unwrap single attribute element.
-			if ( !attribute.isSimilar( node ) && node instanceof AttributeElement && this._unwrapAttributeElement( attribute, node ) ) {
-				const start = this.mergeAttributes( breakStart );
-
-				if ( !start.isEqual( breakStart ) ) {
-					breakEnd.offset--;
-				}
-
-				const end = this.mergeAttributes( breakEnd );
-
-				return new Range( start, end );
-			}
-		}
-
 		const parentContainer = breakStart.parent;
 
 		// Unwrap children located between break points.
@@ -1076,9 +1051,7 @@ export default class DowncastWriter {
 	 *		// Creates fake selection with label.
 	 *		const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
 	 *
-	 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection|
-	 * module:engine/view/position~Position|Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|
-	 * module:engine/view/item~Item|null} [selectable=null]
+	 * @param {module:engine/view/selection~Selectable} [selectable=null]
 	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
@@ -1091,16 +1064,16 @@ export default class DowncastWriter {
 	}
 
 	/**
-	 * Wraps children with provided `attribute`. Only children contained in `parent` element between
+	 * Wraps children with provided `wrapElement`. Only children contained in `parent` element between
 	 * `startOffset` and `endOffset` will be wrapped.
 	 *
 	 * @private
 	 * @param {module:engine/view/element~Element} parent
 	 * @param {Number} startOffset
 	 * @param {Number} endOffset
-	 * @param {module:engine/view/element~Element} attribute
+	 * @param {module:engine/view/element~Element} wrapElement
 	 */
-	_wrapChildren( parent, startOffset, endOffset, attribute ) {
+	_wrapChildren( parent, startOffset, endOffset, wrapElement ) {
 		let i = startOffset;
 		const wrapPositions = [];
 
@@ -1111,10 +1084,27 @@ export default class DowncastWriter {
 			const isEmpty = child.is( 'emptyElement' );
 			const isUI = child.is( 'uiElement' );
 
-			// Wrap text, empty elements, ui elements or attributes with higher or equal priority.
-			if ( isText || isEmpty || isUI || ( isAttribute && shouldABeOutsideB( attribute, child ) ) ) {
+			//
+			// (In all examples, assume that `wrapElement` is `<span class="foo">` element.)
+			//
+			// Check if `wrapElement` can be joined with the wrapped element. One of requirements is having same name.
+			// If possible, join elements.
+			//
+			// <p><span class="bar">abc</span></p>  -->  <p><span class="foo bar">abc</span></p>
+			//
+			if ( isAttribute && this._wrapAttributeElement( wrapElement, child ) ) {
+				wrapPositions.push(	new Position( parent, i ) );
+			}
+			//
+			// Wrap the child if it is not an attribute element or if it is an attribute element that should be inside
+			// `wrapElement` (due to priority).
+			//
+			// <p>abc</p>                   -->  <p><span class="foo">abc</span></p>
+			// <p><strong>abc</strong></p>  -->  <p><span class="foo"><strong>abc</strong></span></p>
+			//
+			else if ( isText || isEmpty || isUI || ( isAttribute && shouldABeOutsideB( wrapElement, child ) ) ) {
 				// Clone attribute.
-				const newAttribute = attribute._clone();
+				const newAttribute = wrapElement._clone();
 
 				// Wrap current node with new attribute.
 				child._remove();
@@ -1125,9 +1115,13 @@ export default class DowncastWriter {
 
 				wrapPositions.push(	new Position( parent, i ) );
 			}
-			// If other nested attribute is found start wrapping there.
+			//
+			// If other nested attribute is found and it wasn't wrapped (see above), continue wrapping inside it.
+			//
+			// <p><a href="foo.html">abc</a></p>  -->  <p><a href="foo.html"><span class="foo">abc</span></a></p>
+			//
 			else if ( isAttribute ) {
-				this._wrapChildren( child, 0, child.childCount, attribute );
+				this._wrapChildren( child, 0, child.childCount, wrapElement );
 			}
 
 			i++;
@@ -1157,25 +1151,40 @@ export default class DowncastWriter {
 	}
 
 	/**
-	 * Unwraps children from provided `attribute`. Only children contained in `parent` element between
+	 * Unwraps children from provided `unwrapElement`. Only children contained in `parent` element between
 	 * `startOffset` and `endOffset` will be unwrapped.
 	 *
 	 * @private
 	 * @param {module:engine/view/element~Element} parent
 	 * @param {Number} startOffset
 	 * @param {Number} endOffset
-	 * @param {module:engine/view/element~Element} attribute
+	 * @param {module:engine/view/element~Element} unwrapElement
 	 */
-	_unwrapChildren( parent, startOffset, endOffset, attribute ) {
+	_unwrapChildren( parent, startOffset, endOffset, unwrapElement ) {
 		let i = startOffset;
 		const unwrapPositions = [];
 
 		// Iterate over each element between provided offsets inside parent.
+		// We don't use tree walker or range iterator because we will be removing and merging potentially multiple nodes,
+		// so it could get messy. It is safer to it manually in this case.
 		while ( i < endOffset ) {
 			const child = parent.getChild( i );
 
-			// If attributes are the similar, then unwrap.
-			if ( child.isSimilar( attribute ) ) {
+			// Skip all text nodes. There should be no container element's here either.
+			if ( !child.is( 'attributeElement' ) ) {
+				i++;
+
+				continue;
+			}
+
+			//
+			// (In all examples, assume that `unwrapElement` is `<span class="foo">` element.)
+			//
+			// If the child is similar to the given attribute element, unwrap it - it will be completely removed.
+			//
+			// <p><span class="foo">abc</span>xyz</p>  -->  <p>abcxyz</p>
+			//
+			if ( child.isSimilar( unwrapElement ) ) {
 				const unwrapped = child.getChildren();
 				const count = child.childCount;
 
@@ -1191,18 +1200,39 @@ export default class DowncastWriter {
 					new Position( parent, i + count )
 				);
 
-				// Skip elements that were unwrapped. Assuming that there won't be another element to unwrap in child
-				// elements.
+				// Skip elements that were unwrapped. Assuming there won't be another element to unwrap in child elements.
 				i += count;
 				endOffset += count - 1;
-			} else {
-				// If other nested attribute is found start unwrapping there.
-				if ( child.is( 'attributeElement' ) ) {
-					this._unwrapChildren( child, 0, child.childCount, attribute );
-				}
+
+				continue;
+			}
+
+			//
+			// If the child is not similar but is an attribute element, try partial unwrapping - remove the same attributes/styles/classes.
+			// Partial unwrapping will happen only if the elements have the same name.
+			//
+			// <p><span class="foo bar">abc</span>xyz</p>  -->  <p><span class="bar">abc</span>xyz</p>
+			// <p><i class="foo">abc</i>xyz</p>            -->  <p><i class="foo">abc</i>xyz</p>
+			//
+			if ( this._unwrapAttributeElement( unwrapElement, child ) ) {
+				unwrapPositions.push(
+					new Position( parent, i ),
+					new Position( parent, i + 1 )
+				);
 
 				i++;
+
+				continue;
 			}
+
+			//
+			// If other nested attribute is found, look through it's children for elements to unwrap.
+			//
+			// <p><i><span class="foo">abc</span></i><p>  -->  <p><i>abc</i><p>
+			//
+			this._unwrapChildren( child, 0, child.childCount, unwrapElement );
+
+			i++;
 		}
 
 		// Merge at each unwrap.
@@ -1241,43 +1271,12 @@ export default class DowncastWriter {
 	 * @returns {module:engine/view/range~Range} New range after wrapping, spanning over wrapping attribute element.
 	 */
 	_wrapRange( range, attribute ) {
-		// Range is inside single attribute and spans on all children.
-		if ( rangeSpansOnAllChildren( range ) && this._wrapAttributeElement( attribute, range.start.parent ) ) {
-			const parent = range.start.parent;
-
-			const end = this.mergeAttributes( Position._createAfter( parent ) );
-			const start = this.mergeAttributes( Position._createBefore( parent ) );
-
-			return new Range( start, end );
-		}
-
 		// Break attributes at range start and end.
 		const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
-
-		// Range around one element.
-		if ( breakEnd.isEqual( breakStart.getShiftedBy( 1 ) ) ) {
-			const node = breakStart.nodeAfter;
-
-			if ( node instanceof AttributeElement && this._wrapAttributeElement( attribute, node ) ) {
-				const start = this.mergeAttributes( breakStart );
-
-				if ( !start.isEqual( breakStart ) ) {
-					breakEnd.offset--;
-				}
-
-				const end = this.mergeAttributes( breakEnd );
-
-				return new Range( start, end );
-			}
-		}
-
 		const parentContainer = breakStart.parent;
 
-		// Unwrap children located between break points.
-		const unwrappedRange = this._unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
-
 		// Wrap all children with attribute.
-		const newRange = this._wrapChildren( parentContainer, unwrappedRange.start.offset, unwrappedRange.end.offset, attribute );
+		const newRange = this._wrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
 
 		// Merge attributes at the both ends and return a new range.
 		const start = this.mergeAttributes( newRange.start );
@@ -1811,17 +1810,6 @@ function mergeTextNodes( t1, t2 ) {
 	return new Position( t1, nodeBeforeLength );
 }
 
-// Returns `true` if range is located in same {@link module:engine/view/attributeelement~AttributeElement AttributeElement}
-// (`start` and `end` positions are located inside same {@link module:engine/view/attributeelement~AttributeElement AttributeElement}),
-// starts on 0 offset and ends after last child node.
-//
-// @param {module:engine/view/range~Range} Range
-// @returns {Boolean}
-function rangeSpansOnAllChildren( range ) {
-	return range.start.parent == range.end.parent && range.start.parent.is( 'attributeElement' ) &&
-		range.start.offset === 0 && range.end.offset === range.start.parent.childCount;
-}
-
 // Checks if provided nodes are valid to insert. Checks if each node is an instance of
 // {@link module:engine/view/text~Text Text} or {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
 // {@link module:engine/view/containerelement~ContainerElement ContainerElement},

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -77,6 +77,10 @@ export default class EditableElement extends ContainerElement {
 		}
 	}
 
+	destroy() {
+		this.stopListening();
+	}
+
 	/**
 	 * Returns document associated with the editable.
 	 *

+ 13 - 11
packages/ckeditor5-engine/src/view/element.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -106,7 +106,7 @@ export default class Element extends Node {
 		 * Map of styles.
 		 *
 		 * @protected
-		 * @member {Set} module:engine/view/element~Element#_styles
+		 * @member {Map} module:engine/view/element~Element#_styles
 		 */
 		this._styles = new Map();
 
@@ -121,7 +121,7 @@ export default class Element extends Node {
 		 * Custom properties can be added to element instance, will be cloned but not rendered into DOM.
 		 *
 		 * @protected
-		 * @memeber {Map}
+		 * @member {Map}
 		 */
 		this._customProperties = new Map();
 	}
@@ -147,9 +147,15 @@ export default class Element extends Node {
 	}
 
 	/**
-	 * Checks whether given view tree object is of given type.
+	 * Checks whether this view object is of the given type.
 	 *
-	 * Read more in {@link module:engine/view/node~Node#is}.
+	 *		obj.is( 'element' ); // true
+	 *		obj.is( 'li' ); // true
+	 *		obj.is( 'element', 'li' ); // true
+	 *		obj.is( 'text' ); // false
+	 *		obj.is( 'element', 'img' ); // false
+	 *
+	 * Read more in {@link module:engine/view/node~Node#is `Node#is()`}.
 	 *
 	 * @param {String} type
 	 * @param {String} [name] Element name.
@@ -206,11 +212,7 @@ export default class Element extends Node {
 			yield 'style';
 		}
 
-		// This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454.
-		// It can be simplified to `yield* this._attrs.keys();`.
-		for ( const key of this._attrs.keys() ) {
-			yield key;
-		}
+		yield* this._attrs.keys();
 	}
 
 	/**

+ 2 - 2
packages/ckeditor5-engine/src/view/elementdefinition.jsdoc

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -16,6 +16,8 @@ import Node from './node';
  *
  * To create a new empty element use the
  * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement `downcastWriter#createEmptyElement()`} method.
+ *
+ * @extends module:engine/view/element~Element
  */
 export default class EmptyElement extends Element {
 	/**

+ 3 - 3
packages/ckeditor5-engine/src/view/filler.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /* globals window */
@@ -37,7 +37,7 @@ import isText from '@ckeditor/ckeditor5-utils/src/dom/istext';
  */
 
 /**
- * `<br> filler creator. This is a function which creates `<br data-cke-filler="true">` element.
+ * `<br>` filler creator. This is a function which creates `<br data-cke-filler="true">` element.
  * It defines how the filler is created.
  *
  * @see module:engine/view/filler~NBSP_FILLER

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

+ 4 - 4
packages/ckeditor5-engine/src/view/matcher.js

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -364,7 +364,7 @@ function matchStyles( patterns, element ) {
  *
  *		// Match view element which has matching attributes.
  *		const pattern = {
- *			attribute: {
+ *			attributes: {
  *				title: 'foobar',	// Attribute title should equal 'foobar'.
  *				foo: /^\w+/,		// Attribute foo should match /^\w+/ regexp.
  *				bar: true			// Attribute bar should be set (can be empty).
@@ -384,7 +384,7 @@ function matchStyles( patterns, element ) {
  *		// Multiple classes to match.
  *		const pattern = {
  *			classes: [ 'baz', 'bar', /foo.../ ]
- *		}:
+ *		};
  *
  *		// Match view element which has given styles.
  *		const pattern = {

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -13,6 +13,9 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
 import { clone } from 'lodash-es';
 
+// To check if component is loaded more than once.
+import '@ckeditor/ckeditor5-utils/src/version';
+
 /**
  * Abstract tree view node class.
  *
@@ -287,7 +290,7 @@ export default class Node {
 	}
 
 	/**
-	 * Checks whether given view tree object is of given type.
+	 * Checks whether this view object is of the given type.
 	 *
 	 * This method is useful when processing view tree objects that are of unknown type. For example, a function
 	 * may return {@link module:engine/view/documentfragment~DocumentFragment} or {@link module:engine/view/node~Node}
@@ -300,7 +303,6 @@ export default class Node {
 	 *		obj.is( 'p' ); // shortcut for obj.is( 'element', 'p' )
 	 *		obj.is( 'text' ); // true for text node, false for element and document fragment
 	 *
-	 * @method #is
 	 * @param {'element'|'containerElement'|'attributeElement'|'emptyElement'|'uiElement'|
 	 * 'rootElement'|'documentFragment'|'text'|'textProxy'} type
 	 * @returns {Boolean}

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

@@ -1,7 +1,7 @@
 
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -37,7 +37,7 @@ export default class FocusObserver extends DomEventObserver {
 			// overwrite new DOM selection with selection from the view.
 			// See https://github.com/ckeditor/ckeditor5-engine/issues/795 for more details.
 			// Long timeout is needed to solve #676 and https://github.com/ckeditor/ckeditor5-engine/issues/1157 issues.
-			this._renderTimeoutId = setTimeout( () => view.render(), 50 );
+			this._renderTimeoutId = setTimeout( () => view.forceRender(), 50 );
 		} );
 
 		document.on( 'blur', ( evt, data ) => {
@@ -47,7 +47,7 @@ export default class FocusObserver extends DomEventObserver {
 				document.isFocused = false;
 
 				// Re-render the document to update view elements.
-				view.render();
+				view.forceRender();
 			}
 		} );
 

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -55,7 +55,7 @@ export default class MutationObserver extends Observer {
 		this.domConverter = view.domConverter;
 
 		/**
-		 * Reference to the {@link module:engine/view/view~View#renderer}.
+		 * Reference to the {@link module:engine/view/view~View#_renderer}.
 		 *
 		 * @member {module:engine/view/renderer~Renderer}
 		 */
@@ -248,7 +248,7 @@ export default class MutationObserver extends Observer {
 
 		// If nothing changes on `mutations` event, at this point we have "dirty DOM" (changed) and de-synched
 		// view (which has not been changed). In order to "reset DOM" we render the view again.
-		this.view.render();
+		this.view.forceRender();
 
 		function sameNodes( child1, child2 ) {
 			// First level of comparison (array of children vs array of children) – use the Lodash's default behavior.

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

@@ -1,6 +1,6 @@
 /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
 /**
@@ -52,7 +52,7 @@ export default class Observer {
 
 	/**
 	 * Enables the observer. This method is called when the observer is registered to the
-	 * {@link module:engine/view/view~View} and after {@link module:engine/view/view~View#render rendering}
+	 * {@link module:engine/view/view~View} and after {@link module:engine/view/view~View#forceRender rendering}
 	 * (all observers are {@link #disable disabled} before rendering).
 	 *
 	 * A typical use case for disabling observers is that mutation observers need to be disabled for the rendering.
@@ -66,7 +66,7 @@ export default class Observer {
 
 	/**
 	 * Disables the observer. This method is called before
-	 * {@link module:engine/view/view~View#render rendering} to prevent firing events during rendering.
+	 * {@link module:engine/view/view~View#forceRender rendering} to prevent firing events during rendering.
 	 *
 	 * @see module:engine/view/observer/observer~Observer#enable
 	 */

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