Explorar o código

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

Maciej Gołaszewski %!s(int64=6) %!d(string=hai) anos
pai
achega
a55cbf719a
Modificáronse 100 ficheiros con 4457 adicións e 2753 borrados
  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 */
 /* eslint-env node */

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

@@ -1,33 +1,33 @@
 sudo: required
 sudo: required
 dist: trusty
 dist: trusty
 addons:
 addons:
+  chrome: stable
   firefox: latest
   firefox: latest
-  apt:
-    sources:
-    - google-chrome
-    packages:
-    - google-chrome-stable
 language: node_js
 language: node_js
 node_js:
 node_js:
 - '8'
 - '8'
 cache:
 cache:
-- node_modules
+  yarn: true
 branches:
 branches:
-   except:
-   - stable
+  except:
+  - stable
 before_install:
 before_install:
+- export START_TIME=$( date +%s )
 - export DISPLAY=:99.0
 - export DISPLAY=:99.0
 - sh -e /etc/init.d/xvfb start
 - 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:
 install:
-- npm install @ckeditor/ckeditor5-dev-tests
+- yarn add @ckeditor/ckeditor5-dev-tests
 - ckeditor5-dev-tests-install-dependencies
 - ckeditor5-dev-tests-install-dependencies
 script:
 script:
 - ckeditor5-dev-tests-travis
 - ckeditor5-dev-tests-travis
 after_success:
 after_success:
 - ckeditor5-dev-tests-save-revision
 - ckeditor5-dev-tests-save-revision
+after_script:
+- export END_TIME=$( date +%s )
+- ckeditor5-dev-tests-notify-travis-status
 env:
 env:
   global:
   global:
   - secure: CSFKXnTb2oQW8YIjh+SWXYSYzx1jcIg1wGcl6jWrbaOU06JHEO77YLKlhMxLfRniFtilBEksK4ugIcn+lihykhHleMxI27+kifILdLLZKvwWlZiDrtpJSzLk08SpRNDwwthSlHVKrxbHVFE1K9SYpX4s3qTpxullDMPgc13OP4c=
   - secure: CSFKXnTb2oQW8YIjh+SWXYSYzx1jcIg1wGcl6jWrbaOU06JHEO77YLKlhMxLfRniFtilBEksK4ugIcn+lihykhHleMxI27+kifILdLLZKvwWlZiDrtpJSzLk08SpRNDwwthSlHVKrxbHVFE1K9SYpX4s3qTpxullDMPgc13OP4c=
   - secure: WGdwDEavTOIE/yw8pdpWmx3Cehm2/GN8gZMimuot8sWPbOZRuPh+2lnVWpASLe25qJUKKeLWYLRHccpMLKbyweiordSEBwzNrOGOFyUuaDPITNZZrp5ekh2vHBp1+mwhRpRekdyWdzzVsm01RkFrnJqQlaXxX9i/r9Tu4EsK54M=
   - 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
 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)
 ## [11.0.0](https://github.com/ckeditor/ckeditor5-engine/compare/v10.2.0...v11.0.0) (2018-10-08)
 
 
 ### Bug fixes
 ### 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>
 **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).
 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
 ## 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
 ## 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.
 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
 	isBlock: true
 } );
 } );
 schema.register( '$text', {
 schema.register( '$text', {
-	allowIn: '$block'
+	allowIn: '$block',
+	isInline: true
 } );
 } );
 ```
 ```
 
 

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@ckeditor/ckeditor5-engine",
   "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.",
   "description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
   "keywords": [
   "keywords": [
     "wysiwyg",
     "wysiwyg",
@@ -21,35 +21,36 @@
     "ckeditor 5"
     "ckeditor 5"
   ],
   ],
   "dependencies": {
   "dependencies": {
-    "@ckeditor/ckeditor5-utils": "^11.0.0",
+    "@ckeditor/ckeditor5-utils": "^12.1.1",
     "lodash-es": "^4.17.10"
     "lodash-es": "^4.17.10"
   },
   },
   "devDependencies": {
   "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": "^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"
     "lint-staged": "^7.0.0"
   },
   },
   "engines": {
   "engines": {
-    "node": ">=6.9.0",
-    "npm": ">=3.0.0"
+    "node": ">=8.0.0",
+    "npm": ">=5.7.1"
   },
   },
   "author": "CKSource (http://cksource.com/)",
   "author": "CKSource (http://cksource.com/)",
   "license": "GPL-2.0-or-later",
   "license": "GPL-2.0-or-later",
-  "homepage": "https://ckeditor.com",
+  "homepage": "https://ckeditor.com/ckeditor-5",
   "bugs": "https://github.com/ckeditor/ckeditor5-engine/issues",
   "bugs": "https://github.com/ckeditor/ckeditor5-engine/issues",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
@@ -61,8 +62,7 @@
     "theme"
     "theme"
   ],
   ],
   "scripts": {
   "scripts": {
-    "lint": "eslint --quiet '**/*.js'",
-    "precommit": "lint-staged"
+    "lint": "eslint --quiet '**/*.js'"
   },
   },
   "lint-staged": {
   "lint-staged": {
     "**/*.js": [
     "**/*.js": [
@@ -72,5 +72,10 @@
   "eslintIgnore": [
   "eslintIgnore": [
     "src/lib/**",
     "src/lib/**",
     "packages/**"
     "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 Mapper from '../conversion/mapper';
 
 
 import DowncastDispatcher from '../conversion/downcastdispatcher';
 import DowncastDispatcher from '../conversion/downcastdispatcher';
-import { insertText } from '../conversion/downcast-converters';
+import { insertText } from '../conversion/downcasthelpers';
 
 
 import UpcastDispatcher from '../conversion/upcastdispatcher';
 import UpcastDispatcher from '../conversion/upcastdispatcher';
-import { convertText, convertToModelFragment } from '../conversion/upcast-converters';
+import { convertText, convertToModelFragment } from '../conversion/upcasthelpers';
 
 
 import ViewDocumentFragment from '../view/documentfragment';
 import ViewDocumentFragment from '../view/documentfragment';
 import ViewDocument from '../view/document';
 import ViewDocument from '../view/document';
@@ -35,6 +35,11 @@ import ModelRange from '../model/range';
  * * downcast converters,
  * * downcast converters,
  * * upcast 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
  * @mixes module:utils/observablemixin~ObservableMixin
  */
  */
 export default class DataController {
 export default class DataController {
@@ -58,7 +63,7 @@ export default class DataController {
 		 * Data processor used during the conversion.
 		 * Data processor used during the conversion.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {module:engine/dataProcessor~DataProcessor}
+		 * @member {module:engine/dataprocessor/dataprocessor~DataProcessor}
 		 */
 		 */
 		this.processor = dataProcessor;
 		this.processor = dataProcessor;
 
 
@@ -103,18 +108,50 @@ export default class DataController {
 		this.upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
 		this.upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
 
 
 		this.decorate( 'init' );
 		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
 	 * Returns the model's data converted by downcast dispatchers attached to {@link #downcastDispatcher} and
 	 * formatted by the {@link #processor data processor}.
 	 * 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.
 	 * @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
 	 * **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.
 	 * 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
 	 * @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.
 	 * @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 ) {
 		if ( this.model.document.version ) {
 			/**
 			/**
 			 * Cannot set initial data to not empty {@link module:engine/model/document~Document}.
 			 * 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.' );
 			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 => {
 		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();
 		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
 	 * This method also creates a batch with all the changes applied. If all you need is to parse data, use
 	 * the {@link #parse} method.
 	 * 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 => {
 		this.model.enqueueChange( 'transparent', writer => {
 			writer.setSelection( null );
 			writer.setSelection( null );
 			writer.removeSelectionAttribute( this.model.document.selection.getAttributeKeys() );
 			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.
 	 * 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
 	 * @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 View from '../view/view';
 import Mapper from '../conversion/mapper';
 import Mapper from '../conversion/mapper';
 import DowncastDispatcher from '../conversion/downcastdispatcher';
 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 ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 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,
  * 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
 		// See  https://github.com/ckeditor/ckeditor5-engine/issues/1528
 		this.listenTo( this.model, '_beforeChanges', () => {
 		this.listenTo( this.model, '_beforeChanges', () => {
-			this.view._renderingDisabled = true;
+			this.view._disableRendering( true );
 		}, { priority: 'highest' } );
 		}, { priority: 'highest' } );
 
 
 		this.listenTo( this.model, '_afterChanges', () => {
 		this.listenTo( this.model, '_afterChanges', () => {
-			this.view._renderingDisabled = false;
-			this.view.render();
+			this.view._disableRendering( false );
 		}, { priority: 'lowest' } );
 		}, { priority: 'lowest' } );
 
 
 		// Whenever model document is changed, convert those changes to the view (using model.Document#differ).
 		// 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 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.
  * 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
  * We recommend reading the {@glink framework/guides/architecture/editing-engine Editing engine architecture} guide first to
  * understand the core concepts of the conversion mechanisms.
  * 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
  * {@link module:core/editor/editor~Editor#conversion `editor.conversion`} property
  * and by default has the following groups of dispatchers (i.e. directions of conversion):
  * and by default has the following groups of dispatchers (i.e. directions of conversion):
  *
  *
@@ -36,23 +26,26 @@ import {
  * * `dataDowncast`
  * * `dataDowncast`
  * * `upcast`
  * * `upcast`
  *
  *
+ * # One-way converters
+ *
  * To add a converter to a specific group, use the {@link module:engine/conversion/conversion~Conversion#for `for()`}
  * To add a converter to a specific group, use the {@link module:engine/conversion/conversion~Conversion#for `for()`}
  * method:
  * method:
  *
  *
  *		// Add a converter to editing downcast and data downcast.
  *		// 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:
  *		// 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:
  *		// 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):
  * class to add two-way converters (upcast and downcast):
  *
  *
  * * {@link module:engine/conversion/conversion~Conversion#elementToElement `elementToElement()`} &ndash;
  * * {@link module:engine/conversion/conversion~Conversion#elementToElement `elementToElement()`} &ndash;
@@ -65,95 +58,132 @@ import {
 export default class Conversion {
 export default class Conversion {
 	/**
 	/**
 	 * Creates a new conversion instance.
 	 * 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
 		 * @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.
 	 * @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 ) {
 	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.
 	 * 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).
 	 *		// 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.
 	 *		// 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.
 	 *		// View specified as an object instead of a string.
-	 *		conversion.elementToElement( {
+	 *		editor.conversion.elementToElement( {
 	 *			model: 'fancyParagraph',
 	 *			model: 'fancyParagraph',
 	 *			view: {
 	 *			view: {
 	 *				name: 'p',
 	 *				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.
 	 *		// Use `upcastAlso` to define other view elements that should also be converted to a `paragraph` element.
-	 *		conversion.elementToElement( {
+	 *		editor.conversion.elementToElement( {
 	 *			model: 'paragraph',
 	 *			model: 'paragraph',
 	 *			view: 'p',
 	 *			view: 'p',
 	 *			upcastAlso: [
 	 *			upcastAlso: [
@@ -191,7 +221,7 @@ export default class Conversion {
 	 *		} );
 	 *		} );
 	 *
 	 *
 	 *		// `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
 	 *		// `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
-	 *		conversion.elementToElement( {
+	 *		editor.conversion.elementToElement( {
 	 *			model: 'heading',
 	 *			model: 'heading',
 	 *			view: 'h2',
 	 *			view: 'h2',
 	 *			// Convert "headling-like" paragraphs to headings.
 	 *			// Convert "headling-like" paragraphs to headings.
@@ -229,17 +259,16 @@ export default class Conversion {
 	 */
 	 */
 	elementToElement( definition ) {
 	elementToElement( definition ) {
 		// Set up downcast converter.
 		// Set up downcast converter.
-		this.for( 'downcast' ).add( downcastElementToElement( definition ) );
+		this.for( 'downcast' ).elementToElement( definition );
 
 
 		// Set up upcast converter.
 		// Set up upcast converter.
 		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
 		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
-			this.for( 'upcast' ).add(
-				upcastElementToElement( {
+			this.for( 'upcast' )
+				.elementToElement( {
 					model,
 					model,
 					view,
 					view,
 					converterPriority: definition.converterPriority
 					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.
 	 * 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).
 	 *		// 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.
 	 *		// 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.
 	 *		// View specified as an object instead of a string.
-	 *		conversion.attributeToElement( {
+	 *		editor.conversion.attributeToElement( {
 	 *			model: 'bold',
 	 *			model: 'bold',
 	 *			view: {
 	 *			view: {
 	 *				name: 'span',
 	 *				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.
 	 *		// 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.
 	 *		// The same attribute on different elements may then be handled by a different converter.
-	 *		conversion.attributeToElement( {
+	 *		editor.conversion.attributeToElement( {
 	 *			model: {
 	 *			model: {
 	 *				key: 'textDecoration',
 	 *				key: 'textDecoration',
 	 *				values: [ 'underline', 'lineThrough' ],
 	 *				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.
 	 *		// Use `upcastAlso` to define other view elements that should also be converted to the `bold` attribute.
-	 *		conversion.attributeToElement( {
+	 *		editor.conversion.attributeToElement( {
 	 *			model: 'bold',
 	 *			model: 'bold',
 	 *			view: 'strong',
 	 *			view: 'strong',
 	 *			upcastAlso: [
 	 *			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`).
 	 *		// 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.
 	 *		// `upcastAlso` set as callback enables a conversion of a wide range of different view elements.
-	 *		conversion.attributeToElement( {
+	 *		editor.conversion.attributeToElement( {
 	 *			model: {
 	 *			model: {
 	 *				key: 'fontSize',
 	 *				key: 'fontSize',
 	 *				values: [ 'big', 'small' ]
 	 *				values: [ 'big', 'small' ]
@@ -402,29 +431,31 @@ export default class Conversion {
 	 */
 	 */
 	attributeToElement( definition ) {
 	attributeToElement( definition ) {
 		// Set up downcast converter.
 		// Set up downcast converter.
-		this.for( 'downcast' ).add( downcastAttributeToElement( definition ) );
+		this.for( 'downcast' ).attributeToElement( definition );
 
 
 		// Set up upcast converter.
 		// Set up upcast converter.
 		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
 		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
-			this.for( 'upcast' ).add(
-				upcastElementToAttribute( {
+			this.for( 'upcast' )
+				.elementToAttribute( {
 					view,
 					view,
 					model,
 					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).
 	 * 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).
 	 * 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).
 	 *		// 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.
 	 *		// Attribute values are strictly specified.
-	 *		conversion.attributeToAttribute( {
+	 *		editor.conversion.attributeToAttribute( {
 	 *			model: {
 	 *			model: {
 	 *				name: 'image',
 	 *				name: 'image',
 	 *				key: 'aside',
 	 *				key: 'aside',
@@ -440,7 +471,7 @@ export default class Conversion {
 	 *		} );
 	 *		} );
 	 *
 	 *
 	 *		// Set the style attribute.
 	 *		// Set the style attribute.
-	 *		conversion.attributeToAttribute( {
+	 *		editor.conversion.attributeToAttribute( {
 	 *			model: {
 	 *			model: {
 	 *				name: 'image',
 	 *				name: 'image',
 	 *				key: 'aside',
 	 *				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`).
 	 *		// 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.
 	 *		// Use `upcastAlso` to define other view elements that should also be converted to the `align=right` attribute.
-	 *		conversion.attributeToAttribute( {
+	 *		editor.conversion.attributeToAttribute( {
 	 *			model: {
 	 *			model: {
 	 *				key: 'align',
 	 *				key: 'align',
 	 *				values: [ 'right', 'center' ]
 	 *				values: [ 'right', 'center' ]
@@ -525,43 +556,41 @@ export default class Conversion {
 	 */
 	 */
 	attributeToAttribute( definition ) {
 	attributeToAttribute( definition ) {
 		// Set up downcast converter.
 		// Set up downcast converter.
-		this.for( 'downcast' ).add( downcastAttributeToAttribute( definition ) );
+		this.for( 'downcast' ).attributeToAttribute( definition );
 
 
 		// Set up upcast converter.
 		// Set up upcast converter.
 		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
 		for ( const { model, view } of _getAllUpcastDefinitions( definition ) ) {
-			this.for( 'upcast' ).add(
-				upcastAttributeToAttribute( {
+			this.for( 'upcast' )
+				.attributeToAttribute( {
 					view,
 					view,
 					model
 					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
 	 * @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.
  * @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
 // Helper function that creates a joint array out of an item passed in `definition.view` and items passed in
 // `definition.upcastAlso`.
 // `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.
 	 * 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 = {} ) {
 	constructor( conversionApi = {} ) {
 		/**
 		/**
 		 * Interface passed by dispatcher to the events callbacks.
 		 * Interface passed by dispatcher to the events callbacks.
 		 *
 		 *
-		 * @member {Object}
+		 * @member {module:engine/conversion/downcastdispatcher~DowncastConversionApi}
 		 */
 		 */
 		this.conversionApi = extend( { dispatcher: this }, conversionApi );
 		this.conversionApi = extend( { dispatcher: this }, conversionApi );
 	}
 	}
@@ -294,7 +296,7 @@ export default class DowncastDispatcher {
 
 
 			// Do not fire event if the attribute has been consumed.
 			// Do not fire event if the attribute has been consumed.
 			if ( this.conversionApi.consumable.test( selection, 'attribute:' + data.attributeKey ) ) {
 			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.
 		// In markers' case, event name == consumable name.
 		const eventName = 'addMarker:' + markerName;
 		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;
 			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 );
 		this.conversionApi.consumable = this._createConsumableForRange( markerRange, eventName );
 
 
-		// Create separate event for each node in the range.
 		for ( const item of markerRange.getItems() ) {
 		for ( const item of markerRange.getItems() ) {
 			// Do not fire event for already consumed items.
 			// Do not fire event for already consumed items.
 			if ( !this.conversionApi.consumable.test( item, eventName ) ) {
 			if ( !this.conversionApi.consumable.test( item, eventName ) ) {
@@ -481,7 +489,8 @@ export default class DowncastDispatcher {
 	 * @param {Object} data Additional information about the change.
 	 * @param {Object} data Additional information about the change.
 	 * @param {module:engine/model/item~Item} data.item Inserted item.
 	 * @param {module:engine/model/item~Item} data.item Inserted item.
 	 * @param {module:engine/model/range~Range} data.range Range spanning over inserted item.
 	 * @param {module:engine/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 {Object} data Additional information about the change.
 	 * @param {module:engine/model/position~Position} data.position Position from which the node has been removed.
 	 * @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 {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.attributeOldValue Attribute value before the change. This is `null` when selection attribute is converted.
 	 * @param {*} data.attributeNewValue New attribute value.
 	 * @param {*} data.attributeNewValue New attribute value.
 	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
 	 * @param {module:engine/conversion/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
 	 * @event selection
 	 * @param {module:engine/model/selection~Selection} selection Selection that is converted.
 	 * @param {module:engine/model/selection~Selection} selection Selection that is converted.
 	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
 	 * @param {module:engine/conversion/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 {module:engine/model/range~Range} data.markerRange Marker range.
 	 * @param {String} data.markerName Marker name.
 	 * @param {String} data.markerName Marker name.
 	 * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
 	 * @param {module:engine/conversion/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 {Object} data Additional information about the change.
 	 * @param {module:engine/model/range~Range} data.markerRange Marker range.
 	 * @param {module:engine/model/range~Range} data.markerRange Marker range.
 	 * @param {String} data.markerName Marker name.
 	 * @param {String} data.markerName Marker name.
-	 * @param {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;
 	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
  * Maps elements, positions and markers between {@link module:engine/view/document~Document the view} and
  * {@link module:engine/model/model the model}.
  * {@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}.
  * 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
  * To map complex model to/from view relations, you may provide custom callbacks for
@@ -91,14 +94,8 @@ export default class Mapper {
 				return;
 				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 );
 			const modelOffset = this._toModelOffset( data.viewPosition.parent, data.viewPosition.offset, viewBlock );
 
 
 			data.modelPosition = ModelPosition._createAt( modelParent, modelOffset );
 			data.modelPosition = ModelPosition._createAt( modelParent, modelOffset );
@@ -338,6 +335,23 @@ export default class Mapper {
 		this._viewToModelLengthCallbacks.set( viewElementName, lengthCallback );
 		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.
 	 * 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.
  * {@link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly.
  * However, it is important to understand how consumable values can be
  * However, it is important to understand how consumable values can be
  * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}.
  * {@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
  * 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
  * 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`:
  * Examples of providing callbacks for `UpcastDispatcher`:
  *
  *
  *		// Converter for links (<a>).
  *		// 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' ] } ) ) {
  *			if ( conversionApi.consumable.consume( data.viewItem, { name: true, attributes: [ 'href' ] } ) ) {
  *				// <a> element is inline and is represented by an attribute in the model.
  *				// <a> element is inline and is represented by an attribute in the model.
  *				// This is why we need to convert only children.
  *				// 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).
  *		// 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.
  *  	 	// When element is already consumed by higher priority converters then do nothing.
  *  	 	if ( conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
  *  	 	if ( conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
  *  	 			const paragraph = conversionApi.writer.createElement( 'paragraph' );
  *  	 			const paragraph = conversionApi.writer.createElement( 'paragraph' );
@@ -102,21 +102,20 @@ export default class UpcastDispatcher {
 	/**
 	/**
 	 * Creates a `UpcastDispatcher` that operates using passed API.
 	 * 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
 	 * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired
 	 * by `UpcastDispatcher`.
 	 * by `UpcastDispatcher`.
 	 */
 	 */
 	constructor( conversionApi = {} ) {
 	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
 		 * 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.
 		 * 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 );
 		this.conversionApi = Object.assign( {}, conversionApi );
 
 
@@ -140,6 +139,7 @@ export default class UpcastDispatcher {
 		this.conversionApi.convertItem = this._convertItem.bind( this );
 		this.conversionApi.convertItem = this._convertItem.bind( this );
 		this.conversionApi.convertChildren = this._convertChildren.bind( this );
 		this.conversionApi.convertChildren = this._convertChildren.bind( this );
 		this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.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.
 		// Do the conversion.
 		const { modelRange } = this._convertItem( viewItem, this._modelCursor );
 		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();
 		const documentFragment = writer.createDocumentFragment();
 
 
 		// When there is a conversion result.
 		// When there is a conversion result.
 		if ( modelRange ) {
 		if ( modelRange ) {
-			// Remove all empty elements that was added to #_removeIfEmpty list.
+			// Remove all empty elements that were create while splitting.
 			this._removeEmptyElements();
 			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() ) ) {
 			for ( const item of Array.from( this._modelCursor.parent.getChildren() ) ) {
 				writer.append( item, documentFragment );
 				writer.append( item, documentFragment );
 			}
 			}
@@ -196,8 +196,8 @@ export default class UpcastDispatcher {
 		// Clear context position.
 		// Clear context position.
 		this._modelCursor = null;
 		this._modelCursor = null;
 
 
-		// Clear split elements.
-		this._removeIfEmpty.clear();
+		// Clear split elements lists.
+		this._splitParts.clear();
 
 
 		// Clear conversion API.
 		// Clear conversion API.
 		this.conversionApi.writer = null;
 		this.conversionApi.writer = null;
@@ -209,7 +209,7 @@ export default class UpcastDispatcher {
 
 
 	/**
 	/**
 	 * @private
 	 * @private
-	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#convertItem
+	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertItem
 	 */
 	 */
 	_convertItem( viewItem, modelCursor ) {
 	_convertItem( viewItem, modelCursor ) {
 		const data = Object.assign( { viewItem, modelCursor, modelRange: null } );
 		const data = Object.assign( { viewItem, modelCursor, modelRange: null } );
@@ -239,7 +239,7 @@ export default class UpcastDispatcher {
 
 
 	/**
 	/**
 	 * @private
 	 * @private
-	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#convertChildren
+	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertChildren
 	 */
 	 */
 	_convertChildren( viewItem, modelCursor ) {
 	_convertChildren( viewItem, modelCursor ) {
 		const modelRange = new ModelRange( modelCursor );
 		const modelRange = new ModelRange( modelCursor );
@@ -259,11 +259,11 @@ export default class UpcastDispatcher {
 
 
 	/**
 	/**
 	 * @private
 	 * @private
-	 * @see module:engine/conversion/upcastdispatcher~ViewConversionApi#splitToAllowedParent
+	 * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#splitToAllowedParent
 	 */
 	 */
 	_splitToAllowedParent( node, modelCursor ) {
 	_splitToAllowedParent( node, modelCursor ) {
 		// Try to find allowed parent.
 		// 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`.
 		// When there is no parent that allows to insert node then return `null`.
 		if ( !allowedParent ) {
 		if ( !allowedParent ) {
@@ -283,14 +283,31 @@ export default class UpcastDispatcher {
 		// Split element to allowed parent.
 		// Split element to allowed parent.
 		const splitResult = this.conversionApi.writer.split( modelCursor, allowedParent );
 		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
 	 * @private
 	 */
 	 */
 	_removeEmptyElements() {
 	_removeEmptyElements() {
-		let removed = false;
+		let anyRemoved = false;
 
 
-		for ( const element of this._removeIfEmpty ) {
+		for ( const element of this._splitParts.keys() ) {
 			if ( element.isEmpty ) {
 			if ( element.isEmpty ) {
 				this.conversionApi.writer.remove( element );
 				this.conversionApi.writer.remove( element );
-				this._removeIfEmpty.delete( element );
-				removed = true;
+				this._splitParts.delete( element );
+
+				anyRemoved = true;
 			}
 			}
 		}
 		}
 
 
-		if ( removed ) {
+		if ( anyRemoved ) {
 			this._removeEmptyElements();
 			this._removeEmptyElements();
 		}
 		}
 	}
 	}
@@ -348,7 +402,7 @@ export default class UpcastDispatcher {
 	 * Change this value for the next converter to tell where the conversion should continue.
 	 * 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
 	 * @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.
 	 * 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;
 	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 ) {
 function createContextTree( contextDefinition, writer ) {
 	let position;
 	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}
  * and is passed as one of parameters when {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher dispatcher}
  * fires it's events.
  * 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:element
  * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
  * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
  * @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
  * @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.
  * @param {module:engine/model/position~Position} modelCursor Position of conversion.
  * @returns {Object} result Conversion result.
  * @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.
  * @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}.
  * 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
  * {@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.
  * 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 UpcastDispatcher from '../conversion/upcastdispatcher';
 import Mapper from '../conversion/mapper';
 import Mapper from '../conversion/mapper';
 import {
 import {
-	convertRangeSelection,
 	convertCollapsedSelection,
 	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 { isPlainObject } from 'lodash-es';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 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.
  * 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:
  * **Note:** A {@link module:engine/model/text~Text text} node that contains attributes will be represented as:
  *
  *
  *		<$text attribute="value">Text data</$text>
  *		<$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 {module:engine/model/model~Model} model
  * @param {Object} [options]
  * @param {Object} [options]
  * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true`, the selection will
  * @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.
  * 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
  * **Note:** Remember to register elements in the {@link module:engine/model/model~Model#schema model's schema} before
  * trying to use them.
  * trying to use them.
  *
  *
@@ -81,6 +91,9 @@ getData._stringify = stringify;
  *
  *
  *		<$text attribute="value">Text data</$text>
  *		<$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 {module:engine/model/model~Model} model
  * @param {String} data HTML-like string to write into the document.
  * @param {String} data HTML-like string to write into the document.
  * @param {Object} options
  * @param {Object} options
@@ -231,6 +244,7 @@ export function stringify( node, selectionOrPositionOrRange = null, markers = nu
 
 
 		return new ViewContainerElement( modelItem.name, attributes );
 		return new ViewContainerElement( modelItem.name, attributes );
 	} ) );
 	} ) );
+
 	downcastDispatcher.on( 'selection', convertRangeSelection() );
 	downcastDispatcher.on( 'selection', convertRangeSelection() );
 	downcastDispatcher.on( 'selection', convertCollapsedSelection() );
 	downcastDispatcher.on( 'selection', convertCollapsedSelection() );
 	downcastDispatcher.on( 'addMarker', insertUIElement( ( data, writer ) => {
 	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 {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/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 {Object} [options={}] Additional configuration.
  * @param {Array<Object>} [options.selectionAttributes] A list of attributes which will be passed to the selection.
  * @param {Array<Object>} [options.selectionAttributes] A list of attributes which will be passed to the selection.
  * @param {Boolean} [options.lastRangeBackward=false] If set to `true`, the last range will be added as backward.
  * @param {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.
 // Checks if a given priority is allowed. Returns null if the priority cannot be converted.
 //
 //
 // @param {String} priorityString
 // @param {String} priorityString
-// returns {Number|Null}
+// returns {Number|null}
 function _convertPriority( priorityString ) {
 function _convertPriority( priorityString ) {
 	const priority = parseInt( priorityString, 10 );
 	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 'remove':
 			case 'move':
 			case 'move':
 			case 'reinsert': {
 			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 sourceParentInserted = this._isInInsertedElement( operation.sourcePosition.parent );
 				const targetParentInserted = this._isInInsertedElement( operation.targetPosition.parent );
 				const targetParentInserted = this._isInInsertedElement( operation.targetPosition.parent );
 
 
@@ -292,6 +301,23 @@ export default class Differ {
 		return result;
 		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.
 	 * 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' ) {
 				if ( old.type == 'attribute' ) {
 					// There are only two conflicting scenarios possible here:
 					// There are only two conflicting scenarios possible here:
 					if ( inc.offset >= old.offset && incEnd <= oldEnd ) {
 					if ( inc.offset >= old.offset && incEnd <= oldEnd ) {
@@ -1061,7 +1107,7 @@ function _generateActionsFromChanges( oldChildrenLength, changes ) {
 		} else {
 		} else {
 			actions.push( ...'a'.repeat( change.howMany ).split( '' ) );
 			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;
 			offset = change.offset + change.howMany;
 			// We changed `howMany` old nodes, update `oldChildrenHandled`.
 			// We changed `howMany` old nodes, update `oldChildrenHandled`.
 			oldChildrenHandled += change.howMany;
 			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.
 		 * The {@link module:engine/model/model~Model model} that the document is a part of.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {module:engine/model/model~Model}
+		 * @type {module:engine/model/model~Model}
 		 */
 		 */
 		this.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.
 		 * a {@link module:utils/ckeditorerror~CKEditorError model-document-applyOperation-wrong-version} error is thrown.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {Number}
+		 * @type {Number}
 		 */
 		 */
 		this.version = 0;
 		this.version = 0;
 
 
@@ -66,7 +66,7 @@ export default class Document {
 		 * The document's history.
 		 * The document's history.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {module:engine/model/history~History}
+		 * @type {module:engine/model/history~History}
 		 */
 		 */
 		this.history = new History( this );
 		this.history = new History( this );
 
 
@@ -74,7 +74,7 @@ export default class Document {
 		 * The selection in this document.
 		 * The selection in this document.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {module:engine/model/documentselection~DocumentSelection}
+		 * @type {module:engine/model/documentselection~DocumentSelection}
 		 */
 		 */
 		this.selection = new DocumentSelection( this );
 		this.selection = new DocumentSelection( this );
 
 
@@ -83,7 +83,7 @@ export default class Document {
 		 * {@link #getRoot} to manipulate it.
 		 * {@link #getRoot} to manipulate it.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {module:utils/collection~Collection}
+		 * @type {module:utils/collection~Collection}
 		 */
 		 */
 		this.roots = new Collection( { idProperty: 'rootName' } );
 		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.
 		 * The model differ object. Its role is to buffer changes done on the model document and then calculate a diff of those changes.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {module:engine/model/differ~Differ}
+		 * @type {module:engine/model/differ~Differ}
 		 */
 		 */
 		this.differ = new Differ( model.markers );
 		this.differ = new Differ( model.markers );
 
 
@@ -99,10 +99,18 @@ export default class Document {
 		 * Post-fixer callbacks registered to the model document.
 		 * Post-fixer callbacks registered to the model document.
 		 *
 		 *
 		 * @private
 		 * @private
-		 * @member {Set}
+		 * @type {Set.<Function>}
 		 */
 		 */
 		this._postFixers = new Set();
 		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.
 		// Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
 		this.createRoot( '$root', graveyardName );
 		this.createRoot( '$root', graveyardName );
 
 
@@ -144,29 +152,8 @@ export default class Document {
 		}, { priority: 'low' } );
 		}, { priority: 'low' } );
 
 
 		// Listen to selection changes. If selection changed, mark it.
 		// Listen to selection changes. If selection changed, mark it.
-		let hasSelectionChanged = false;
-
 		this.listenTo( this.selection, 'change', () => {
 		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.
 		// 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
 	 * 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
 	 * 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;
 		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
 	 * 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.
 	 * {@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.
 	 * Performs post-fixer loops. Executes post-fixer callbacks as long as none of them has done any changes to the model.
 	 *
 	 *
 	 * @private
 	 * @private
+	 * @param {module:engine/model/writer~Writer} writer The writer on which post-fixer callbacks will be called.
 	 */
 	 */
 	_callPostFixers( writer ) {
 	_callPostFixers( writer ) {
 		let wasFixed = false;
 		let wasFixed = false;
 
 
 		do {
 		do {
 			for ( const callback of this._postFixers ) {
 			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 );
 				wasFixed = callback( writer );
 
 
 				if ( wasFixed ) {
 				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 Text from './text';
 import TextProxy from './textproxy';
 import TextProxy from './textproxy';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 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 CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import log from '@ckeditor/ckeditor5-utils/src/log';
 import log from '@ckeditor/ckeditor5-utils/src/log';
 import uid from '@ckeditor/ckeditor5-utils/src/uid';
 import uid from '@ckeditor/ckeditor5-utils/src/uid';
@@ -149,6 +150,17 @@ export default class DocumentSelection {
 		return this._selection.isGravityOverridden;
 		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.
 	 * 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>b</paragraph>
 	 *		<paragraph>]c</paragraph> // this block will not be returned
 	 *		<paragraph>]c</paragraph> // this block will not be returned
 	 *
 	 *
-	 * @returns {Iterator.<module:engine/model/element~Element>}
+	 * @returns {Iterable.<module:engine/model/element~Element>}
 	 */
 	 */
 	getSelectedBlocks() {
 	getSelectedBlocks() {
 		return this._selection.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
 	 * 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.
 	 * 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 );
 		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.
 	 * 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.
 	 * 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
 	 * 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.
 	 * Should be used only within the {@link module:engine/model/writer~Writer#setSelection} method.
 	 *
 	 *
 	 * @see module:engine/model/writer~Writer#setSelection
 	 * @see module:engine/model/writer~Writer#setSelection
 	 * @protected
 	 * @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 {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
 	 * @param {Object} [options]
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
@@ -506,6 +560,12 @@ class LiveSelection extends Selection {
 	constructor( doc ) {
 	constructor( doc ) {
 		super();
 		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.
 		// Document which owns this selection.
 		//
 		//
 		// @protected
 		// @protected
@@ -546,7 +606,27 @@ class LiveSelection extends Selection {
 		// @type {Set}
 		// @type {Set}
 		this._overriddenGravityRegister = new 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', () => {
 		this.on( 'change:range', () => {
 			for ( const range of this.getRanges() ) {
 			for ( const range of this.getRanges() ) {
 				if ( !this._document._validateSelectionRange( range ) ) {
 				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 );
 			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() {
 	get isCollapsed() {
@@ -651,12 +717,12 @@ class LiveSelection extends Selection {
 
 
 	setTo( selectable, optionsOrPlaceOrOffset, options ) {
 	setTo( selectable, optionsOrPlaceOrOffset, options ) {
 		super.setTo( selectable, optionsOrPlaceOrOffset, options );
 		super.setTo( selectable, optionsOrPlaceOrOffset, options );
-		this._refreshAttributes();
+		this._updateAttributes( true );
 	}
 	}
 
 
 	setFocus( itemOrPosition, offset ) {
 	setFocus( itemOrPosition, offset ) {
 		super.setFocus( itemOrPosition, offset );
 		super.setFocus( itemOrPosition, offset );
-		this._refreshAttributes();
+		this._updateAttributes( true );
 	}
 	}
 
 
 	setAttribute( key, value ) {
 	setAttribute( key, value ) {
@@ -683,7 +749,7 @@ class LiveSelection extends Selection {
 		this._overriddenGravityRegister.add( overrideUid );
 		this._overriddenGravityRegister.add( overrideUid );
 
 
 		if ( this._overriddenGravityRegister.size === 1 ) {
 		if ( this._overriddenGravityRegister.size === 1 ) {
-			this._refreshAttributes();
+			this._updateAttributes( true );
 		}
 		}
 
 
 		return overrideUid;
 		return overrideUid;
@@ -696,7 +762,8 @@ class LiveSelection extends Selection {
 			 * UID obtained from the {@link module:engine/model/writer~Writer#overrideSelectionGravity} to restore.
 			 * UID obtained from the {@link module:engine/model/writer~Writer#overrideSelectionGravity} to restore.
 			 *
 			 *
 			 * @error document-selection-gravity-wrong-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(
 			throw new CKEditorError(
 				'document-selection-gravity-wrong-restore: Attempting to restore the selection gravity for an unknown UID.',
 				'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.
 		// Restore gravity only when all overriding have been restored.
 		if ( !this.isGravityOverridden ) {
 		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() {
 	_popRange() {
 		this._ranges.pop().detach();
 		this._ranges.pop().detach();
 	}
 	}
@@ -767,6 +829,32 @@ class LiveSelection extends Selection {
 		return liveRange;
 		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}.
 	// Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.
 	//
 	//
 	// @protected
 	// @protected
@@ -953,10 +1041,9 @@ class LiveSelection extends Selection {
 					break;
 					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();
 					attrs = value.item.getAttributes();
+					break;
 				}
 				}
 			}
 			}
 		} else {
 		} 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.name; // 'listItem'
 	 *		obj instanceof Element; // true
 	 *		obj instanceof Element; // true
@@ -100,7 +100,7 @@ export default class Element extends Node {
 	 *		obj.is( 'text' ); // false
 	 *		obj.is( 'text' ); // false
 	 *		obj.is( 'element', 'image' ); // 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.
 	 * @param {String} type Type to check when `name` parameter is present.
 	 * Otherwise, it acts like the `name` parameter.
 	 * Otherwise, it acts like the `name` parameter.

+ 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
 	 * @method module:engine/model/liveposition~LivePosition._createAfter
 	 * @see module:engine/model/position~Position._createAfter
 	 * @see module:engine/model/position~Position._createAfter
 	 * @param {module:engine/model/node~Node} node
 	 * @param {module:engine/model/node~Node} node
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone']
 	 * @returns {module:engine/model/liveposition~LivePosition}
 	 * @returns {module:engine/model/liveposition~LivePosition}
 	 */
 	 */
 
 
@@ -97,6 +98,18 @@ export default class LivePosition extends Position {
 	 * @method module:engine/model/liveposition~LivePosition._createBefore
 	 * @method module:engine/model/liveposition~LivePosition._createBefore
 	 * @see module:engine/model/position~Position._createBefore
 	 * @see module:engine/model/position~Position._createBefore
 	 * @param {module:engine/model/node~Node} node
 	 * @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}
 	 * @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;
 		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}.
 	 * 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`.
 	 * Fired whenever marker is added, updated or removed from `MarkerCollection`.
 	 *
 	 *
 	 * @event update
 	 * @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
 	 * @param {module:engine/model/range~Range|null} oldRange Marker range before the update. When is not defined it
 	 * means that marker is just added.
 	 * means that marker is just added.
 	 * @param {module:engine/model/range~Range|null} newRange Marker range after update. When is not defined it
 	 * @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
  * Markers downcast happens on {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} and
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} events.
  * {@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.
  * 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}.
  * 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}.
  * `Marker` instances are created and destroyed only by {@link ~MarkerCollection MarkerCollection}.
  */
  */
 class Marker {
 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
 			isBlock: true
 		} );
 		} );
 		this.schema.register( '$text', {
 		this.schema.register( '$text', {
-			allowIn: '$block'
+			allowIn: '$block',
+			isInline: true
 		} );
 		} );
 		this.schema.register( '$clipboardHolder', {
 		this.schema.register( '$clipboardHolder', {
 			allowContentOf: '$root',
 			allowContentOf: '$root',
@@ -102,12 +103,15 @@ export default class Model {
 		} );
 		} );
 		this.schema.extend( '$text', { allowIn: '$clipboardHolder' } );
 		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 );
 		injectSelectionPostFixer( this );
@@ -331,15 +335,16 @@ export default class Model {
 	 *
 	 *
 	 * @fires insertContent
 	 * @fires insertContent
 	 * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
 	 * @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.
 	 * 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`.
 	 * @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.
 	 * 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( 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`)
 	 * * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
 	 * * `<heading1>^</heading1>` with enabled (`doNotResetEntireContent == true`)
 	 * * `<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( selection, options ) {
 		deleteContent( this, 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
 	 * 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 {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}
 	 * @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;
 			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;
 				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
 	 * Note: This method is also available as
 	 * {@link module:engine/model/writer~Writer#createSelection `Writer#createSelection()`}.
 	 * {@link module:engine/model/writer~Writer#createSelection `Writer#createSelection()`}.
@@ -650,9 +684,7 @@ export default class Model {
 	 *		// Creates backward selection.
 	 *		// Creates backward selection.
 	 *		const selection = writer.createSelection( range, { backward: true } );
 	 *		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 {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
 	 * @param {Object} [options]
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
 	 * @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 );
 			const callbackReturnValue = this._pendingChanges[ 0 ].callback( this._currentWriter );
 			ret.push( callbackReturnValue );
 			ret.push( callbackReturnValue );
 
 
-			// Fire internal `_change` event.
-			this.fire( '_change', this._currentWriter );
+			this.document._handleChangeBlock( this._currentWriter );
 
 
 			this._pendingChanges.shift();
 			this._pendingChanges.shift();
 			this._currentWriter = null;
 			this._currentWriter = null;
@@ -717,17 +748,6 @@ export default class Model {
 		return ret;
 		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
 	 * Fired when entering the outermost {@link module:engine/model/model~Model#enqueueChange} or
 	 * {@link module:engine/model/model~Model#change} block.
 	 * {@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 CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
 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.
  * Model node. Most basic structure of model tree.
  *
  *
@@ -99,7 +102,7 @@ export default class Node {
 	 * This means that model tree got broken.
 	 * This means that model tree got broken.
 	 *
 	 *
 	 * @readonly
 	 * @readonly
-	 * @type {Number|Null}
+	 * @type {Number|null}
 	 */
 	 */
 	get startOffset() {
 	get startOffset() {
 		let pos;
 		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 InsertOperation from './insertoperation';
 import AttributeOperation from './attributeoperation';
 import AttributeOperation from './attributeoperation';
 import RenameOperation from './renameoperation';
 import RenameOperation from './renameoperation';
@@ -301,7 +306,7 @@ export function transformSets( operationsA, operationsB, options ) {
 		originalOperationsBCount: operationsB.length
 		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( operationsA );
 	contextFactory.setOriginalOperations( operationsB );
 	contextFactory.setOriginalOperations( operationsB );
 
 
@@ -381,13 +386,17 @@ class ContextFactory {
 	// @param {module:engine/model/document~Document} document Document which the operations change.
 	// @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
 	// @param {Boolean} useRelations Whether during transformation relations should be used (used during undo for
 	// better conflict resolution).
 	// 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.
 		// `model.History` instance which information about undone operations will be taken from.
 		this._history = document.history;
 		this._history = document.history;
 
 
 		// Whether additional context should be used.
 		// Whether additional context should be used.
 		this._useRelations = useRelations;
 		this._useRelations = useRelations;
 
 
+		this._forceWeakRemove = !!forceWeakRemove;
+
 		// For each operation that is created during transformation process, we keep a reference to the original operation
 		// 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
 		// 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.
 		// 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' );
 							this._setRelation( opA, opB, 'mergeTargetNotMoved' );
 						}
 						}
 
 
+						if ( opA.sourcePosition.isEqual( opB.targetPosition ) ) {
+							this._setRelation( opA, opB, 'mergeSourceNotMoved' );
+						}
+
 						if ( opA.sourcePosition.isEqual( opB.sourcePosition ) ) {
 						if ( opA.sourcePosition.isEqual( opB.sourcePosition ) ) {
 							this._setRelation( opA, opB, 'mergeSameElement' );
 							this._setRelation( opA, opB, 'mergeSameElement' );
 						}
 						}
@@ -515,6 +528,55 @@ class ContextFactory {
 
 
 				break;
 				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 ),
 			aWasUndone: this._wasUndone( opA ),
 			bWasUndone: this._wasUndone( opB ),
 			bWasUndone: this._wasUndone( opB ),
 			abRelation: this._useRelations ? this._getRelation( opA, opB ) : null,
 			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 ];
 	return [ a ];
 } );
 } );
 
 
-setTransformation( MarkerOperation, MoveOperation, ( a, b ) => {
+setTransformation( MarkerOperation, MoveOperation, ( a, b, context ) => {
 	if ( a.oldRange ) {
 	if ( a.oldRange ) {
 		a.oldRange = Range._createFromRanges( a.oldRange._getTransformedByMoveOperation( b ) );
 		a.oldRange = Range._createFromRanges( a.oldRange._getTransformedByMoveOperation( b ) );
 	}
 	}
 
 
 	if ( a.newRange ) {
 	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 ) );
 		a.newRange = Range._createFromRanges( a.newRange._getTransformedByMoveOperation( b ) );
 	}
 	}
 
 
 	return [ a ];
 	return [ a ];
 } );
 } );
 
 
-setTransformation( MarkerOperation, SplitOperation, ( a, b ) => {
+setTransformation( MarkerOperation, SplitOperation, ( a, b, context ) => {
 	if ( a.oldRange ) {
 	if ( a.oldRange ) {
 		a.oldRange = a.oldRange._getTransformedBySplitOperation( b );
 		a.oldRange = a.oldRange._getTransformedBySplitOperation( b );
 	}
 	}
 
 
 	if ( a.newRange ) {
 	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 );
 		a.newRange = a.newRange._getTransformedBySplitOperation( b );
 	}
 	}
 
 
@@ -1223,7 +1322,7 @@ setTransformation( MergeOperation, MoveOperation, ( a, b, context ) => {
 	//
 	//
 	const removedRange = Range._createFromPositionAndShift( b.sourcePosition, b.howMany );
 	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 ) ) {
 		if ( a.deletionPosition.hasSameParentAs( b.sourcePosition ) && removedRange.containsPosition( a.sourcePosition ) ) {
 			return [ new NoOperation( 0 ) ];
 			return [ new NoOperation( 0 ) ];
 		}
 		}
@@ -1339,19 +1438,34 @@ setTransformation( MergeOperation, SplitOperation, ( a, b, context ) => {
 
 
 	// Case 2:
 	// 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.
 	// 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
 	// If only one of operations is a remove operation, we force remove operation to be the "stronger" one
 	// to provide more expected results.
 	// 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;
 		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;
 		aIsStrong = false;
 	}
 	}
 
 
@@ -1678,7 +1792,7 @@ setTransformation( MoveOperation, SplitOperation, ( a, b, context ) => {
 	if ( b.graveyardPosition ) {
 	if ( b.graveyardPosition ) {
 		const movesGraveyardElement = moveRange.start.isEqual( b.graveyardPosition ) || moveRange.containsPosition( 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 ) );
 			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 );
 	const movedRange = Range._createFromPositionAndShift( a.sourcePosition, a.howMany );
 
 
 	if ( b.deletionPosition.hasSameParentAs( a.sourcePosition ) && movedRange.containsPosition( b.sourcePosition ) ) {
 	if ( b.deletionPosition.hasSameParentAs( a.sourcePosition ) && movedRange.containsPosition( b.sourcePosition ) ) {
-		if ( a.type == 'remove' ) {
+		if ( a.type == 'remove' && !context.forceWeakRemove ) {
 			// Case 1:
 			// Case 1:
 			//
 			//
 			// The element to remove got merged.
 			// The element to remove got merged.
@@ -1704,21 +1818,23 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
 				const results = [];
 				const results = [];
 
 
 				let gyMoveSource = b.graveyardPosition.clone();
 				let gyMoveSource = b.graveyardPosition.clone();
-				let splitNodesMoveSource = b.targetPosition.clone();
+				let splitNodesMoveSource = b.targetPosition._getTransformedByMergeOperation( b );
 
 
 				if ( a.howMany > 1 ) {
 				if ( a.howMany > 1 ) {
 					results.push( new MoveOperation( a.sourcePosition, a.howMany - 1, a.targetPosition, 0 ) );
 					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 );
 					splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( a.sourcePosition, a.targetPosition, a.howMany - 1 );
 				}
 				}
 
 
 				const gyMoveTarget = b.deletionPosition._getCombined( a.sourcePosition, a.targetPosition );
 				const gyMoveTarget = b.deletionPosition._getCombined( a.sourcePosition, a.targetPosition );
 				const gyMove = new MoveOperation( gyMoveSource, 1, gyMoveTarget, 0 );
 				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 );
 				const splitNodesMove = new MoveOperation( splitNodesMoveSource, b.howMany, splitNodesMoveTarget, 0 );
 
 
 				results.push( gyMove );
 				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.
 		// 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`.
 		// 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 sourcePosition = a.splitPosition._getTransformedByMoveOperation( b );
 
 
 			const newParentPosition = a.graveyardPosition._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.
 	// 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.splitPosition.isEqual( b.splitPosition ) ) {
 		if ( !a.graveyardPosition && !b.graveyardPosition ) {
 		if ( !a.graveyardPosition && !b.graveyardPosition ) {
 			return [ new NoOperation( 0 ) ];
 			return [ new NoOperation( 0 ) ];
@@ -2074,6 +2195,20 @@ setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => {
 		if ( a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual( b.graveyardPosition ) ) {
 		if ( a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual( b.graveyardPosition ) ) {
 			return [ new NoOperation( 0 ) ];
 			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:
 	// Case 2:
@@ -2205,9 +2340,7 @@ function _makeMoveOperationsFromRanges( ranges, targetPosition ) {
 		const op = new MoveOperation(
 		const op = new MoveOperation(
 			range.start,
 			range.start,
 			range.end.offset - range.start.offset,
 			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
 			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 Text from './text';
 import { last } from 'lodash-es';
 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.
  * Represents a position in the model tree.
  *
  *
@@ -828,11 +831,13 @@ export default class Position {
 	 * * {@link module:engine/model/position~Position._createAfter}.
 	 * * {@link module:engine/model/position~Position._createAfter}.
 	 *
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
 	 * @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}.
 	 * first parameter is a {@link module:engine/model/item~Item model item}.
 	 * @protected
 	 * @protected
 	 */
 	 */
-	static _createAt( itemOrPosition, offset ) {
+	static _createAt( itemOrPosition, offset, stickiness = 'toNone' ) {
 		if ( itemOrPosition instanceof Position ) {
 		if ( itemOrPosition instanceof Position ) {
 			return new Position( itemOrPosition.root, itemOrPosition.path, itemOrPosition.stickiness );
 			return new Position( itemOrPosition.root, itemOrPosition.path, itemOrPosition.stickiness );
 		} else {
 		} else {
@@ -841,9 +846,9 @@ export default class Position {
 			if ( offset == 'end' ) {
 			if ( offset == 'end' ) {
 				offset = node.maxOffset;
 				offset = node.maxOffset;
 			} else if ( offset == 'before' ) {
 			} else if ( offset == 'before' ) {
-				return this._createBefore( node );
+				return this._createBefore( node, stickiness );
 			} else if ( offset == 'after' ) {
 			} else if ( offset == 'after' ) {
-				return this._createAfter( node );
+				return this._createAfter( node, stickiness );
 			} else if ( offset !== 0 && !offset ) {
 			} else if ( offset !== 0 && !offset ) {
 				/**
 				/**
 				 * {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}
 				 * {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}
@@ -869,7 +874,7 @@ export default class Position {
 
 
 			path.push( offset );
 			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}.
 	 * 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/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}
 	 * @returns {module:engine/model/position~Position}
 	 * @protected
 	 * @protected
 	 */
 	 */
-	static _createAfter( item ) {
+	static _createAfter( item, stickiness ) {
 		if ( !item.parent ) {
 		if ( !item.parent ) {
 			/**
 			/**
 			 * You can not make a position after a root element.
 			 * 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 } );
 			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}.
 	 * 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/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}
 	 * @returns {module:engine/model/position~Position}
 	 * @protected
 	 * @protected
 	 */
 	 */
-	static _createBefore( item ) {
+	static _createBefore( item, stickiness ) {
 		if ( !item.parent ) {
 		if ( !item.parent ) {
 			/**
 			/**
 			 * You can not make a position before a root element.
 			 * 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 } );
 			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.
 	 * 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 {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
 	 * @param {module:engine/model/position~Position} [options.startPosition]
 	 * @param {module:engine/model/position~Position} [options.startPosition]
 	 * @param {Boolean} [options.singleCharacters=false]
 	 * @param {Boolean} [options.singleCharacters=false]
@@ -562,6 +572,21 @@ export default class Range {
 	 * @returns {module:engine/model/range~Range}
 	 * @returns {module:engine/model/range~Range}
 	 */
 	 */
 	_getTransformedByMergeOperation( operation ) {
 	_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 start = this.start._getTransformedByMergeOperation( operation );
 		let end = this.end._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() {
 	constructor() {
 		this._sourceDefinitions = {};
 		this._sourceDefinitions = {};
 
 
+		/**
+		 * A dictionary containing attribute properties.
+		 *
+		 * @private
+		 * @member {Object.<String,String>}
+		 */
+		this._attributeProperties = {};
+
 		this.decorate( 'checkChild' );
 		this.decorate( 'checkChild' );
 		this.decorate( 'checkAttribute' );
 		this.decorate( 'checkAttribute' );
 
 
@@ -64,7 +72,25 @@ export default class Schema {
 	 */
 	 */
 	register( itemName, definition ) {
 	register( itemName, definition ) {
 		if ( this._sourceDefinitions[ itemName ] ) {
 		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.', {
 			throw new CKEditorError( 'schema-cannot-register-item-twice: A single item cannot be registered twice in the schema.', {
 				itemName
 				itemName
 			} );
 			} );
@@ -103,7 +129,15 @@ export default class Schema {
 	 */
 	 */
 	extend( itemName, definition ) {
 	extend( itemName, definition ) {
 		if ( !this._sourceDefinitions[ itemName ] ) {
 		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.', {
 			throw new CKEditorError( 'schema-cannot-extend-missing-item: Cannot extend an item which was not registered yet.', {
 				itemName
 				itemName
 			} );
 			} );
@@ -204,7 +238,7 @@ export default class Schema {
 
 
 	/**
 	/**
 	 * Returns `true` if the given item is defined to be
 	 * 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( 'paragraph' ); // -> false
 	 *		schema.isObject( 'image' ); // -> true
 	 *		schema.isObject( 'image' ); // -> true
@@ -220,6 +254,24 @@ export default class Schema {
 		return !!( def && def.isObject );
 		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.
 	 * Checks whether the given node (`child`) can be a child of the given context.
 	 *
 	 *
@@ -395,7 +447,7 @@ export default class Schema {
 	 * Example:
 	 * Example:
 	 *
 	 *
 	 *		// Disallow bold on $text inside heading1.
 	 *		// Disallow bold on $text inside heading1.
-	 *		schema.addChildCheck( ( context, attributeName ) => {
+	 *		schema.addAttributeCheck( ( context, attributeName ) => {
 	 *			if ( context.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
 	 *			if ( context.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
 	 *				return false;
 	 *				return false;
 	 *			}
 	 *			}
@@ -431,6 +483,58 @@ export default class Schema {
 		}, { priority: 'high' } );
 		}, { 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
 	 * Returns the lowest {@link module:engine/model/schema~Schema#isLimit limit element} containing the entire
 	 * selection/range/position or the root otherwise.
 	 * 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 {Array.<module:engine/model/range~Range>} ranges Ranges to be validated.
 	 * @param {String} attribute The name of the attribute to check.
 	 * @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 ) {
 	* getValidRanges( ranges, attribute ) {
 		ranges = convertToMinimalFlatRanges( ranges );
 		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
 	 * 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.
 	 * 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},
 	 * 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.
 	 * {@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/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.
 	 * @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;
 		let parent = position.parent;
 
 
 		while ( parent ) {
 		while ( parent ) {
@@ -738,6 +806,42 @@ export default class Schema {
 			return false;
 			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 );
 mix( Schema, ObservableMixin );
@@ -873,7 +977,7 @@ mix( Schema, ObservableMixin );
  * * `allowAttributesOf` &ndash; A string or an array of strings. Inherits attributes from other items.
  * * `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.
  * * `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`.
  * * `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
  * # 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.
  * 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:
  * * `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
  * `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
  * # Generic items
  *
  *
@@ -905,7 +1010,8 @@ mix( Schema, ObservableMixin );
  *			isBlock: true
  *			isBlock: true
  *		} );
  *		} );
  *		this.schema.register( '$text', {
  *		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
  * 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
  * @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 ) {
 function compileBaseItemRule( sourceItemRules, itemName ) {
 	const itemRule = {
 	const itemRule = {
 		name: itemName,
 		name: itemName,
@@ -1459,7 +1575,7 @@ function* combineWalkers( backward, forward ) {
 // all those minimal flat ranges.
 // all those minimal flat ranges.
 //
 //
 // @param {Array.<module:engine/model/range~Range>} ranges Ranges to process.
 // @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 ) {
 function* convertToMinimalFlatRanges( ranges ) {
 	for ( const range of ranges ) {
 	for ( const range of ranges ) {
 		yield* range.getMinimalFlatRanges();
 		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 {
 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.
 	 * or creates an empty selection if no arguments were passed.
 	 *
 	 *
 	 *		// Creates empty selection without ranges.
 	 *		// Creates empty selection without ranges.
@@ -77,9 +72,7 @@ export default class Selection {
 	 *		// Creates backward selection.
 	 *		// Creates backward selection.
 	 *		const selection = writer.createSelection( range, { backward: true } );
 	 *		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 {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
 	 * @param {Object} [options]
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
 	 * @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
 	 * 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.
 	 *		// Removes all selection's ranges.
 	 *		selection.setTo( null );
 	 *		selection.setTo( null );
@@ -368,9 +359,7 @@ export default class Selection {
 	 *		// Sets backward selection.
 	 *		// Sets backward selection.
 	 *		const selection = writer.createSelection( range, { backward: true } );
 	 *		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 {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
 	 * @param {Object} [options]
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
 	 * @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 );
 			this._setRanges( selectable, placeOrOffset && !!placeOrOffset.backward );
 		} else {
 		} 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
 			 * @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;
 		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.
 	 * 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
 	 * Checks whether the selection contains the entire content of the given element. This means that selection must start
 	 * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
 	 * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
@@ -791,10 +833,25 @@ function isUnvisitedBlockContainer( element, visited ) {
 }
 }
 
 
 // Finds the lowest element in position's ancestors which is a block.
 // Finds the lowest element in position's ancestors which is a block.
+// It will search until first ancestor that is a limit element.
 // Marks all ancestors as already visited to not include any of them later on.
 // Marks all ancestors as already visited to not include any of them later on.
 function getParentBlock( position, visited ) {
 function getParentBlock( position, visited ) {
+	const schema = position.parent.document.model.schema;
+
 	const ancestors = position.parent.getAncestors( { parentFirst: true, includeSelf: true } );
 	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
 	// 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.
 	// the found block may be a child of another block.
@@ -802,3 +859,37 @@ function getParentBlock( position, visited ) {
 
 
 	return block;
 	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 a text data contained in the node.
 	 *
 	 *
-	 * @returns {String}
+	 * @readonly
+	 * @type {String}
 	 */
 	 */
 	get data() {
 	get data() {
 		return this._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`)
  * * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
  * * `<heading>^</heading>` with enabled (`doNotResetEntireContent == true`).
  * * `<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 = {} ) {
 export default function deleteContent( model, selection, options = {} ) {
 	if ( selection.isCollapsed ) {
 	if ( selection.isCollapsed ) {
 		return;
 		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;
 	const schema = model.schema;
 
 
 	model.change( writer => {
 	model.change( writer => {
@@ -59,7 +78,6 @@ export default function deleteContent( model, selection, options = {} ) {
 			return;
 			return;
 		}
 		}
 
 
-		const selRange = selection.getFirstRange();
 		const startPos = selRange.start;
 		const startPos = selRange.start;
 		const endPos = LivePosition.fromPosition( selRange.end, 'toNext' );
 		const endPos = LivePosition.fromPosition( selRange.end, 'toNext' );
 
 
@@ -79,7 +97,7 @@ export default function deleteContent( model, selection, options = {} ) {
 		if ( !options.leaveUnmerged ) {
 		if ( !options.leaveUnmerged ) {
 			mergeBranches( writer, startPos, endPos );
 			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
 			// 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.
 			// 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 );
 			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).
 		// 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 ) ) {
 		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();
 		endPos.detach();
@@ -195,11 +217,7 @@ function insertParagraph( writer, position, selection ) {
 
 
 	writer.insert( paragraph, position );
 	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 ) {
 function replaceEntireContentWithParagraph( writer, selection ) {
@@ -228,3 +246,13 @@ function shouldEntireContentBeReplacedWithParagraph( schema, selection ) {
 
 
 	return schema.checkChild( limitElement, 'paragraph' );
 	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
  * 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).
  * 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.
  * **Note:** Use {@link module:engine/model/model~Model#insertContent} instead of this function.
  * This function is only exposed to be reusable in algorithms which change the {@link module:engine/model/model~Model#insertContent}
  * This function is only exposed to be reusable in algorithms which change the {@link module:engine/model/model~Model#insertContent}
  * method's behavior.
  * 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
  * @param {module:engine/model/model~Model} model The model in context of which the insertion
  * should be performed.
  * should be performed.
  * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
  * @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.
  * Selection into which the content should be inserted.
  * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
  * @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 ) {
 export default function insertContent( model, content, selectable, placeOrOffset ) {
-	model.change( writer => {
+	return model.change( writer => {
 		let selection;
 		let selection;
 
 
 		if ( !selectable ) {
 		if ( !selectable ) {
@@ -47,11 +50,13 @@ export default function insertContent( model, content, selectable, placeOrOffset
 			selection = writer.createSelection( selectable, placeOrOffset );
 			selection = writer.createSelection( selectable, placeOrOffset );
 		}
 		}
 
 
+		const insertionPosition = selection.getFirstPosition();
+
 		if ( !selection.isCollapsed ) {
 		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;
 		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.' );
 			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.schema = model.schema;
 
 
 		this._filterAttributesOf = [];
 		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 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}
 	 * @returns {module:engine/model/range~Range|null}
 	 */
 	 */
@@ -180,6 +207,33 @@ class Insertion {
 		return this.model.schema.getNearestSelectionRange( this.position );
 		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.
 	 * Handles insertion of a single node.
 	 *
 	 *
@@ -219,10 +273,10 @@ class Insertion {
 		// E.g.:
 		// E.g.:
 		// <p>x^</p> + <p>y</p> => <p>x</p><p>y</p> => <p>xy[]</p>
 		// <p>x^</p> + <p>y</p> => <p>x</p><p>y</p> => <p>xy[]</p>
 		// and:
 		// 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:
 		// 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><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)
 		//																	 it's exactly the same case as above)
 		this._mergeSiblingsOf( node, context );
 		this._mergeSiblingsOf( node, context );
 	}
 	}
@@ -278,6 +332,7 @@ class Insertion {
 
 
 		const livePos = LivePosition.fromPosition( this.position, 'toNext' );
 		const livePos = LivePosition.fromPosition( this.position, 'toNext' );
 
 
+		this._setAffectedBoundaries( this.position );
 		this.writer.insert( node, this.position );
 		this.writer.insert( node, this.position );
 
 
 		this.position = livePos.toPosition();
 		this.position = livePos.toPosition();
@@ -293,6 +348,37 @@ class Insertion {
 		this._filterAttributesOf.push( node );
 		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
 	 * @private
 	 * @param {module:engine/model/node~Node} node The node which could potentially be merged.
 	 * @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 );
 			const livePosition = LivePosition.fromPosition( this.position );
 			livePosition.stickiness = 'toNext';
 			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 );
 			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();
 			this.position = livePosition.toPosition();
 			livePosition.detach();
 			livePosition.detach();
 		}
 		}
@@ -335,10 +451,22 @@ class Insertion {
 
 
 			// OK:  <p>xx[]</p> + <p>yy</p> => <p>xx[]yy</p> (when sticks to previous)
 			// 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)
 			// 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 );
 			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();
 			this.position = livePosition.toPosition();
 			livePosition.detach();
 			livePosition.detach();
 		}
 		}
@@ -426,19 +554,25 @@ class Insertion {
 			}
 			}
 
 
 			if ( this.position.isAtStart ) {
 			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;
 				const parent = this.position.parent;
+
 				this.position = this.writer.createPositionBefore( 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 ) {
 				if ( parent.isEmpty ) {
 					this.writer.remove( parent );
 					this.writer.remove( parent );
 				}
 				}
 			} else if ( this.position.isAtEnd ) {
 			} 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 );
 				this.position = this.writer.createPositionAfter( this.position.parent );
 			} else {
 			} else {
 				const tempPos = this.writer.createPositionAfter( this.position.parent );
 				const tempPos = this.writer.createPositionAfter( this.position.parent );
 
 
+				this._setAffectedBoundaries( this.position );
 				this.writer.split( this.position );
 				this.writer.split( this.position );
 
 
 				this.position = tempPos;
 				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>foo]</block>    ->    <block>[foo]</block>
 		// - [<block>foo]</block>    ->    <block>[foo]</block>
 		// - <block>f[oo</block>]    ->    <block>f[oo]</block>
 		// - <block>f[oo</block>]    ->    <block>f[oo]</block>
+		// - [<block>foo</block><object></object>]    ->    <block>[foo</block><object></object>]
 		if ( checkSelectionOnNonLimitElements( start, end, schema ) ) {
 		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
 	// 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.
 	// then the range crossed limit element boundaries and needs to be fixed.
 	if ( isStartInLimit || isEndInLimit ) {
 	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.
 		// 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).
 		// 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 );
 		return new Range( fixedStart, fixedEnd );
 	}
 	}
@@ -224,40 +234,39 @@ function tryFixingNonCollapsedRage( range, schema ) {
 	return null;
 	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 {module:engine/model/schema~Schema} schema
 // @param {String} expandToDirection Direction of expansion - either 'start' or 'end' of the range.
 // @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).
 	// Find outer most isLimit block as such blocks might be nested (ie. in tables).
 	while ( schema.isLimit( parent ) && parent.parent ) {
 	while ( schema.isLimit( parent ) && parent.parent ) {
-		node = parent;
+		isLimitNode = parent;
 		parent = parent.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} start
 // @param {module:engine/model/position~Position} end
 // @param {module:engine/model/position~Position} end
 // @param {module:engine/model/schema~Schema} schema
 // @param {module:engine/model/schema~Schema} schema
+// @returns {Boolean}
 function checkSelectionOnNonLimitElements( start, end, schema ) {
 function checkSelectionOnNonLimitElements( start, end, schema ) {
 	const startIsOnBlock = ( start.nodeAfter && !schema.isLimit( start.nodeAfter ) ) || schema.checkChild( start, '$text' );
 	const startIsOnBlock = ( start.nodeAfter && !schema.isLimit( start.nodeAfter ) ) || schema.checkChild( start, '$text' );
 	const endIsOnBlock = ( end.nodeBefore && !schema.isLimit( end.nodeBefore ) ) || schema.checkChild( end, '$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
 // @param {Array.<module:engine/model/range~Range>} ranges
 // @returns {Array.<module:engine/model/range~Range>}
 // @returns {Array.<module:engine/model/range~Range>}
 function mergeIntersectingRanges( ranges ) {
 function mergeIntersectingRanges( ranges ) {
@@ -282,3 +291,12 @@ function mergeIntersectingRanges( ranges ) {
 
 
 	return deIntersected;
 	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 ) {
 	insert( item, itemOrPosition, offset = 0 ) {
 		this._assertWriterUsedCorrectly();
 		this._assertWriterUsedCorrectly();
 
 
+		if ( item instanceof Text && item.data == '' ) {
+			return;
+		}
+
 		const position = Position._createAt( itemOrPosition, offset );
 		const position = Position._createAt( itemOrPosition, offset );
 
 
 		// If item has a parent already.
 		// If item has a parent already.
@@ -466,6 +470,14 @@ export default class Writer {
 
 
 		const position = Position._createAt( itemOrPosition, offset );
 		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 ) ) {
 		if ( !isSameTree( range.root, position.root ) ) {
 			/**
 			/**
 			 * Range is going to be moved within not the same document. Please use
 			 * Range is going to be moved within not the same document. Please use
@@ -491,17 +503,14 @@ export default class Writer {
 	remove( itemOrRange ) {
 	remove( itemOrRange ) {
 		this._assertWriterUsedCorrectly();
 		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 nodeBefore = position.nodeBefore;
 		const nodeAfter = position.nodeAfter;
 		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 ) ) {
 		if ( !( nodeBefore instanceof Element ) ) {
 			/**
 			/**
 			 * Node before merge position must be an 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()`}.
 	 * 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 {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
 	 * @param {Object} [options]
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
 	 * @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/position~Position} position Position of split.
 	 * @param {module:engine/model/node~Node} [limitElement] Stop splitting when this element will be reached.
 	 * @param {module:engine/model/node~Node} [limitElement] Stop splitting when this element will be reached.
 	 * @returns {Object} result Split result.
 	 * @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.
 	 * at the beginning of the first copy element.
 	 */
 	 */
 	split( position, limitElement ) {
 	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.' );
 			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' ) {
 		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(
 			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
 	 * 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.
 	 * 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
 	 * 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.
 	 * 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
 	 * 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
 	 * {@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.
 	 * 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
 	 * @see module:engine/model/markercollection~Marker
 	 * @param {String} markerOrName Name of a marker to update, or a marker instance.
 	 * @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 {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.
 	 * @param {Boolean} [options.usingOperation] Flag indicated whether the marker should be added by MarkerOperation.
 	 * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
 	 * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
 	 * @param {Boolean} [options.affectsData] Flag indicating that the marker changes the editor data.
 	 * @param {Boolean} [options.affectsData] Flag indicating that the marker changes the editor data.
 	 */
 	 */
-	updateMarker( markerOrName, options = {} ) {
+	updateMarker( markerOrName, options ) {
 		this._assertWriterUsedCorrectly();
 		this._assertWriterUsedCorrectly();
 
 
 		const markerName = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
 		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.' );
 			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 hasUsingOperationDefined = typeof options.usingOperation == 'boolean';
 		const affectsDataDefined = typeof options.affectsData == '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.
 	 *		// Sets selection to the given range.
 	 *		const range = writer.createRange( start, end );
 	 *		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.
 	 * 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 {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
 	 * @param {Object} [options]
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
 	 * @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.' );
 			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.
 // 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}.
 		 * This property is managed by {@link module:engine/view/downcastwriter~DowncastWriter}.
 		 *
 		 *
 		 * @protected
 		 * @protected
-		 * @member {Set|null}
+		 * @member {Set.<module:engine/view/attributeelement~AttributeElement>|null}
 		 */
 		 */
 		this._clonesGroup = 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}.
 	 * Element priority. Decides in what order elements are wrapped by {@link module:engine/view/downcastwriter~DowncastWriter}.
 	 *
 	 *
 	 * @readonly
 	 * @readonly
-	 * @returns {Number}
+	 * @type {Number}
 	 */
 	 */
 	get priority() {
 	get priority() {
 		return this._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`.
 	 * and then two elements are considered similar if, and only if they have the same `id`.
 	 *
 	 *
 	 * @readonly
 	 * @readonly
-	 * @returns {String|Number}
+	 * @type {String|Number}
 	 */
 	 */
 	get id() {
 	get id() {
 		return this._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}.
 		 * model root using {@link module:engine/model/document~Document#createRoot}.
 		 *
 		 *
 		 * @readonly
 		 * @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' } );
 		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}
 	 * As a parameter, a post-fixer callback receives a {@link module:engine/view/downcastwriter~DowncastWriter downcast writer}
 	 * instance connected with the executed changes block.
 	 * 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
 	 * @param {Function} postFixer
 	 */
 	 */
 	registerPostFixer( postFixer ) {
 	registerPostFixer( postFixer ) {
 		this._postFixers.add( 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.
 	 * 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.
 	 *		// Creates fake selection with label.
 	 *		const selection = new DocumentSelection( range, { fake: true, label: 'foo' } );
 	 *		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 {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
 	 * @param {Object} [options]
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
@@ -276,12 +274,27 @@ export default class DocumentSelection {
 		return this._selection.isSimilar( otherSelection );
 		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
 	 * 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.
 	 *		// Sets selection to the given range.
 	 *		const range = writer.createRange( start, end );
 	 *		const range = writer.createRange( start, end );
@@ -331,8 +344,7 @@ export default class DocumentSelection {
 	 *
 	 *
 	 * @protected
 	 * @protected
 	 * @fires change
 	 * @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 {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
 	 * @param {Object} [options]
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
 	 * @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
  * 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.
  * {@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
  * 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}).
  * 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
 	 * 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:
 	 * Usage:
 	 *
 	 *
@@ -114,8 +111,7 @@ export default class DowncastWriter {
 	 * 		// (and be  properly handled by screen readers).
 	 * 		// (and be  properly handled by screen readers).
 	 *		writer.setSelection( range, { fake: true, label: 'foo' } );
 	 *		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 {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
 	 * @param {Object} [options]
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
 	 * @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.
 		// Break attributes at range start and end.
 		const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
 		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;
 		const parentContainer = breakStart.parent;
 
 
 		// Unwrap children located between break points.
 		// Unwrap children located between break points.
@@ -1076,9 +1051,7 @@ export default class DowncastWriter {
 	 *		// Creates fake selection with label.
 	 *		// Creates fake selection with label.
 	 *		const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
 	 *		const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
 	 *
 	 *
-	 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection|
-	 * module:engine/view/position~Position|Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|
-	 * 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 {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
 	 * @param {Object} [options]
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
 	 * @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.
 	 * `startOffset` and `endOffset` will be wrapped.
 	 *
 	 *
 	 * @private
 	 * @private
 	 * @param {module:engine/view/element~Element} parent
 	 * @param {module:engine/view/element~Element} parent
 	 * @param {Number} startOffset
 	 * @param {Number} startOffset
 	 * @param {Number} endOffset
 	 * @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;
 		let i = startOffset;
 		const wrapPositions = [];
 		const wrapPositions = [];
 
 
@@ -1111,10 +1084,27 @@ export default class DowncastWriter {
 			const isEmpty = child.is( 'emptyElement' );
 			const isEmpty = child.is( 'emptyElement' );
 			const isUI = child.is( 'uiElement' );
 			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.
 				// Clone attribute.
-				const newAttribute = attribute._clone();
+				const newAttribute = wrapElement._clone();
 
 
 				// Wrap current node with new attribute.
 				// Wrap current node with new attribute.
 				child._remove();
 				child._remove();
@@ -1125,9 +1115,13 @@ export default class DowncastWriter {
 
 
 				wrapPositions.push(	new Position( parent, i ) );
 				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 ) {
 			else if ( isAttribute ) {
-				this._wrapChildren( child, 0, child.childCount, attribute );
+				this._wrapChildren( child, 0, child.childCount, wrapElement );
 			}
 			}
 
 
 			i++;
 			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.
 	 * `startOffset` and `endOffset` will be unwrapped.
 	 *
 	 *
 	 * @private
 	 * @private
 	 * @param {module:engine/view/element~Element} parent
 	 * @param {module:engine/view/element~Element} parent
 	 * @param {Number} startOffset
 	 * @param {Number} startOffset
 	 * @param {Number} endOffset
 	 * @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;
 		let i = startOffset;
 		const unwrapPositions = [];
 		const unwrapPositions = [];
 
 
 		// Iterate over each element between provided offsets inside parent.
 		// 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 ) {
 		while ( i < endOffset ) {
 			const child = parent.getChild( i );
 			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 unwrapped = child.getChildren();
 				const count = child.childCount;
 				const count = child.childCount;
 
 
@@ -1191,18 +1200,39 @@ export default class DowncastWriter {
 					new Position( parent, i + count )
 					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;
 				i += count;
 				endOffset += count - 1;
 				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++;
 				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.
 		// 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.
 	 * @returns {module:engine/view/range~Range} New range after wrapping, spanning over wrapping attribute element.
 	 */
 	 */
 	_wrapRange( range, attribute ) {
 	_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.
 		// Break attributes at range start and end.
 		const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
 		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;
 		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.
 		// 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.
 		// Merge attributes at the both ends and return a new range.
 		const start = this.mergeAttributes( newRange.start );
 		const start = this.mergeAttributes( newRange.start );
@@ -1811,17 +1810,6 @@ function mergeTextNodes( t1, t2 ) {
 	return new Position( t1, nodeBeforeLength );
 	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
 // 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/text~Text Text} or {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
 // {@link module:engine/view/containerelement~ContainerElement ContainerElement},
 // {@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.
 	 * 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.
 		 * Map of styles.
 		 *
 		 *
 		 * @protected
 		 * @protected
-		 * @member {Set} module:engine/view/element~Element#_styles
+		 * @member {Map} module:engine/view/element~Element#_styles
 		 */
 		 */
 		this._styles = new Map();
 		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.
 		 * Custom properties can be added to element instance, will be cloned but not rendered into DOM.
 		 *
 		 *
 		 * @protected
 		 * @protected
-		 * @memeber {Map}
+		 * @member {Map}
 		 */
 		 */
 		this._customProperties = new 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} type
 	 * @param {String} [name] Element name.
 	 * @param {String} [name] Element name.
@@ -206,11 +212,7 @@ export default class Element extends Node {
 			yield 'style';
 			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
  * To create a new empty element use the
  * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement `downcastWriter#createEmptyElement()`} method.
  * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement `downcastWriter#createEmptyElement()`} method.
+ *
+ * @extends module:engine/view/element~Element
  */
  */
 export default class EmptyElement extends 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 */
 /* 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.
  * It defines how the filler is created.
  *
  *
  * @see module:engine/view/filler~NBSP_FILLER
  * @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.
  *		// Match view element which has matching attributes.
  *		const pattern = {
  *		const pattern = {
- *			attribute: {
+ *			attributes: {
  *				title: 'foobar',	// Attribute title should equal 'foobar'.
  *				title: 'foobar',	// Attribute title should equal 'foobar'.
  *				foo: /^\w+/,		// Attribute foo should match /^\w+/ regexp.
  *				foo: /^\w+/,		// Attribute foo should match /^\w+/ regexp.
  *				bar: true			// Attribute bar should be set (can be empty).
  *				bar: true			// Attribute bar should be set (can be empty).
@@ -384,7 +384,7 @@ function matchStyles( patterns, element ) {
  *		// Multiple classes to match.
  *		// Multiple classes to match.
  *		const pattern = {
  *		const pattern = {
  *			classes: [ 'baz', 'bar', /foo.../ ]
  *			classes: [ 'baz', 'bar', /foo.../ ]
- *		}:
+ *		};
  *
  *
  *		// Match view element which has given styles.
  *		// Match view element which has given styles.
  *		const pattern = {
  *		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 compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
 import { clone } from 'lodash-es';
 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.
  * 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
 	 * 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}
 	 * 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( 'p' ); // shortcut for obj.is( 'element', 'p' )
 	 *		obj.is( 'text' ); // true for text node, false for element and document fragment
 	 *		obj.is( 'text' ); // true for text node, false for element and document fragment
 	 *
 	 *
-	 * @method #is
 	 * @param {'element'|'containerElement'|'attributeElement'|'emptyElement'|'uiElement'|
 	 * @param {'element'|'containerElement'|'attributeElement'|'emptyElement'|'uiElement'|
 	 * 'rootElement'|'documentFragment'|'text'|'textProxy'} type
 	 * 'rootElement'|'documentFragment'|'text'|'textProxy'} type
 	 * @returns {Boolean}
 	 * @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.
 			// overwrite new DOM selection with selection from the view.
 			// See https://github.com/ckeditor/ckeditor5-engine/issues/795 for more details.
 			// 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.
 			// 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 ) => {
 		document.on( 'blur', ( evt, data ) => {
@@ -47,7 +47,7 @@ export default class FocusObserver extends DomEventObserver {
 				document.isFocused = false;
 				document.isFocused = false;
 
 
 				// Re-render the document to update view elements.
 				// 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;
 		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}
 		 * @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
 		// 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.
 		// 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 ) {
 		function sameNodes( child1, child2 ) {
 			// First level of comparison (array of children vs array of children) – use the Lodash's default behavior.
 			// 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
 	 * 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).
 	 * (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.
 	 * 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
 	 * 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
 	 * @see module:engine/view/observer/observer~Observer#enable
 	 */
 	 */

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio